Quelle est la manière la plus efficace de récupérer des plages de dates avec une structure de table comme celle-ci?
create table SomeDateTable
(
id int identity(1, 1) not null,
StartDate datetime not null,
EndDate datetime not null
)
go
Supposons que vous souhaitiez une plage pour StartDate
et EndDate
. En d'autres termes, si StartDate
se situe entre @StartDateBegin
et @StartDateEnd
et EndDate
se situe entre @EndDateBegin
et @EndDateEnd
, alors, fais quelque chose.
Je sais qu'il y a plusieurs façons de s'y prendre, mais quel est le plus conseillé?
C'est un problème difficile à résoudre en général, mais il y a quelques choses que nous pouvons faire pour aider l'optimiseur à choisir un plan. Ce script crée une table avec 10 000 lignes avec une distribution pseudo-aléatoire connue de lignes pour illustrer:
CREATE TABLE dbo.SomeDateTable
(
Id INTEGER IDENTITY(1, 1) PRIMARY KEY NOT NULL,
StartDate DATETIME NOT NULL,
EndDate DATETIME NOT NULL
);
GO
SET STATISTICS XML OFF
SET NOCOUNT ON;
DECLARE
@i INTEGER = 1,
@s FLOAT = Rand(20120104),
@e FLOAT = Rand();
WHILE @i <= 10000
BEGIN
INSERT dbo.SomeDateTable
(
StartDate,
EndDate
)
VALUES
(
DATEADD(DAY, @s * 365, {d '2009-01-01'}),
DATEADD(DAY, @s * 365 + @e * 14, {d '2009-01-01'})
)
SELECT
@s = Rand(),
@e = Rand(),
@i += 1
END
La première question est de savoir comment indexer ce tableau. Une option consiste à fournir deux index sur les colonnes DATETIME
, afin que l'optimiseur puisse au moins choisir de rechercher sur StartDate
ou EndDate
.
CREATE INDEX nc1 ON dbo.SomeDateTable (StartDate, EndDate)
CREATE INDEX nc2 ON dbo.SomeDateTable (EndDate, StartDate)
Naturellement, les inégalités sur StartDate
et EndDate
signifient qu'une seule colonne dans chaque index peut prendre en charge une recherche dans l'exemple de requête, mais c'est à peu près le mieux que nous puissions faire. Nous pourrions envisager de faire de la deuxième colonne de chaque index un INCLUDE
plutôt qu'une clé, mais nous pourrions avoir d'autres requêtes qui peuvent effectuer une recherche d'égalité sur la colonne de tête et une recherche d'inégalité sur la deuxième colonne. De plus, nous pouvons obtenir de meilleures statistiques de cette façon. En tous cas...
DECLARE
@StartDateBegin DATETIME = {d '2009-08-01'},
@StartDateEnd DATETIME = {d '2009-10-15'},
@EndDateBegin DATETIME = {d '2009-08-05'},
@EndDateEnd DATETIME = {d '2009-10-22'}
SELECT
COUNT_BIG(*)
FROM dbo.SomeDateTable AS sdt
WHERE
sdt.StartDate BETWEEN @StartDateBegin AND @StartDateEnd
AND sdt.EndDate BETWEEN @EndDateBegin AND @EndDateEnd
Cette requête utilise des variables, donc en général l'optimiseur devine la sélectivité et la distribution, ce qui donne une estimation de cardinalité de 81 lignes . En fait, la requête produit 2076 lignes, une différence qui pourrait être importante dans un exemple plus complexe.
Sur SQL Server 2008 SP1 CU5 ou version ultérieure (ou R2 RTM CU1), nous pouvons profiter de Parameter Embedding Optimization pour obtenir de meilleures estimations, simplement en ajoutant la fonction OPTION (RECOMPILE)
à la requête SELECT
ci-dessus. Cela provoque une compilation juste avant l'exécution du lot, permettant à SQL Server de "voir" les valeurs réelles des paramètres et d'optimiser celles-ci. Avec cette modification, l'estimation passe à 468 lignes (bien que vous ayez besoin de vérifier le plan d'exécution pour le voir). Cette estimation est meilleure que 81 lignes, mais toujours pas si proche. les extensions de modélisation activées par indicateur de trace 2301 peuvent aider dans certains cas, mais pas avec cette requête.
Le problème est où les lignes qualifiées par les deux recherches de plage se chevauchent. L'une des hypothèses simplificatrices faites dans la composante d'estimation des coûts et de la cardinalité de l'optimiseur est que les prédicats sont indépendants (donc si les deux ont une sélectivité de 50%, le résultat de l'application des deux est supposé qualifier 50% de 50% = 25% des lignes ). Lorsque ce type de corrélation est un problème, nous pouvons souvent le contourner avec des statistiques multi-colonnes et/ou filtrées. Avec deux plages avec des points de début et de fin inconnus, cela devient impossible. C'est là que nous devons parfois recourir à la réécriture de la requête dans un formulaire qui produit une meilleure estimation:
SELECT COUNT(*) FROM
(
SELECT
sdt.Id
FROM dbo.SomeDateTable AS sdt
WHERE
sdt.StartDate BETWEEN @StartDateBegin AND @StartDateEnd
INTERSECT
SELECT
sdt.Id
FROM dbo.SomeDateTable AS sdt
WHERE
sdt.EndDate BETWEEN @EndDateBegin AND @EndDateEnd
) AS intersected (id)
OPTION (RECOMPILE)
Ce formulaire produit une estimation d'exécution de 2110 lignes (contre 2076 réelles). Sauf si vous avez TF 2301 allumé, auquel cas les techniques de modélisation les plus avancées voient à travers l'astuce et produisent exactement la même estimation qu'auparavant: 468 lignes.
Un jour, SQL Server peut obtenir une prise en charge native des intervalles. Si cela vient avec un bon support statistique, les développeurs pourraient redouter un peu moins les plans de requête de réglage comme celui-ci.
Je ne connais pas de solution rapide pour toutes les distributions de données, mais si toutes vos plages sont courtes, nous pouvons généralement l'accélérer. Si, par exemple, les plages sont inférieures à un jour, au lieu de cette requête:
SELECT TaskId ,
TaskDescription ,
StartedAt ,
FinishedAt
FROM dbo.Tasks
WHERE '20101203' BETWEEN StartedAt AND FinishedAt
nous pouvons ajouter une autre condition:
SELECT TaskId ,
TaskDescription ,
StartedAt ,
FinishedAt
FROM dbo.Tasks
WHERE '20101203' BETWEEN StartedAt AND FinishedAt
AND StartedAt >= '20101202'
AND FinishedAt <= '20101204' ;
Par conséquent, au lieu d'analyser l'intégralité de la table, la requête analysera uniquement la plage de deux jours, ce qui est plus rapide. Si les plages peuvent être plus longues, nous pouvons les stocker sous forme de séquences de plus courtes. Détails ici: Optimisation des requêtes SQL à l'aide de contraintes