Mengapa butuh hingga 30 detik untuk membuat grup baris CCI sederhana?

20

Saya sedang mengerjakan demo yang melibatkan CCI ketika saya perhatikan bahwa beberapa sisipan saya memakan waktu lebih lama dari yang diharapkan. Definisi tabel untuk mereproduksi:

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);

Untuk tes saya memasukkan semua 1048576 baris dari tabel pementasan. Itu cukup untuk mengisi tepat satu grup baris terkompresi selama tidak dipangkas untuk beberapa alasan.

Jika saya memasukkan semua integer mod 17000, ini membutuhkan waktu kurang dari satu detik:

TRUNCATE TABLE dbo.CCI_BIGINT;

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

Waktu Eksekusi SQL Server: Waktu CPU = 359 ms, waktu yang berlalu = 364 ms.

Namun, jika saya memasukkan bilangan bulat yang sama mod 16000 kadang-kadang membutuhkan waktu lebih dari 30 detik:

TRUNCATE TABLE dbo.CCI_BIGINT;

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

Waktu Eksekusi SQL Server: Waktu CPU = 32062 ms, waktu yang berlalu = 32511 ms.

Ini adalah tes berulang yang telah dilakukan pada banyak mesin. Tampaknya ada pola yang jelas dalam waktu yang berlalu ketika nilai mod berubah:

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

Jika Anda ingin menjalankan tes sendiri, silakan memodifikasi kode tes yang saya tulis di sini .

Saya tidak dapat menemukan sesuatu yang menarik di sys.dm_os_wait_stats untuk memasukkan mod 16000:

╔════════════════════════════════════╦══════════════╗
             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 
╚════════════════════════════════════╩══════════════╝

Mengapa penyisipan ID % 16000membutuhkan waktu lebih lama daripada penyisipan ID % 17000?

Joe Obbish
sumber

Jawaban:

12

Dalam banyak hal, ini adalah perilaku yang diharapkan. Setiap rangkaian rutinitas kompresi akan memiliki kinerja yang luas tergantung pada distribusi data input. Kami berharap untuk memperdagangkan kecepatan pemuatan data untuk ukuran penyimpanan dan kinerja kueri runtime.

Ada batasan pasti untuk seberapa detail jawaban yang akan Anda dapatkan di sini, karena VertiPaq adalah implementasi berpemilik, dan detailnya adalah rahasia yang dijaga ketat. Meski begitu, kita tahu bahwa VertiPaq berisi rutinitas untuk:

  • Pengkodean nilai (penskalaan dan / atau menerjemahkan nilai agar sesuai dengan sejumlah kecil bit)
  • Pengkodean kamus (referensi bilangan bulat ke nilai unik)
  • Run Length Encoding (menyimpan run dari nilai yang berulang sebagai pasangan [value, count])
  • Pengepakan bit (menyimpan arus sesedikit mungkin)

Biasanya, data akan berupa nilai atau kamus yang disandikan, kemudian RLE atau pengepakan bit akan diterapkan (atau gabungan dari RLE dan pengepakan bit yang digunakan pada subbagian berbeda dari data segmen). Proses memutuskan teknik mana yang harus diterapkan dapat melibatkan pembuatan histogram untuk membantu menentukan bagaimana penghematan bit maksimum dapat dicapai.

Menangkap kasus yang lambat dengan Windows Performance Recorder dan menganalisis hasilnya dengan Windows Performance Analyzer, kita dapat melihat bahwa sebagian besar waktu eksekusi digunakan untuk melihat pengelompokan data, membuat histogram, dan memutuskan bagaimana cara mempartisi untuk yang terbaik tabungan:

Analisis WPA

Pemrosesan yang paling mahal terjadi untuk nilai yang muncul setidaknya 64 kali dalam segmen tersebut. Ini adalah heuristik untuk menentukan kapan RLE murni kemungkinan bermanfaat. Kasing yang lebih cepat menghasilkan penyimpanan yang tidak murni, misalnya representasi yang dikemas sedikit, dengan ukuran penyimpanan akhir yang lebih besar. Dalam kasus hibrid, nilai dengan 64 pengulangan atau lebih dikodekan RLE, dan sisanya dikemas sedikit.

Durasi terpanjang terjadi ketika jumlah maksimum nilai yang berbeda dengan 64 repetisi muncul di segmen terbesar yang mungkin yaitu 1.048.576 baris dengan 16.384 set nilai dengan masing-masing 64 entri. Inspeksi kode mengungkapkan batas waktu kode keras untuk pemrosesan yang mahal. Ini dapat dikonfigurasi dalam implementasi VertiPaq lain misalnya SSAS, tetapi tidak dalam SQL Server sejauh yang saya tahu.

Beberapa wawasan tentang pengaturan penyimpanan akhir dapat diperoleh dengan menggunakan perintah tidak berdokumenDBCC CSINDEX . Ini menunjukkan entri header dan array RLE, bookmark apa pun ke dalam data RLE, dan ringkasan singkat dari data paket bit (jika ada).

Untuk informasi lebih lanjut, lihat:

Paul White mengatakan GoFundMonica
sumber
9

Saya tidak dapat mengatakan dengan tepat mengapa perilaku ini terjadi tetapi saya percaya saya telah mengembangkan model perilaku yang baik melalui pengujian brute force. Kesimpulan berikut hanya berlaku saat memuat data ke dalam satu kolom dan dengan bilangan bulat yang didistribusikan dengan sangat baik.

Pertama saya mencoba memvariasikan jumlah baris yang dimasukkan ke dalam CCI menggunakan TOP. Saya menggunakan ID % 16000semua tes. Di bawah ini adalah grafik yang membandingkan baris yang dimasukkan ke ukuran segmen grup baris terkompresi:

grafik atas vs ukuran

Di bawah ini adalah grafik baris yang dimasukkan ke waktu CPU dalam ms. Perhatikan bahwa sumbu X memiliki titik awal yang berbeda:

atas vs cpu

Kita dapat melihat bahwa ukuran segmen rowgroup tumbuh pada tingkat linier dan menggunakan sejumlah kecil CPU hingga sekitar 1 M baris. Pada saat itu ukuran grup baris menurun secara dramatis dan penggunaan CPU meningkat secara dramatis. Tampaknya kita membayar mahal pada CPU untuk kompresi itu.

Ketika memasukkan kurang dari 1024000 baris saya berakhir dengan grup baris terbuka di CCI. Namun, memaksa kompresi menggunakan REORGANIZEatau REBUILDtidak berpengaruh pada ukuran. Sebagai tambahan, saya menemukan hal menarik bahwa ketika saya menggunakan variabel untuk TOPsaya berakhir dengan rowgroup terbuka tetapi dengan RECOMPILEsaya berakhir dengan rowgroup tertutup.

Selanjutnya saya diuji dengan memvariasikan nilai modulus sambil menjaga jumlah baris yang sama. Berikut adalah contoh data saat memasukkan 102400 baris:

╔═══════════╦═════════╦═══════════════╦═════════════╗
 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 
╚═══════════╩═════════╩═══════════════╩═════════════╝

Hingga nilai mod 1600 ukuran segmen baris meningkat secara linear sebesar 80 byte untuk setiap tambahan 10 nilai unik. Ini adalah kebetulan yang menarik bahwa secara BIGINTtradisional memakan 8 byte dan ukuran segmen meningkat 8 byte untuk setiap nilai unik tambahan. Melewati nilai mod 1600 ukuran segmen meningkat dengan cepat sampai stabil.

Juga membantu untuk melihat data ketika meninggalkan nilai modulus yang sama dan mengubah jumlah baris yang dimasukkan:

╔═══════════╦═════════╦═══════════════╦═════════════╗
 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 
╚═══════════╩═════════╩═══════════════╩═════════════╝

Sepertinya ketika jumlah baris yang dimasukkan <~ 64 * jumlah nilai unik yang kita lihat kompresi relatif buruk (2 byte per baris untuk mod <= 65000) dan rendah, penggunaan CPU linier. Ketika jumlah baris yang dimasukkan> ~ 64 * jumlah nilai unik kita melihat kompresi yang lebih baik dan lebih tinggi, penggunaan CPU masih linier. Ada transisi antara dua kondisi yang tidak mudah bagi saya untuk membuat model tetapi dapat dilihat pada grafik. Tampaknya tidak benar bahwa kita melihat penggunaan CPU maksimum saat memasukkan 64 baris persis untuk setiap nilai unik. Sebaliknya, kami hanya dapat memasukkan maksimum 1048576 baris ke dalam grup baris dan kami melihat penggunaan dan kompresi CPU jauh lebih tinggi begitu ada lebih dari 64 baris per nilai unik.

Di bawah ini adalah plot kontur tentang bagaimana waktu cpu berubah ketika jumlah baris yang disisipkan dan jumlah baris unik berubah. Kita bisa melihat pola yang dijelaskan di atas:

cpu kontur

Di bawah ini adalah plot kontur ruang yang digunakan oleh segmen tersebut. Setelah titik tertentu kita mulai melihat kompresi yang jauh lebih baik, seperti dijelaskan di atas:

ukuran kontur

Sepertinya setidaknya ada dua algoritma kompresi yang berbeda yang bekerja di sini. Mengingat hal di atas, masuk akal bahwa kita akan melihat penggunaan CPU maksimum saat memasukkan 1048576 baris. Juga masuk akal bahwa kita melihat penggunaan CPU paling banyak pada saat itu ketika memasukkan sekitar 16000 baris. 1048576/64 = 16384.

Saya mengunggah semua data mentah saya di sini kalau-kalau ada yang ingin menganalisisnya.

Layak disebutkan apa yang terjadi dengan rencana paralel. Saya hanya mengamati perilaku ini dengan nilai yang didistribusikan secara merata. Saat melakukan insert paralel, seringkali ada elemen keacakan dan untaian biasanya tidak seimbang.

Letakkan 2097152 baris di tabel pementasan:

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;

Sisipan ini selesai dalam waktu kurang dari satu detik dan memiliki kompresi yang buruk:

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);

Kita dapat melihat efek dari utas yang tidak seimbang:

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

Ada berbagai trik yang dapat kita lakukan untuk memaksa agar benang seimbang dan memiliki distribusi baris yang sama. Ini salah satunya:

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)

Memilih bilangan ganjil untuk modulus penting di sini. SQL Server memindai tabel pementasan secara serial, menghitung nomor baris, kemudian menggunakan distribusi round robin untuk meletakkan baris pada utas paralel. Itu berarti bahwa kita akan berakhir dengan utas yang seimbang sempurna.

saldo 1

Sisipan membutuhkan waktu sekitar 40 detik yang mirip dengan penyisipan serial. Kami mendapat grup baris terkompresi dengan baik:

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

Kita bisa mendapatkan hasil yang sama dengan memasukkan data dari tabel pementasan asli:

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);

Di sini distribusi round robin digunakan untuk tabel turunan ssehingga satu pemindaian tabel dilakukan pada setiap thread paralel:

seimbang 2

Kesimpulannya, saat memasukkan bilangan bulat yang terdistribusi secara merata Anda dapat melihat kompresi yang sangat tinggi ketika setiap bilangan bulat unik muncul lebih dari 64 kali. Ini mungkin karena algoritma kompresi yang digunakan berbeda. Mungkin ada biaya tinggi dalam CPU untuk mencapai kompresi ini. Perubahan kecil dalam data dapat menyebabkan perbedaan dramatis dalam ukuran segmen rowgroup terkompresi. Saya menduga bahwa melihat kasus terburuk (dari perspektif CPU) akan jarang terjadi, setidaknya untuk kumpulan data ini. Bahkan lebih sulit untuk dilihat saat melakukan sisipan paralel.

Joe Obbish
sumber
8

Saya percaya, ini ada hubungannya dengan optimisasi internal kompresi untuk tabel kolom tunggal, dan angka ajaib dari 64 KB yang ditempati oleh kamus.

Contoh: jika Anda menjalankan dengan MOD 16600 , hasil akhir dari ukuran Grup Row akan menjadi 1,683 MB , sementara menjalankan MOD 17000 akan memberi Anda Grup Grup dengan ukuran 2,001 MB .

Sekarang, lihat kamus yang dibuat (Anda dapat menggunakan pustaka CISL saya untuk itu, Anda akan memerlukan fungsi cstore_GetDictionaries, atau secara alternatif buka dan kueri sys.column_store_dictionaries DMV):

(MOD 16600) 61 KB

masukkan deskripsi gambar di sini

(MOD 17000) 65 KB

masukkan deskripsi gambar di sini

Lucunya, jika Anda akan menambahkan kolom lain ke meja Anda, dan sebut saja 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);

Muat ulang data untuk 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);

Kali ini pelaksanaannya akan cepat, karena pengoptimal akan memutuskan untuk tidak bekerja terlalu keras dan mengompresnya terlalu jauh:

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;

Meskipun akan ada perbedaan kecil antara ukuran Grup Row, itu akan diabaikan (2.000 (MOD 16600) vs 2,001 (MOD 17000))

Untuk skenario ini, kamus untuk MOD 16000 akan lebih besar daripada untuk skenario pertama dengan 1 kolom (0,63 vs 0,61).

Niko Neugebuer
sumber