web-dev-qa-db-fra.com

Pourquoi une table temporaire est-elle une solution plus efficace au problème d'Halloween qu'une bobine impatiente?

Considérez la requête suivante qui insère des lignes d'une table source uniquement si elles ne sont pas déjà dans la table cible:

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
OPTION (MAXDOP 1, QUERYTRACEON 7470);

Une forme de plan possible comprend une jointure de fusion et une bobine désireuse. L'opérateur de bobine désireux est présent pour résoudre le problème d'Halloween :

first plan

Sur ma machine, le code ci-dessus s'exécute en environ 6900 ms. Un code de repro pour créer les tableaux est inclus au bas de la question. Si je ne suis pas satisfait des performances, je pourrais essayer de charger les lignes à insérer dans une table temporaire au lieu de compter sur la bobine désireuse. Voici une implémentation possible:

DROP TABLE IF EXISTS #CONSULTANT_RECOMMENDED_TEMP_TABLE;
CREATE TABLE #CONSULTANT_RECOMMENDED_TEMP_TABLE (
    ID BIGINT,
    PRIMARY KEY (ID)
);

INSERT INTO #CONSULTANT_RECOMMENDED_TEMP_TABLE WITH (TABLOCK)
SELECT maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
OPTION (MAXDOP 1, QUERYTRACEON 7470);

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT new_rows.ID
FROM #CONSULTANT_RECOMMENDED_TEMP_TABLE new_rows
OPTION (MAXDOP 1);

Le nouveau code s'exécute en environ 4400 ms. Je peux obtenir des plans réels et utiliser Actual Time Statistics ™ pour examiner où le temps est passé au niveau de l'opérateur. Notez que demander un plan réel ajoute des frais généraux importants pour ces requêtes, de sorte que les totaux ne correspondront pas aux résultats précédents.

╔═════════════╦═════════════╦══════════════╗
║  operator   ║ first query ║ second query ║
╠═════════════╬═════════════╬══════════════╣
║ big scan    ║ 1771        ║ 1744         ║
║ little scan ║ 163         ║ 166          ║
║ sort        ║ 531         ║ 530          ║
║ merge join  ║ 709         ║ 669          ║
║ spool       ║ 3202        ║ N/A          ║
║ temp insert ║ N/A         ║ 422          ║
║ temp scan   ║ N/A         ║ 187          ║
║ insert      ║ 3122        ║ 1545         ║
╚═════════════╩═════════════╩══════════════╝

Le plan de requête avec la bobine désireuse semble passer beaucoup plus de temps sur les opérateurs d'insertion et de bobine que le plan qui utilise la table temporaire.

Pourquoi le plan avec la table temporaire est-il plus efficace? De toute façon, une bobine impatiente n'est-elle pas juste une table temporaire interne? Je crois que je suis à la recherche de réponses qui se concentrent sur les internes. Je peux voir comment les piles d'appels sont différentes mais je ne peux pas comprendre la situation dans son ensemble.

Je suis sur SQL Server 2017 CU 11 au cas où quelqu'un voudrait le savoir. Voici le code pour remplir les tables utilisées dans les requêtes ci-dessus:

DROP TABLE IF EXISTS dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR;

CREATE TABLE dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR (
ID BIGINT NOT NULL,
PRIMARY KEY (ID)
);

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT TOP (20000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
OPTION (MAXDOP 1);


DROP TABLE IF EXISTS dbo.A_HEAP_OF_MOSTLY_NEW_ROWS;

CREATE TABLE dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (
ID BIGINT NOT NULL
);

INSERT INTO dbo.A_HEAP_OF_MOSTLY_NEW_ROWS WITH (TABLOCK)
SELECT TOP (1900000) 19999999 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
14
Joe Obbish

C'est ce que j'appelle la protection manuelle d'Halloween .

Vous pouvez trouver un exemple de son utilisation avec une déclaration de mise à jour dans mon article Optimizing Update Queries . Il faut être un peu prudent pour conserver la même sémantique, par exemple en verrouillant la table cible contre toutes les modifications simultanées pendant l'exécution des requêtes distinctes, si cela est pertinent dans votre scénario.

Pourquoi le plan avec la table temporaire est-il plus efficace? De toute façon, une bobine impatiente n'est-elle pas juste une table temporaire interne?

Une bobine a certaines des caractéristiques d'une table temporaire, mais les deux ne sont pas des équivalents exacts. En particulier, une bobine est essentiellement une ligne par ligne insert non ordonné à une structure b-tree . Il bénéficie des optimisations de verrouillage et de journalisation, mais ne prend pas en charge les optimisations de chargement en masse .

Par conséquent, on peut souvent obtenir de meilleures performances en divisant la requête de manière naturelle: chargement en masse des nouvelles lignes dans une table ou une variable temporaire, puis exécution d'une insertion optimisée (sans protection Halloween explicite) à partir de l'objet temporaire.

Cette séparation vous offre également une liberté supplémentaire pour régler séparément les parties de lecture et d'écriture de la déclaration d'origine.

En remarque, il est intéressant de réfléchir à la façon dont le problème d'Halloween pourrait être résolu en utilisant des versions en ligne. Peut-être qu'une future version de SQL Server fournira cette fonctionnalité dans des circonstances appropriées.


Comme Michael Kutz y a fait allusion dans un commentaire, vous pouvez également explorer la possibilité d'exploiter le optimisation du remplissage des trous pour éviter les HP explicites. Une façon d'y parvenir pour la démo est de créer un index unique (en cluster si vous le souhaitez) sur la colonne ID de A_HEAP_OF_MOSTLY_NEW_ROWS.

CREATE UNIQUE INDEX i ON dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (ID);

Avec cette garantie en place, l'optimiseur peut utiliser le remplissage de trous et le partage de jeux de lignes:

MERGE dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (SERIALIZABLE) AS HICETY
USING dbo.A_HEAP_OF_MOSTLY_NEW_ROWS AS AHOMNR
    ON AHOMNR.ID = HICETY.ID
WHEN NOT MATCHED BY TARGET
THEN INSERT (ID) VALUES (AHOMNR.ID);

MERGE plan

Bien qu'intéressant, vous pourrez toujours obtenir de meilleures performances dans de nombreux cas en utilisant une protection manuelle d'Halloween soigneusement mise en œuvre.

14
Paul White 9

Pour développer un peu la réponse de Paul, une partie de la différence de temps écoulé entre la bobine et les approches de table temporaire semble résulter du manque de support pour le DML Request Sort option dans le plan de spool. Avec l'indicateur de trace non documenté 8795, le temps écoulé pour l'approche de la table temporaire passe de 4400 ms à 5600 ms.

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT new_rows.ID
FROM #CONSULTANT_RECOMMENDED_TEMP_TABLE new_rows
OPTION (MAXDOP 1, QUERYTRACEON 8795);

Notez que ce n'est pas exactement équivalent à l'insertion effectuée par le plan de spool. Cette requête écrit beaucoup plus de données dans le journal des transactions.

Le même effet peut être vu en sens inverse avec une supercherie. Il est possible d'encourager SQL Server à utiliser un tri au lieu d'une bobine pour la protection Halloween. Une mise en œuvre:

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT TOP (987654321) 
maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
ORDER BY maybe_new_rows.ID, maybe_new_rows.ID + 1
OPTION (MAXDOP 1, QUERYTRACEON 7470, MERGE JOIN);

Le plan a maintenant un opérateur TOP N Sort à la place de la bobine. Le tri est un opérateur de blocage donc le spool n'est plus nécessaire:

enter image description here

Plus important encore, nous avons maintenant un support pour le DML Request Sort option. En regardant à nouveau les statistiques de temps réel, l'opérateur d'insertion ne prend plus que 1623 ms. Le plan entier prend environ 5400 ms pour s'exécuter sans demander de plan réel.

Comme Hugo explique , l'opérateur Eager Spool conserve l'ordre. Cela peut être vu plus facilement avec un TOP PERCENT plan. Il est regrettable que la requête d'origine avec le spouleur ne puisse pas mieux tirer parti de la nature triée des données dans le spouleur.

5
Joe Obbish