各グループの最新のエントリーを取得したいテーブルがあります。これがテーブルです:
DocumentStatusLogs
テーブル
|ID| DocumentID | Status | DateCreated |
| 2| 1 | S1 | 7/29/2011 |
| 3| 1 | S2 | 7/30/2011 |
| 6| 1 | S1 | 8/02/2011 |
| 1| 2 | S1 | 7/28/2011 |
| 4| 2 | S2 | 7/30/2011 |
| 5| 2 | S3 | 8/01/2011 |
| 6| 3 | S1 | 8/02/2011 |
テーブルはDocumentID
でグループ化され、降順でDateCreated
でソートされます。 DocumentID
ごとに、最新のステータスを取得したいです。
私の好みの出力:
| DocumentID | Status | DateCreated |
| 1 | S1 | 8/02/2011 |
| 2 | S3 | 8/01/2011 |
| 3 | S1 | 8/02/2011 |
各グループからトップのみを取得するための集計関数はありますか?以下の擬似コードGetOnlyTheTop
を参照してください。
SELECT
DocumentID,
GetOnlyTheTop(Status),
GetOnlyTheTop(DateCreated)
FROM DocumentStatusLogs
GROUP BY DocumentID
ORDER BY DateCreated DESC
そのような機能が存在しない場合、私が望む出力を達成することができる方法はありますか?
status
も親テーブルに入れるべきかと思います。詳細については、親の表を参照してください。
現在のDocuments
テーブル
| DocumentID | Title | Content | DateCreated |
| 1 | TitleA | ... | ... |
| 2 | TitleB | ... | ... |
| 3 | TitleC | ... | ... |
簡単にそのステータスにアクセスできるように、親テーブルはこのようにする必要がありますか?
| DocumentID | Title | Content | DateCreated | CurrentStatus |
| 1 | TitleA | ... | ... | s1 |
| 2 | TitleB | ... | ... | s3 |
| 3 | TitleC | ... | ... | s1 |
_ update _ このような問題への対処がより簡単になる「apply」の使い方を学びました。
;WITH cte AS
(
SELECT *,
ROW_NUMBER() OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC) AS rn
FROM DocumentStatusLogs
)
SELECT *
FROM cte
WHERE rn = 1
あなたが1日に2つのエントリを期待するなら、これは任意に1つを選ぶでしょう。 1日の両方のエントリを取得するには、代わりにDENSE_RANKを使用してください。
正規化されているかどうかに関しては、それがあなたがしたいかどうかによって異なります。
現状では、ステータスの履歴は保存されています。親テーブルに最新のステータス(非正規化)も必要な場合は、親のステータスを維持するためのトリガが必要です。またはこのステータス履歴テーブルを削除してください。
cross apply
の使い方を学びました。このシナリオでそれを使用する方法は次のとおりです。
select d.DocumentID, ds.Status, ds.DateCreated
from Documents as d
cross apply
(select top 1 Status, DateCreated
from DocumentStatusLogs
where DocumentID = d.DocumentId
order by DateCreated desc) as ds
ここではさまざまな推奨事項についていくつかのタイミングを取りましたが、結果は実際には関連するテーブルのサイズによって異なりますが、最も一貫した解決策はCROSS APPLYを使用することです。 6,500レコード、および1億3,700万レコードの別の(同一スキーマ)。照会されている列は表の主キーの一部であり、表の幅は非常に小さい(約30バイト)。時間は実際の実行計画からSQL Serverによって報告されます。
Query Time for 6500 (ms) Time for 137M(ms)
CROSS APPLY 17.9 17.9
SELECT WHERE col = (SELECT MAX(COL)…) 6.6 854.4
DENSE_RANK() OVER PARTITION 6.6 907.1
本当に驚くべきことは、含まれる行数に関係なく、CROSS APPLYの時間がどれほど一貫しているかだと思います。
SELECT * FROM
DocumentStatusLogs JOIN (
SELECT DocumentID, MAX(DateCreated) DateCreated
FROM DocumentStatusLogs
GROUP BY DocumentID
) max_date USING (DocumentID, DateCreated)
どのデータベースサーバー?このコードはそれらすべてに対して機能するわけではありません。
あなたの質問の後半については、ステータスをコラムとして含めることは私には合理的に思えます。 DocumentStatusLogs
をログとして残すことはできますが、それでもメインテーブルに最新の情報を格納します。
ところで、既にDocumentsテーブルにDateCreated
カラムがある場合は、それを使ってDocumentStatusLogs
を結合することができます(DateCreated
がDocumentStatusLogs
内で一意である限り)。
編集:MsSQLはUSINGをサポートしていないので、次のように変更してください。
ON DocumentStatusLogs.DocumentID = max_date.DocumentID AND DocumentStatusLogs.DateCreated = max_date.DateCreated
パフォーマンスが気になる場合は、MAX()でも可能です。
SELECT *
FROM DocumentStatusLogs D
WHERE DateCreated = (SELECT MAX(DateCreated) FROM DocumentStatusLogs WHERE ID = D.ID)
ROW_NUMBER()にはSELECTステートメント内のすべての行の並べ替えが必要ですが、MAXには必要ありません。劇的にあなたの質問をスピードアップするべきです。
私はこれが古いスレッドであることを知っています、しかしTOP 1 WITH TIES
解決策はとてもいいです、そして、解決策を通していくらかの読書に役に立つかもしれません。
select top 1 with ties
DocumentID
,Status
,DateCreated
from DocumentStatusLogs
order by row_number() over (partition by DocumentID order by DateCreated desc)
TOP句についての詳細は こちら をご覧ください。
これはかなり古いスレッドですが、受け入れられた答えが私にとって特にうまく機能しなかったのと同じように、2セントをスローすると思いました。私は大規模なデータセットに対してgbnの解決策を試してみましたが、それが非常に遅いことがわかりました(SQL Server 2012では、500万件以上のレコードで45秒を超える)。実行計画を見ると、問題はSORT操作を必要とするため、処理が大幅に遅くなることが明らかです。
これは、SORT操作を必要とせず、非クラスタ化インデックス検索を実行するエンティティフレームワークから引き上げた代替手段です。これにより、前述のレコードセットの実行時間が2秒未満に短縮されます。
SELECT
[Limit1].[DocumentID] AS [DocumentID],
[Limit1].[Status] AS [Status],
[Limit1].[DateCreated] AS [DateCreated]
FROM (SELECT DISTINCT [Extent1].[DocumentID] AS [DocumentID] FROM [dbo].[DocumentStatusLogs] AS [Extent1]) AS [Distinct1]
OUTER APPLY (SELECT TOP (1) [Project2].[ID] AS [ID], [Project2].[DocumentID] AS [DocumentID], [Project2].[Status] AS [Status], [Project2].[DateCreated] AS [DateCreated]
FROM (SELECT
[Extent2].[ID] AS [ID],
[Extent2].[DocumentID] AS [DocumentID],
[Extent2].[Status] AS [Status],
[Extent2].[DateCreated] AS [DateCreated]
FROM [dbo].[DocumentStatusLogs] AS [Extent2]
WHERE ([Distinct1].[DocumentID] = [Extent2].[DocumentID])
) AS [Project2]
ORDER BY [Project2].[ID] DESC) AS [Limit1]
今、私は元の質問で完全に指定されていない何かを仮定しています、しかしあなたのテーブルデザインがあなたのIDカラムが自動インクリメントIDであり、DateCreatedが各挿入で現在の日付に設定される上記のクエリで実行しなくても、 DateCreatedでの注文ではなくIDでの注文で gbnのソリューションのパフォーマンスを大幅に向上させることができます(実行時間の約半分)。ソート。
各グループからトップ1を選ぶための私のコード
#DocumentStatusLogsからa。*を選択します。 日付の作成日(#DocumentStatusLogs b から作成したトップ1の日付を選択します。 a.documentid = b.documentid の順序でdatecreated desc )
これはこのトピックで最も簡単に見つけられる質問の1つなので、私はそれに現代的な答えを出したいと思いました(私の参考のためにも、他の人を手助けするためにも)。 overとfirstの値を使うことで上記のクエリを短くすることができます。
select distinct DocumentID
, first_value(status) over (partition by DocumentID order by DateCreated Desc) as Status
, first_value(DateCreated) over (partition by DocumentID order by DateCreated Desc) as DateCreated
From DocumentStatusLogs
これは、SQL Server 2008以降で動作するはずです。最初の値は、over句を使用するときにselect top 1を達成する方法として考えることができます。 (多くの既存の答えがするように)入れ子になったサブクエリを書く代わりに、選択リストでグループ化を許します、これはより読みやすい方法でそれをします。お役に立てれば。
上からクリントの素晴らしいと正しい答えを検証する:
以下の2つのクエリ間のパフォーマンスは興味深いものです。 52%がトップです。そして48%が2番目です。 ORDER BYの代わりにDISTINCTを使用すると、パフォーマンスが4%向上します。しかし、ORDER BYには複数の列でソートするという利点があります。
IF (OBJECT_ID('tempdb..#DocumentStatusLogs') IS NOT NULL) BEGIN DROP TABLE #DocumentStatusLogs END
CREATE TABLE #DocumentStatusLogs (
[ID] int NOT NULL,
[DocumentID] int NOT NULL,
[Status] varchar(20),
[DateCreated] datetime
)
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (2, 1, 'S1', '7/29/2011 1:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (3, 1, 'S2', '7/30/2011 2:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (6, 1, 'S1', '8/02/2011 3:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (1, 2, 'S1', '7/28/2011 4:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (4, 2, 'S2', '7/30/2011 5:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (5, 2, 'S3', '8/01/2011 6:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (6, 3, 'S1', '8/02/2011 7:00:00')
オプション1:
SELECT
[Extent1].[ID],
[Extent1].[DocumentID],
[Extent1].[Status],
[Extent1].[DateCreated]
FROM #DocumentStatusLogs AS [Extent1]
OUTER APPLY (
SELECT TOP 1
[Extent2].[ID],
[Extent2].[DocumentID],
[Extent2].[Status],
[Extent2].[DateCreated]
FROM #DocumentStatusLogs AS [Extent2]
WHERE [Extent1].[DocumentID] = [Extent2].[DocumentID]
ORDER BY [Extent2].[DateCreated] DESC, [Extent2].[ID] DESC
) AS [Project2]
WHERE ([Project2].[ID] IS NULL OR [Project2].[ID] = [Extent1].[ID])
オプション2:
SELECT
[Limit1].[DocumentID] AS [ID],
[Limit1].[DocumentID] AS [DocumentID],
[Limit1].[Status] AS [Status],
[Limit1].[DateCreated] AS [DateCreated]
FROM (
SELECT DISTINCT [Extent1].[DocumentID] AS [DocumentID] FROM #DocumentStatusLogs AS [Extent1]
) AS [Distinct1]
OUTER APPLY (
SELECT TOP (1) [Project2].[ID] AS [ID], [Project2].[DocumentID] AS [DocumentID], [Project2].[Status] AS [Status], [Project2].[DateCreated] AS [DateCreated]
FROM (
SELECT
[Extent2].[ID] AS [ID],
[Extent2].[DocumentID] AS [DocumentID],
[Extent2].[Status] AS [Status],
[Extent2].[DateCreated] AS [DateCreated]
FROM #DocumentStatusLogs AS [Extent2]
WHERE [Distinct1].[DocumentID] = [Extent2].[DocumentID]
) AS [Project2]
ORDER BY [Project2].[ID] DESC
) AS [Limit1]
M $のManagement Studio:最初のブロックを強調表示して実行した後、オプション1とオプション2の両方を強調表示し、右クリック - > [推定実行計画の表示]を選択します。それから結果を見るために全部を実行してください。
オプション1の結果:
ID DocumentID Status DateCreated
6 1 S1 8/2/11 3:00
5 2 S3 8/1/11 6:00
6 3 S1 8/2/11 7:00
オプション2の結果:
ID DocumentID Status DateCreated
6 1 S1 8/2/11 3:00
5 2 S3 8/1/11 6:00
6 3 S1 8/2/11 7:00
注意:
結合を1対1にする場合はAPPLYを使用する傾向があります。
結合を1対多、または多対多にしたい場合は、JOINを使用します。
ROW_NUMBER()でCTEを回避するには、高度な操作が必要で、ウィンドウ処理のパフォーマンスが低下しても問題ありません。
また、WHERE句またはON句でEXISTS/IN副問合せを使用しないようにしています。これを経験したことがあるため、実行計画がいくつかひどくなります。しかし、走行距離は異なります。実行計画とプロファイルのパフォーマンスを必要な場所と時に見直してください。
SELECT o.*
FROM `DocumentStatusLogs` o
LEFT JOIN `DocumentStatusLogs` b
ON o.DocumentID = b.DocumentID AND o.DateCreated < b.DateCreated
WHERE b.DocumentID is NULL ;
DateCreatedによる最近のドキュメントの注文のみを返したい場合は、DocumentIDによるトップ1のドキュメントのみを返します。
Row_count()の使用を避けたいシナリオでは、左結合も使用できます。
select ds.DocumentID, ds.Status, ds.DateCreated
from DocumentStatusLogs ds
left join DocumentStatusLogs filter
ON ds.DocumentID = filter.DocumentID
-- Match any row that has another row that was created after it.
AND ds.DateCreated < filter.DateCreated
-- then filter out any rows that matched
where filter.DocumentID is null
サンプルのスキーマでは、 "not in subquery"を使用することもできます。これは通常、左結合と同じ出力にコンパイルされます。
select ds.DocumentID, ds.Status, ds.DateCreated
from DocumentStatusLogs ds
WHERE ds.ID NOT IN (
SELECT filter.ID
FROM DocumentStatusLogs filter
WHERE ds.DocumentID = filter.DocumentID
AND ds.DateCreated < filter.DateCreated)
テーブルに少なくとも1つのシングルカラムのユニークキー/制約/インデックスがない場合、サブクエリパターンは機能しません。
これら2つのクエリは、(Query Analyzerによる測定で)row_count()クエリよりも「高価」になる傾向があります。ただし、結果が早く返されたり、他の最適化が有効になったりするシナリオが発生する可能性があります。
SELECT doc_id,status,date_created FROM (
SELECT a.*,Row_Number() OVER(PARTITION BY doc_id ORDER BY date_created DESC ) AS rnk FROM doc a)
WHERE rnk=1;
問題に対する3つの個別のアプローチと、各クエリのインデックス作成の最良の選択肢を次に示します(インデックスを自分で試して、論理読み取り、経過時間、実行計画を確認してください。この特定の問題を実行せずにそのようなクエリ)。
アプローチ1:ROW_NUMBER()を使用します。行ストアインデックスがパフォーマンスを向上できない場合、非クラスター化/クラスター化列ストアインデックスを使用できます。集計とグループ化を使用したクエリ、および常に異なる列で並べ替えられるテーブルについては、通常、列ストアインデックスが最適です。
;WITH CTE AS
(
SELECT *,
RN = ROW_NUMBER() OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
FROM DocumentStatusLogs
)
SELECT ID
,DocumentID
,Status
,DateCreated
FROM CTE
WHERE RN = 1;
アプローチ2:FIRST_VALUEを使用します。行ストアインデックスがパフォーマンスを向上できない場合、非クラスター化/クラスター化列ストアインデックスを使用できます。集計とグループ化を使用したクエリ、および常に異なる列で並べ替えられるテーブルについては、通常、列ストアインデックスが最適です。
SELECT DISTINCT
ID = FIRST_VALUE(ID) OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
,DocumentID
,Status = FIRST_VALUE(Status) OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
,DateCreated = FIRST_VALUE(DateCreated) OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
FROM DocumentStatusLogs;
アプローチ:CROSS APPLYを使用します。クエリで使用される列をカバーするDocumentStatusLogsテーブルに行ストアインデックスを作成すれば、列ストアインデックスがなくてもクエリをカバーできます。
SELECT DISTINCT
ID = CA.ID
,DocumentID = D.DocumentID
,Status = CA.Status
,DateCreated = CA.DateCreated
FROM DocumentStatusLogs D
CROSS APPLY (
SELECT TOP 1 I.*
FROM DocumentStatusLogs I
WHERE I.DocumentID = D.DocumentID
ORDER BY I.DateCreated DESC
) CA;
これを試して:
SELECT [DocumentID],
[tmpRez].value('/x[2]','varchar(20)') as [Status],
[tmpRez].value('/x[3]','datetime') as [DateCreated]
FROM (
SELECT [DocumentID],
cast('<x>'+max(cast([ID] as varchar(10))+'</x><x>'+[Status]+'</x><x>'
+cast([DateCreated] as varchar(20)))+'</x>' as XML) as [tmpRez]
FROM DocumentStatusLogs
GROUP by DocumentID) as [tmpQry]