web-dev-qa-db-ja.com

最新のレコードを取得する最速の方法

各参照番号の最新のレコードを返す最速の方法を探しています。

私は BrentOzar.comからのソリューション を非常に気に入っていましたが、3番目の条件(SequenceId)を追加すると機能しないようです。 Idと作成日を指定した場合にのみ機能するようです。

私の問題を理解するには、変更されたサンプルテーブルを作成する必要があります。これは、基本的に上記の参照Webサイトのテーブルのコピーですが、少しひねりを加えています。

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

Webサイトと同様のクエリを実行すると、まったく同じ結果が得られます。

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

ただし、少し気が付くと思いますが、テーブルにSequenceId列を追加しました。この列の目的は、クライアントが過去の日付の日付付きの投稿を行う可能性があるためです。このエントリは、過去の同じ日に行われた他のエントリに優先する必要があります。投稿日付のエントリを追加する前にクエリを実行すると、以前と同じ結果が得られます。

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

以下のように2つの投稿日付のエントリを追加すると、興味深い結果が得られます。

INSERT INTO TestTable(Id,EffectiveDate,CustomerId,AccountNo) values
(
      2,'20090103','Blue','Orange'
);

INSERT INTO TestTable(Id,EffectiveDate,CustomerId,AccountNo) values
(
      2,'20090105','Blue','Orange'
);

あなたが気づくべきことは、以下の2つのクエリが、あなたのウェブサイトにあるものと同様のものを使用するか、別の条件(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

クエリで実行したいのは、特定の日の最後のシーケンス番号に基づいて、参照番号(Id)の最後のレコードを返すことです。つまり、最新のEffectiveDateの最後のシーケンス番号を持つレコード。

9
Pasca

行数が少ないと自己結合は安っぽく見えますが、行数が増えるとI/Oは指数関数的になります。 SQL Server 2000を使用しているのでない限り、これを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, EffectiveDate, SequenceId, CustomerId, AccountNo
  FROM cte
  WHERE rn = 1
  ORDER BY Id; -- because you can't rely on sorting without ORDER BY

これはまだスキャンする必要がありますが、常に2つのスキャンがある(またはスキャンとシークが複数回実行され、インデックスが改善される可能性がある)すべての自己結合バリアントと比較して、1回スキャンするだけで済みます。

より効率的なクエリが必要な場合(高価なソートを削除し、書き込みの潜在的なコストを削減し、おそらくこのソートをサポートする必要のない他のクエリも)、クエリパターンに一致するように主キーを変更します。

PRIMARY KEY CLUSTERED 
(
  [Id] ASC,
  [EffectiveDate] DESC,
  [SequenceId] DESC
)

セカンダリ列の方向は一意性に影響を与えません。テーブルの幅がそれほど広くなく、IDあたりの行数が極端に多い場合を除いて、書き込みの変更は最小限に抑える必要があります。

10
Aaron Bertrand

greatest-n-per-group タグには、このタイプの問題に関連するいくつかの質問と回答があり、SQL Serverの標準的な例は次のとおりです。

グループごとにn行を取得

主なオプションは次の2つです。

したがって、質問はおそらくそれの複製です(回答テクニックは同じであるという観点から)、の具体的な実装は次のとおりです特定の問題のAPPLYソリューションパターン:

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;

ロジックはかなり単純です。

  1. 一意のIDのセットを取得する
  2. 各IDに必要な単一の行を見つけます

既存のインデックス付けにより、実行プランも同様にシンプルになります。

Execution plan

この計画の形からの結果は、利用可能になるとすぐにクライアントにストリーミングされます(サーバー側の処理の最後に一度にすべてではなく)。ストリーム集約は、計画内で唯一の部分的にブロックする演算子です。ID順に行を受け取るため、2番目のIDが検出されるとすぐに、集約は最初の結果をネストされたループ結合に返すことができます。

クラスタ化インデックスは、Stream Aggregateに順番に行を提供する場合と、IDごとに非常に効率的な単一行ルックアップ(降順)の両方に役立ちます。これにより、計画内の不要なブロッキングソートが回避されます。非常に多くのIDがなく、平均してIDあたりの行数が少ない場合を除いて、これは非常に効率的なソリューションです。代替アプローチとして適切なインデックスが提供されます。

ROW_NUMBERソリューションも同じように効率的である可能性があります(データ分散によってはより効率的かもしれません)が、SQL Serverクエリプロセッサは、提供されたインデックスを使用してソートを回避できません(論理的には可能です)。

試験結果

Mage Magooの回答 で提供されたより大きなデータセットでは、実行プランは基本的に同じままですが、並列処理を使用します。

Parallel plan

私のマシンでの3つの方法のテスト結果は次のとおりです。

Results

もちろん、これはROW_NUMBERメソッドに少し不公平です。なぜなら、提供されたインデックスはそのソリューションに最適ではないからです。

9
Paul White 9

あなたが検討するかもしれない代替案は、このようなネストされたグループです。

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;

CTE/row_numberメソッドとはかなり異なる計画がありますが、場合によってはより効率的です。

以下のテストハーネスでは、ネストされたグループのメソッドは、100万行の入力で約300ミリ秒の平均継続時間で出力されますが、CTE/ROW_NUMBERは同じ入力データで約600ミリ秒で出力されます。

これは、偽のデータに対する1回のテスト(10回実行)なので、実際の結果は異なりますが、両方の方法をテストして、目的に最適な方法を確認してください。

もちろん、私が疑っているように、テーブル全体ではなく、毎回1つの特定のIDをターゲットにしている場合、アーロンのCTEは、読み取り/保守を容易にするためだけに優れていることをお勧めします。その選択は簡単です。

テストハーネス:

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