Por que leva até 30 segundos para criar um simples grupo de linhas CCI?

20

Eu estava trabalhando em uma demonstração envolvendo CCIs quando percebi que algumas das minhas inserções estavam demorando mais que o esperado. Definições de tabela para reproduzir:

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

Para os testes, estou inserindo todas as 1048576 linhas da tabela de preparação. Isso é suficiente para preencher exatamente um grupo de linhas compactado, desde que não seja aparado por algum motivo.

Se eu inserir todos os números inteiros mod 17000, leva menos de um segundo:

TRUNCATE TABLE dbo.CCI_BIGINT;

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

Tempos de execução do SQL Server: tempo de CPU = 359 ms, tempo decorrido = 364 ms.

No entanto, se eu inserir os mesmos números inteiros mod 16000, às vezes leva mais de 30 segundos:

TRUNCATE TABLE dbo.CCI_BIGINT;

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

Tempos de execução do SQL Server: tempo de CPU = 32062 ms, tempo decorrido = 32511 ms.

Este é um teste repetível que foi realizado em várias máquinas. Parece haver um padrão claro no tempo decorrido à medida que o valor do mod muda:

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

Se você deseja executar testes, sinta-se à vontade para modificar o código de teste que escrevi aqui .

Não encontrei nada interessante em sys.dm_os_wait_stats para a inserção 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 
╚════════════════════════════════════╩══════════════╝

Por que a pastilha ID % 16000demora tanto mais que a pastilha ID % 17000?

Joe Obbish
fonte

Respostas:

12

Em muitos aspectos, esse é um comportamento esperado. Qualquer conjunto de rotinas de compactação terá um desempenho abrangente, dependendo da distribuição de dados de entrada. Esperamos negociar a velocidade de carregamento de dados pelo tamanho do armazenamento e desempenho da consulta em tempo de execução.

Há um limite definido para a resposta detalhada que você obterá aqui, já que o VertiPaq é uma implementação proprietária e os detalhes são um segredo bem guardado. Mesmo assim, sabemos que o VertiPaq contém rotinas para:

  • Codificação de valor (dimensionamento e / ou conversão de valores para caber em um pequeno número de bits)
  • Codificação de dicionário (referências inteiras a valores exclusivos)
  • Codificação de duração da execução (armazenando execuções de valores repetidos como pares [valor, contagem])
  • Empacotamento de bits (armazenando o fluxo no menor número possível de bits)

Normalmente, os dados serão codificados por valor ou dicionário e, em seguida, RLE ou empacotamento de bits será aplicado (ou um híbrido de RLE e empacotamento de bits usado em diferentes subseções dos dados do segmento). O processo de decisão de quais técnicas aplicar pode envolver a geração de um histograma para ajudar a determinar como a economia máxima de bits pode ser alcançada.

Capturando o caso lento com o Windows Performance Recorder e analisando o resultado com o Windows Performance Analyzer, podemos ver que a grande maioria do tempo de execução é consumida ao observar o agrupamento de dados, criar histogramas e decidir como particioná-lo da melhor maneira. poupança:

Análise WPA

O processamento mais caro ocorre para valores que aparecem pelo menos 64 vezes no segmento. Essa é uma heurística para determinar quando o RLE puro provavelmente será benéfico. Os casos mais rápidos resultam em armazenamento impuro , por exemplo, uma representação compactada em bits, com um tamanho final maior de armazenamento. Nos casos híbridos, valores com 64 ou mais repetições são codificados em RLE e o restante é compactado em bits.

A duração mais longa ocorre quando o número máximo de valores distintos com 64 repetições aparece no maior segmento possível, ou seja, 1.048.576 linhas com 16.384 conjuntos de valores com 64 entradas cada. A inspeção do código revela um limite de tempo codificado para o processamento caro. Isso pode ser configurado em outras implementações do VertiPaq, por exemplo, SSAS, mas não no SQL Server, tanto quanto eu sei.

Algumas dicas sobre o arranjo final de armazenamento podem ser obtidas usando o comando não documentadoDBCC CSINDEX . Isso mostra as entradas do cabeçalho e da matriz do RLE, todos os indicadores nos dados do RLE e um breve resumo dos dados do pacote de bits (se houver).

Para mais informações, veja:

Paul White diz que a GoFundMonica
fonte
9

Não sei dizer exatamente por que esse comportamento está ocorrendo, mas acredito que desenvolvi um bom modelo por meio do teste de força bruta. As conclusões a seguir se aplicam apenas ao carregar dados em uma única coluna e com números inteiros muito bem distribuídos.

Primeiro, tentei variar o número de linhas inseridas no CCI usando TOP. Eu usei ID % 16000para todos os testes. Abaixo está um gráfico comparando as linhas inseridas no tamanho do segmento do grupo de linhas compactado:

gráfico da parte superior vs tamanho

Abaixo está um gráfico de linhas inseridas no tempo da CPU em ms. Observe que o eixo X tem um ponto de partida diferente:

top vs cpu

Podemos ver que o tamanho do segmento do grupo de linhas cresce a uma taxa linear e usa uma pequena quantidade de CPU até cerca de 1 milhão de linhas. Nesse ponto, o tamanho do grupo de linhas diminui drasticamente e o uso da CPU aumenta drasticamente. Parece que pagamos um preço muito alto na CPU por essa compactação.

Ao inserir menos de 1024000 linhas, acabei com um grupo de linhas aberto no CCI. No entanto, forçar a compactação usando REORGANIZEouREBUILD não afetou o tamanho. Como um aparte, achei interessante que, quando usei uma variável TOP, acabei com um grupo de linhas aberto, mas RECOMPILEcom um grupo de linhas fechado.

Em seguida, testei variando o valor do módulo, mantendo o número de linhas iguais. Aqui está uma amostra dos dados ao inserir 102400 linhas:

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

Até um valor mod de 1600, o tamanho do segmento do grupo de linhas aumenta linearmente em 80 bytes para cada 10 valores exclusivos adicionais. É uma coincidência interessante que BIGINTtradicionalmente ocupe 8 bytes e o tamanho do segmento aumente em 8 bytes para cada valor exclusivo adicional. Após um valor mod de 1600, o tamanho do segmento aumenta rapidamente até estabilizar.

Também é útil examinar os dados ao deixar o valor do módulo igual e alterar o número de linhas inseridas:

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

Parece que quando o número inserido de linhas <~ 64 *, o número de valores exclusivos, vemos uma compactação relativamente baixa (2 bytes por linha para mod <= 65000) e baixo uso linear da CPU. Quando o número de linhas inserido> ~ 64 *, o número de valores exclusivos, vemos uma compressão muito melhor e um uso ainda mais linear da CPU. Há uma transição entre os dois estados que não é fácil para mim modelar, mas pode ser vista no gráfico. Não parece ser verdade que vemos o uso máximo da CPU ao inserir exatamente 64 linhas para cada valor exclusivo. Em vez disso, podemos inserir apenas um máximo de 1048576 linhas em um grupo de linhas e vemos um uso e compactação de CPU muito mais altos quando houver mais de 64 linhas por valor exclusivo.

Abaixo está um gráfico de contorno de como o tempo da CPU muda conforme o número de linhas inseridas e o número de linhas exclusivas. Podemos ver os padrões descritos acima:

CPU de contorno

Abaixo está um gráfico de contorno do espaço usado pelo segmento. Após um certo ponto, começamos a ver uma compressão muito melhor, como descrito acima:

tamanho do contorno

Parece que existem pelo menos dois algoritmos de compressão diferentes em funcionamento aqui. Dado o exposto, faz sentido vermos o uso máximo da CPU ao inserir 1048576 linhas. Também faz sentido que vejamos o maior uso de CPU nesse ponto ao inserir cerca de 16000 linhas. 1048576/64 = 16384.

Enviei todos os meus dados brutos aqui , caso alguém queira analisá-los.

Vale mencionar o que acontece com os planos paralelos. Eu só observei esse comportamento com valores uniformemente distribuídos. Ao fazer uma inserção paralela, geralmente há um elemento aleatório e os threads geralmente são desequilibrados.

Coloque 2097152 linhas na tabela de preparação:

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;

Esta inserção termina em menos de um segundo e tem baixa compactação:

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

Podemos ver o efeito dos segmentos desequilibrados:

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

Existem vários truques que podemos fazer para forçar os segmentos a serem equilibrados e a ter a mesma distribuição de linhas. Aqui está um deles:

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)

A escolha de um número ímpar para o módulo é importante aqui. O SQL Server verifica a tabela temporária em série, calcula o número da linha e usa a distribuição round robin para colocar as linhas em threads paralelos. Isso significa que acabaremos com threads perfeitamente equilibrados.

saldo 1

A inserção leva cerca de 40 segundos, o que é semelhante à inserção serial. Temos grupos de linhas bem compactados:

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

Podemos obter os mesmos resultados inserindo dados da tabela de preparação original:

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

Aqui a distribuição round robin é usada para a tabela derivada, spara que uma varredura da tabela seja feita em cada encadeamento paralelo:

equilibrado 2

Em conclusão, ao inserir números inteiros distribuídos uniformemente, é possível ver uma compressão muito alta quando cada número inteiro único aparece mais de 64 vezes. Isso pode dever-se ao uso de um algoritmo de compactação diferente. Pode haver um alto custo na CPU para obter essa compactação. Pequenas alterações nos dados podem levar a diferenças drásticas no tamanho do segmento de grupo de linhas compactado. Eu suspeito que ver o pior caso (do ponto de vista da CPU) seja incomum, pelo menos para esse conjunto de dados. É ainda mais difícil ver ao fazer inserções paralelas.

Joe Obbish
fonte
8

Eu acredito que isso tem a ver com as otimizações internas da compactação para as tabelas de coluna única e com o número mágico dos 64 KB ocupados pelo dicionário.

Exemplo: se você executar com o MOD 16600 , o resultado final do tamanho do Grupo de Linhas será 1,683 MB , enquanto a execução do MOD 17000 fornecerá um Grupo de Linhas com o tamanho de 2.001 MB .

Agora, dê uma olhada nos dicionários criados (você pode usar minha biblioteca CISL para isso, precisará da função cstore_GetDictionaries ou, alternativamente, vá e consulte sys.column_store_dictionaries DMV):

(MOD 16600) 61 KB

insira a descrição da imagem aqui

(MOD 17000) 65 KB

insira a descrição da imagem aqui

Engraçado, se você adicionar outra coluna à sua tabela, e vamos chamá-la de 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);

Recarregue os dados para o 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);

Desta vez, a execução será rápida, porque o otimizador decidirá não sobrecarregar e compactar demais:

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;

Mesmo que haja uma pequena diferença entre os tamanhos do Grupo de linhas, ele será insignificante (2.000 (MOD 16600) vs 2.001 (MOD 17000))

Para esse cenário, o dicionário do MOD 16000 será maior que o do primeiro cenário com 1 coluna (0,63 vs 0,61).

Niko Neugebuer
fonte