SQL Serverは通常、挿入する前に、大きな挿入をクラスター化インデックスの順序に並べ替えようとします。
ただし、挿入のソースがテーブル変数である場合、テーブル変数が入力された後にステートメントが再コンパイルされない限り、カーディナリティは考慮されません。これがないと、挿入は1行のみであると想定されます。
以下のスクリプトは、3つの可能なシナリオを示しています。
- 挿入ソースはすでに正確に正しい順序になっています。
- 挿入ソースは正確に逆の順序になっています。
- 挿入ソースは正確に逆の順序ですが、
OPTION (RECOMPILE)
SQLServerが1,000,000行の挿入に適したプランをコンパイルするために使用されます。
実行計画
3つ目は、挿入された値を最初にクラスター化されたインデックス順に取得するためのソート演算子を備えています。
/*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 |
+------+------------+------------------------------+----------------+----------------------------+
結論
この場合、3つすべてがまったく同じページ数を使用することになりました。ただしTmp2
、他の2つはわずか0.4%であるのに対し、99.97%は断片化されています。最初に追加のソートステップが必要だったため、挿入にTmp3
最も時間がかかりましたが、最小の断片化のテーブルに対する将来のスキャンの利点に対して、この1回限りのコストを設定する必要があります。
非常に断片化されている理由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に割り当てられましたが、これはページ26817ではなくページ26819でした。
Tmp2
挿入に時間がかかった理由の1つTmp1
は、行がページにまったく逆の順序で挿入されるため、挿入するたびに、ページTmp2
のスロット配列を書き換えて、前のすべてのエントリを上に移動して、新参者。