Чому для створення простої групи рядків 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 рядків із таблиці постановки. Цього достатньо, щоб заповнити рівно одну стиснуту групу рядків до тих пір, поки вона чомусь не буде оброблена.

Якщо я вставляю всі цілі числа mod 17000, це займає менше секунди:

TRUNCATE TABLE dbo.CCI_BIGINT;

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

Часи виконання SQL Server: час процесора = 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: час процесора = 32062 мс, минулий час = 32511 мс.

Це повторюваний тест, зроблений на декількох машинах. Здається, що пройшов чіткий зразок за минулий час, коли значення моди змінюється:

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

Якщо ви хочете запустити тести, можете змінити код тесту, який я написав тут .

Я не зміг знайти нічого цікавого у sys.dm_os_wait_stats для вставки 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 
╚════════════════════════════════════╩══════════════╝

Чому вставка ID % 16000займає так багато часу, ніж вставка ID % 17000?

Відповіді:


12

Багато в чому це очікувана поведінка. Будь-який набір процедур стиснення матиме широкі показники роботи залежно від розподілу вхідних даних. Ми очікуємо торгувати швидкістю завантаження даних для розміру пам’яті та виконання запитів.

Існує певний обмеження щодо детальної відповіді, яку ви збираєтеся отримати тут, оскільки VertiPaq є власною реалізацією, а деталі є чітко захищеною таємницею. Тим не менш, ми знаємо, що VertiPaq містить підпрограми для:

  • Кодування значення (масштабування та / або переклад значень на невелику кількість біт)
  • Кодування словника (цілі посилання на унікальні значення)
  • Кодування довжини запуску (зберігання прогонів повторних значень як [значення, кількість] пар)
  • Упаковка бітів (зберігання потоку якомога менше бітів)

Зазвичай дані будуть кодовані значеннями або словником, тоді буде застосовано RLE або біт-упаковка (або гібрид RLE і біт-упаковки, що використовується в різних підрозділах даних сегмента). Процес вирішення того, які методи застосувати, може включати створення гістограми, яка допоможе визначити, як можна досягти максимальної економії бітів.

Записуючи повільний випадок із Windows Recorder Recorder та аналізуючи результат за допомогою Windows Performance Analyzer, ми можемо побачити, що переважна більшість часу на виконання витрачається на перегляд кластеризації даних, побудову гістограм та вирішення питання про те, як найкраще розділити їх. економія:

WPA-аналіз

Найдорожча обробка відбувається для значень, які з’являються принаймні 64 рази в сегменті. Це евристика, щоб визначити, коли чистий RLE може бути корисним. Більш швидкі випадки призводять до нечистого зберігання, наприклад, трохи упакованого представлення з більшим кінцевим розміром зберігання. У гібридних випадках значення з 64 і більше повторами кодуються RLE, а решта - бітовими.

Найдовша тривалість трапляється, коли максимальна кількість чітких значень із 64 повтореннями з’являється у найбільшому можливому сегменті, тобто 1,048,576 рядків із 16 384 наборами значень із 64 записами у кожному. Перевірка коду виявляє жорстко обмежений час для дорогої обробки. Це можна налаштувати в інших реалізаціях VertiPaq, наприклад, SSAS, але не на SQL Server, наскільки я можу сказати.

Деякі розуміння кінцевого розташування для зберігання можна отримати , використовуючи недокументовані DBCC CSINDEXкоманди . Це показує записи заголовка та масиву RLE, будь-які закладки в даних RLE та короткий підсумок даних бітових пакетів (якщо такі є).

Для отримання додаткової інформації див:


9

Я не можу точно сказати, чому така поведінка трапляється, але я вважаю, що я розробив гарну модель поведінки за допомогою грубої перевірки. Наступні висновки застосовуються лише при завантаженні даних в один стовпчик і з цілими числами, які дуже добре розподілені.

Спочатку я спробував змінити кількість рядків, вставлених у ІТП, використовуючи TOP. Я використовував ID % 16000для всіх тестів. Нижче наведено графік, який порівнює рядки, вставлені до розміру сегмента стислої групи рядків:

графік верху проти розміру

Нижче наведено графік рядків, вставлених до часу процесора в мс. Зауважте, що вісь X має іншу вихідну точку:

top vs CPU

Ми можемо бачити, що розмір сегмента групи рядків зростає лінійною швидкістю і використовує невелику кількість ЦП до приблизно 1 М рядків. У цей момент розмір групи рядів різко зменшується, а використання процесора різко збільшується. Здається, ми платимо високу ціну в процесорі за це стиснення.

Коли я вставляв менше 1024000 рядків, я опинився з відкритою групою рядків у ТПП. Однак примушування стиснення з використанням 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 розмір сегмента групи рядків лінійно збільшується на 80 байт за кожне додаткове 10 унікальних значень. Цікавий збіг, що BIGINTтрадиційно займає 8 байт, а розмір сегмента збільшується на 8 байт за кожне додаткове унікальне значення. Минуле значення мода 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 * кількість унікальних значень ми бачимо відносно погану компресію (2 байти в рядку для мод <= 65000) та низьке лінійне використання процесора. Коли вставлена ​​кількість рядків> ~ 64 * кількість унікальних значень, ми бачимо набагато кращу компресію та більше, все ж лінійне використання процесора. Існує перехід між двома станами, який мені нелегко моделювати, але це можна побачити на графіку. Не видається, що ми бачимо максимальне використання процесора, вставляючи рівно 64 рядки для кожного унікального значення. Швидше за все, ми можемо вставити максимум 1048576 рядків у групу рядків, і ми бачимо набагато більше використання CPU та стиснення, коли є більше 64 рядків за унікальне значення.

Нижче наводиться контурний графік того, як змінюється час процесора в міру зміни кількості вставлених рядків та кількості унікальних рядків. Ми можемо побачити описані вище шаблони:

контурний процесор

Нижче представлена ​​контурна ділянка простору, що використовується сегментом. Після певного моменту ми починаємо набагато краще стискати, як описано вище:

розмір контуру

Здається, що тут принаймні два різні алгоритми стиснення. З огляду на вищесказане, має сенс, що ми побачили максимальне використання процесора при вставці 1048576 рядків. Це також має сенс, що ми бачимо найбільше використання процесора в цей момент, вставляючи близько 16000 рядків. 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;

Ця вставка закінчується менше ніж за секунду і має слабке стиснення:

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

Існують різні хитрощі, які ми можемо зробити, щоб змусити нитки бути врівноваженими і мати однаковий розподіл рядків. Ось один із них:

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тому по одному скануванню таблиці робиться кожен паралельний потік:

збалансований 2

На закінчення, при вставці рівномірно розподілених цілих чисел можна побачити дуже високу компресію, коли кожне унікальне ціле число з’являється більше 64 разів. Це може бути пов'язано з іншим алгоритмом стиснення, який використовується. Для досягнення цього стиснення може бути висока вартість процесора. Невеликі зміни в даних можуть призвести до кардинальних відмінностей у розмірі сегмента стислих груп рядків. Я підозрюю, що бачити найгірший випадок (з точки зору процесора) буде нечасто в дикій природі, принаймні для цього набору даних. Ще важче це помітити, роблячи паралельні вставки.


8

Я вважаю, що це пов'язане з внутрішніми оптимізаціями стиснення для таблиць з одним стовпцем та магічним числом 64 КБ, зайнятим словником.

Приклад: якщо ви працюєте з MOD 16600 , кінцевий результат розміру групи рядків становитиме 1.683 Мб , а під час запуску MOD 17000 ви отримаєте групу рядків розміром 2,00 МБ .

Тепер погляньте на створені словники (ви можете використовувати для цього мою бібліотеку CISL ; вам знадобиться функція cstore_GetDic Dictionary, або, як альтернатива, перейдіть на запит sys.column_store_dic slova DMV):

(MOD 16600) 61 Кб

введіть тут опис зображення

(MOD 17000) 65 Кб

введіть тут опис зображення

Смішна річ, якщо ви додасте ще один стовпець до своєї таблиці, і назвемо його РЕАЛІДНИМ:

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.