it-swarm.com.de

Holen Sie sich in jeder Gruppe die erste Zeile

Ich habe eine Tabelle, die für jede Gruppe den neuesten Eintrag erhalten soll. Hier ist die Tabelle:

DocumentStatusLogs Tabelle

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

Die Tabelle wird nach DocumentID gruppiert und nach DateCreated in absteigender Reihenfolge sortiert. Für jeden DocumentID möchte ich den neuesten Status erhalten. 

Meine bevorzugte Ausgabe:

| DocumentID | Status | DateCreated |
| 1          | S1     | 8/02/2011   |
| 2          | S3     | 8/01/2011   |
| 3          | S1     | 8/02/2011   |
  • Gibt es eine Aggregatfunktion, um von jeder Gruppe nur die Spitze zu erhalten? Siehe den Pseudo-Code GetOnlyTheTop unten:

    SELECT
      DocumentID,
      GetOnlyTheTop(Status),
      GetOnlyTheTop(DateCreated)
    FROM DocumentStatusLogs
    GROUP BY DocumentID
    ORDER BY DateCreated DESC
    
  • Wenn eine solche Funktion nicht vorhanden ist, kann ich dann die gewünschte Ausgabe erreichen?

  • Oder könnte dies an erster Stelle auf eine nicht normalisierte Datenbank zurückzuführen sein? Ich denke, da das, was ich suche, nur eine Zeile ist, sollte sich status auch in der übergeordneten Tabelle befinden?

Weitere Informationen finden Sie in der übergeordneten Tabelle:

Aktuelle Documents-Tabelle

| DocumentID | Title  | Content  | DateCreated |
| 1          | TitleA | ...      | ...         |
| 2          | TitleB | ...      | ...         |
| 3          | TitleC | ...      | ...         |

Soll die übergeordnete Tabelle so sein, damit ich leicht auf ihren Status zugreifen kann?

| DocumentID | Title  | Content  | DateCreated | CurrentStatus |
| 1          | TitleA | ...      | ...         | s1            |
| 2          | TitleB | ...      | ...         | s3            |
| 3          | TitleC | ...      | ...         | s1            |

UPDATE Ich habe gerade gelernt, wie man "Anwenden" verwendet, was es einfacher macht, solche Probleme anzugehen.

417
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

Wenn Sie 2 Einträge pro Tag erwarten, wird dies willkürlich ausgewählt. Um beide Einträge für einen Tag abzurufen, verwenden Sie stattdessen DENSE_RANK

Normalisiert oder nicht, es hängt davon ab, ob Sie:

  • status an 2 Stellen beibehalten
  • statushistorie erhalten
  • ...

In der jetzigen Form bewahren Sie die Statushistorie auf. Wenn Sie auch den neuesten Status in der übergeordneten Tabelle haben möchten (was Denormalisierung ist), benötigen Sie einen Auslöser, um den "Status" im übergeordneten Element zu erhalten. oder löschen Sie diese Statusverlaufstabelle.

616
gbn

Ich habe gerade gelernt, cross apply zu benutzen. So verwenden Sie es in diesem Szenario:

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

Ich habe einige Zeitvorgaben für die verschiedenen Empfehlungen gemacht, und die Ergebnisse hängen wirklich von der Größe der Tabelle ab. Die konsistenteste Lösung ist jedoch die Verwendung von CROSS APPLY 6.500 Datensätze und ein weiteres (identisches Schema) mit 137 Millionen Datensätzen. Die abgefragten Spalten sind Teil des Primärschlüssels der Tabelle und die Tabellenbreite ist sehr klein (etwa 30 Byte). Die Zeiten werden von SQL Server aus dem tatsächlichen Ausführungsplan gemeldet.

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

Ich denke, das wirklich Erstaunliche war, wie konstant die Zeit für das CROSS APPLY war, unabhängig von der Anzahl der beteiligten Zeilen.

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

Welcher Datenbankserver? Dieser Code funktioniert nicht bei allen.

In der zweiten Hälfte Ihrer Frage erscheint es mir sinnvoll, den Status als Spalte anzugeben. Sie können DocumentStatusLogs als Protokoll belassen, die neuesten Informationen jedoch weiterhin in der Haupttabelle speichern.

Übrigens, wenn Sie bereits die DateCreated-Spalte in der Documents-Tabelle haben, können Sie einfach DocumentStatusLogs verwenden, sofern Sie DateCreated in DocumentStatusLogs eindeutig ist.

Bearbeiten: MsSQL unterstützt USING nicht. Ändern Sie es daher in:

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

Wenn Sie sich Sorgen um die Leistung machen, können Sie dies auch mit MAX () tun:

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

ROW_NUMBER () erfordert eine Sortierung aller Zeilen in Ihrer SELECT-Anweisung, MAX dagegen nicht. Sollte Ihre Abfrage drastisch beschleunigen.

21
Daniel Cotter

Ich weiß, dass dies ein alter Thread ist, aber die TOP 1 WITH TIES-Lösungen sind ziemlich nett und könnten beim Durchlesen der Lösungen hilfreich sein.

select top 1 with ties
   DocumentID
  ,Status
  ,DateCreated
from DocumentStatusLogs
order by row_number() over (partition by DocumentID order by DateCreated desc)

Mehr zur TOP-Klausel finden Sie hier .

16
Josh Gilfillan

Dies ist ein ziemlich alter Faden, aber ich dachte, ich würde meine zwei Cents genauso einsetzen, wie die akzeptierte Antwort für mich nicht besonders gut funktionierte. Ich habe die Lösung von gbn mit einem großen Dataset ausprobiert und fand es äußerst langsam (> 45 Sekunden bei mehr als 5 Millionen Datensätzen in SQL Server 2012). Bei der Betrachtung des Ausführungsplans ist es offensichtlich, dass das Problem darin besteht, dass eine SORT-Operation erforderlich ist, die die Ausführung erheblich verlangsamt.

Hier ist eine Alternative, die ich aus dem Entity-Framework genommen habe, die keine SORT-Operation benötigt und eine Suche nach NON-Clustered Index durchführt. Dies reduziert die Ausführungszeit auf den oben genannten Datensatz auf <2 Sekunden.

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]

Nun gehe ich von etwas aus, das in der ursprünglichen Frage nicht vollständig angegeben ist, aber wenn Ihr Tabellendesign so ist, dass Ihre ID-Spalte eine Auto-Inkrement-ID ist und DateCreated bei jeder Einfügung auf das aktuelle Datum gesetzt wird, dann sogar Ohne die oben genannte Abfrage auszuführen, könnten Sie die Leistung von gbn (etwa die Hälfte der Ausführungszeit) um einen beträchtlichen Leistungsschub erhöhen, indem Sie einfach von auf ID bestellen statt auf DateCreated bestellen , da dies eine identische Sortierreihenfolge bietet und eine schnellere Reihenfolge ergibt Sortieren.

9
Clint

Mein Code zur Auswahl von Top 1 aus jeder Gruppe

wählen Sie einen. * aus #DocumentStatusLogs ein Where 
 datecreated in (Wählen Sie das erste Datum aus, das mit #DocumentStatusLogs erstellt wurde. b 
 wobei 
 a.documentid = b.documentid 
 Sortierung nach datecreated ab 
5
AnuPrakash

Dies ist eine der am leichtesten zu findenden Fragen zu diesem Thema, deshalb wollte ich eine moderne Antwort darauf geben (sowohl als Referenz als auch als Hilfe für andere). Durch Verwendung von "over" und "first value" können Sie die obige Abfrage schnell erledigen:

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

Dies sollte in SQL Server 2008 und höher funktionieren. Der erste Wert kann als eine Möglichkeit betrachtet werden, select top 1 zu erreichen, wenn eine over-Klausel verwendet wird. Over ermöglicht das Gruppieren in der Auswahlliste. Statt verschachtelte Unterabfragen zu schreiben (wie dies bei vielen vorhandenen Antworten der Fall ist), ist dies auf eine lesbarere Art und Weise möglich. Hoffe das hilft.

3
Randall

Clint's tolle und richtige Antwort wird von oben überprüft:

Die Leistung zwischen den beiden folgenden Abfragen ist interessant. 52% sind die Besten. Und 48% sind der zweite. Eine Verbesserung der Leistung um 4% mit DISTINCT anstelle von ORDER BY. ORDER BY hat jedoch den Vorteil, nach mehreren Spalten zu sortieren.

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

Option 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])

Option 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]

Management Studio von M $: Nachdem Sie den ersten Block markiert und ausgeführt haben, markieren Sie sowohl Option 1 als auch Option 2, Rechtsklick -> [Geschätzten Ausführungsplan anzeigen]. Führen Sie dann die gesamte Sache aus, um die Ergebnisse anzuzeigen.

Option 1 Ergebnisse:

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

Option 2 Ergebnisse:

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

Hinweis:

Ich neige dazu, APPLY zu verwenden, wenn ich möchte, dass ein Join eins zu eins (1 von vielen) ist.

Ich verwende einen JOIN, wenn der Join 1-zu-viele oder viele-zu-viele sein soll.

Ich vermeide CTE mit ROW_NUMBER (), es sei denn, ich muss etwas Fortgeschrittenes tun und bin mit der Leistungseinbußen bei der Fensterherstellung in Ordnung.

Ich vermeide auch EXISTS/IN-Unterabfragen in der WHERE- oder ON-Klausel, da ich erfahren habe, dass dies schreckliche Ausführungspläne zur Folge hatte. Die Laufleistung variiert jedoch. Überprüfen Sie den Ausführungsplan und die Profilleistung wo und wann erforderlich!

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 ;

Wenn Sie nur die letzte Dokumentreihenfolge von DateCreated zurückgeben möchten, wird nur das erste Dokument nach Dokument-ID zurückgegeben

2
cho

In Szenarien, in denen Sie die Verwendung von row_count () vermeiden möchten, können Sie auch einen linken Join verwenden:

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 

Für das Beispielschema können Sie auch eine "nicht in Unterabfrage" verwenden, die im Allgemeinen zu der gleichen Ausgabe wie der linke Join kompiliert wird: 

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)

Beachten Sie, dass das Unterabfragemuster nicht funktionieren würde, wenn die Tabelle nicht über mindestens einen einspaltigen eindeutigen Schlüssel/Einschränkung/Index verfügt, in diesem Fall den Primärschlüssel "Id".

Beide Abfragen sind tendenziell "teurer" als die Abfrage row_count () (gemessen mit dem Query Analyzer). Es kann jedoch vorkommen, dass Szenarien schneller Ergebnisse liefern oder andere Optimierungen ermöglichen.

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

Hier finden Sie 3 verschiedene Lösungsansätze für das vorliegende Problem sowie die besten Indizierungsoptionen für jede dieser Abfragen (probieren Sie die Indizes selbst aus und sehen Sie sich den logischen Lese-, Zeit- und Ausführungsplan an. Ich habe die Vorschläge aus meiner Erfahrung heraus geliefert solche Abfragen ohne Ausführung für dieses spezifische Problem).

Ansatz 1: Verwenden von ROW_NUMBER (). Wenn der Rowstore-Index die Leistung nicht verbessern kann, können Sie den Nonclustered/Clustered-Columnstore-Index ausprobieren. Dies gilt für Abfragen mit Aggregation und Gruppierung sowie für Tabellen, die ständig nach verschiedenen Spalten sortiert sind. In der Regel ist der Columnstore-Index die beste Wahl.

;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;

Ansatz 2: FIRST_VALUE verwenden. Wenn der Rowstore-Index die Leistung nicht verbessern kann, können Sie den Nonclustered/Clustered-Columnstore-Index ausprobieren. Dies gilt für Abfragen mit Aggregation und Gruppierung sowie für Tabellen, die ständig nach verschiedenen Spalten sortiert sind. In der Regel ist der Columnstore-Index die beste Wahl.

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;

Ansatz: Verwenden von CROSS APPLY. Das Erstellen eines Rowstore-Indexes für die DocumentStatusLogs-Tabelle, der die in der Abfrage verwendeten Spalten abdeckt, sollte ausreichen, um die Abfrage abzudecken, ohne dass ein Columnstore-Index erforderlich ist.

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

Versuche dies:

        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