web-dev-qa-db-ja.com

単純なCCI行グループを作成するのに最大30秒かかるのはなぜですか?

一部の挿入が予想よりも長くかかっていることに気付いたとき、CCIを含むデモに取り組んでいました。再現するテーブル定義:

DROP TABLE IF EXISTS dbo.STG_1048576;
CREATE TABLE dbo.STG_1048576 (ID BIGINT NOT NULL);
INSERT INTO dbo.STG_1048576
SELECT TOP (1048576) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

テストでは、ステージングテーブルからすべての1048576行を挿入しています。なんらかの理由でトリミングされない限り、1つの圧縮された行グループを満たすだけで十分です。

すべての整数mod 17000を挿入すると、1秒もかかりません。

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 17000
FROM dbo.STG_1048576
OPTION (MAXDOP 1);

SQL Server実行時間:CPU時間= 359ミリ秒、経過時間= 364ミリ秒。

ただし、同じ整数mod 16000を挿入すると、30秒以上かかる場合があります。

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16000
FROM dbo.STG_1048576
OPTION (MAXDOP 1);

SQL Server実行時間:CPU時間= 32062 ms、経過時間= 32511 ms。

これは、複数のマシンで行われた再現可能なテストです。 mod値が変化するにつれて、経過時間には明確なパターンがあるようです。

MOD_NUM TIME_IN_MS
1000    2036
2000    3857
3000    5463
4000    6930
5000    8414
6000    10270
7000    12350
8000    13936
9000    17470
10000   19946
11000   21373
12000   24950
13000   28677
14000   31030
15000   34040
16000   37000
17000   563
18000   583
19000   576
20000   584

自分でテストを実行したい場合は、私が書いたテストコードを自由に変更してください here

Mod 16000挿入のsys.dm_os_wait_statsで興味深いものを見つけることができませんでした。

╔════════════════════════════════════╦══════════════╗
║             wait_type              ║ diff_wait_ms ║
╠════════════════════════════════════╬══════════════╣
║ XE_DISPATCHER_WAIT                 ║       164406 ║
║ QDS_PERSIST_TASK_MAIN_LOOP_SLEEP   ║       120002 ║
║ LAZYWRITER_SLEEP                   ║        97718 ║
║ LOGMGR_QUEUE                       ║        97298 ║
║ DIRTY_PAGE_POLL                    ║        97254 ║
║ HADR_FILESTREAM_IOMGR_IOCOMPLETION ║        97111 ║
║ SQLTRACE_INCREMENTAL_FLUSH_SLEEP   ║        96008 ║
║ REQUEST_FOR_DEADLOCK_SEARCH        ║        95001 ║
║ XE_TIMER_EVENT                     ║        94689 ║
║ SLEEP_TASK                         ║        48308 ║
║ BROKER_TO_FLUSH                    ║        48264 ║
║ CHECKPOINT_QUEUE                   ║        35589 ║
║ SOS_SCHEDULER_YIELD                ║           13 ║
╚════════════════════════════════════╩══════════════╝

なぜID % 16000の挿入よりもはるかに時間がかかるID % 17000

20
Joe Obbish

多くの点で、これは予想される動作です。圧縮ルーチンのセットは、入力データの分布に応じて、幅広いパフォーマンスを発揮します。ストレージのサイズと実行時のクエリのパフォーマンスとデータの読み込み速度のトレードオフを期待しています。

VertiPaqは独自の実装であり、詳細は厳重に保護された秘密であるため、ここで取得する回答の詳細には明確な制限があります。それでも、VertiPaqには次のルーチンが含まれていることがわかります。

  • 値のエンコーディング(少数のビットに収まるように値をスケーリングまたは変換、あるいはその両方)
  • 辞書エンコーディング(一意の値への整数参照)
  • ランレングスエンコーディング(繰り返し値のランを[値、カウント]のペアとして保存)
  • ビットパッキング(ストリームをできるだけ少ないビット数で格納)

通常、データは値または辞書でエンコードされ、RLEまたはビットパッキングが適用されます(またはRLEとビットパッキングのハイブリッドがセグメントデータの異なるサブセクションで使用されます)。適用する手法を決定するプロセスには、最大のビット節約を実現する方法を決定するのに役立つヒストグラムの生成が含まれます。

Windowsパフォーマンスレコーダーを使用して遅いケースをキャプチャし、Windowsパフォーマンスアナライザーを使用して結果を分析すると、実行時間の大部分が、データのクラスター化の確認、ヒストグラムの作成、および最適な分割方法の決定に費やされていることがわかります。節約:

WPA Analysis

最もコストのかかる処理は、セグメントに少なくとも64回出現する値に対して発生します。これは、pureRLEが有益である可能性が高い時期を判断するためのヒューリスティックです。より速いケースでは、不純なストレージになります。最終的な格納サイズが大きいビットパック表現。ハイブリッドの場合、64回以上の繰り返しがある値はRLEエンコードされ、残りはビットパックされます。

最長の継続時間は、最大64の繰り返しを持つ個別の値の最大数が、可能な最大のセグメントに表示される場合に発生します。コードを検査すると、高価な処理のハードコードされた時間制限が明らかになります。これは、他のVertiPaq実装で構成できます。 SSASですが、SQL Serverにはありません。

最終的なストレージの配置に関する洞察は、 ドキュメント化されていないDBCC CSINDEXコマンド を使用して取得できます。これは、RLEヘッダーと配列エントリ、RLEデータへのブックマーク、およびビットパックデータ(存在する場合)の簡単な概要を示しています。

詳細については、以下を参照してください。

12
Paul White 9

この動作が発生している理由を正確に述べることはできませんが、私はブルートフォーステストによって動作の優れたモデルを開発したと信じています。次の結論は、データを単一の列に読み込み、整数が非常によく分散している場合にのみ適用されます。

最初に、TOPを使用してCCIに挿入される行数を変更してみました。 ID % 16000すべてのテスト。以下は、圧縮された行グループセグメントサイズに挿入された行を比較したグラフです。

graph of top vs size

以下は、CPU時間(ミリ秒)に挿入された行のグラフです。 X軸の開始点が異なることに注意してください。

top vs cpu

行グループセグメントのサイズが直線的に増加し、約100万行まで少量のCPUを使用していることがわかります。その時点で、行グループのサイズは劇的に減少し、CPU使用率は劇的に増加します。その圧縮にはCPUに重い代償を払っているようです。

1024000未満の行を挿入すると、CCIで開いた行グループになります。ただし、REORGANIZEまたはREBUILDを使用して強制的に圧縮しても、サイズに影響はありませんでした。余談ですが、TOPの変数を使用すると、行グループが開いたままになりましたが、RECOMPILEの場合は、行グループが閉じたのが興味深いです。

次に、行数を同じに保ちながら係数値を変化させてテストしました。 102400行を挿入する場合のデータのサンプルを次に示します。

╔═══════════╦═════════╦═══════════════╦═════════════╗
║ TOP_VALUE ║ MOD_NUM ║ SIZE_IN_BYTES ║ CPU_TIME_MS ║
╠═══════════╬═════════╬═══════════════╬═════════════╣
║    102400 ║    1580 ║         13504 ║         352 ║
║    102400 ║    1590 ║         13584 ║         316 ║
║    102400 ║    1600 ║         13664 ║         317 ║
║    102400 ║    1601 ║         19624 ║         270 ║
║    102400 ║    1602 ║         25568 ║         283 ║
║    102400 ║    1603 ║         31520 ║         286 ║
║    102400 ║    1604 ║         37464 ║         288 ║
║    102400 ║    1605 ║         43408 ║         273 ║
║    102400 ║    1606 ║         49360 ║         269 ║
║    102400 ║    1607 ║         55304 ║         265 ║
║    102400 ║    1608 ║         61256 ║         262 ║
║    102400 ║    1609 ║         67200 ║         255 ║
║    102400 ║    1610 ║         73144 ║         265 ║
║    102400 ║    1620 ║        132616 ║         132 ║
║    102400 ║    1621 ║        138568 ║         100 ║
║    102400 ║    1622 ║        144512 ║          91 ║
║    102400 ║    1623 ║        150464 ║          75 ║
║    102400 ║    1624 ║        156408 ║          60 ║
║    102400 ║    1625 ║        162352 ║          47 ║
║    102400 ║    1626 ║        164712 ║          41 ║
╚═══════════╩═════════╩═══════════════╩═════════════╝

1600のmod値まで、行グループセグメントサイズは、追加の10個の一意の値ごとに80バイトずつ直線的に増加します。興味深いことに、BIGINTは従来8バイトを占め、一意の値が追加されるたびにセグメントサイズが8バイト増加します。 1600のmod値を超えると、セグメントサイズは安定するまで急速に増加します。

モジュラス値を同じにして、挿入する行数を変更する場合も、データを確認すると便利です。

╔═══════════╦═════════╦═══════════════╦═════════════╗
║ TOP_VALUE ║ MOD_NUM ║ SIZE_IN_BYTES ║ CPU_TIME_MS ║
╠═══════════╬═════════╬═══════════════╬═════════════╣
║    300000 ║    5000 ║        600656 ║         131 ║
║    305000 ║    5000 ║        610664 ║         124 ║
║    310000 ║    5000 ║        620672 ║         127 ║
║    315000 ║    5000 ║        630680 ║         132 ║
║    320000 ║    5000 ║         40688 ║        2344 ║
║    325000 ║    5000 ║         40696 ║        2577 ║
║    330000 ║    5000 ║         40704 ║        2589 ║
║    335000 ║    5000 ║         40712 ║        2673 ║
║    340000 ║    5000 ║         40728 ║        2715 ║
║    345000 ║    5000 ║         40736 ║        2744 ║
║    350000 ║    5000 ║         40744 ║        2157 ║
╚═══════════╩═════════╩═══════════════╩═════════════╝

挿入された行の数<〜64 *一意の値の数が比較的低い場合、圧縮率は比較的低く(modの場合は行あたり2バイト<= 65000)、CPU使用率は低くなります。挿入された行数>〜64 *一意の値の数の場合、はるかに優れた圧縮とより高い線形のCPU使用率がわかります。 2つの状態の間に遷移があり、モデル化するのは簡単ではありませんが、グラフで確認できます。一意の値ごとに正確に64行を挿入したときに最大のCPU使用率が見られるとは思われません。むしろ、行グループには最大1048576行しか挿入できず、一意の値ごとに64行を超えると、CPU使用率と圧縮がはるかに高くなります。

以下は、挿入された行の数と一意の行の数が変化したときのCPU時間の変化の等高線図です。上記のパターンを確認できます。

contour cpu

以下は、セグメントが使用する空間の等高線図です。上記のように、特定のポイントの後、はるかに優れた圧縮が開始されます。

contour size

ここでは、少なくとも2つの異なる圧縮アルゴリズムが機能しているようです。上記を考えると、1048576行を挿入するときに最大CPU使用率が表示されるのは当然です。また、約16000行を挿入すると、その時点でCPU使用率が最も高くなることもわかります。 1048576/64 = 16384。

私はすべての生データをアップロードしました ここ 誰かがそれを分析したい場合に備えて。

並列プランで何が起こるかについて言及する価値があります。この動作は、値が均等に分散されている場合にのみ観察されました。並列挿入を行う場合、ランダム性の要素がしばしばあり、スレッドは通常不均衡です。

2097152行をステージングテーブルに入れます。

DROP TABLE IF EXISTS STG_2097152;
CREATE TABLE dbo.STG_2097152 (ID BIGINT NOT NULL);
INSERT INTO dbo.STG_2097152 WITH (TABLOCK)
SELECT TOP (2097152) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

この挿入は1秒未満で終了し、圧縮率は低くなります。

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16000
FROM dbo.STG_2097152 
OPTION (MAXDOP 2);

不均衡なスレッドの影響を確認できます。

╔════════════╦════════════╦══════════════╦═══════════════╗
║ state_desc ║ total_rows ║ deleted_rows ║ size_in_bytes ║
╠════════════╬════════════╬══════════════╬═══════════════╣
║ OPEN       ║      13540 ║            0 ║        311296 ║
║ COMPRESSED ║    1048576 ║            0 ║       2095872 ║
║ COMPRESSED ║    1035036 ║            0 ║       2070784 ║
╚════════════╩════════════╩══════════════╩═══════════════╝

スレッドのバランスを強制し、行の分布を同じにするために実行できるさまざまなトリックがあります。以下はその1つです。

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT FLOOR(0.5 * ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))  % 15999
FROM dbo.STG_2097152
OPTION (MAXDOP 2)

ここでは、係数に奇数を選択することが重要です。 SQL Serverはステージングテーブルをシリアルにスキャンし、行番号を計算してから、ラウンドロビン分散を使用して行を並列スレッドに配置します。つまり、スレッドは完全にバランスが取れた状態になります。

balance 1

挿入には、シリアル挿入と同様に約40秒かかります。うまく圧縮された行グループを取得します。

╔════════════╦════════════╦══════════════╦═══════════════╗
║ state_desc ║ total_rows ║ deleted_rows ║ size_in_bytes ║
╠════════════╬════════════╬══════════════╬═══════════════╣
║ COMPRESSED ║    1048576 ║            0 ║        128568 ║
║ COMPRESSED ║    1048576 ║            0 ║        128568 ║
╚════════════╩════════════╩══════════════╩═══════════════╝

元のステージングテーブルからデータを挿入しても、同じ結果が得られます。

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT t.ID % 16000 ID
FROM  (
    SELECT TOP (2) ID 
    FROM (SELECT 1 ID UNION ALL SELECT 2 ) r
) s
CROSS JOIN dbo.STG_1048576 t
OPTION (MAXDOP 2, NO_PERFORMANCE_SPOOL);

ここでは、派生テーブルsにラウンドロビン分散が使用されているため、テーブルのスキャンは各並列スレッドで実行されます。

balanced 2

結論として、均等に分散された整数を挿入するとき、各一意の整数が64回を超えて出現すると、非常に高い圧縮が見られます。これは、使用されている別の圧縮アルゴリズムが原因である可能性があります。この圧縮を実現するには、CPUに高いコストがかかる可能性があります。データの小さな変更により、圧縮された行グループセグメントのサイズに劇的な違いが生じる可能性があります。少なくともこのデータセットでは、(CPUの観点から)最悪のケースを確認することは一般的ではないでしょう。並列挿入を行うと、さらにわかりにくくなります。

9
Joe Obbish

これは、単一列テーブルの圧縮の内部最適化と、辞書が占める64 KBのマジックナンバーに関係していると思います。

例:MOD 16600で実行した場合、行グループサイズの最終結果は1.683 MB、実行中MOD 170002.001のサイズの行グループを提供しますMB

ここで、作成されたディクショナリを確認します(このためにmy CISLライブラリ を使用できます。関数cstore_GetDictionariesが必要です。または、sys.column_store_dictionaries DMVにクエリを実行します)。

(MOD 16600)61 KB

enter image description here

(MOD 17000)65 KB

enter image description here

おかしなことに、テーブルに別の列を追加し、それをREALIDと呼びましょう。

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, REALID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

MOD 16600のデータをリロードします。

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16600, ID
FROM dbo.STG_1048576
OPTION (MAXDOP 1);

今回は、オプティマイザが過度に働きすぎて圧縮しすぎないように決定するため、実行は高速になります。

select column_id, segment_id, cast(sum(seg.on_disk_size) / 1024. / 1024 as Decimal(8,3) ) as SizeInMB
    from sys.column_store_segments seg
        inner join sys.partitions part
            on seg.hobt_id = part.hobt_id 
    where object_id = object_id('dbo.CCI_BIGINT')
    group by column_id, segment_id;

行グループのサイズにはわずかな違いがありますが、無視できます(2.000(MOD 16600)と2.001(MOD 17000))。

このシナリオでは、MOD 16000のディクショナリは、1列の最初のシナリオよりも大きくなります(0.63対0.61)。

8
Niko Neugebuer