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


20

挿入物の一部が予想よりも長くかかっていることに気付いたとき、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ミリ秒、経過時間= 32511ミリ秒。

これは、複数のマシンで実行された繰り返し可能なテストです。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

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

mod 16000 insertの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ですか?

回答:


12

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

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

  • 値のエンコード(少数のビットに収まるように値をスケーリングおよび/または変換)
  • 辞書エンコード(一意の値への整数参照)
  • ランレングスエンコーディング(反復値のランを[値、カウント]ペアとして保存)
  • ビットパッキング(可能な限り少ないビットでストリームを保存する)

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

Windows Performance Recorderでスローケースをキャプチャし、Windows Performance Analyzerで結果を分析すると、データのクラスター化の確認、ヒストグラムの構築、最適なパーティション分割方法の決定に実行時間の大部分が消費されていることがわかります。節約:

WPA分析

最も高価な処理は、セグメントに少なくとも64回出現する値に対して発生します。これは、純粋な RLEが有益であると判断するための発見的手法です。より高速な場合、最終的なストレージサイズが大きくなるなど、ビットパック表現などのストレージが不純になります。ハイブリッドの場合、64回以上の繰り返しがある値はRLEエンコードされ、残りはビットパックされます。

最長の期間は、64の繰り返しを持つ個別の値の最大数が、可能な最大セグメント、つまり、それぞれ64エントリを持つ16,384セットの値を持つ1,048,576行に現れるときに発生します。コードを調べると、高価な処理のハードコーディングされた時間制限が明らかになります。これは、SSASなどの他のVertiPaq実装で構成できますが、私が知る限り、SQL Serverでは構成できません。

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

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


9

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

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

トップとサイズのグラフ

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

トップvs CPU

行グループセグメントサイズが線形レートで増加し、約1 M行まで少量の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 
╚═══════════╩═════════╩═══════════════╩═════════════╝

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

モジュラス値を同じままにして、挿入される行の数を変更するときにデータを確認することも役立ちます。

╔═══════════╦═════════╦═══════════════╦═════════════╗
 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時間の変化の等高線図です。上記のパターンを確認できます。

輪郭CPU

以下は、セグメントが使用する空間の等高線図です。ある時点の後、上で説明したように、はるかに優れた圧縮が見られるようになります。

輪郭サイズ

ここでは、少なくとも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は、ステージングテーブルをシリアルでスキャンし、行番号を計算し、ラウンドロビン分散を使用して、並列スレッドに行を配置します。つまり、完全にバランスの取れたスレッドになります。

バランス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各並列スレッドでテーブルのスキャンが1回実行されます。

バランスの取れた2

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


8

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

例:MOD 16600で実行すると、行グループサイズの最終結果は1.683 MBになりますが、MOD 17000を実行すると、サイズが2.001 MBの行グループになります。

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

(MOD 16600)61 KB

ここに画像の説明を入力してください

(MOD 17000)65 KB

ここに画像の説明を入力してください

おもしろいことに、テーブルに別の列を追加し、それを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)vs 2.001(MOD 17000))

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.