Database
 sql >> Base de données >  >> RDS >> Database

UNION ALL Optimisation

La concaténation de deux ou plusieurs ensembles de données est le plus souvent exprimée en T-SQL en utilisant le UNION ALL clause. Étant donné que l'optimiseur SQL Server peut souvent réorganiser des éléments tels que les jointures et les agrégats pour améliorer les performances, il est tout à fait raisonnable de s'attendre à ce que SQL Server envisage également de réorganiser les entrées de concaténation, là où cela présenterait un avantage. Par exemple, l'optimiseur pourrait envisager les avantages de la réécriture de A UNION ALL B comme B UNION ALL A .

En fait, l'optimiseur SQL Server ne fait pas fais ça. Plus précisément, il y avait une prise en charge limitée de la réorganisation des entrées de concaténation dans les versions de SQL Server jusqu'à 2008 R2, mais cela a été supprimé dans SQL Server 2012, et n'a pas refait surface depuis.

SQL Server 2008 R2

Intuitivement, l'ordre des entrées de concaténation n'a d'importance que s'il existe un objectif de ligne . Par défaut, SQL Server optimise les plans d'exécution en partant du principe que toutes les lignes éligibles seront renvoyées au client. Lorsqu'un objectif de ligne est effectif, l'optimiseur essaie de trouver un plan d'exécution qui produira rapidement les premières lignes.

Les objectifs de ligne peuvent être définis de plusieurs façons, par exemple en utilisant TOP , un FAST n indice de requête, ou en utilisant EXISTS (qui, par nature, doit trouver au plus une ligne). Lorsqu'il n'y a pas d'objectif de ligne (c'est-à-dire que le client a besoin de toutes les lignes), l'ordre dans lequel les entrées de concaténation sont lues n'a généralement pas d'importance :chaque entrée sera finalement entièrement traitée dans tous les cas.

La prise en charge limitée dans les versions jusqu'à SQL Server 2008 R2 s'applique lorsqu'il existe un objectif d'exactement une ligne . Dans cette circonstance spécifique, SQL Server réorganisera les entrées de concaténation sur la base du coût prévu.

Cela n'est pas fait pendant l'optimisation basée sur les coûts (comme on pourrait s'y attendre), mais plutôt comme une réécriture post-optimisation de dernière minute de la sortie normale de l'optimiseur. Cette disposition a l'avantage de ne pas augmenter l'espace de recherche de plan basé sur les coûts (potentiellement une alternative pour chaque réorganisation possible), tout en produisant un plan optimisé pour retourner rapidement la première ligne.

Exemples

Les exemples suivants utilisent deux tables au contenu identique :Un million de lignes d'entiers de un à un million. Une table est un segment de mémoire sans index non cluster ; l'autre a un index clusterisé unique :

CREATE TABLE dbo.Expensive
(
    Val bigint NOT NULL
);
 
CREATE TABLE dbo.Cheap
(
    Val bigint NOT NULL, 
 
    CONSTRAINT [PK dbo.Cheap Val]
        UNIQUE CLUSTERED (Val)
);
GO
INSERT dbo.Cheap WITH (TABLOCKX)
    (Val)
SELECT TOP (1000000)
    Val = ROW_NUMBER() OVER (ORDER BY SV1.number)
FROM master.dbo.spt_values AS SV1
CROSS JOIN master.dbo.spt_values AS SV2
ORDER BY
    Val
OPTION (MAXDOP 1);
GO
INSERT dbo.Expensive WITH (TABLOCKX)
    (Val)
SELECT
    C.Val
FROM dbo.Cheap AS C
OPTION (MAXDOP 1);

Aucun objectif de ligne

La requête suivante recherche les mêmes lignes dans chaque table et renvoie la concaténation des deux ensembles :

SELECT E.Val 
FROM dbo.Expensive AS E 
WHERE 
    E.Val BETWEEN 751000 AND 751005
 
UNION ALL
 
SELECT C.Val
FROM dbo.Cheap AS C 
WHERE 
    C.Val BETWEEN 751000 AND 751005;

Le plan d'exécution produit par l'optimiseur de requête est :

L'avertissement sur la racine SELECT L'opérateur nous alerte de l'index manquant évident sur la table de tas. L'avertissement sur l'opérateur Table Scan est ajouté par Sentry One Plan Explorer. Il attire notre attention sur le coût d'E/S du prédicat résiduel caché dans l'analyse.

L'ordre des entrées de la concaténation n'a pas d'importance ici, car nous n'avons pas défini d'objectif de ligne. Les deux entrées seront entièrement lues pour renvoyer toutes les lignes de résultats. Il est intéressant (bien que cela ne soit pas garanti) de noter que l'ordre des entrées suit l'ordre textuel de la requête d'origine. Notez également que l'ordre des lignes du résultat final n'est pas spécifié non plus, puisque nous n'avons pas utilisé de niveau supérieur ORDER BY clause. Nous supposerons que c'est délibéré et que la commande finale est sans conséquence pour la tâche à accomplir.

Si nous inversons l'ordre d'écriture des tables dans la requête comme ceci :

SELECT C.Val
FROM dbo.Cheap AS C 
WHERE 
    C.Val BETWEEN 751000 AND 751005
 
UNION ALL
 
SELECT E.Val 
FROM dbo.Expensive AS E 
WHERE 
    E.Val BETWEEN 751000 AND 751005;

Le plan d'exécution suit le changement, en accédant d'abord à la table clusterisée (là encore, ce n'est pas garanti) :

On peut s'attendre à ce que les deux requêtes aient les mêmes caractéristiques de performances, car elles effectuent les mêmes opérations, mais dans un ordre différent.

Avec un objectif de ligne

De toute évidence, le manque d'indexation sur la table de tas rendra normalement la recherche de lignes spécifiques plus coûteuse, par rapport à la même opération sur la table en cluster. Si nous demandons à l'optimiseur un plan qui renvoie rapidement la première ligne, nous nous attendrions à ce que SQL Server réorganise les entrées de concaténation afin que la table en cluster bon marché soit consultée en premier.

En utilisant la requête qui mentionne la table de tas en premier, et en utilisant un indicateur de requête FAST 1 pour spécifier l'objectif de ligne :

SELECT E.Val 
FROM dbo.Expensive AS E 
WHERE 
    E.Val BETWEEN 751000 AND 751005
 
UNION ALL
 
SELECT C.Val
FROM dbo.Cheap AS C 
WHERE 
    C.Val BETWEEN 751000 AND 751005
OPTION (FAST 1);

Le plan d'exécution estimé produit sur une instance de SQL Server 2008 R2 est :

Notez que les entrées de concaténation ont été réorganisées pour réduire le coût estimé du renvoi de la première ligne. Notez également que les avertissements d'index manquant et d'E/S résiduelles ont disparu. Aucun problème n'a d'importance avec cette forme de plan lorsque l'objectif est de renvoyer une seule ligne le plus rapidement possible.

La même requête exécutée sur SQL Server 2016 (en utilisant l'un ou l'autre des modèles d'estimation de cardinalité) est :

SQL Server 2016 n'a pas réorganisé les entrées de concaténation. L'avertissement d'E/S de Plan Explorer est revenu, mais malheureusement, l'optimiseur n'a pas produit d'avertissement d'index manquant cette fois (bien qu'il soit pertinent).

Réorganisation générale

Comme mentionné, la réécriture post-optimisation qui réorganise les entrées de concaténation n'est efficace que pour :

  • SQL Server 2008 R2 et versions antérieures
  • Un objectif de ligne d'exactement un

Si nous voulons vraiment qu'une seule ligne soit renvoyée, plutôt qu'un plan optimisé pour renvoyer rapidement la première ligne (mais qui finira par renvoyer toutes les lignes), nous pouvons utiliser un TOP clause avec une table dérivée ou une expression de table commune (CTE) :

SELECT TOP (1)
    UA.Val
FROM
(
    SELECT E.Val 
    FROM dbo.Expensive AS E 
    WHERE 
        E.Val BETWEEN 751000 AND 751005
 
    UNION ALL
 
    SELECT C.Val
    FROM dbo.Cheap AS C 
    WHERE 
        C.Val BETWEEN 751000 AND 751005
) AS UA;

Sur SQL Server 2008 R2 ou version antérieure, cela produit le plan d'entrée réorganisé optimal :

Sur SQL Server 2012, 2014 et 2016, aucune réorganisation post-optimisation ne se produit :

Si nous voulons que plus d'une ligne soit renvoyée, par exemple en utilisant TOP (2) , la réécriture souhaitée ne sera pas appliquée sur SQL Server 2008 R2 même si un FAST 1 indice est également utilisé. Dans cette situation, nous devons recourir à des astuces comme utiliser TOP avec une variable et un OPTIMIZE FOR indice :

DECLARE @TopRows bigint = 2; -- Number of rows actually needed
 
SELECT TOP (@TopRows)
    UA.Val
FROM
(
    SELECT E.Val 
    FROM dbo.Expensive AS E 
    WHERE 
        E.Val BETWEEN 751000 AND 751005
 
    UNION ALL
 
    SELECT C.Val
    FROM dbo.Cheap AS C 
    WHERE 
        C.Val BETWEEN 751000 AND 751005
) AS UA
OPTION (OPTIMIZE FOR (@TopRows = 1)); -- Just a hint

L'indicateur de requête est suffisant pour définir un objectif de ligne de un, tandis que la valeur d'exécution de la variable garantit que le nombre de lignes souhaité (2) est renvoyé.

Le plan d'exécution réel sur SQL Server 2008 R2 est :

Les deux lignes renvoyées proviennent de l'entrée de recherche réordonnée et le balayage de table n'est pas exécuté du tout. Plan Explorer affiche le nombre de lignes en rouge car l'estimation concernait une ligne (en raison de l'indice) alors que deux lignes ont été rencontrées au moment de l'exécution.

Sans UNION TOUS

Ce problème ne se limite pas non plus aux requêtes écrites explicitement avec UNION ALL . D'autres constructions telles que EXISTS et OR peut également amener l'optimiseur à introduire un opérateur de concaténation, qui peut souffrir du manque de réorganisation des entrées. Il y avait une question récente sur Database Administrators Stack Exchange avec exactement ce problème. Transformer la requête à partir de cette question pour utiliser nos exemples de tableaux :

SELECT
    CASE
        WHEN 
            EXISTS 
            (
                SELECT 1
                FROM dbo.Expensive AS E
                WHERE E.Val BETWEEN 751000 AND 751005
            ) 
            OR EXISTS 
            (
                SELECT 1
                FROM dbo.Cheap AS C 
                WHERE C.Val BETWEEN 751000 AND 751005
            ) 
            THEN 1
        ELSE 0
    END;

Le plan d'exécution sur SQL Server 2016 a la table de tas sur la première entrée :

Sur SQL Server 2008 R2, l'ordre des entrées est optimisé pour refléter l'objectif de ligne unique de la semi-jointure :

Dans le plan le plus optimal, l'analyse du tas n'est jamais exécutée.

Solutions de contournement

Dans certains cas, il apparaîtra à l'auteur de la requête que l'une des entrées de concaténation sera toujours moins chère à exécuter que les autres. Si cela est vrai, il est tout à fait valable de réécrire la requête afin que les entrées de concaténation les moins chères apparaissent en premier dans l'ordre écrit. Bien sûr, cela signifie que l'auteur de la requête doit être conscient de cette limitation de l'optimiseur et prêt à s'appuyer sur un comportement non documenté.

Un problème plus difficile se pose lorsque le coût des entrées de concaténation varie selon les circonstances, peut-être en fonction des valeurs des paramètres. Utilisation de OPTION (RECOMPILE) n'aidera pas sur SQL Server 2012 ou version ultérieure. Cette option peut être utile sur SQL Server 2008 R2 ou version antérieure, mais uniquement si l'exigence d'objectif de ligne unique est également remplie.

S'il y a des inquiétudes quant au fait de s'appuyer sur le comportement observé (entrées de concaténation du plan de requête correspondant à l'ordre textuel de la requête), un guide de plan peut être utilisé pour forcer la forme du plan. Lorsque différents ordres d'entrée sont optimaux pour différentes circonstances, plusieurs guides de plan peuvent être utilisés, où les conditions peuvent être codées avec précision à l'avance. Ce n'est pourtant pas idéal.

Réflexions finales

L'optimiseur de requête SQL Server contient en fait un outil basé sur les coûts règle d'exploration, UNIAReorderInputs , qui est capable de générer des variations d'ordre d'entrée de concaténation et d'explorer des alternatives lors de l'optimisation basée sur les coûts (et non comme une réécriture post-optimisation unique).

Cette règle n'est actuellement pas activée pour une utilisation générale. Autant que je sache, il n'est activé que lorsqu'un plan guide ou USE PLAN l'indice est présent. Cela permet au moteur de forcer avec succès un plan qui a été généré pour une requête qualifiée pour la réécriture de réorganisation des entrées, même lorsque la requête actuelle n'est pas qualifiée.

Mon sentiment est que cette règle d'exploration est délibérément limitée à cette utilisation, car les requêtes qui bénéficieraient d'une réorganisation des entrées de concaténation dans le cadre d'une optimisation basée sur les coûts sont considérées comme n'étant pas suffisamment courantes, ou peut-être parce que l'on craint que l'effort supplémentaire ne paie pas désactivé. Mon propre point de vue est que la réorganisation des entrées de l'opérateur de concaténation doit toujours être explorée lorsqu'un objectif de ligne est en vigueur.

Il est également dommage que la réécriture post-optimisation (plus limitée) ne soit pas efficace dans SQL Server 2012 ou version ultérieure. Cela peut être dû à un bogue subtil, mais je n'ai rien trouvé à ce sujet dans la documentation, la base de connaissances ou sur Connect. J'ai ajouté un nouvel élément Connect ici.

Mise à jour du 9 août 2017  :Ceci est maintenant corrigé sous l'indicateur de trace 4199 pour SQL Server 2014 et 2016, voir KB 4023419 :

CORRECTIF :la requête avec UNION ALL et un objectif de ligne peut s'exécuter plus lentement dans SQL Server 2014 ou les versions ultérieures par rapport à SQL Server 2008 R2