web-dev-qa-db-ja.com

各グループのトップ1行を取得

各グループの最新のエントリーを取得したいテーブルがあります。これがテーブルです:

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
    
  • そのような機能が存在しない場合、私が望む出力を達成することができる方法はありますか?

  • あるいはそもそも、これは正規化されていないデータベースによって引き起こされるのでしょうか。探しているのは1行だけなので、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」の使い方を学びました。

438
dpp
;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を使用してください。

正規化されているかどうかに関しては、それがあなたがしたいかどうかによって異なります。

  • 2か所で地位を維持する
  • ステータス履歴を保存する
  • ...

現状では、ステータスの履歴は保存されています。親テーブルに最新のステータス(非正規化)も必要な場合は、親のステータスを維持するためのトリガが必要です。またはこのステータス履歴テーブルを削除してください。

634
gbn

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
141
dpp

ここではさまざまな推奨事項についていくつかのタイミングを取りましたが、結果は実際には関連するテーブルのサイズによって異なりますが、最も一貫した解決策は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の時間がどれほど一貫しているかだと思います。

45
John
SELECT * FROM
DocumentStatusLogs JOIN (
  SELECT DocumentID, MAX(DateCreated) DateCreated
  FROM DocumentStatusLogs
  GROUP BY DocumentID
  ) max_date USING (DocumentID, DateCreated)

どのデータベースサーバー?このコードはそれらすべてに対して機能するわけではありません。

あなたの質問の後半については、ステータスをコラムとして含めることは私には合理的に思えます。 DocumentStatusLogsをログとして残すことはできますが、それでもメインテーブルに最新の情報を格納します。

ところで、既にDocumentsテーブルにDateCreatedカラムがある場合は、それを使ってDocumentStatusLogsを結合することができます(DateCreatedDocumentStatusLogs内で一意である限り)。

編集:MsSQLはUSINGをサポートしていないので、次のように変更してください。

ON DocumentStatusLogs.DocumentID = max_date.DocumentID AND DocumentStatusLogs.DateCreated = max_date.DateCreated
26
Ariel

パフォーマンスが気になる場合は、MAX()でも可能です。

SELECT *
FROM DocumentStatusLogs D
WHERE DateCreated = (SELECT MAX(DateCreated) FROM DocumentStatusLogs WHERE ID = D.ID)

ROW_NUMBER()にはSELECTステートメント内のすべての行の並べ替えが必要ですが、MAXには必要ありません。劇的にあなたの質問をスピードアップするべきです。

22
Daniel Cotter

私はこれが古いスレッドであることを知っています、しかし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句についての詳細は こちら をご覧ください。

20
Josh Gilfillan

これはかなり古いスレッドですが、受け入れられた答えが私にとって特にうまく機能しなかったのと同じように、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のソリューションのパフォーマンスを大幅に向上させることができます(実行時間の約半分)。ソート。

9
Clint

各グループからトップ1を選ぶための私のコード

#DocumentStatusLogsからa。*を選択します。
日付の作成日(#DocumentStatusLogs b 
から作成したトップ1の日付を選択します。
 a.documentid = b.documentid 
の順序でdatecreated desc 
)
5
AnuPrakash

これはこのトピックで最も簡単に見つけられる質問の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を達成する方法として考えることができます。 (多くの既存の答えがするように)入れ子になったサブクエリを書く代わりに、選択リストでグループ化を許します、これはより読みやすい方法でそれをします。お役に立てれば。

5
Randall

上からクリントの素晴らしいと正しい答えを検証する:

以下の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副問合せを使用しないようにしています。これを経験したことがあるため、実行計画がいくつかひどくなります。しかし、走行距離は異なります。実行計画とプロファイルのパフォーマンスを必要な場所と時に見直してください。

2
TamusJRoyce
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のドキュメントのみを返します。

2
cho

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()クエリよりも「高価」になる傾向があります。ただし、結果が早く返されたり、他の最適化が有効になったりするシナリオが発生する可能性があります。

0
BitwiseMan
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;
0
praveen

問題に対する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;
0
san

これを試して:

        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]
0
gng