Comment suggérer une jointure plusieurs-à-plusieurs dans SQL Server?


9

J'ai 3 "grandes" tables qui se rejoignent sur une paire de colonnes (les deux ints).

  • Le tableau 1 compte environ 200 millions de lignes
  • Le tableau 2 compte environ 1,5 million de lignes
  • Table3 a environ 6 millions de lignes

Chaque table possède un index clusterisé sur Key1, Key2puis une autre colonne. Key1a une faible cardinalité et est très asymétrique. Il est toujours référencé dans la WHEREclause. Key2n'est jamais mentionné dans la WHEREclause. Chaque jointure est plusieurs-à-plusieurs.

Le problème est lié à l'estimation de la cardinalité. L'estimation de sortie de chaque jointure devient plus petite au lieu de plus grande . Il en résulte des estimations finales de centaines faibles lorsque le résultat réel est bien en millions.

Existe-t-il un moyen pour moi d'indiquer au CE de faire de meilleures estimations?

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;

Solutions que j'ai essayées:

  • Création de statistiques multi-colonnes sur Key1,Key2
  • Créer des tonnes de statistiques filtrées sur Key1(Cela aide beaucoup, mais je me retrouve avec des milliers de statistiques créées par les utilisateurs dans la base de données.)

Plan d'exécution masqué (désolé pour le mauvais masquage)

Dans le cas que je regarde, le résultat a 9 millions de lignes. Le nouveau CE estime 180 lignes; l'héritage CE estime 6100 lignes.

Voici un exemple reproductible:

DROP TABLE IF EXISTS #Table1, #Table2, #Table3;
CREATE TABLE #Table1 (Key1 INT NOT NULL, Key2 INT NOT NULL, T1Key3 INT NOT NULL, CONSTRAINT pk_t1 PRIMARY KEY CLUSTERED (Key1, Key2, T1Key3));
CREATE TABLE #Table2 (Key1 INT NOT NULL, Key2 INT NOT NULL, T2Key3 INT NOT NULL, CONSTRAINT pk_t2 PRIMARY KEY CLUSTERED (Key1, Key2, T2Key3));
CREATE TABLE #Table3 (Key1 INT NOT NULL, Key2 INT NOT NULL, T3Key3 INT NOT NULL, CONSTRAINT pk_t3 PRIMARY KEY CLUSTERED (Key1, Key2, T3Key3));

-- Table1 
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2),
     DataSize (Key1, NumberOfRows)
     AS (SELECT 1, 2000 UNION
         SELECT 2, 10000 UNION
         SELECT 3, 25000 UNION
         SELECT 4, 50000 UNION
         SELECT 5, 200000)
INSERT INTO #Table1
SELECT Key1
     , Key2 = ROW_NUMBER() OVER (PARTITION BY Key1, T1Key3 ORDER BY Number)
     , T1Key3
FROM DataSize
     CROSS APPLY (SELECT TOP(NumberOfRows) 
                         Number
                       , T1Key3 = Number%(Key1*Key1) + 1 
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smaller number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table2
SELECT DISTINCT 
       Key1
     , Key2
     , T2Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1*10) 
                         T2Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smallest number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table3
SELECT DISTINCT 
       Key1
     , Key2
     , T3Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1) 
                         T3Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;


DROP TABLE IF EXISTS #a;
SELECT col = 1 
INTO #a
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
WHERE t1.Key1 = 1;

DROP TABLE IF EXISTS #b;
SELECT col = 1 
INTO #b
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN #Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;

Réponses:


5

Pour être clair, l'optimiseur sait déjà qu'il s'agit d'une jointure plusieurs-à-plusieurs. Si vous forcez la fusion des jointures et regardez un plan estimé, vous pouvez voir une propriété pour l'opérateur de jointure qui vous indique si la jointure peut être plusieurs-à-plusieurs. Le problème que vous devez résoudre ici est d'augmenter les estimations de cardinalité, probablement pour obtenir un plan de requête plus efficace pour la partie de la requête que vous avez omise.

La première chose que j'essaierais est de mettre les résultats de la jointure depuis Object3et Object5dans une table temporaire. Pour le plan que vous avez publié, il ne s'agit que d'une seule colonne sur 51393 lignes, il ne devrait donc pas prendre de place dans tempdb. Vous pouvez rassembler des statistiques complètes sur la table temporaire et cela seul pourrait suffire pour obtenir une estimation de cardinalité finale suffisamment précise. La collecte de statistiques complètes Object1peut également être utile. Les estimations de cardinalité s'aggravent souvent lorsque vous parcourez un plan de droite à gauche.

Si cela ne fonctionne pas, vous pouvez essayer l' ENABLE_QUERY_OPTIMIZER_HOTFIXESindicateur de requête si vous ne l'avez pas déjà activé au niveau de la base de données ou du serveur. Microsoft verrouille les correctifs de performances affectant le plan pour SQL Server 2016 derrière ce paramètre. Certains d'entre eux concernent des estimations de cardinalité, alors vous aurez peut-être de la chance et l'un des correctifs vous aidera dans votre requête. Vous pouvez également essayer d'utiliser l'estimateur de cardinalité hérité avec un FORCE_LEGACY_CARDINALITY_ESTIMATIONindice de requête. Certains ensembles de données peuvent obtenir de meilleures estimations avec l'ancien CE.

En dernier recours, vous pouvez augmenter manuellement l'estimation de la cardinalité par le facteur que vous souhaitez en utilisant la MANY()fonction d' Adam Machanic . J'en parle dans une autre réponse mais il semble que le lien soit mort. Si vous êtes intéressé, je peux essayer de trouver quelque chose.


La make_parallelfonction d'Adam est utilisée pour aider à atténuer le problème. Je vais voir many. On dirait un pansement assez grossier.
Steven Hibble

2

Les statistiques SQL Server contiennent uniquement un histogramme pour la première colonne de l'objet de statistiques. Par conséquent, vous pouvez créer des statistiques filtrées qui fournissent un histogramme de valeurs pour Key2, mais uniquement parmi les lignes avec Key1 = 1. La création de ces statistiques filtrées sur chaque table corrige les estimations et conduit au comportement que vous attendez de la requête de test: chaque nouvelle jointure n'a pas d'impact sur l'estimation de cardinalité finale (confirmée dans SQL 2016 SP1 et SQL 2017).

-- Note: Add "WITH FULLSCAN" to each if you want a perfect 20,000 row estimate
CREATE STATISTICS st_#Table1 ON #Table1 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table2 ON #Table2 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table3 ON #Table3 (Key2) WHERE Key1 = 1

Sans ces statistiques filtrées, SQL Server adoptera une approche plus heuristique pour estimer la cardinalité de votre jointure. Le livre blanc suivant contient de bonnes descriptions de haut niveau de certaines des heuristiques utilisées par SQL Server: Optimisation de vos plans de requête avec l'estimateur de cardinalité SQL Server 2014 .

Par exemple, l'ajout de l' USE HINT('ASSUME_JOIN_PREDICATE_DEPENDS_ON_FILTERS')indice à votre requête modifiera l'heuristique de confinement de jointure pour supposer une certaine corrélation (plutôt que l'indépendance) entre le Key1prédicat et le Key2prédicat de jointure, ce qui peut être bénéfique pour votre requête. Pour la dernière requête de test, cette indication augmente l'estimation de la cardinalité de 1,175à 7,551, mais reste assez timide par rapport à l' 20,000estimation de ligne correcte produite avec les statistiques filtrées.

Une autre approche que nous avons utilisée dans des situations similaires consiste à extraire le sous-ensemble pertinent des données dans des tables #temp. Surtout maintenant que les nouvelles versions de SQL Server n'écrivent plus avec impatience les tables #temp sur le disque , nous avons eu de bons résultats avec cette approche. Votre description de votre jointure plusieurs-à-plusieurs implique que chaque table #temp individuelle dans votre cas serait relativement petite (ou au moins plus petite que l'ensemble de résultats final), donc cette approche peut être utile d'essayer.

DROP TABLE IF EXISTS #Table1_extract, #Table2_extract, #Table3_extract, #c
-- Extract only the subset of rows that match the filter predicate
-- (Or better yet, extract only the subset of columns you need!)
SELECT * INTO #Table1_extract FROM #Table1 WHERE Key1 = 1
SELECT * INTO #Table2_extract FROM #Table2 WHERE Key1 = 1
SELECT * INTO #Table3_extract FROM #Table3 WHERE Key1 = 1
-- Now perform the join on those extracts, removing the filter predicate
SELECT col = 1
INTO #c 
FROM #Table1_extract t1
JOIN #Table2_extract t2
    ON t1.Key2 = t2.Key2
JOIN #Table3_extract t3
    ON t1.Key2 = t3.Key2

Nous utilisons beaucoup de statistiques filtrées, mais nous en faisons une par Key1valeur sur chaque table. Nous en avons maintenant des milliers.
Steven Hibble

2
@StevenHibble Bon point que des milliers de statistiques filtrées pourraient rendre la gestion difficile. (Nous avons également vu qu'il a un impact négatif sur le temps de compilation du plan.) Il pourrait ne pas correspondre à votre cas d'utilisation, mais j'ai également ajouté une autre approche de table #temp que nous avons utilisée avec succès plusieurs fois.
Geoff Patterson

-1

Une portée. Aucune vraie base autre que d'essayer.

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key2 = t2.Key2
      AND t1.Key1 = 1
      AND t2.Key1 = 1
     JOIN Table3 t3
       ON t2.Key2 = t3.Key2
      AND t3.Key1 = 1;
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.