Pourquoi la fonction LEN () sous-estime-t-elle mal la cardinalité dans SQL Server 2014?

26

J'ai une table avec une colonne de chaînes et un prédicat qui vérifie les lignes d'une certaine longueur. Dans SQL Server 2014, je vois une estimation de 1 ligne quelle que soit la longueur que je vérifie. Cela donne des plans très médiocres car il y a en fait des milliers voire des millions de lignes et SQL Server choisit de placer cette table sur le côté externe d'une boucle imbriquée.

Existe-t-il une explication pour l'estimation de cardinalité de 1.0003 pour SQL Server 2014 alors que SQL Server 2012 estime 31.622 lignes? Existe-t-il une bonne solution de contournement?

Voici une courte reproduction du numéro:

-- Create a table with 1MM rows of dummy data
CREATE TABLE #customers (cust_nbr VARCHAR(10) NOT NULL)
GO

INSERT INTO #customers WITH (TABLOCK) (cust_nbr)
    SELECT TOP 1000000 
        CONVERT(VARCHAR(10),
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))) AS cust_nbr
    FROM master..spt_values v1
    CROSS JOIN master..spt_values v2
GO

-- Looking for string of a certain length.
-- While both CEs yield fairly poor estimates, the 2012 CE is much
-- more conservative (higher estimate) and therefore much more likely
-- to yield an okay plan rather than a drastically understimated loop join.
-- 2012: 31,622 rows estimated, 900K rows actual
-- 2014: 1 row estimated, 900K rows actual
SELECT COUNT(*)
FROM #customers
WHERE LEN(cust_nbr) = 6
OPTION (QUERYTRACEON 9481) -- Optionally, use 2012 CE
GO

Voici un script plus complet montrant des tests supplémentaires

J'ai également lu le livre blanc sur l'estimateur de cardinalité SQL Server 2014 , mais je n'y ai rien trouvé qui clarifie la situation.

Geoff Patterson
la source

Réponses:

20

Pour l'héritage CE, je vois que l'estimation est pour 3,16228% des lignes - et c'est une heuristique de «nombre magique» utilisée pour la colonne = prédicats littéraux (il existe d'autres heuristiques basées sur la construction des prédicats - mais LENenroulées autour de la colonne pour la les résultats CE hérités correspondent à ce cadre de conjectures). Vous pouvez en voir des exemples dans un article sur Selectivity Guesses in absence of Statistics de Joe Sack et Constant-Constant Comparison Estimation de Ian Jose.

-- Legacy CE: 31622.8 rows
SELECT  COUNT(*)
FROM    #customers
WHERE   LEN(cust_nbr) = 6
OPTION  ( QUERYTRACEON 9481); -- Legacy CE
GO

Maintenant, comme pour le nouveau comportement CE, il semble que cela soit désormais visible pour l'optimiseur (ce qui signifie que nous pouvons utiliser des statistiques). J'ai parcouru l'exercice de regarder la sortie de la calculatrice ci-dessous, et vous pouvez regarder la génération automatique de statistiques associée comme un pointeur:

-- New CE: 1.00007 rows
SELECT  COUNT(*)
FROM    #customers
WHERE   LEN(cust_nbr) = 6
OPTION  ( QUERYTRACEON 2312 ); -- New CE
GO

-- View New CE behavior with 2363 (for supported option use XEvents)
SELECT  COUNT(*)
FROM    #customers
WHERE   LEN(cust_nbr) = 6
OPTION  (QUERYTRACEON 2312, QUERYTRACEON 2363, QUERYTRACEON 3604, RECOMPILE); -- New CE
GO

/*
Loaded histogram for column QCOL:
[tempdb].[dbo].[#customers].cust_nbr from stats with id 2
Using ambient cardinality 1e+006 to combine distinct counts:
  999927

Combined distinct count: 999927
Selectivity: 1.00007e-006
Stats collection generated:
  CStCollFilter(ID=2, CARD=1.00007)
      CStCollBaseTable(ID=1, CARD=1e+006 TBL: #customers)

End selectivity computation
*/

EXEC tempdb..sp_helpstats '#customers';


--Check out AVG_RANGE_ROWS values (for example - plenty of ~ 1)
DBCC SHOW_STATISTICS('tempdb..#customers', '_WA_Sys_00000001_B0368087');
--That's my Stats name yours is subject to change

Malheureusement, la logique repose sur une estimation du nombre de valeurs distinctes, qui n'est pas ajustée pour l'effet de la LENfonction.

Solution possible

Vous pouvez obtenir une estimation basée sur trois sous les deux modèles CE en réécrivant le LENcomme LIKE:

SELECT COUNT_BIG(*)
FROM #customers AS C
WHERE C.cust_nbr LIKE REPLICATE('_', 6);

Plan J'aime


Informations sur les indicateurs de trace utilisés:

  • 2363: affiche beaucoup d'informations, y compris les statistiques en cours de chargement.
  • 3604: imprime la sortie des commandes DBCC dans l'onglet messages.
Zane
la source
13

Y a-t-il une explication pour l'estimation de cardinalité de 1.0003 pour SQL 2014 alors que SQL 2012 estime 31.622 lignes?

Je pense que la réponse de @ Zane couvre assez bien cette partie.

Existe-t-il une bonne solution de contournement?

Vous pouvez essayer de créer une colonne calculée non persistante pour LEN(cust_nbr)et (éventuellement) créer un index non clusterisé sur cette colonne calculée. Cela devrait vous fournir des statistiques précises.

J'ai fait quelques tests et voici ce que j'ai trouvé:

  • Les statistiques ont été créées automatiquement sur la colonne calculée non persistante, quand aucun index n'y était défini.
  • L'ajout de l'index non clusterisé sur la colonne calculée n'a pas seulement aidé, il a en fait un peu nui aux performances. Processeur légèrement supérieur et temps écoulé. Coût estimé légèrement supérieur (quel que soit le prix).
  • Faire la colonne calculée comme PERSISTED(pas d'index) était meilleur que les deux autres variantes. Les lignes estimées étaient plus précises. Le CPU et le temps écoulé étaient meilleurs (comme prévu car il n'avait rien à calculer par ligne).
  • Je n'ai pas pu créer d'index filtré ou de statistiques filtrées sur la colonne calculée (car il était calculé), même si c'était PERSISTED:-(
Solomon Rutzky
la source
1
Merci pour la comparaison approfondie entre persistant et non. Il est bon de savoir que même si la colonne calculée persistante a ses avantages, la non-persistance peut être une victoire très rapide avec très peu de frais généraux dans certains cas où les statistiques sur une expression sont bénéfiques.
Geoff Patterson