web-dev-qa-db-fra.com

Le moyen le plus rapide pour obtenir les enregistrements les plus récents

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.

9
Pasca

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.

10
Aaron Bertrand

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:

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:

  1. Obtenez l'ensemble des identifiants uniques
  2. Trouvez la seule ligne que nous voulons pour chaque ID

L'indexation existante rend le plan d'exécution tout aussi simple:

Execution plan

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).

Résultats de test

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:

Parallel plan

Les résultats des tests sur ma machine pour les trois méthodes sont:

Results

Bien sûr, c'est un peu injuste envers le ROW_NUMBER, car l'indexation fournie n'est pas optimale pour cette solution.

9
Paul White 9

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;
1
Mister Magoo