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 % 16000
para todos os testes. Abaixo está um gráfico comparando as linhas inseridas no tamanho do segmento do grupo de linhas compactado:
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:
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 REORGANIZE
ouREBUILD
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 RECOMPILE
com 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 BIGINT
tradicionalmente 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:
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:
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.
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, s
para que uma varredura da tabela seja feita em cada encadeamento paralelo:
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.