SQL Server 通常会在插入之前尝试将大型插入排序为聚集索引顺序。
但是,如果插入的源是表变量,那么它将不考虑基数,除非在填充表变量后重新编译语句。如果没有这个,它将假设插入只会是一行。
下面的脚本演示了三种可能的情况。
- 插入源的顺序已经完全正确。
- 插入源的顺序正好相反。
- 插入源的顺序正好相反,但
OPTION (RECOMPILE)
使用它是为了让 SQL Server 编译一个适合插入 1,000,000 行的计划。
执行计划
第三个有一个排序运算符,可以首先将插入的值按聚集索引顺序排列。
/*Create three separate identical tables*/
CREATE TABLE Tmp1(a int primary key clustered (a))
CREATE TABLE Tmp2(a int primary key clustered (a))
CREATE TABLE Tmp3(a int primary key clustered (a))
DBCC FREEPROCCACHE;
GO
DECLARE @Source TABLE (N INT PRIMARY KEY (N ASC))
INSERT INTO @Source
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT 0))
FROM sys.all_columns c1, sys.all_columns c2, sys.all_columns c3
SET STATISTICS TIME ON;
PRINT 'Tmp1'
INSERT INTO Tmp1
SELECT TOP (1000000) N
FROM @Source
ORDER BY N
PRINT 'Tmp2'
INSERT INTO Tmp2
SELECT TOP (1000000) 1000000 - N
FROM @Source
ORDER BY N
PRINT 'Tmp3'
INSERT INTO Tmp3
SELECT 1000000 - N
FROM @Source
ORDER BY N
OPTION (RECOMPILE)
SET STATISTICS TIME OFF;
验证结果并清理
SELECT object_name(object_id) AS name,
page_count,
avg_fragmentation_in_percent,
fragment_count,
avg_fragment_size_in_pages
FROM
sys.dm_db_index_physical_stats(db_id(), object_id('Tmp1'), 1, NULL, 'DETAILED')
WHERE index_level = 0
UNION ALL
SELECT object_name(object_id) AS name,
page_count,
avg_fragmentation_in_percent,
fragment_count,
avg_fragment_size_in_pages
FROM
sys.dm_db_index_physical_stats(db_id(), object_id('Tmp2'), 1, NULL, 'DETAILED')
WHERE index_level = 0
UNION ALL
SELECT object_name(object_id) AS name,
page_count,
avg_fragmentation_in_percent,
fragment_count,
avg_fragment_size_in_pages
FROM
sys.dm_db_index_physical_stats(db_id(), object_id('Tmp3'), 1, NULL, 'DETAILED')
WHERE index_level = 0
DROP TABLE Tmp1, Tmp2, Tmp3
STATISTICS TIME ON
结果
+------+----------+--------------+
| | CPU Time | Elapsed Time |
+------+----------+--------------+
| Tmp1 | 6718 ms | 6775 ms |
| Tmp2 | 7469 ms | 7240 ms |
| Tmp3 | 7813 ms | 9318 ms |
+------+----------+--------------+
分片结果
+------+------------+------------------------------+----------------+----------------------------+
| name | page_count | avg_fragmentation_in_percent | fragment_count | avg_fragment_size_in_pages |
+------+------------+------------------------------+----------------+----------------------------+
| Tmp1 | 3345 | 0.448430493 | 17 | 196.7647059 |
| Tmp2 | 3345 | 99.97010463 | 3345 | 1 |
| Tmp3 | 3345 | 0.418535127 | 16 | 209.0625 |
+------+------------+------------------------------+----------------+----------------------------+
结论
在这种情况下,他们三个最终都使用了完全相同的页数。然而Tmp2
,99.97% 是分散的,而其他两个只有 0.4%。插入Tmp3
花费的时间最长,因为这首先需要一个额外的排序步骤,但是需要根据未来扫描最小碎片表的好处来设置这一时间成本。
Tmp2
从下面的查询中可以看出碎片如此严重的原因
WITH T AS
(
SELECT TOP 3000 file_id, page_id, a
FROM Tmp2
CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%)
ORDER BY a
)
SELECT file_id, page_id, MIN(a), MAX(a)
FROM T
group by file_id, page_id
ORDER BY MIN(a)
在逻辑碎片为零的情况下,具有下一个最高键值的页面将是文件中的下一个最高页面,但这些页面的顺序与它们应该是完全相反的。
+---------+---------+--------+--------+
| file_id | page_id | Min(a) | Max(a) |
+---------+---------+--------+--------+
| 1 | 26827 | 0 | 143 |
| 1 | 26826 | 144 | 442 |
| 1 | 26825 | 443 | 741 |
| 1 | 26824 | 742 | 1040 |
| 1 | 26823 | 1041 | 1339 |
| 1 | 26822 | 1340 | 1638 |
| 1 | 26821 | 1639 | 1937 |
| 1 | 26820 | 1938 | 2236 |
| 1 | 26819 | 2237 | 2535 |
| 1 | 26818 | 2536 | 2834 |
| 1 | 26817 | 2835 | 2999 |
+---------+---------+--------+--------+
行以降序到达,因此例如值 2834 到 2536 被放入页面 26818,然后为 2535 分配一个新页面,但这是页面 26819 而不是页面 26817。
Tmp2
插入时间比插入时间长的一个可能原因Tmp1
是,由于行在页面上以完全相反的顺序插入,因此每次插入Tmp2
意味着页面上的插槽数组需要重写,所有先前的条目都向上移动以腾出空间新品到货。