web-dev-qa-db-fra.com

Pourquoi peut-il prendre jusqu'à 30 secondes pour créer un groupe de rangs CCI simple?

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?

20
Joe Obbish

À 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:

  • Codage de la valeur (mise à l'échelle et/ou traduction des valeurs pour s'adapter à un petit nombre de bits)
  • Codage du dictionnaire (références entier à des valeurs uniques)
  • Encodage de longueur d'exécution (stockage des exécutions de valeurs répétées comme [valeur, comptage] paires)
  • Emballage bit (stocker le flux dans le moins de bits possible)

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:

WPA Analysis

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:

12
Paul White 9

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é:

graph of top vs size

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:

top vs cpu

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:

contour cpu

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:

contour size

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.

balance 1

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:

balanced 2

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.

9
Joe Obbish

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

enter image description here

(MOD 17000) 65 Ko

enter image description here

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

8
Niko Neugebuer