SQL Server 2008+聚簇索引的排序顺序是否会影响插入性能?
特定情况下的数据类型为integer
,插入的值为升序(Identity
)。因此,索引的排序顺序将与要插入的值的排序顺序相反。
我的猜测是,它会产生影响,但我不知道,也许SQL Server对这种情况有一些优化,或者它的内部数据存储格式对此无动于衷。
请注意,问题与INSERT
表现有关,而不是SELECT
。
更新
更清楚的问题是:当插入的值(integer
)与聚簇索引(ASC
)的顺序相反(DESC
)时会发生什么?
答案 0 :(得分:7)
有区别。插入群集顺序会导致大量碎片。
当您运行以下代码时,DESC聚集索引将在NONLEAF级别生成其他UPDATE操作。
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)
两个插入语句产生完全相同的执行计划,但在查看操作统计数据时,差异显示在[nonleaf_update_count]上。
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)
当SQL正在使用针对IDENTITY运行的DESC索引时,还会有一个额外的操作。 这是因为DESC表变得碎片化(在页面开头插入行),并且发生了额外的更新以维护B树结构。
这个例子最引人注目的是DESC聚集索引的碎片超过99%。 This is recreating the same bad behaviour as using a random GUID for a clustered index. 以下代码演示了碎片。
re.sub()
更新:
在某些测试环境中,我也看到DESC表受到更多WAITS的影响,并增加了[page_io_latch_wait_count]和[page_io_latch_wait_in_ms]
<强>更新强>
当SQL可以执行Backward Scans时,出现了一个关于降序索引的重点的讨论。请阅读有关limitations of Backward Scans的文章。
答案 1 :(得分:7)
插入到聚簇索引中的值的顺序肯定会影响索引的性能,可能会产生大量碎片,并且还会影响插入本身的性能。
我已经构建了一个试验台,看看会发生什么:
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));
第一列是主键,您可以看到值以随机(ish)顺序列出。以随机顺序列出值应该使SQL Server成为:
CRYPT_GEN_RANDOM()
函数用于每行生成1024字节的随机数据,以允许此表使用多个页面,从而使我们能够看到碎片插入的效果。
运行上面的插入后,您可以像这样检查碎片:
SELECT *
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID('TestSort'), 1, 0, 'SAMPLED') ips;
在我的SQL Server 2012 Developer Edition实例上运行此操作会显示90%的平均碎片,表明SQL Server在插入过程中没有排序。
这个特定故事的寓意很可能是,&#34;如果有疑问,排序,如果这将是有益的&#34;。话虽如此,在insert语句中添加ORDER BY
子句并不能保证插入按顺序发生。考虑插入并行时会发生什么,例如。
在非生产系统上,您可以使用跟踪标志2332作为插入语句的选项,以强制&#34;强制&#34; SQL Server在插入输入之前对输入进行排序。 @PaulWhite有一篇有趣的文章,Optimizing T-SQL queries that change data涵盖了该文章和其他细节。请注意,该跟踪标志不受支持,不应在生产系统中使用,因为这可能会使保修失效。在非生产系统中,对于您自己的教育,您可以尝试将其添加到INSERT
语句的末尾:
OPTION (QUERYTRACEON 2332);
如果您已将附加内容添加到插入内容中,请查看该计划,您将看到一个明确的类别:
如果微软能够将其作为支持的跟踪标志,那就太好了。
SQL Server 的 Paul White made me aware会在计划认为其有用时自动将排序运算符引入计划中。对于上面的示例查询,如果我在values
子句中运行带有250个项目的插入,则不会自动执行排序。但是,在251项中,SQL Server会在插入之前自动对值进行排序。为什么截止值是250/251行对我来说仍然是一个谜,除了它似乎是硬编码。如果我将SomeData
列中插入的数据的大小减少到只有一个字节,那么截止值仍然 250/251行,即使两种情况下表的大小都只是一页。有趣的是,查看带有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.
一旦开始增加行大小,排序版本肯定会变得更有效率。在SomeData
中插入4096个字节时,排序后的插入在我的测试装置上几乎是未排序插入的两倍。
作为附注,如果您感兴趣,我使用此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行。我将输出复制并粘贴到上面的INSERT
语句中。
答案 2 :(得分:0)
只要数据按聚集索引排序(无论是上升还是下降),就不会对插入性能产生任何影响。这背后的原因是SQL不关心聚集索引的页面中行的物理顺序。行的顺序保存在所谓的“记录偏移数组”中,这是唯一一个需要为新行重写的数据(无论顺序如何,无论如何都会这样做)。实际的数据行将一个接一个地写入。
在事务日志级别,条目应该是相同的,与方向无关,因此不会对性能产生任何额外影响。通常,事务日志是产生大多数性能问题的日志,但在这种情况下将没有。
您可以在https://www.simple-talk.com/sql/database-administration/sql-server-storage-internals-101/找到关于页面/行的物理结构的详细说明。
所以基本上只要你的插入不会生成页面拆分(如果数据按聚集索引的顺序排列而不管顺序如何),如果对插入性能有任何影响,你的插入将是微不足道的。
答案 3 :(得分:0)
基于以下代码,当所选数据按排序聚簇索引的相反方向排序时,将数据插入到具有已排序聚簇索引的标识列中会更加资源紧张。
在此示例中,逻辑读取几乎是两倍。
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/