web-dev-qa-db-fra.com

Pourquoi SQL Server utilise-t-il un meilleur plan d'exécution lorsque j'inline la variable?

J'ai une requête SQL que j'essaie d'optimiser:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'

SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

MyTable a deux index:

CREATE NONCLUSTERED INDEX IX_MyTable_SomeTimestamp_Includes
ON dbo.MyTable (SomeTimestamp ASC)
INCLUDE(Id, SomeInt)

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp)

Lorsque j'exécute la requête exactement comme indiqué ci-dessus, SQL Server analyse le premier index, ce qui entraîne 189 703 lectures logiques et une durée de 2 à 3 secondes.

Lorsque j'insère le @Id variable et réexécutez la requête, SQL Server recherche le deuxième index, ce qui entraîne seulement 104 lectures logiques et une durée de 0,001 seconde (essentiellement instantanée).

J'ai besoin de la variable, mais je veux que SQL utilise le bon plan. En tant que solution temporaire, j'ai mis un indice sur la requête, et la requête est essentiellement instantanée. Cependant, j'essaie de rester à l'écart des indices d'index lorsque cela est possible. Je suppose généralement que si l'optimiseur de requêtes est incapable de faire son travail, il y a quelque chose que je peux faire (ou arrêter de faire) pour l'aider sans lui dire explicitement quoi faire.

Alors, pourquoi SQL Server propose-t-il un meilleur plan lorsque j'inline la variable?

32
Rainbolt

Dans SQL Server, il existe trois formes courantes de prédicat de non-jointure:

Avec une valeur littérale :

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = 1;

Avec un paramètre :

CREATE PROCEDURE dbo.SomeProc(@Reputation INT)
AS
BEGIN
    SELECT COUNT(*) AS records
    FROM   dbo.Users AS u
    WHERE  u.Reputation = @Reputation;
END;

Avec une variable locale :

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Résultats

Lorsque vous utilisez une valeur littérale et que votre plan n'est pas a) trivial et b) paramétré simple ou c) vous n'avez pas Paramétrage forcé activé, l'optimiseur crée un plan très spécial juste pour cette valeur.

Lorsque vous utilisez un paramètre , l'optimiseur crée un plan pour ce paramètre (il s'agit de reniflage de paramètre ), puis réutiliser ce plan, en l'absence d'indices de recompilation, planifier l'expulsion du cache, etc.

Lorsque vous utilisez une variable locale , l'optimiseur fait un plan pour ... Quelque chose .

Si vous deviez exécuter cette requête:

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Le plan ressemblerait à ceci:

NUTS

Et le nombre estimé de lignes pour cette variable locale ressemblerait à ceci:

NUTS

Même si la requête renvoie un nombre de 4 744 427.

Les variables locales, étant inconnues, n'utilisent pas la "bonne" partie de l'histogramme pour l'estimation de la cardinalité. Ils utilisent une estimation basée sur le vecteur de densité.

NUTS

SELECT 5.280389E-05 * 7250739 AS [poo]

Cela vous donnera 382.86722457471, qui est la supposition de l'optimiseur.

Ces suppositions inconnues sont généralement de très mauvaises suppositions et peuvent souvent conduire à de mauvais plans et à de mauvais choix d'index.

Le réparer?

Vos options sont généralement:

  • Indice de fragilité
  • Conseils de recompilation potentiellement coûteux
  • SQL dynamique paramétré
  • Une procédure stockée
  • Améliorer l'indice actuel

Vos options sont spécifiquement:

Améliorer l'index actuel signifie l'étendre pour couvrir toutes les colonnes nécessaires à la requête:

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp, SomeTimestamp, SomeInt)
WITH (DROP_EXISTING = ON);

En supposant que les valeurs Id sont raisonnablement sélectives, cela vous donnera un bon plan et aidera l'optimiseur en lui donnant une méthode d'accès aux données "évidente".

Plus de lecture

Vous pouvez en savoir plus sur l'intégration des paramètres ici:

45
Erik Darling

Je vais supposer que vous avez des données asymétriques, que vous ne voulez pas utiliser d'indices de requête pour forcer l'optimiseur quoi faire et que vous devez obtenir de bonnes performances pour toutes les valeurs d'entrée possibles de @Id . Vous pouvez obtenir un plan de requête garanti pour ne nécessiter que quelques poignées de lectures logiques pour toute valeur d'entrée possible si vous êtes prêt à créer la paire d'index suivante (ou leur équivalent):

CREATE INDEX GetMinSomeTimestamp ON dbo.MyTable (Id, SomeTimestamp) WHERE SomeBit = 1;
CREATE INDEX GetMaxSomeInt ON dbo.MyTable (Id, SomeInt) WHERE SomeBit = 1;

Ci-dessous mes données de test. J'ai mis 13 M lignes dans le tableau et fait que la moitié d'entre elles ont une valeur de '3A35EA17-CE7E-4637-8319-4C517B6E48CA' Pour la colonne Id.

DROP TABLE IF EXISTS dbo.MyTable;

CREATE TABLE dbo.MyTable (
    Id uniqueidentifier,
    SomeTimestamp DATETIME2,
    SomeInt INT,
    SomeBit BIT,
    FILLER VARCHAR(100)
);

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT NEWID(), CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT '3A35EA17-CE7E-4637-8319-4C517B6E48CA', CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

Cette requête peut sembler un peu étrange au premier abord:

DECLARE @Id UNIQUEIDENTIFIER = '3A35EA17-CE7E-4637-8319-4C517B6E48CA'

SELECT
  @Id,
  st.SomeTimestamp,
  si.SomeInt
FROM (
    SELECT TOP (1) SomeInt, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeInt DESC
) si
CROSS JOIN (
    SELECT TOP (1) SomeTimestamp, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeTimestamp ASC
) st;

Il est conçu pour tirer parti de l'ordre des index pour trouver la valeur min ou max avec quelques lectures logiques. Le CROSS JOIN Est là pour obtenir des résultats corrects lorsqu'il n'y a pas de lignes correspondantes pour la valeur @Id. Même si je filtre sur la valeur la plus populaire du tableau (correspondant à 6,5 millions de lignes), je n'obtiens que 8 lectures logiques:

Tableau 'MyTable'. Nombre de scans 2, lectures logiques 8

Voici le plan de requête:

enter image description here

Les deux index recherchent trouver 0 ou 1 lignes. C'est extrêmement efficace, mais la création de deux index peut être exagérée pour votre scénario. Vous pouvez plutôt considérer l'index suivant:

CREATE INDEX CoveringIndex ON dbo.MyTable (Id) INCLUDE (SomeTimestamp, SomeInt) WHERE SomeBit = 1;

Le plan de requête pour la requête d'origine (avec un indice MAXDOP 1 Facultatif) est maintenant un peu différent:

enter image description here

Les recherches clés ne sont plus nécessaires. Avec un meilleur chemin d'accès qui devrait bien fonctionner pour toutes les entrées, vous ne devriez pas avoir à vous soucier que l'optimiseur choisisse le mauvais plan de requête en raison du vecteur de densité. Cependant, cette requête et cet index ne seront pas aussi efficaces que les autres si vous recherchez une valeur @Id Populaire.

Tableau 'MyTable'. Nombre de numérisations 1, lectures logiques 33757

12
Joe Obbish

Je ne peux pas répondre pourquoi ici, mais la façon rapide et sale de s'assurer que la requête s'exécute comme vous le souhaitez est:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'
SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable WITH (INDEX(IX_MyTable_Id_SomeBit_Includes))
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

Cela entraîne un risque que la table ou les indices changent à l'avenir de sorte que cette optimisation devienne dysfonctionnelle, mais elle est disponible si vous en avez besoin. J'espère que quelqu'un peut vous offrir une réponse à la cause première, comme vous l'avez demandé, plutôt que cette solution de contournement.

2