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


14

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 :

premier plan

Sur ma machine, le code ci-dessus s'exécute en environ 6900 ms. Le 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? N'est-ce pas une bobine impatiente surtout juste une table temporaire interne de toute façon? Je crois que je cherche des 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;

Réponses:


14

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

Vous pouvez en trouver un exemple avec une instruction 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? N'est-ce pas une bobine impatiente surtout juste une table temporaire interne de toute façon?

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 un insert non ordonné ligne par ligne dans 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 bloc .

Par conséquent, on peut souvent obtenir de meilleures performances en fractionnant 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 l' optimisation de remplissage de trous pour éviter les HP explicites. Une façon d'y parvenir pour la démo est de créer un index unique (clusterisé si vous le souhaitez) sur la IDcolonne 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);

Plan MERGE

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.


5

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ésider dans le manque de prise en charge de l' DML Request Sortoption dans le plan de bobine. 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 observé en sens inverse avec quelques ruses. 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:

entrez la description de l'image ici

Plus important encore, nous avons maintenant un support pour l' DML Request Sortoption. En examinant à 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 l' explique Hugo , l'opérateur Eager Spool préserve l'ordre. Cela peut plus facilement être vu avec un TOP PERCENTplan. 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.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.