Sous-requête peu performante avec comparaisons de dates


15

Lorsque vous utilisez une sous-requête pour trouver le nombre total de tous les enregistrements précédents avec un champ correspondant, les performances sont terribles sur une table avec aussi peu que 50 000 enregistrements. Sans la sous-requête, la requête s'exécute en quelques millisecondes. Avec la sous-requête, le temps d'exécution est supérieur à une minute.

Pour cette requête, le résultat doit:

  • N'incluez que les enregistrements dans une plage de dates donnée.
  • Incluez un décompte de tous les enregistrements antérieurs, sans compter l'enregistrement en cours, quelle que soit la plage de dates.

Schéma de table de base

Activity
======================
Id int Identifier
Address varchar(25)
ActionDate datetime2
Process varchar(50)
-- 7 other columns

Exemples de données

Id  Address     ActionDate (Time part excluded for simplicity)
===========================
99  000         2017-05-30
98  111         2017-05-30
97  000         2017-05-29
96  000         2017-05-28
95  111         2017-05-19
94  222         2017-05-30

Résultats attendus

Pour la plage de dates de 2017-05-29à2017-05-30

Id  Address     ActionDate    PriorCount
=========================================
99  000         2017-05-30    2  (3 total, 2 prior to ActionDate)
98  111         2017-05-30    1  (2 total, 1 prior to ActionDate)
94  222         2017-05-30    0  (1 total, 0 prior to ActionDate)
97  000         2017-05-29    1  (3 total, 1 prior to ActionDate)

Les enregistrements 96 et 95 sont exclus du résultat, mais sont inclus dans la PriorCountsous - requête

Requête actuelle

select 
    *.a
    , ( select count(*) 
        from Activity
        where 
            Activity.Address = a.Address
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc

Indice actuel

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON [dbo].[Activity]
(
    [ActionDate] ASC
)
INCLUDE ([Address]) WITH (
    PAD_INDEX = OFF, 
    STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, 
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON
)

Question

  • Quelles stratégies pourraient être utilisées pour améliorer les performances de cette requête?

Edit 1
En réponse à la question de ce que je peux modifier sur la base de données: je peux modifier les index, mais pas la structure de la table.

Edit 2
J'ai maintenant ajouté un index de base sur la Addresscolonne, mais cela ne semble pas beaucoup s'améliorer. Je trouve actuellement de bien meilleures performances en créant une table temporaire et en insérant les valeurs sans le PriorCountpuis en mettant à jour chaque ligne avec leurs nombres spécifiques.

Edit 3
La bobine d'index Joe Obbish (réponse acceptée) trouvée était le problème. Une fois que j'en ai ajouté un nouveau nonclustered index [xyz] on [Activity] (Address) include (ActionDate), les temps de requête sont passés de plus d'une minute à moins d'une seconde sans utiliser de table temporaire (voir éditer 2).

Réponses:


17

Avec la définition d'index que vous avez pour IDX_my_nme, SQL Server pourra rechercher en utilisant la ActionDatecolonne mais pas avec la Addresscolonne. L'index contient toutes les colonnes nécessaires pour couvrir la sous-requête, mais il n'est probablement pas très sélectif pour cette sous-requête. Supposons que presque toutes les données du tableau aient une ActionDatevaleur antérieure à '2017-05-30'. Une recherche de ActionDate < '2017-05-30'retournera presque toutes les lignes de l'index, qui sont ensuite filtrées après que la ligne est extraite de l'index. Si votre requête renvoie 200 lignes, vous feriez probablement près de 200 analyses d'index complètes IDX_my_nme, ce qui signifie que vous lirez environ 50000 * 200 = 10 millions de lignes de l'index.

Il est probable que la recherche sur Addresssera beaucoup plus sélective pour votre sous-requête, bien que vous ne nous ayez pas donné d'informations statistiques complètes sur la requête, c'est donc une hypothèse de ma part. Cependant, supposons que vous créez un index sur juste Addresset que votre table ait 10k valeurs uniques pour Address. Avec le nouvel index, SQL Server n'aura besoin de rechercher que 5 lignes dans l'index pour chaque exécution de la sous-requête, vous lirez donc environ 200 * 5 = 1 000 lignes dans l'index.

Je teste contre SQL Server 2016, il peut donc y avoir quelques différences de syntaxe mineures. Voici quelques exemples de données dans lesquelles j'ai fait des hypothèses similaires à celles ci-dessus pour la distribution des données:

CREATE TABLE #Activity (
    Id int NOT NULL,
    [Address] varchar(25) NULL,
    ActionDate datetime2 NULL,
    FILLER varchar(100),
    PRIMARY KEY (Id)
);

INSERT INTO #Activity WITH (TABLOCK)
SELECT TOP (50000) -- 50k total rows
x.RN
, x.RN % 10000 -- 10k unique addresses
, DATEADD(DAY, x.RN / 100, '20160201') -- 100 rows per day
, REPLICATE('Z', 100)
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) x;

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([ActionDate] ASC) INCLUDE ([Address]);

J'ai créé votre index comme décrit dans la question. Je teste cette requête qui renvoie les mêmes données que celle de la question:

select 
    a.*
    , ( select count(*) 
        from #Activity Activity
        where 
            Activity.[Address] = a.[Address]
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from #Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc;

Je reçois une bobine d'index. Au niveau de base, cela signifie que l'optimiseur de requêtes crée un index temporaire à la volée car aucun des index existants sur la table ne convient.

bobine d'index

La requête se termine toujours rapidement pour moi. Peut-être que vous n'obtenez pas l'optimisation du spouleur d'index sur votre système ou qu'il y a quelque chose de différent dans la définition de la table ou la requête. À des fins éducatives, je peux utiliser une fonction non documentée OPTION (QUERYRULEOFF BuildSpool)pour désactiver la bobine d'indexation. Voici à quoi ressemble le plan:

mauvaise recherche d'index

Ne vous laissez pas berner par l'apparition d'une simple recherche d'index. SQL Server lit près de 10 millions de lignes de l'index:

10 millions de lignes de l'index

Si je vais exécuter la requête plusieurs fois, cela n'a probablement aucun sens pour l'optimiseur de requête de créer un index à chaque exécution. Je pourrais créer un index à l'avance qui serait plus sélectif pour cette requête:

CREATE NONCLUSTERED INDEX [IDX_my_nme_2] ON #Activity
([Address] ASC) INCLUDE (ActionDate);

Le plan est similaire à celui d'avant:

recherche d'index

Cependant, avec le nouvel index, SQL Server ne lit que 1 000 lignes à partir de l'index. 800 des lignes sont retournées pour être comptées. L'index pourrait être défini pour être plus sélectif, mais cela pourrait être suffisant en fonction de la distribution de vos données.

bonne recherche

Si vous n'êtes pas en mesure de définir des index supplémentaires sur la table, j'envisagerais d'utiliser des fonctions de fenêtre. Les éléments suivants semblent fonctionner:

SELECT t.*
FROM
(
    select 
        a.*
        , -1 + ROW_NUMBER() OVER (PARTITION BY [Address] ORDER BY ActionDate) PriorCount
    from #Activity a
) t
where t.ActionDate between '2017-05-29' and '2017-05-30'
order by t.ActionDate desc;

Cette requête effectue une seule analyse des données mais effectue un tri coûteux et calcule la ROW_NUMBER()fonction pour chaque ligne de la table, il semble donc qu'il y ait du travail supplémentaire à faire ici:

mauvais tri

Cependant, si vous aimez vraiment ce modèle de code, vous pouvez définir un index pour le rendre plus efficace:

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([Address], [ActionDate]) INCLUDE (FILLER);

Cela déplace le tri vers la fin qui sera beaucoup moins cher:

bon genre

Si rien de tout cela ne vous aide, vous devrez ajouter plus d'informations à la question, y compris de préférence des plans d'exécution réels.


1
La bobine d'index que vous avez trouvée était le problème. Une fois que j'en ai ajouté une nouvelle nonclustered index [xyz] on [Activity] (Address) include (ActionDate), les temps de requête sont passés de plus d'une minute à moins d'une seconde. +10 si je pouvais. Merci!
Metro Smurf
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.