Je travaillais sur une démo impliquant des CCI lorsque j'ai remarqué que certains de mes inserts prenaient plus de temps que prévu. Définitions de table à reproduire:
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);
Pour les tests, j'insère toutes les lignes de 1048576 de la table d'étatage. Cela suffit pour remplir exactement un groupe de rangée comprimé tant qu'il ne sera pas coupé pour une raison quelconque.
Si j'insère tous les entiers mod 17000, il faut moins d'une seconde:
TRUNCATE TABLE dbo.CCI_BIGINT;
INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 17000
FROM dbo.STG_1048576
OPTION (MAXDOP 1);
Temps d'exécution SQL Server: Time CPU = 359 ms, temps écoulé = 364 ms.
Cependant, si j'insère les mêmes entiers Mod 16000, cela prend parfois plus de 30 secondes:
TRUNCATE TABLE dbo.CCI_BIGINT;
INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16000
FROM dbo.STG_1048576
OPTION (MAXDOP 1);
SQL Server Execution Times: Time CPU = 32062 ms, temps écoulé = 32511 ms.
Ceci est un test reproductible qui a été effectué sur plusieurs machines. Il semble y avoir un modèle clair dans le temps écoulé, car la valeur modale change:
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
Si vous souhaitez exécuter des tests vous-même, n'hésitez pas à modifier le code de test que j'ai écrit ici .
Je ne pouvais rien trouver intéressant dans sys.dm_os_wait_stats pour l'insertion 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 ║
╚════════════════════════════════════╩══════════════╝
Pourquoi l'insert pour ID % 16000
Prenez tellement plus longtemps que l'insert pour ID % 17000
?
À bien des égards, cela est attendu Comportement. Tout ensemble de routines de compression aura une performance largement élevée en fonction de la distribution des données d'entrée. Nous prévoyons de négocier la vitesse de chargement des données pour la taille de la taille et la performance d'interrogation d'exécution.
Il y a une limite définitive à la façon dont vous allez avoir une réponse détaillée, car VertiPAQ est une mise en œuvre exclusive et les détails sont un secret de près. Malgré tout, nous savons que VertiPaq contient des routines pour:
En règle générale, les données seront codées de la valeur ou du dictionnaire, puis de rle ou d'emballage bit seront appliqués (ou un hybride de rle et d'emballage bit utilisé sur différentes sous-sections des données de segment). Le processus de décider quelles techniques à appliquer peut impliquer de générer un histogramme pour déterminer la manière dont les économies de bits maximales peuvent être obtenues.
Capturer le boîtier lent avec l'enregistreur de performance Windows et analyser le résultat avec l'analyseur de performance Windows, nous pouvons constater que la grande majorité du temps d'exécution est consommée en regardant le regroupement des données, de construire des histogrammes et de décider de la manière de la classer au mieux des économies:
Le traitement le plus coûteux survient pour les valeurs qui apparaissent au moins 64 fois dans le segment. C'est une heuristique de déterminer quand Pure RLE est susceptible d'être bénéfique. Les cas plus rapides résultent impur Stockage E.G. une représentation à emballement avec une taille de stockage finale plus grande. Dans les cas hybrides, les valeurs avec 64 répétitions ou plus sont codées rle et le reste est emballé sur des bits.
La durée la plus longue survient lorsque le nombre maximal de valeurs distinctes avec 64 répétitions apparaît dans le plus grand segment possible I.E. 1 048 576 lignes de 16 384 ensembles de valeurs avec 64 entrées chacune. L'inspection du code révèle une limite de temps codée dur pour le traitement coûteux. Ceci peut être configuré dans d'autres implémentations VERTIPAQ E.G. SSAS, mais pas dans SQL Server autant que je puisse dire.
Certaines informations sur l'arrangement final de stockage peuvent être acquises à l'aide de non documenté DBCC CSINDEX
commande . Cela indique les entrées RLE en-tête et les entrées de réseau, tous les signets dans les données RLE et un bref résumé des données de bit-pack (le cas échéant).
Pour plus d'informations, voir:
Je ne peux pas dire exactement pourquoi ce comportement se produit, mais je crois que j'ai développé un bon modèle de comportement via des tests de force brutale. Les conclusions suivantes s'appliquent uniquement lors du chargement des 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 la CCI en utilisant TOP
. J'ai utilisé ID % 16000
Pour tous tes tests. Vous trouverez ci-dessous un graphique comparant des lignes insérées sur la taille du segment de groupe de rangée comprimé:
Vous trouverez ci-dessous un graphique des lignes insérées au temps de la CPU dans la SP. Notez que l'axe X a un point de départ différent:
Nous pouvons voir que la taille du segment de groupe de rangée se développe à une fréquence linéaire et utilise une petite quantité de CPU jusqu'à environ 1 m de lignes. À ce stade, la taille du groupe de rangée diminue considérablement et l'utilisation de la CPU augmente considérablement. Il semblerait que nous payions un prix important dans la CPU pour cette compression.
Lorsque vous insérez moins de 1024000 lignes, je suis retrouvé avec un groupe de rangée ouvert dans la CCI. Cependant, forcer la compression en utilisant REORGANIZE
ou REBUILD
_ n'a pas eu d'effet sur la taille. En dehors, j'ai trouvé intéressant que lorsque j'ai utilisé une variable pour TOP
je me suis retrouvé avec un groupe de rangée ouvert, mais avec RECOMPILE
j'ai fini par un groupe de rangée fermé.
Ensuite, j'ai testé en faisant varier la valeur du module tout en conservant le nombre de lignes de la même manière. Voici un échantillon des données lors de l'insertion de 102400 rangées:
╔═══════════╦═════════╦═══════════════╦═════════════╗
║ 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 MOD de 1600, la taille du segment de groupe de rangées augmente de manière linéaire de 80 octets pour chaque 10 valeurs uniques supplémentaires. C'est une coïncidence intéressante qu'un BIGINT
prend traditionnellement 8 octets et la taille du segment augmente de 8 octets pour chaque valeur unique supplémentaire. Passé une valeur MOD de 1600 La taille du segment augmente rapidement jusqu'à sa stabilisation.
Il est également utile de regarder les données lors de la sortie de la valeur du module et de modifier 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 ║
╚═══════════╩═════════╩═══════════════╩═════════════╝
On dirait que le nombre inséré de lignes <~ 64 * Le nombre de valeurs uniques que nous voyons une compression relativement médiocre (2 octets par ligne pour MOD <= 65000) et une utilisation de la CPU linéaire faible et linéaire. Lorsque le nombre inséré de lignes> ~ 64 * Le nombre de valeurs uniques Nous constatons une meilleure meilleure compression et une consommation de processeur plus élevée, toujours linéaire. Il y a une transition entre les deux États qui ne m'est pas facile de modeler, mais on peut le voir dans le graphique. Il ne semble pas être vrai que nous voyons l'utilisation maximale de la CPU lors de l'insertion exactement 64 lignes pour chaque valeur unique. Au lieu de cela, nous ne pouvons que insérer un maximum de 1 48576 lignes dans un groupe de rangée et nous voyons une utilisation et une compression beaucoup plus élevées de la CPU une fois qu'il y a plus de 64 rangées par valeur unique.
Vous trouverez ci-dessous un tracé de contour de la manière dont le temps de CPU change comme le nombre de lignes insérées et le nombre de lignes uniques change. Nous pouvons voir les modèles décrits ci-dessus:
Vous trouverez ci-dessous un tracé de contour utilisé par le segment. Après un certain point, nous commençons à voir une plus grande meilleure compression, comme décrit ci-dessus:
Il semble qu'il y ait au moins deux algorithmes de compression différents au travail ici. Compte tenu de ce qui précède, il est logique que nous verrions l'utilisation maximale de la CPU lors de l'insertion de 1048576 rangées. Il est également logique que nous voyions la plus grande utilisation de la CPU à ce point lors de l'insertion d'environ 16 000 rangées. 1048576/64 = 16384.
J'ai téléchargé toutes mes données brutes ici au cas où quelqu'un veut l'analyser.
Il convient de mentionner ce qui se passe avec des plans parallèles. Je n'ai observé que ce comportement avec des valeurs uniformément distribuées. Lorsque vous effectuez un insert parallèle, il y a souvent un élément de caractère aléatoire et de threads est généralement déséquilibré.
Mettez 2097152 rangées dans la table d'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 y a divers astuces que nous pouvons faire pour forcer les threads à équilibrer 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)
Choisir un nombre impair pour le module est important ici. SQL Server analyse la table de stockage en série, calcule le numéro de ligne, puis utilise la distribution ronde Robin pour placer les lignes sur des filets parallèles. Cela signifie que nous allons nous retrouver avec des fils parfaitement équilibrés.
L'insert prend environ 40 secondes qui est similaire à l'insert série. Nous obtenons des groupes de rangs bien comprimé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 d'intermédiaire 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 rond Robin Distribution est utilisé pour la table dérivée s
_ Donc, une analyse de la table est effectuée sur chaque fil parallèle:
En conclusion, lors de l'insertion d'entiers répartis uniformément distribués, 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é dans la CPU pour atteindre cette compression. De petits changements dans les données peuvent entraîner des différences dramatiques de la taille du segment de groupe de rangées comprimé. Je soupçonne que voir le pire des cas (d'une perspective de la CPU) sera rare dans la nature, du moins pour ce jeu de données. Il est encore plus difficile de voir quand faire des inserts parallèles.
Je crois que cela concerne les optimisations internes de la compression pour les tables de colonne unique et le nombre magique des 64 Ko occupés par le dictionnaire.
Exemple: si vous exécutez avec MOD 16600 , le résultat final de la taille du groupe de lignes sera 1,683 mb , en cours d'exécution MOD 17000 vous donnera un groupe de lignes avec la taille de 2.001 MB .
Maintenant, jetez un coup d'œil aux dictionnaires créés (vous pouvez utiliser My CISL Bibliothèque pour cela, vous aurez besoin de la fonction CSTore_getDictionnaires, ou à la varitation des sys.column_store_dictions DMV):
(MOD 16600) 61 KB
(MOD 17000) 65 Ko
Thing drôle, si vous allez ajouter une autre colonne à votre table et appelons-le 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);
Recharger les données du 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);
Cette fois, l'exécution sera rapide, car l'optimisateur décidera de ne pas surcharger et de le comprimer trop loin:
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;
Même s'il y aura une petite différence entre la taille des groupes de lignes, il sera négligeable (2.000 (MOD 16600) VS 2.001 (MOD 17000))
Pour ce scénario, le dictionnaire du MOD 16000 sera plus grand que pour le premier scénario avec 1 colonne (0,63 VS 0,61).