web-dev-qa-db-fra.com

Sargable où la clause pour deux colonnes de date

J'ai ce qui est, pour moi une question intéressante sur la sargabilité. Dans ce cas, il s'agit d'utiliser un prédicat sur la différence entre deux colonnes de date. Voici la configuration:

USE [tempdb]
SET NOCOUNT ON  

IF OBJECT_ID('tempdb..#sargme') IS NOT NULL
BEGIN
DROP TABLE #sargme
END

SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO #sargme
FROM sys.[messages] AS [m]

ALTER TABLE [#sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [#sargme] ([DateCol1], [DateCol2])

Ce que je vais voir assez fréquemment, c'est quelque chose comme ça:

/*definitely not sargable*/
SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48;

... ce qui n'est certainement pas sargable. Il en résulte une analyse d'index, lit toutes les 1000 lignes, sans bon. Des rangées estimées puantent. Vous ne l'auriez jamais mis en production.

No sir, I didn't like it.

Ce serait bien si nous pouvions matérialiser des CTES, car cela nous aiderait à faire cela, bien, plus sargable-er, techniquement parlant. Mais non, nous obtenons le même plan d'exécution que de haut en haut.

/*would be Nice if it were sargable*/
WITH    [x] AS ( SELECT
                * ,
                DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
               FROM
                [#sargme] AS [s])
     SELECT
        *
     FROM
        [x]
     WHERE
        [x].[ddif] >= 48;

Et bien sûr, puisque nous n'utilisons pas de constantes, ce code ne change rien et n'est même pas à moitié sargable. Pas drôle. Même plan d'exécution.

/*not even half sargable*/
SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

Si vous vous sentez de la chance et que vous obéissez à toutes les options de réglage ANSI dans vos chaînes de connexion, vous pouvez ajouter une colonne calculée et la recherche dessus ...

ALTER TABLE [#sargme] ADD [ddiff] AS 
DATEDIFF(DAY, DateCol1, DateCol2) PERSISTED

CREATE NONCLUSTERED INDEX [ix_dates2] ON [#sargme] ([ddiff], [DateCol1], [DateCol2])

SELECT [s].[ID] ,
       [s].[DateCol1] ,
       [s].[DateCol2]
FROM [#sargme] AS [s]
WHERE [ddiff] >= 48

Cela vous fera observer une recherche d'index avec trois questions. L'homme étrange est l'endroit où nous ajoutons 48 jours à Datecol1. La requête avec DATEDIFF dans la clause WHERE, la clause CTE et la requête finale avec un prédicat sur la colonne calculée vous donnent un plan beaucoup plus agréable avec beaucoup d'estimations plus agréables, et tout ça.

I could live with this.

Ce qui m'amène à la question: dans une seule requête, y a-t-il une façon sargable d'effectuer cette recherche?

Pas de tables Temps, pas de variables de table, pas de modification de la structure de la table et de non vue.

Je vais bien avec des jointures auto-jointes, des CTES, des sous-requêtes ou de plusieurs passes multiples sur les données. Peut fonctionner avec n'importe quelle version de SQL Server.

Éviter la colonne calculée est une limitation artificielle parce que je suis plus intéressé par une solution de requête que toute autre chose.

24
Erik Darling

Il suffit d'ajouter cela rapidement pour qu'il existe comme une réponse (bien que je sache que ce n'est pas la réponse que vous voulez).

Une colonne calculée indexée est généralement la bonne solution pour ce type de problème.

Il:

  • rend le prédicat une expression indexable
  • permet de créer des statistiques automatiques pour une meilleure estimation de cardinalité
  • n'a pas - besoin Pour prendre n'importe quel espace dans la table de base

Pour être clair sur ce dernier point, la colonne calculée est pas nécessaire à persister Dans ce cas:

-- Note: not PERSISTED, metadata change only
ALTER TABLE #sargme
ADD DayDiff AS DATEDIFF(DAY, DateCol1, DateCol2);

-- Index the expression
CREATE NONCLUSTERED INDEX index_name
ON #sargme (DayDiff)
INCLUDE (DateCol1, DateCol2);

Maintenant la requête:

SELECT
    S.ID,
    S.DateCol1,
    S.DateCol2,
    DATEDIFF(DAY, S.DateCol1, S.DateCol2)
FROM
    #sargme AS S
WHERE
    DATEDIFF(DAY, S.DateCol1, S.DateCol2) >= 48;

... donne le suivant trivial plan:

Execution plan

Comme l'a dit Martin Smith, si vous avez des connexions utilisant les options de jeu incorrectes, vous pouvez créer une colonne régulière et maintenir la valeur calculée à l'aide de déclencheurs.

Tout cela ne compte vraiment que (défi de code) S'il y a un problème réel à résoudre, car Aaron dit dans sa réponse .

C'est amusant de penser, mais je ne sais pas un moyen de réaliser ce que vous voulez raisonnablement donné les contraintes de la question. Il semble que toute solution optimale nécessiterait une nouvelle structure de données de type; Le plus proche que nous ayons être l'approximation de "index de fonction" fournie par un index sur une colonne calculée non persistante comme ci-dessus.

16
Paul White 9

J'ai essayé un tas de variations farfelu, mais je n'ai trouvé aucune version mieux que l'une des vôtres. Le problème principal est que votre index ressemble à ceci en termes de trimestre de la date1 et de la date2. La première colonne va être dans une belle ligne stipulée alors que l'écart entre eux va être très déchiqueté. Vous voulez que cela ressemble plus à un entonnoir que la façon dont il va vraiment:

Date1    Date2
-----    -------
*             *
*             *
*              *
 *       * 
 *        *
 *         *
  *      *
  *           *

Je ne peux vraiment penser à ce que je puisse faire de cette recherche pour un certain delta (ou une gamme de deltas) entre les deux points. Et je veux dire une seule recherche qui est exécutée une fois + une plage de gamme, pas une recherche qui est exécutée pour chaque rangée. Cela impliquera une analyse et/ou une sorte à un moment donné, et ce sont des choses que vous voulez éviter évidemment. C'est dommage que vous ne puissiez pas utiliser les expressions comme DATEADD/DATEDIFF dans les index filtrés ou effectuez des modifications de schéma possibles permettant de définir une sorte sur le produit du diff (comme le calcul du Delta. au temps d'insertion/mise à jour). Comme c'est le cas, cela semble être l'un de ces cas où une analyse est en réalité la méthode de récupération optimale.

Vous avez dit que cette requête n'était pas amusante, mais si vous regardez de plus près, c'est de loin le meilleur (et serait encore meilleur si vous laissez la sortie scalaire Compute):

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

La raison est qu'éviser le DATEDIFF _ potentiellement alime certains CPU par rapport à un calcul contre niquement la colonne clé non avancée de l'index et évite également certaines conversions implicites désessibles sur datetimeoffset(7) (Ne me demandez pas pourquoi ceux-ci sont là, mais ils sont). Voici la version DATEDIFF:

<Prédicat>
[ DatetimeOffset (7), [splget]. [DBO]. [SARGME]. [DateCol2] comme [S]. [DateCol2], 0)> = (48) ">

Et voici celui-ci sans DATEDIFF:

<Prédicat>
[. . [SARGME]. [DateCol1] comme [S]. [DateCol1]) ">

Aussi, j'ai trouvé des résultats légèrement meilleurs en termes de durée lorsque j'ai changé l'index en seulement -InclureDateCol2 (Et lorsque les deux index étaient présents, SQL Server a toujours choisi celui-ci avec une clé et une Inclure la colonne vs multi-clé). Pour cette requête, puisque nous devons scanner toutes les lignes pour trouver la plage de toute façon, il n'y a aucun avantage à avoir la colonne Deuxième date dans le cadre de la clé et triée de quelque manière que ce soit. Et pendant que je sais que nous ne pouvons pas obtenir une recherche ici, il y a quelque chose de sens intrinsèquement bon non Handing la capacité d'en obtenir une en forçant les calculs contre la colonne clé principale et ne les exécutant que contre secondaire. ou des colonnes incluses.

Si c'était moi, et j'ai abandonné la recherche de la solution sargable, je sais lequel je choisirais - celui qui rend SQL Server faire le moins de travail (même si le delta est presque inexistant). Ou mieux encore, je détendrais mes restrictions sur le changement de schéma et autres.

Et combien ça compte? Je ne sais pas. J'ai fait le tableau 10 millions de lignes et toutes les variations de requête ci-dessus sont encore terminées en moins d'une seconde. Et ceci est sur un VM sur un ordinateur portable (accordé, avec SSD).

9
Aaron Bertrand

Wiki communautaire Réponse ajouté à l'origine par l'auteur de la question comme édition de la question

Après avoir laissé cet asseoir un peu, et des personnes vraiment intelligentes qui ont chiming, ma première pensée à ce sujet semble correcte: il n'y a pas de moyen sens unique et sargable d'écrire cette requête sans ajouter une colonne, calculée ou maintenue via un autre mécanisme, à savoir déclenche.

J'ai essayé quelques autres choses et j'ai d'autres observations qui peuvent être intéressantes ou non pour que quiconque lisait.

Tout d'abord, reprogrammer la configuration à l'aide d'une table régulière plutôt que d'une table Temp.

  • Même si je connais leur réputation, je voulais essayer des statistiques multi-colonnes. Ils étaient inutiles.
  • Je voulais voir quelles statistiques ont été utilisées

Voici la nouvelle configuration:

USE [tempdb]
SET NOCOUNT ON  

DBCC FREEPROCCACHE

IF OBJECT_ID('tempdb..sargme') IS NOT NULL
BEGIN
DROP TABLE sargme
END

SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO sargme
FROM sys.[messages] AS [m]

ALTER TABLE [sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [sargme] ([DateCol1], [DateCol2])

CREATE STATISTICS [s_sargme] ON [sargme] ([DateCol1], [DateCol2])

Ensuite, exécutez la première requête, il utilise l'index ix_dates et analyse, tout comme avant. Pas de changement ici. Cela semble redondant, mais coller avec moi.

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [sargme] AS [s]
WHERE
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48

Exécutez la requête CTE à nouveau, toujours la même ...

WITH    [x] AS ( SELECT
                * ,
                DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
               FROM
                [sargme] AS [s])
     SELECT
        *
     FROM
        [x]
     WHERE
        [x].[ddif] >= 48;

Bien! Exécutez à nouveau la requête non demi-sargable:

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

Maintenant, ajoutez la colonne calculée et réexécutez les trois, ainsi que la requête qui frappe la colonne calculée:

ALTER TABLE [sargme] ADD [ddiff] AS 
DATEDIFF(DAY, DateCol1, DateCol2) PERSISTED

CREATE NONCLUSTERED INDEX [ix_dates2] ON [sargme] ([ddiff], [DateCol1], [DateCol2])

SELECT [s].[ID] ,
       [s].[DateCol1] ,
       [s].[DateCol2]
FROM [sargme] AS [s]
WHERE [ddiff] >= 48

Si vous êtes coincé avec moi ici, merci. C'est la partie d'observation intéressante du poste.

Exécution d'une requête avec un drapeau de trace non documenté par Fabiano amorim pour voir quelles statistiques utilisées chaque requête utilisée est assez cool. Voir que Aucun plan n'a touché un objet Statistiques jusqu'à ce que la colonne calculée ait été créée et indexée semblait impair.

What the bloodclot

Heck, même la requête qui a frappé la colonne calculée n'a pas touché un objet de statistiques tant que je ne l'ai pas rencontré à quelques reprises. Il a eu un paramétrage simple. Donc, même s'ils ont tous initialement analysé l'indice IX_DATES, ils ont utilisé des estimations de cardinalité codées durement (30% du tableau) plutôt que tout objet de statistiques à leur disposition.

Un autre point qui a soulevé un sourcil ici est que lorsque j'ai ajouté uniquement l'index non clusterisé, les plans de requête ont tous numérisé le tas, plutôt que d'utiliser l'index non cluster sur les deux colonnes de date.

Merci à tous ceux qui ont répondu. Tu es tout merveilleux.

3
Paul White 9