Je ne peux pas dire exactement pourquoi ce problème se produit, mais je pense avoir développé un bon modèle de comportement via des tests de force brute. Les conclusions suivantes s'appliquent uniquement lors du chargement de données dans une seule colonne et avec des entiers très bien distribués.
J'ai d'abord essayé de faire varier le nombre de lignes insérées dans le CCI à l'aide de TOP
. J'ai utilisé ID % 16000
pour tous les tests. Voici un graphique comparant les lignes insérées à la taille du segment de groupe de lignes compressé:
Voici un graphique des lignes insérées dans le temps CPU en ms. Notez que l'axe X a un point de départ différent:
Nous pouvons voir que la taille du segment de groupe de lignes augmente à un rythme linéaire et utilise une petite quantité de CPU jusqu'à environ 1 M de lignes. À ce stade, la taille du groupe de lignes diminue considérablement et l'utilisation du processeur augmente considérablement. Il semblerait que nous payons un prix élevé en CPU pour cette compression.
Lors de l'insertion de moins de 1024000 lignes, je me suis retrouvé avec un groupe de lignes ouvert dans le CCI. Cependant, forcer la compression en utilisant REORGANIZE
ou REBUILD
n'a pas eu d'effet sur la taille. En passant, j'ai trouvé intéressant que lorsque j'utilisais une variable pour TOP
je me suis retrouvé avec un groupe de lignes ouvert mais avec RECOMPILE
je me suis retrouvé avec un groupe de lignes fermé.
Ensuite, j'ai testé en faisant varier la valeur du module tout en gardant le même nombre de lignes. Voici un exemple des données lors de l'insertion de 102400 lignes:
╔═══════════╦═════════╦═══════════════╦═════════════╗
║ 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 ║
╚═══════════╩═════════╩═══════════════╩═════════════╝
Jusqu'à une valeur de mod de 1600, la taille du segment de groupe de lignes augmente linéairement de 80 octets pour 10 valeurs uniques supplémentaires. C'est une coïncidence intéressante qu'un a BIGINT
traditionnellement occupe 8 octets et la taille du segment augmente de 8 octets pour chaque valeur unique supplémentaire. Au-delà d'une valeur de mod de 1600, la taille du segment augmente rapidement jusqu'à ce qu'elle se stabilise.
Il est également utile de regarder les données lorsque vous laissez la même valeur de module et changez le nombre de lignes insérées:
╔═══════════╦═════════╦═══════════════╦═════════════╗
║ 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 ║
╚═══════════╩═════════╩═══════════════╩═════════════╝
Il semble que lorsque le nombre de lignes insérées <~ 64 * le nombre de valeurs uniques, nous voyons une compression relativement médiocre (2 octets par ligne pour le mod <= 65000) et une faible utilisation du processeur linéaire. Lorsque le nombre de lignes insérées> ~ 64 * le nombre de valeurs uniques, nous constatons une compression bien meilleure et une utilisation du processeur encore plus linéaire. Il y a une transition entre les deux états qui n'est pas facile pour moi de modéliser mais cela peut être vu dans le graphique. Il ne semble pas vrai que nous voyons l'utilisation maximale du processeur lorsque nous insérons exactement 64 lignes pour chaque valeur unique. Au contraire, nous ne pouvons insérer qu'un maximum de 1048576 lignes dans un groupe de lignes et nous constatons une utilisation et une compression du processeur beaucoup plus élevées une fois qu'il y a plus de 64 lignes par valeur unique.
Vous trouverez ci-dessous un tracé de contour de la façon dont le temps processeur change en fonction du nombre de lignes insérées et du nombre de lignes uniques. Nous pouvons voir les modèles décrits ci-dessus:
Vous trouverez ci-dessous un tracé de contour de l'espace utilisé par le segment. Après un certain point, nous commençons à voir une compression bien meilleure, comme décrit ci-dessus:
Il semble qu'il y ait au moins deux algorithmes de compression différents à l'œuvre ici. Compte tenu de ce qui précède, il est logique que nous voyions l'utilisation maximale du processeur lors de l'insertion de 1048576 lignes. Il est également logique que nous constations la plus grande utilisation du processeur à ce stade lors de l'insertion d'environ 16 000 lignes. 1048576/64 = 16384.
J'ai téléchargé toutes mes données brutes ici au cas où quelqu'un voudrait les analyser.
Il convient de mentionner ce qui se passe avec les plans parallèles. Je n'ai observé ce comportement qu'avec des valeurs uniformément réparties. Lors d'une insertion parallèle, il y a souvent un élément aléatoire et les threads sont généralement déséquilibrés.
Mettez 2097152 lignes dans la table intermédiaire:
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;
Cet insert se termine en moins d'une seconde et a une mauvaise compression:
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);
Nous pouvons voir l'effet des threads déséquilibrés:
╔════════════╦════════════╦══════════════╦═══════════════╗
║ state_desc ║ total_rows ║ deleted_rows ║ size_in_bytes ║
╠════════════╬════════════╬══════════════╬═══════════════╣
║ OPEN ║ 13540 ║ 0 ║ 311296 ║
║ COMPRESSED ║ 1048576 ║ 0 ║ 2095872 ║
║ COMPRESSED ║ 1035036 ║ 0 ║ 2070784 ║
╚════════════╩════════════╩══════════════╩═══════════════╝
Il existe différentes astuces que nous pouvons faire pour forcer les threads à être équilibrés et à avoir la même distribution de lignes. Voici l'un d'entre eux:
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)
Le choix d'un nombre impair pour le module est important ici. SQL Server analyse la table de transfert en série, calcule le numéro de ligne, puis utilise la distribution à tour de rôle pour placer les lignes sur des threads parallèles. Cela signifie que nous nous retrouverons avec des threads parfaitement équilibrés.
L'insert prend environ 40 secondes, ce qui est similaire à l'insert série. Nous obtenons des groupes de lignes bien compressés:
╔════════════╦════════════╦══════════════╦═══════════════╗
║ state_desc ║ total_rows ║ deleted_rows ║ size_in_bytes ║
╠════════════╬════════════╬══════════════╬═══════════════╣
║ COMPRESSED ║ 1048576 ║ 0 ║ 128568 ║
║ COMPRESSED ║ 1048576 ║ 0 ║ 128568 ║
╚════════════╩════════════╩══════════════╩═══════════════╝
Nous pouvons obtenir les mêmes résultats en insérant des données de la table de transfert d'origine:
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);
Ici, la distribution à tour de rôle est utilisée pour la table dérivée, de s
sorte qu'une analyse de la table est effectuée sur chaque thread parallèle:
En conclusion, lors de l'insertion d'entiers uniformément répartis, vous pouvez voir une compression très élevée lorsque chaque entier unique apparaît plus de 64 fois. Cela peut être dû à un algorithme de compression différent utilisé. Il peut y avoir un coût élevé en CPU pour réaliser cette compression. De petits changements dans les données peuvent entraîner des différences dramatiques dans la taille du segment de groupe de lignes compressé. Je soupçonne que voir le pire des cas (du point de vue du processeur) sera rare dans la nature, au moins pour cet ensemble de données. C'est encore plus difficile à voir lors des insertions parallèles.