Je recherche le moyen le plus rapide de renvoyer le record le plus récent pour chaque numéro de référence.
J'ai bien aimé la solution de BrentOzar.com , mais cela ne semble pas fonctionner lorsque j'ajoute une troisième condition (SequenceId). Cela semble fonctionner uniquement lorsque je spécifie l'ID et la date de création.
Pour comprendre mon problème, vous devrez créer l'exemple de table modifié, qui est essentiellement une copie de la table sur le site Web référencé ci-dessus, mais avec une petite touche.
CREATE TABLE [dbo].[TestTable](
[Id] [int] NOT NULL,
[EffectiveDate] [date] NOT NULL,
[SequenceId] [bigint] IDENTITY(1,1) NOT NULL,
[CustomerId] [varchar](50) NOT NULL,
[AccountNo] [varchar](50) NOT NULL,
CONSTRAINT [PK_TestTable] PRIMARY KEY CLUSTERED
(
[Id] ASC,
[EffectiveDate] ASC,
[SequenceId] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[TestTable] ON
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (1, CAST(0xDF300B00 AS Date), 1, N'Blue', N'Green')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (2, CAST(0xDF300B00 AS Date), 2, N'Yellow', N'Blue')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (1, CAST(0xE0300B00 AS Date), 3, N'Red', N'Yellow')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (3, CAST(0xE0300B00 AS Date), 4, N'Green', N'Purple')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (1, CAST(0xE1300B00 AS Date), 5, N'Orange', N'Purple')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (2, CAST(0xE3300B00 AS Date), 6, N'Blue', N'Orange')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (3, CAST(0xE6300B00 AS Date), 7, N'Red', N'Blue')
SET IDENTITY_INSERT [dbo].[TestTable] OFF
GO
Si j'exécute une requête similaire à celle du site Web, j'obtiens exactement le même résultat.
SELECT tt.*
FROM dbo.TestTable tt
LEFT OUTER JOIN dbo.TestTable ttNewer
ON tt.id = ttNewer.id AND tt.EffectiveDate < ttNewer.EffectiveDate
WHERE ttNewer.id IS NULL
Cependant, la petite torsion est que j'ai ajouté une colonne SequenceId au tableau comme vous l'avez peut-être remarqué. Le but de cette colonne est que le client peut vouloir faire une entrée postdatée pour une date dans le passé. Cette entrée doit remplacer les autres entrées faites à la même date qui est dans le passé. Si j'exécute la requête avant d'ajouter les entrées postdatées, j'obtiens le même résultat que précédemment.
SELECT tt.*
FROM dbo.TestTable tt
LEFT OUTER JOIN dbo.TestTable ttNewer
ON (
tt.id = ttNewer.id AND tt.EffectiveDate < ttNewer.EffectiveDate
AND tt.SequenceId < ttNewer.SequenceId
)
WHERE ttNewer.Id IS NULL
Si j'ajoute deux entrées postdatées comme indiqué ci-dessous, je commence à obtenir des résultats intéressants.
INSERT INTO TestTable(Id,EffectiveDate,CustomerId,AccountNo) values
(
2,'20090103','Blue','Orange'
);
INSERT INTO TestTable(Id,EffectiveDate,CustomerId,AccountNo) values
(
2,'20090105','Blue','Orange'
);
Ce que vous devez remarquer, c'est que les deux requêtes ci-dessous ne renvoient plus le dernier enregistrement, que j'utilise celui qui est similaire à ce qui se trouve sur votre site Web ou que j'utilise celui qui ajoute une autre condition (SequenceId)
SELECT tt.*
FROM dbo.TestTable tt
LEFT OUTER JOIN dbo.TestTable ttNewer
ON tt.id = ttNewer.id AND tt.EffectiveDate < ttNewer.EffectiveDate
WHERE ttNewer.id IS NULL
SELECT tt.*
FROM dbo.TestTable tt
LEFT OUTER JOIN dbo.TestTable ttNewer
ON (
tt.id = ttNewer.id AND tt.EffectiveDate < ttNewer.EffectiveDate
AND tt.SequenceId < ttNewer.SequenceId
)
WHERE ttNewer.Id IS NULL
Ce que j'aimerais que la requête fasse, c'est de renvoyer le dernier enregistrement pour un numéro de référence (Id) basé sur le dernier numéro de séquence pour un jour donné. En d'autres termes, l'enregistrement avec le dernier numéro de séquence sur la date effective la plus récente.
Les auto-jointures semblent bon marché avec un faible nombre de lignes, mais les E/S sont exponentielles lorsque le nombre de lignes augmente. Je préférerais résoudre cela de la manière CTE, sauf si vous êtes sur SQL Server 2000 (veuillez toujours spécifier la version que vous devez prendre en charge, en utilisant une balise spécifique à la version):
;WITH cte AS
(
SELECT Id, EffectiveDate, SequenceId, CustomerId, AccountNo,
rn = ROW_NUMBER() OVER (PARTITION BY Id
ORDER BY EffectiveDate DESC, SequenceId DESC)
FROM dbo.TestTable
)
SELECT Id, EffectiveDate, SequenceId, CustomerId, AccountNo
FROM cte
WHERE rn = 1
ORDER BY Id; -- because you can't rely on sorting without ORDER BY
Cela doit encore être analysé, mais il ne doit être analysé qu'une seule fois, par rapport à toutes les variantes d'auto-jointure, qui auront toujours deux analyses (ou potentiellement une analyse et une recherche exécutées plusieurs fois, avec de meilleurs index).
Si vous souhaitez une requête plus efficace (en éliminant un tri coûteux, au coût potentiel des écritures et peut-être d'autres requêtes qui n'ont pas besoin de prendre en charge ce tri), modifiez la clé primaire pour qu'elle corresponde au modèle de requête:
PRIMARY KEY CLUSTERED
(
[Id] ASC,
[EffectiveDate] DESC,
[SequenceId] DESC
)
La direction des colonnes secondaires n'a aucun effet sur l'unicité et devrait modifier les écritures de manière minimale tant que la table n'est pas très large et qu'il n'y a pas un nombre extrêmement élevé de lignes par identifiant.
La balise most-n-per-group contient un certain nombre de questions et réponses pertinentes pour ce type de problème, avec l'exemple canonique pour SQL Server:
Récupération de n lignes par groupe
Les deux options principales étant:
ROW_NUMBER
(comme dans réponse d'Aaron ); etAPPLY
Donc, alors que la question est très probablement un doublon de cela (du point de vue que les techniques de réponse sont les mêmes), voici une implémentation spécifique de le modèle de solution APPLY
pour votre problème particulier:
SELECT
CA.Id,
CA.EffectiveDate,
CA.SequenceId,
CA.CustomerId,
CA.AccountNo
FROM
(
-- Per Id
SELECT DISTINCT Id
FROM dbo.TestTable
) AS TT
CROSS APPLY
(
-- Single row with the highest EffectiveDate then SequenceId
SELECT TOP (1) TT2.*
FROM dbo.TestTable AS TT2
WHERE TT2.Id = TT.Id
ORDER BY TT2.EffectiveDate DESC, TT2.SequenceId DESC
) AS CA
ORDER BY
CA.Id;
La logique est assez simple:
L'indexation existante rend le plan d'exécution tout aussi simple:
Les résultats de cette forme de plan seront diffusés au client dès qu'ils seront disponibles (plutôt que d'un seul coup à la fin du traitement côté serveur). Le Stream Aggregate est le seul opérateur à blocage partiel dans le plan: il reçoit les lignes dans l'ordre des ID, donc dès que le deuxième ID est rencontré, l'agrégat peut renvoyer son premier résultat à la jointure des boucles imbriquées, et ainsi de suite.
L'index cluster est utile à la fois pour fournir des lignes dans le Stream Aggregate et pour une recherche très efficace d'une seule ligne par ID (dans l'ordre décroissant). Cela évite toute sorte de blocage inutile dans le plan. Cela devrait être une solution assez efficace, à moins qu'il y ait beaucoup d'ID, quelques lignes par ID en moyenne, et une indexation appropriée soit fournie pour une approche alternative.
Le ROW_NUMBER
la solution pourrait être tout aussi efficace - peut-être plus, selon la distribution des données - mais le processeur de requêtes SQL Server ne peut actuellement pas utiliser l'index fourni pour éviter un tri (bien qu'il le puisse logiquement).
Sur l'ensemble de données plus volumineux fourni utilement dans réponse de Monsieur Magoo , le plan d'exécution reste fondamentalement le même, mais utilise le parallélisme:
Les résultats des tests sur ma machine pour les trois méthodes sont:
Bien sûr, c'est un peu injuste envers le ROW_NUMBER
, car l'indexation fournie n'est pas optimale pour cette solution.
Une alternative que vous pourriez envisager est les groupes imbriqués, comme celui-ci.
select tt.Id, tt.EffectiveDate, tt.SequenceId, tt.CustomerId, tt.AccountNo
from dbo.TestTable tt
join (
-- Find maximum SequenceID for each maximum EffectiveDate for each Id
select it.id, it.EffDate, max(t1.SequenceId) SeqId
from dbo.TestTable t1
join (
-- Find maximum EffectiveDate for each Id
select t0.id, max(t0.EffectiveDate) EffDate
from dbo.TestTable t0
group by t0.id
) it
on t1.id = it.id
and t1.EffectiveDate = it.EffDate
group by it.id, it.EffDate
) tg
on tg.id = tt.id
and tg.EffDate = tt.EffectiveDate
and tg.SeqId = tt.SequenceId
order by tt.id;
Il a un plan assez différent de la méthode CTE/row_number, mais pourrait être plus efficace dans certains cas.
Dans le faisceau de test ci-dessous, la méthode des groupes imbriqués sort à une durée moyenne d'environ 300 ms pour une entrée de 1 M de lignes, tandis que le CTE/ROW_NUMBER sort à environ 600 ms pour les mêmes données d'entrée.
Maintenant, c'est un test (exécuté 10 fois) contre de fausses données, donc vos résultats réels seront différents, mais veuillez tester les deux méthodes pour voir celle qui convient le mieux à votre objectif.
Bien sûr, si, comme je le soupçonne, vous ciblez un ID particulier à chaque fois, plutôt que l'ensemble de la table, je suggérerais que le CTE d'Aaron est meilleur simplement pour la facilité de lecture/maintenance, et ils fonctionneront probablement tous les deux suffisamment rapidement pour faire ce choix simple.
Harnais de test:
USE tempdb;
GO
--== CREATE SOME TEST DATA IF WE DON'T ALREADY HAVE IT
IF OBJECT_ID('[dbo].[TestTable]') IS NULL
BEGIN
CREATE TABLE [dbo].[TestTable](
[Id] [int] NOT NULL,
[EffectiveDate] [date] NOT NULL,
[SequenceId] [bigint] IDENTITY(1,1) NOT NULL,
[CustomerId] [varchar](50) NOT NULL,
[AccountNo] [varchar](50) NOT NULL,
CONSTRAINT [PK_TestTable] PRIMARY KEY CLUSTERED
(
[Id] ASC,
[EffectiveDate] ASC,
[SequenceId] ASC
)
);
INSERT dbo.TestTable(Id, EffectiveDate, CustomerID, AccountNo)
SELECT TOP 1000000 abs(checksum(newid()))%1000, dateadd(day, abs(checksum(newid()))%1000, '1 jan 2009'), datename(dw, dateadd(day, abs(checksum(newid()))%1000, '1 jan 2009')), datename(month,dateadd(day, abs(checksum(newid()))%1000, '1 jan 2009'))
FROM sys.all_columns a1, sys.all_columns a2
--== UNCOMMENT TO CHECK THE SAMPLE DATA IS GOOD
--SELECT *
--FROM dbo.TestTable
--ORDER BY ID, EffectiveDate, SequenceId;
END
--== CREATE SOMEWHERE TO STORE THE TIMINGS
if object_id('tempdb..#results') is not null drop table #results;
create table #results(
name nvarchar(50) not null,
startTime datetime2 not null default(sysutcdatetime()),
endTime datetime2 null,
rows int null,
duration as (datediff(millisecond, startTime, endTime))
);
create clustered index #ix_results on #results(name, endTime);
go
--== CLEAN UP BEFORE EACH RUN
dbcc freeproccache;
dbcc dropcleanbuffers;
--== AARON'S CTE
go
declare @id int, @Ed date, @sid bigint, @cid varchar(50), @ano varchar(50);
insert #results(name) values('Aaron''s CTE');
;WITH cte AS
(
SELECT Id, EffectiveDate, SequenceId, CustomerId, AccountNo,
rn = ROW_NUMBER() OVER (PARTITION BY Id
ORDER BY EffectiveDate DESC, SequenceId DESC)
FROM dbo.TestTable
)
SELECT @id = Id, @Ed = EffectiveDate, @sid = SequenceId, @cid = CustomerId, @ano = AccountNo
FROM cte
WHERE rn = 1
ORDER BY Id; -- because you can't rely on sorting without ORDER BY
update #results set endTime = sysutcdatetime(), rows=@@rowcount where name='Aaron''s CTE' and endTime is null;
go 10
dbcc freeproccache;
dbcc dropcleanbuffers;
--== MAGOO'S NESTED GROUPS
go
declare @id int, @Ed date, @sid bigint, @cid varchar(50), @ano varchar(50);
insert #results(name) values('Magoo''s Nested Groups');
SELECT @id = tt.Id, @Ed = tt.EffectiveDate, @sid = tt.SequenceId, @cid = tt.CustomerId, @ano = tt.AccountNo
from dbo.TestTable tt
join (
-- Find maximum SequenceID for each maximum EffectiveDate for each Id
select it.id, it.EffDate, max(t1.SequenceId) SeqId
from dbo.TestTable t1
join (
-- Find maximum EffectiveDate for each Id
select t0.id, max(t0.EffectiveDate) EffDate
from dbo.TestTable t0
group by t0.id
) it
on t1.id = it.id
and t1.EffectiveDate = it.EffDate
group by it.id, it.EffDate
) tg
on tg.id = tt.id
and tg.EffDate = tt.EffectiveDate
and tg.SeqId = tt.SequenceId
order by tt.id;
update #results set endTime = sysutcdatetime(), rows=@@rowcount where name='Magoo''s Nested Groups' and endTime is null;
go 10
--== SUMMARISE THE RESULTS
select
name,
rows,
min(duration) as MinimumDuration,
max(duration) as MaximumDuration,
avg(duration) as AverageDuration
from #results
group by name, rows;