Pourquoi ma requête SELECT DISTINCT TOP N analyse-t-elle la table entière?

28

J'ai rencontré quelques SELECT DISTINCT TOP Nrequêtes qui semblent mal optimisées par l'optimiseur de requêtes SQL Server. Commençons par considérer un exemple trivial: une table d'un million de lignes avec deux valeurs alternées. Je vais utiliser la fonction GetNums pour générer les données:

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

Pour la requête suivante:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL Server peut trouver deux valeurs distinctes simplement en analysant la première page de données de la table, mais il analyse toutes les données à la place . Pourquoi SQL Server n'analyse-t-il pas uniquement jusqu'à ce qu'il trouve le nombre demandé de valeurs distinctes?

Pour cette question, veuillez utiliser les données de test suivantes qui contiennent 10 millions de lignes avec 10 valeurs distinctes générées en blocs:

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

Les réponses pour une table avec un index cluster sont également acceptables:

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

La requête suivante analyse les 10 millions de lignes de la table . Comment puis-je obtenir quelque chose qui ne balaye pas la table entière? J'utilise SQL Server 2016 SP1.

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);
Joe Obbish
la source

Réponses:

30

Il semble y avoir trois règles d'optimisation différentes qui peuvent effectuer l' DISTINCTopération dans la requête ci-dessus. La requête suivante renvoie une erreur qui suggère que la liste est exhaustive:

SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);

Msg 8622, niveau 16, état 1, ligne 1

Le processeur de requêtes n'a pas pu produire un plan de requête en raison des indications définies dans cette requête. Renvoyez la requête sans spécifier d'indices et sans utiliser SET FORCEPLAN.

GbAggToSortimplémente l'agrégat groupé (distinct) en tant que tri distinct. Il s'agit d'un opérateur de blocage qui lira toutes les données de l'entrée avant de produire des lignes. GbAggToStrmimplémente l'agrégat de regroupement en tant qu'agrégat de flux (qui nécessite également un tri d'entrée dans ce cas). Il s'agit également d'un opérateur de blocage. GbAggToHSimplémente en tant que correspondance de hachage, ce que nous avons vu dans le mauvais plan de la question, mais il peut être implémenté en tant que correspondance de hachage (agrégat) ou correspondance de hachage (flux distinct).

L' opérateur de correspondance de hachage ( flux distinct ) est un moyen de résoudre ce problème car il ne bloque pas. SQL Server doit pouvoir arrêter l'analyse une fois qu'il trouve suffisamment de valeurs distinctes.

L'opérateur logique Flow Distinct analyse l'entrée, supprimant les doublons. Alors que l'opérateur Distinct consomme toutes les entrées avant de produire une sortie, l'opérateur Flow Distinct renvoie chaque ligne telle qu'elle est obtenue à partir de l'entrée (sauf si cette ligne est un doublon, auquel cas elle est supprimée).

Pourquoi la requête de la question utilise-t-elle une correspondance de hachage (agrégat) au lieu d'une correspondance de hachage (flux distinct)? À mesure que le nombre de valeurs distinctes change dans la table, je m'attends à ce que le coût de la requête de correspondance de hachage (flux distinct) diminue car l'estimation du nombre de lignes dont il a besoin pour numériser vers la table devrait diminuer. Je m'attendrais à ce que le coût du plan de concordance de hachage (agrégé) augmente, car la table de hachage qu'il doit construire augmentera. Une façon d'enquêter est de créer un guide de plan . Si je crée deux copies des données mais que j'applique un guide de plan à l'une d'entre elles, je devrais pouvoir comparer la correspondance de hachage (agrégée) à la correspondance de hachage (distincte) côte à côte avec les mêmes données. Notez que je ne peux pas faire cela en désactivant les règles de l'optimiseur de requête car la même règle s'applique aux deux plans ( GbAggToHS).

Voici une façon d'obtenir le guide de plan que je recherche:

DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;

CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;

-- run this query
SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

Obtenez le descripteur de plan et utilisez-le pour créer un guide de plan:

-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM 
sys.dm_exec_query_stats AS qs   
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st  
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;

EXEC sp_create_plan_guide_from_handle 
'EVIL_PLAN_GUIDE', 
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;

Les guides de plan ne fonctionnent que sur le texte exact de la requête, nous allons donc le recopier à partir du guide de plan:

SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';

Réinitialisez les données:

TRUNCATE TABLE X_PLAN_GUIDE_TARGET;

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

Obtenez un plan de requête pour la requête avec le guide de plan appliqué:

SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

Cela a l'opérateur de correspondance de hachage (flux distinct) que nous voulions avec nos données de test. Notez que SQL Server s'attend à lire toutes les lignes de la table et que le coût estimé est exactement le même que pour le plan avec la correspondance de hachage (agrégat). Les tests que j'ai effectués ont suggéré que les coûts des deux plans sont identiques lorsque l'objectif de ligne du plan est supérieur ou égal au nombre de valeurs distinctes que SQL Server attend de la table, qui dans ce cas peut simplement être dérivé de la statistiques. Malheureusement (pour notre requête), l'optimiseur choisit la correspondance de hachage (agrégat) sur la correspondance de hachage (flux distinct) lorsque les coûts sont les mêmes. Nous sommes donc à 0,0000001 unités d'optimisation magique loin du plan que nous voulons.

Une façon d'attaquer ce problème consiste à diminuer l'objectif de ligne. Si l'objectif de ligne du point de vue de l'optimiseur est inférieur au nombre distinct de lignes, nous obtiendrons probablement une correspondance de hachage (flux distinct). Cela peut être accompli avec l' OPTIMIZE FORindice de requête:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Pour cette requête, l'optimiseur crée un plan comme si la requête avait juste besoin de la première ligne, mais lorsque la requête est exécutée, elle récupère les 10 premières lignes. Sur ma machine, cette requête scanne 892800 lignes X_10_DISTINCT_HEAPet se termine en 299 ms avec 250 ms de temps CPU et 2537 lectures logiques.

Notez que cette technique ne fonctionnera pas si les statistiques ne rapportent qu'une seule valeur distincte, ce qui pourrait se produire pour les statistiques échantillonnées par rapport aux données asymétriques. Cependant, dans ce cas, il est peu probable que vos données soient suffisamment compactées pour justifier l'utilisation de techniques comme celle-ci. Vous ne perdrez peut-être pas grand-chose en scannant toutes les données du tableau, surtout si cela peut être fait en parallèle.

Une autre façon d'attaquer ce problème consiste à augmenter le nombre de valeurs distinctes estimées que SQL Server s'attend à obtenir de la table de base. C'était plus difficile que prévu. L'application d'une fonction déterministe ne peut pas augmenter le nombre distinct de résultats. Si l'optimiseur de requête est conscient de ce fait mathématique (certains tests suggèrent que c'est au moins pour nos besoins), alors l'application de fonctions déterministes (qui inclut toutes les fonctions de chaîne ) n'augmentera pas le nombre estimé de lignes distinctes.

Beaucoup de fonctions non déterministes n'ont pas fonctionné non plus, y compris les choix évidents de NEWID()et RAND(). Cependant, LAG()fait l'affaire pour cette requête. L'optimiseur de requêtes attend 10 millions de valeurs distinctes par rapport à l' LAGexpression, ce qui encouragera un plan de correspondance de hachage (flux distinct) :

SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

Sur ma machine, cette requête scanne 892800 lignes X_10_DISTINCT_HEAPet se termine en 1165 ms avec 1109 ms de temps CPU et 2537 lectures logiques, ce qui LAG()ajoute un peu de surcharge relative. @Paul White a suggéré d'essayer le traitement en mode batch pour cette requête. Sur SQL Server 2016, nous pouvons obtenir le traitement en mode batch même avec MAXDOP 1. Une façon d'obtenir le traitement en mode batch pour une table rowstore est de se joindre à une CCI vide comme suit:

CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;

SELECT DISTINCT TOP 10 VAL
FROM
(
    SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
    FROM X_10_DISTINCT_HEAP
    LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);

Ce code entraîne ce plan de requête .

Paul a souligné que je devais changer la requête à utiliser LAG(..., 1)car LAG(..., 0)il ne semble pas être éligible pour l'optimisation de Window Aggregate. Ce changement a réduit le temps écoulé à 520 ms et le temps CPU à 454 ms.

Notez que l' LAG()approche n'est pas la plus stable. Si Microsoft modifie l'hypothèse d'unicité par rapport à la fonction, cela peut ne plus fonctionner. Il a une estimation différente avec l'héritage CE. De plus, ce type d'optimisation par rapport à un tas n'est pas nécessairement une bonne idée. Si la table est reconstruite, il est possible de se retrouver dans le pire des cas où presque toutes les lignes doivent être lues dans la table.

Contre une table avec une colonne unique (comme l'exemple d'index clusterisé dans la question), nous avons de meilleures options. Par exemple, nous pouvons tromper l'optimiseur en utilisant une SUBSTRINGexpression qui renvoie toujours une chaîne vide. SQL Server ne pense pas que cela SUBSTRINGchangera le nombre de valeurs distinctes, donc si nous l'appliquons à une colonne unique, telle que PK, le nombre estimé de lignes distinctes est de 10 millions. Cette requête suivante obtient l'opérateur de correspondance de hachage (flux distinct):

SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);

Sur ma machine, cette requête analyse 900 000 lignes X_10_DISTINCT_CIet se termine en 333 ms avec 297 ms de temps CPU et 3011 lectures logiques.

En résumé, l'optimiseur de requêtes semble supposer que toutes les lignes seront lues dans la table pour les SELECT DISTINCT TOP Nrequêtes lorsque N> = le nombre de lignes distinctes estimées de la table. L'opérateur de correspondance de hachage (agrégat) peut avoir le même coût que l'opérateur de correspondance de hachage (flux distinct), mais l'optimiseur choisit toujours l'opérateur d'agrégation. Cela peut entraîner des lectures logiques inutiles lorsque suffisamment de valeurs distinctes sont situées près du début de l'analyse de la table. L'optimiseur peut être amené à utiliser l'opérateur de correspondance de hachage (flux distinct) de deux manières différentes: réduire l'objectif de ligne à l'aide de l' OPTIMIZE FORindice ou augmenter le nombre estimé de lignes distinctes à l'aide LAG()ou SUBSTRINGsur une colonne unique.

Joe Obbish
la source
12

Vous avez déjà répondu correctement à vos propres questions.

Je veux juste ajouter une observation que le moyen le plus efficace est en fait de scanner la table entière - si elle peut être organisée comme un «tas» de colonnes :

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

La simple requête:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

donne alors:

Plan d'exécution

Table 'X_10_DISTINCT_HEAP'. Nombre de numérisations 1,
 lecture logique 0, lecture physique 0, lecture anticipée lecture 0, 
 lob logique lit 66 , lob physique lit 0, lob read-ahead lit 0.
Table 'X_10_DISTINCT_HEAP'. Le segment lit 13, le segment a sauté 0.

 Temps d'exécution de SQL Server:
   Temps CPU = 0 ms, temps écoulé = 11 ms.

Hash Match (Flow Distinct) ne peut actuellement pas s'exécuter en mode batch. Les méthodes qui utilisent cela sont beaucoup plus lentes en raison de la transition coûteuse (invisible) du traitement par lots au traitement en ligne. Par exemple:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

Donne:

Plan d'exécution distinct de flux

Table 'X_10_DISTINCT_HEAP'. Nombre de numérisations 1,
 lecture logique 0, lecture physique 0, lecture anticipée lecture 0, 
 lob logique lit 20 , lob physique lit 0, lob read-ahead lit 0.
Table 'X_10_DISTINCT_HEAP'. Le segment lit 4 , le segment a sauté 0.

 Temps d'exécution de SQL Server:
   Temps CPU = 640 ms, temps écoulé = 680 ms.

Cela est plus lent que lorsque la table est organisée en tant que segment de magasin de lignes.

Paul White dit GoFundMonica
la source
5

Voici une tentative d'émulation d'un balayage partiel répété (similaire mais pas identique à un saut de balayage) à l'aide d'un CTE récursif. Le but - puisque nous n'avons pas d'index sur (id)- est d'éviter les tris et les scans multiples sur la table.

Il fait quelques astuces pour contourner certaines restrictions CTE récursives:

  • Non TOPautorisé dans la partie récursive. Nous utilisons une sous-requête et à la ROW_NUMBER()place.
  • Nous ne pouvons pas avoir plusieurs références à la partie constante ou utiliser LEFT JOINou utiliser à NOT IN (SELECT id FROM cte)partir de la partie récursive. Pour contourner, nous construisons une VARCHARchaîne qui accumule toutes les idvaleurs, similaires à STRING_AGGou à hierarchyID, puis comparons avec LIKE.

Pour un tas (en supposant que la colonne est nommée id) test-1 sur rextester.com .

Cela - comme les tests l'ont montré - n'évite pas plusieurs analyses, mais fonctionne correctement lorsque différentes valeurs sont trouvées dans les premières pages. Si toutefois les valeurs ne sont pas réparties uniformément, il peut effectuer plusieurs analyses sur de grandes parties de la table - ce qui entraîne bien sûr de mauvaises performances.

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

et lorsque la table est en cluster (CI activéunique_key ), test-2 sur rextester.com .

Cela utilise l'index cluster ( WHERE x.unique_key > ct.unique_key) pour éviter plusieurs analyses:

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;
ypercubeᵀᴹ
la source
Il y a un problème de performances assez subtil avec cette solution. Il finit par faire une recherche supplémentaire sur la table après avoir trouvé la Nième valeur. Donc, s'il y a 10 valeurs distinctes pour un top 10, il recherchera une 11e valeur qui n'est pas là. Vous vous retrouvez avec une analyse complète supplémentaire et les 10 millions de calculs ROW_NUMBER () s'additionnent vraiment. J'ai une solution de contournement ici qui accélère la requête 20X sur ma machine. Qu'est-ce que tu penses? brentozar.com/pastetheplan/?id=SkDhAmFKe
Joe Obbish
2

Pour être complet, une autre façon d'aborder ce problème consiste à utiliser OUTER APPLY . Nous pouvons ajouter un OUTER APPLYopérateur pour chaque valeur distincte que nous devons trouver. Ceci est similaire dans son concept à l'approche récursive d'Ypercube, mais a effectivement la récursion écrite à la main. Un avantage est que nous pouvons utiliser TOPdans les tables dérivées au lieu de la ROW_NUMBER()solution de contournement. Un gros inconvénient est que le texte de la requête s'allonge à mesure qu'il Naugmente.

Voici une implémentation de la requête sur le tas:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Voici le plan de requête réel pour la requête ci-dessus. Sur ma machine, cette requête se termine en 713 ms avec 625 ms de temps CPU et 12605 lectures logiques. Nous obtenons une nouvelle valeur distincte toutes les 100k lignes, je m'attends donc à ce que cette requête analyse environ 900000 * 10 * 0,5 = 4500000 lignes. En théorie, cette requête devrait faire cinq fois les lectures logiques de cette requête de l'autre réponse:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

Cette requête a effectué 2537 lectures logiques. 2537 * 5 = 12685 qui est assez proche de 12605.

Pour la table avec l'index clusterisé, nous pouvons faire mieux. Cela est dû au fait que nous pouvons passer la dernière valeur de clé en cluster dans la table dérivée pour éviter d'analyser deux fois les mêmes lignes. Une mise en œuvre:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Voici le plan de requête réel pour la requête ci-dessus. Sur ma machine, cette requête se termine en 154 ms avec 140 ms de temps CPU et 3203 lectures logiques. Cela semblait fonctionner un peu plus vite que la OPTIMIZE FORrequête sur la table d'index cluster. Je ne m'attendais pas à cela, alors j'ai essayé de mesurer les performances plus attentivement. Ma méthodologie consistait à exécuter chaque requête dix fois sans jeux de résultats et à examiner les nombres agrégés à partir de sys.dm_exec_sessionset sys.dm_exec_session_wait_stats. La session 56 était la APPLYrequête et la session 63 était la OPTIMIZE FORrequête.

Sortie de sys.dm_exec_sessions:

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

Il semble y avoir un net avantage dans cpu_time et elapsed_time pour la APPLYrequête.

Sortie de sys.dm_exec_session_wait_stats:

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

La OPTIMIZE FORrequête a un type d'attente supplémentaire, RESERVED_MEMORY_ALLOCATION_EXT . Je ne sais pas exactement ce que cela signifie. Il peut simplement s'agir d'une mesure de la surcharge dans l'opérateur de correspondance de hachage (flux distinct). Dans tous les cas, cela ne vaut peut-être pas la peine de s'inquiéter d'une différence de 70 ms dans le temps CPU.

Joe Obbish
la source
1

Je pense que vous avez une réponse sur la raison pour laquelle
cela peut être un moyen de le résoudre.Je
sais que cela semble désordonné, mais le plan d'exécution indique que le top 2 distinct représente 84% du coût.

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;
paparazzo
la source
Ce code a pris 5 secondes sur ma machine. Il semble que les jointures à la variable de table ajoutent un peu de surcharge. Dans la requête finale, la variable de table a été analysée 892800 fois. Cette requête a pris 1359 ms de temps CPU et 1374 ms de temps écoulé. Certainement plus que ce à quoi je m'attendais. L'ajout d'une clé primaire à la variable de table semble aider, même si je ne sais pas pourquoi. Il peut y avoir d'autres optimisations possibles.
Joe Obbish