SQL Server 2008+ 聚集索引的排序顺序

Sort order of an SQL Server 2008+ clustered index

SQL Server 2008+ 聚集索引的排序顺序是否影响插入性能?

特定情况下的数据类型是 integer 并且插入的值是升序的 (Identity)。因此,索引的排序顺序将与要插入的值的排序顺序相反。

我的猜测是,它会产生影响,但我不知道,也许SQL服务器针对这种情况进行了一些优化,或者它的内部数据存储格式对此无动于衷。

请注意,问题是关于 INSERT 性能,而不是 SELECT

更新
更清楚地了解这个问题:当将要插入的值 (integer) 的顺序 (ASC) 与聚簇索引 (DESC) 的顺序相反时会发生什么?

只要数据按聚簇索引排序(无论是升序还是降序),都不会对插入性能产生任何影响。这背后的原因是 SQL 不关心聚集索引页中行的物理顺序。行的顺序保存在所谓的 "Record Offset Array" 中,这是唯一需要为新行重写的行(不管顺序如何,无论如何都会完成)。实际的数据行将一个接一个地写入。

在事务日志级别,无论方向如何,条目都应该相同,因此这不会对性能产生任何额外影响。通常事务日志是产生大部分性能问题的日志,但在这种情况下会有 none.

您可以在此处找到有关页面/行的物理结构的很好解释https://www.simple-talk.com/sql/database-administration/sql-server-storage-internals-101/

所以基本上只要您的插入不会产生页面拆分(如果数据按聚簇索引的顺序出现,无论顺序如何,它都不会),您的插入对插入性能的影响可以忽略不计.

有区别。乱序插入会导致大量碎片。

当您运行以下代码时,DESC 聚簇索引在 NONLEAF 级别生成额外的 UPDATE 操作。

CREATE TABLE dbo.TEST_ASC(ID INT IDENTITY(1,1) 
                            ,RandNo FLOAT
                            );
GO
CREATE CLUSTERED INDEX cidx ON dbo.TEST_ASC(ID ASC);
GO

CREATE TABLE dbo.TEST_DESC(ID INT IDENTITY(1,1) 
                            ,RandNo FLOAT
                            );
GO
CREATE CLUSTERED INDEX cidx ON dbo.TEST_DESC(ID DESC);
GO

INSERT INTO dbo.TEST_ASC VALUES(RAND());
GO 100000

INSERT INTO dbo.TEST_DESC VALUES(RAND());
GO 100000

这两个 Insert 语句产生完全相同的执行计划,但在查看操作统计数据时,差异显示 [nonleaf_update_count]。

SELECT 
OBJECT_NAME(object_id)
,* 
FROM sys.dm_db_index_operational_stats(DB_ID(),OBJECT_ID('TEST_ASC'),null,null)
UNION
SELECT 
OBJECT_NAME(object_id)
,* 
FROM sys.dm_db_index_operational_stats(DB_ID(),OBJECT_ID('TEST_DESC'),null,null)

当 SQL 使用针对 IDENTITY 的 运行 的 DESC 索引时,有一个额外的 - 在引擎盖下 - 操作正在进行。 这是因为 DESC table 变得支离破碎(行插入到页面的开头)并且会发生其他更新以维护 B 树结构。

关于此示例最值得注意的是 DESC 聚簇索引变得超过 99% 是碎片化的。 This is recreating the same bad behaviour as using a random GUID for a clustered index. 下面的代码演示了碎片。

SELECT 
OBJECT_NAME(object_id)
,* 
FROM sys.dm_db_index_physical_stats  (DB_ID(), OBJECT_ID('dbo.TEST_ASC'), NULL, NULL ,NULL) 
UNION
SELECT 
OBJECT_NAME(object_id)
,* 
FROM sys.dm_db_index_physical_stats  (DB_ID(), OBJECT_ID('dbo.TEST_DESC'), NULL, NULL ,NULL) 

更新:

在某些测试环境中,我还看到随着 [page_io_latch_wait_count] 和 [page_io_latch_wait_in_ms][=15= 的增加,DESC table 受到更多 WAITS 的影响]

更新:

当 SQL 可以执行向后扫描时,出现了一些关于降序索引的意义所在的讨论。请阅读这篇关于 limitations of Backward Scans 的文章。

根据下面的代码,当所选数据的排序方向与排序的聚集索引相反时,将数据插入具有排序聚集索引的标识列会消耗更多资源。

在此示例中,逻辑读取几乎翻了一番。

运行 10 次后,排序的升序逻辑读取平均为 2284,排序的降序逻辑读取平均为 4301。

--Drop Table Destination;
Create Table Destination (MyId INT IDENTITY(1,1))

Create Clustered Index ClIndex On Destination(MyId ASC)

set identity_insert destination on 
Insert into Destination (MyId)
SELECT TOP (1000) n = ROW_NUMBER() OVER (ORDER BY [object_id]) 
FROM sys.all_objects 
ORDER BY n


set identity_insert destination on 
Insert into Destination (MyId)
SELECT TOP (1000) n = ROW_NUMBER() OVER (ORDER BY [object_id]) 
FROM sys.all_objects 
ORDER BY n desc;

更多关于逻辑读取的信息,如果您有兴趣: https://www.brentozar.com/archive/2012/06/tsql-measure-performance-improvements/

插入聚集索引的值的顺序肯定会影响索引的性能,因为它可能会产生大量碎片,并且还会影响插入本身的性能。

我构建了一个 test-bed 看看会发生什么:

USE tempdb;

CREATE TABLE dbo.TestSort
(
    Sorted INT NOT NULL
        CONSTRAINT PK_TestSort
        PRIMARY KEY CLUSTERED
    , SomeData VARCHAR(2048) NOT NULL
);

INSERT INTO dbo.TestSort (Sorted, SomeData)
VALUES  (1797604285, CRYPT_GEN_RANDOM(1024))
    , (1530768597, CRYPT_GEN_RANDOM(1024))
    , (1274169954, CRYPT_GEN_RANDOM(1024))
    , (-1972758125, CRYPT_GEN_RANDOM(1024))
    , (1768931454, CRYPT_GEN_RANDOM(1024))
    , (-1180422587, CRYPT_GEN_RANDOM(1024))
    , (-1373873804, CRYPT_GEN_RANDOM(1024))
    , (293442810, CRYPT_GEN_RANDOM(1024))
    , (-2126229859, CRYPT_GEN_RANDOM(1024))
    , (715871545, CRYPT_GEN_RANDOM(1024))
    , (-1163940131, CRYPT_GEN_RANDOM(1024))
    , (566332020, CRYPT_GEN_RANDOM(1024))
    , (1880249597, CRYPT_GEN_RANDOM(1024))
    , (-1213257849, CRYPT_GEN_RANDOM(1024))
    , (-155893134, CRYPT_GEN_RANDOM(1024))
    , (976883931, CRYPT_GEN_RANDOM(1024))
    , (-1424958821, CRYPT_GEN_RANDOM(1024))
    , (-279093766, CRYPT_GEN_RANDOM(1024))
    , (-903956376, CRYPT_GEN_RANDOM(1024))
    , (181119720, CRYPT_GEN_RANDOM(1024))
    , (-422397654, CRYPT_GEN_RANDOM(1024))
    , (-560438983, CRYPT_GEN_RANDOM(1024))
    , (968519165, CRYPT_GEN_RANDOM(1024))
    , (1820871210, CRYPT_GEN_RANDOM(1024))
    , (-1348787729, CRYPT_GEN_RANDOM(1024))
    , (-1869809700, CRYPT_GEN_RANDOM(1024))
    , (423340320, CRYPT_GEN_RANDOM(1024))
    , (125852107, CRYPT_GEN_RANDOM(1024))
    , (-1690550622, CRYPT_GEN_RANDOM(1024))
    , (570776311, CRYPT_GEN_RANDOM(1024))
    , (2120766755, CRYPT_GEN_RANDOM(1024))
    , (1123596784, CRYPT_GEN_RANDOM(1024))
    , (496886282, CRYPT_GEN_RANDOM(1024))
    , (-571192016, CRYPT_GEN_RANDOM(1024))
    , (1036877128, CRYPT_GEN_RANDOM(1024))
    , (1518056151, CRYPT_GEN_RANDOM(1024))
    , (1617326587, CRYPT_GEN_RANDOM(1024))
    , (410892484, CRYPT_GEN_RANDOM(1024))
    , (1826927956, CRYPT_GEN_RANDOM(1024))
    , (-1898916773, CRYPT_GEN_RANDOM(1024))
    , (245592851, CRYPT_GEN_RANDOM(1024))
    , (1826773413, CRYPT_GEN_RANDOM(1024))
    , (1451000899, CRYPT_GEN_RANDOM(1024))
    , (1234288293, CRYPT_GEN_RANDOM(1024))
    , (1433618321, CRYPT_GEN_RANDOM(1024))
    , (-1584291587, CRYPT_GEN_RANDOM(1024))
    , (-554159323, CRYPT_GEN_RANDOM(1024))
    , (-1478814392, CRYPT_GEN_RANDOM(1024))
    , (1326124163, CRYPT_GEN_RANDOM(1024))
    , (701812459, CRYPT_GEN_RANDOM(1024));

第一列是主键,如您所见,值以随机(大概)顺序列出。以随机顺序列出值应该使 SQL 服务器:

  1. 对数据进行排序,pre-insert
  2. 没有对数据进行排序,导致碎片化table。

函数CRYPT_GEN_RANDOM()用于每行生成1024字节的随机数据,让这个table消耗多页,进而让我们看到分片插入的效果。

一旦你运行上面的插入,你可以像这样检查碎片:

SELECT * 
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('TestSort'), 1, 0, 'SAMPLED') ips;

运行 我的 SQL Server 2012 Developer Edition 实例上的这个显示平均碎片率为 90%,表明 SQL 服务器在插入期间没有排序。

这个特殊故事的寓意可能是,“有疑问时,排序,如果它会有益”。话虽如此,向插入语句添加和 ORDER BY 子句并不能保证插入将按该顺序发生。例如,考虑如果插入平行会发生什么。

在 non-production 系统上,您可以使用跟踪标志 2332 作为插入语句的一个选项,以“强制”SQL 服务器在插入输入之前对输入进行排序。 @PaulWhite has an interesting article, Optimizing T-SQL queries that change data 包括那个和其他细节。请注意,跟踪标志不受支持,不应在生产系统中使用,因为这可能会使您的保修失效。在 non-production 系统中,为了您自己的教育,您可以尝试将此添加到 INSERT 语句的末尾:

OPTION (QUERYTRACEON 2332);

将其附加到插入后,查看计划,您会看到明确的排序:

如果 Microsoft 将其设为受支持的跟踪标志,那就太好了。

Paul White made me aware SQL 服务器 在认为排序运算符有用时自动将排序运算符引入计划。对于上面的示例查询,如果我 运行 在 values 子句中插入 250 个项目,则不会自动执行任何排序。但是,在 251 个项目中,SQL 服务器会在插入之前自动对值进行排序。为什么截断是 250/251 行对我来说仍然是个谜,除了它似乎是 hard-coded。如果我将 SomeData 列中插入的数据大小减少到仅一个字节,则截止值为 still 250/251 行,即使 [=92] 的大小=] 在这两种情况下都只是一个页面。有趣的是,查看带有 SET STATISTICS IO, TIME ON; 的插入显示带有单字节 SomeData 值的插入在排序时花费两倍的时间。

没有排序(即插入 250 行):

SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 16 ms, elapsed time = 16 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.
Table 'TestSort'. Scan count 0, logical reads 501, physical reads 0, 
   read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob 
   read-ahead reads 0.

(250 row(s) affected)

(1 row(s) affected)

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 11 ms.

随着排序(即插入 251 行):

SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 15 ms, elapsed time = 17 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.
Table 'TestSort'. Scan count 0, logical reads 503, physical reads 0, 
   read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob 
   read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, 
   read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob 
   read-ahead reads 0.

(251 row(s) affected)

(1 row(s) affected)

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 21 ms.

一旦开始增加行大小,排序版本肯定会变得更加高效。将 4096 字节插入 SomeData 时,排序插入在我的测试平台上的速度几乎是未排序插入的两倍。


作为 side-note,如果您有兴趣,我使用 T-SQL:

生成了 VALUES (...) 子句
;WITH s AS (
    SELECT v.Item
    FROM (VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9)) v(Item)
)
, v AS (
    SELECT Num = CONVERT(int, CRYPT_GEN_RANDOM(10), 0)
)
, o AS (
    SELECT v.Num
        , rn = ROW_NUMBER() OVER (PARTITION BY v.Num ORDER BY NEWID())
    FROM s s1
        CROSS JOIN s s2
        CROSS JOIN s s3
        CROSS JOIN v 
)
SELECT TOP(50) ', (' 
        + REPLACE(CONVERT(varchar(11), o.Num), '*', '0') 
        + ', CRYPT_GEN_RANDOM(1024))'
FROM o
WHERE rn = 1
ORDER BY NEWID();

这会生成 1,000 个随机值,仅选择第一列中具有唯一值的前 50 行。我把copied-and-pasted输出到上面的INSERT语句中。