Почему создание простой группы строк 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 строк из промежуточной таблицы. Этого достаточно, чтобы заполнить ровно одну сжатую группу строк, если по какой-то причине она не обрезается.

Если я вставлю все целые числа 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 мс.

Однако, если я вставлю те же самые целочисленные моды 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 для вставки мода 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 Performance Recorder и анализируя результаты с помощью Windows Performance Analyzer, мы видим, что подавляющее большинство времени выполнения затрачивается на анализ кластеризации данных, построение гистограмм и принятие решения о том, как их лучше разделить. сбережения:

Анализ WPA

Самая дорогая обработка происходит для значений, которые появляются в сегменте не менее 64 раз. Это эвристика для определения того, когда чистый RLE может быть полезным. Более быстрые случаи приводят к нечистому хранению, например, битовому представлению, с большим конечным размером хранилища. В гибридных случаях значения с 64 или более повторениями кодируются RLE, а остальные битовые пакеты.

Самая длинная продолжительность наступает, когда максимальное число различных значений с 64 повторениями появляется в максимально возможном сегменте, то есть 1 048 576 строк с 16 384 наборами значений с 64 записями в каждом. Проверка кода выявляет жестко ограниченное время для дорогостоящей обработки. Это может быть настроено в других реализациях VertiPaq, например SSAS, но не в SQL Server, насколько я могу судить.

Некоторое понимание окончательного расположения хранилища можно получить с помощью недокументированной DBCC CSINDEXкоманды . Здесь показаны заголовок RLE и записи массива, любые закладки в данных RLE и краткая сводка данных битового пакета (если есть).

Для получения дополнительной информации см .:

Пол Уайт говорит, что GoFundMonica
источник
9

Я не могу точно сказать, почему это происходит, но я считаю, что я разработал хорошую модель поведения с помощью тестирования методом грубой силы. Следующие выводы применимы только при загрузке данных в один столбец и с очень хорошо распределенными целыми числами.

Сначала я попытался изменить количество строк, вставленных в CCI, используя TOP. Я использовал ID % 16000для всех тестов. Ниже приведен график сравнения строк, вставленных в сжатый размер сегмента группы строк:

график вершины против размера

Ниже приведен график строк, вставленных в процессорное время в мс. Обратите внимание, что ось X имеет другую начальную точку:

топ против процессора

Мы можем видеть, что размер сегмента группы строк растет с линейной скоростью и использует небольшое количество процессора до примерно 1 млн. Строк. В этот момент размер группы строк резко уменьшается, а загрузка ЦП резко возрастает. Казалось бы, мы платим высокую цену в 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 размер сегмента группы строк линейно увеличивается на 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 строк в группу строк, и мы увидим гораздо более высокую загрузку ЦП и сжатие, если на уникальное значение будет приходиться более 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.001 МБ .

Теперь взглянем на созданные словари (для этого вы можете использовать мою библиотеку CISL , вам понадобится функция cstore_GetDictionaries или, альтернативно, пойти и запросить sys.column_store_dictionaries DMV):

(MOD 16600) 61 КБ

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

(MOD 17000) 65 КБ

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

Забавно, если вы добавите еще один столбец в свою таблицу и назовем его 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).

Нико Нойгебер
источник