J'ai une table avec quelques dizaines de lignes. La configuration simplifiée suit
CREATE TABLE #data ([Id] int, [Status] int);
INSERT INTO #data
VALUES (100, 1), (101, 2), (102, 3), (103, 2);
Et j'ai une requête qui joint cette table à un ensemble de lignes construites par valeur de table (faites de variables et de constantes), comme
DECLARE @id1 int = 101, @id2 int = 105;
SELECT
COALESCE(p.[Code], 'X') AS [Code],
COALESCE(d.[Status], 0) AS [Status]
FROM (VALUES
(@id1, 'A'),
(@id2, 'B')
) p([Id], [Code])
FULL JOIN #data d ON d.[Id] = p.[Id];
Le plan d'exécution de la requête montre que la décision de l'optimiseur est d'utiliser FULL LOOP JOIN
stratégie, qui semble appropriée, car les deux entrées ont très peu de lignes. Une chose que j'ai remarquée (et je ne suis pas d'accord), cependant, c'est que les lignes TVC sont mises en file d'attente (voir la zone du plan d'exécution dans l'encadré rouge).
Pourquoi l'optimiseur introduit le spool ici, quelle est la raison de le faire? Il n'y a rien de complexe au-delà de la bobine. On dirait que ce n'est pas nécessaire. Comment s'en débarrasser dans ce cas, quelles sont les voies possibles?
Le plan ci-dessus a été obtenu le
Microsoft SQL Server 2014 (SP2-CU11) (KB4077063) - 12.0.5579.0 (X64)
Pourquoi l'optimiseur introduit le spool ici, quelle est la raison de le faire? Il n'y a rien de complexe au-delà de la bobine.
La chose au-delà du spool n'est pas une simple référence de table, qui pourrait simplement être dupliquée lorsque la jointure gauche/anti semi-jointure alternative est générée.
Il peut ressembler un peu à une table (Constant Scan) mais pour l'optimiseur * c'est un UNION ALL
des lignes séparées de la clause VALUES
.
La complexité supplémentaire est suffisante pour que l'optimiseur choisisse de spouler et de relire les lignes source, et de ne pas remplacer le spouleur par un simple "get table" plus tard. Par exemple, la transformation initiale de la jointure complète ressemble à ceci:
Remarquez les bobines supplémentaires introduites par la transformation générale. Les spools au-dessus d'une simple table get sont nettoyés plus tard par la règle SpoolGetToGet
.
Si l'optimiseur avait une règle SpoolConstGetToConstGet
correspondante, cela pourrait fonctionner comme vous le souhaitez, en principe.
Comment s'en débarrasser dans ce cas, quelles sont les voies possibles?
Utilisez une vraie table (temporaire ou variable), ou écrivez manuellement la transformation à partir de la jointure complète, par exemple:
WITH
p([Id], [Code]) AS
(
SELECT @id1, 'A'
UNION ALL
SELECT @id2, 'B'
),
FullJoin AS
(
SELECT
p.Code,
d.[Status]
FROM p
LEFT JOIN #data d
ON d.[Id] = p.[Id]
UNION ALL
SELECT
NULL,
D.[Status]
FROM #data AS D
WHERE NOT EXISTS
(
SELECT *
FROM p
WHERE p.Id = D.Id
)
)
SELECT
COALESCE(FullJoin.Code, 'X') AS Code,
COALESCE(FullJoin.Status, 0) AS [Status]
FROM FullJoin;
Planifier une réécriture manuelle:
Cela a un coût estimé à 0,0067201 unités, contre 0,0203412 unités pour l'original.
* Il peut être observé comme un LogOp_UnionAll
dans l'arborescence convertie (TF 8605). Dans l'arborescence d'entrée (TF 8606), il s'agit d'un LogOp_ConstTableGet
. L'arbre converti montre l'arbre des éléments d'expression de l'optimiseur après l'analyse, la normalisation, l'algèbre, la liaison et certains autres travaux préparatoires. L'arbre d'entrée affiche les éléments après la conversion en forme normale de négation (conversion NNF), l'effondrement des constantes d'exécution et quelques autres bits et bobs. La conversion NNF inclut la logique pour réduire les unions logiques et les récupérations de table communes, entre autres.
Le spouleur de table crée simplement une table à partir des deux ensembles de tuples présents dans la clause VALUES
.
Vous pouvez éliminer le spool en insérant d'abord ces valeurs dans une table temporaire, comme ceci:
DROP TABLE IF EXISTS #data;
CREATE TABLE #data ([Id] int, [Status] int);
INSERT INTO #data
VALUES (100, 1), (101, 2), (102, 3), (103, 2);
DROP TABLE IF EXISTS #p;
CREATE TABLE #p
(
Id int NOT NULL
, Code char(1) NOT NULL
);
DECLARE @id1 int = 101, @id2 int = 105;
INSERT INTO #p (Id, Code)
VALUES
(@id1, 'A'),
(@id2, 'B');
SELECT
COALESCE(p.[Code], 'X') AS [Code],
COALESCE(d.[Status], 0) AS [Status]
FROM #p p
FULL JOIN #data d ON d.[Id] = p.[Id];
En regardant le plan d'exécution de votre requête, nous voyons que la liste de sortie contient deux colonnes qui utilisent le préfixe Union
; c'est un indice que la bobine crée une table à partir d'une source syndiquée:
Le FULL OUTER JOIN
nécessite que SQL Server accède aux valeurs dans p
deux fois, une fois pour chaque "côté" de la jointure. La création d'un spool permet à la jointure de boucles internes résultante d'accéder aux données spoule.
Fait intéressant, si vous remplacez le FULL OUTER JOIN
avec un LEFT JOIN
et un RIGHT JOIN
et UNION
les résultats ensemble, SQL Server n'utilise pas de spoule.
SELECT
COALESCE(p.[Code], 'X') AS [Code],
COALESCE(d.[Status], 0) AS [Status]
FROM (VALUES
(101, 'A'),
(105, 'B')
) p([Id], [Code])
LEFT JOIN #data d ON d.[Id] = p.[Id]
UNION
SELECT
COALESCE(p.[Code], 'X') AS [Code],
COALESCE(d.[Status], 0) AS [Status]
FROM (VALUES
(101, 'A'),
(105, 'B')
) p([Id], [Code])
RIGHT JOIN #data d ON d.[Id] = p.[Id];
Remarque, je ne suggère pas d'utiliser la requête UNION
ci-dessus; pour de plus grands ensembles d'entrée, il peut ne pas être plus efficace que le simple FULL OUTER JOIN
tu as déjà.