web-dev-qa-db-fra.com

Obtenez le premier rang de chaque groupe

J'ai une table dont je veux obtenir la dernière entrée pour chaque groupe. Voici la table:

DocumentStatusLogs Table

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

Le tableau sera groupé par DocumentID et trié par DateCreated par ordre décroissant. Pour chaque DocumentID, je souhaite obtenir le dernier statut. 

Ma sortie préférée:

| DocumentID | Status | DateCreated |
| 1          | S1     | 8/02/2011   |
| 2          | S3     | 8/01/2011   |
| 3          | S1     | 8/02/2011   |
  • Existe-t-il une fonction d'agrégation pour obtenir uniquement le sommet de chaque groupe? Voir le pseudo-code GetOnlyTheTop ci-dessous:

    SELECT
      DocumentID,
      GetOnlyTheTop(Status),
      GetOnlyTheTop(DateCreated)
    FROM DocumentStatusLogs
    GROUP BY DocumentID
    ORDER BY DateCreated DESC
    
  • Si une telle fonction n'existe pas, y a-t-il un moyen de réaliser le résultat souhaité?

  • Ou en premier lieu, cela pourrait-il être causé par une base de données non normalisée? Je pense que, puisque ce que je cherche, c’est une seule ligne, est-ce que status devrait également se trouver dans la table parente?

Veuillez consulter la table des parents pour plus d'informations:

Tableau Documents actuel

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

La table parente devrait-elle être ainsi pour que je puisse facilement accéder à son statut?

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

UPDATE Je viens d'apprendre à utiliser "apply", ce qui facilite la résolution de tels problèmes.

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

Si vous vous attendez à 2 entrées par jour, cela en choisira une arbitrairement. Pour obtenir les deux entrées d'un jour, utilisez DENSE_RANK à la place.

Que ce soit normalisé ou non, cela dépend si vous voulez:

  • maintenir le statut à 2 endroits
  • préserver l'historique du statut
  • ...

En l'état actuel, vous conservez l'historique du statut. Si vous voulez également le dernier statut de la table parent (dénormalisation), vous aurez besoin d'un déclencheur pour conserver "statut" dans le parent. ou supprimez cette table d'historique d'état.

616
gbn

Je viens d'apprendre à utiliser cross apply. Voici comment l'utiliser dans ce scénario:

 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

J'ai effectué quelques chronométrages sur les différentes recommandations ici, et les résultats dépendent vraiment de la taille de la table impliquée, mais la solution la plus cohérente consiste à utiliser l'application CROSS. 6 500 enregistrements et un autre (schéma identique) avec 137 millions d’enregistrements. Les colonnes interrogées font partie de la clé primaire de la table et la largeur de la table est très petite (environ 30 octets). Les heures sont signalées par SQL Server à partir du plan d'exécution réel.

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

Je pense que la chose vraiment étonnante était la constance du temps alloué pour l'application CROSS, quel que soit le nombre de lignes impliquées.

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

Quel serveur de base de données? Ce code ne fonctionne pas sur tous.

En ce qui concerne la seconde partie de votre question, il me semble raisonnable d’inclure le statut sous forme de colonne. Vous pouvez laisser DocumentStatusLogs en tant que journal, tout en conservant les dernières informations dans la table principale.

En passant, si vous avez déjà la colonne DateCreated dans la table des documents, vous pouvez simplement rejoindre DocumentStatusLogs en l’utilisant (tant que DateCreated est unique dans DocumentStatusLogs).

Edit: MsSQL ne supporte pas USING, donc changez le en:

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

Si vous êtes préoccupé par les performances, vous pouvez également le faire avec MAX ():

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

ROW_NUMBER () requiert une sorte de toutes les lignes de votre instruction SELECT, contrairement à MAX. Devrait accélérer considérablement votre requête.

21
Daniel Cotter

Je sais que c’est un vieux fil de discussion, mais les solutions TOP 1 WITH TIES sont assez jolies et pourraient être utiles pour lire certaines solutions.

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

Plus d'informations sur la clause TOP peuvent être trouvées ici .

16
Josh Gilfillan

C'est un sujet assez ancien, mais je pensais que je mettrais mes deux sous de la même manière car la réponse acceptée ne me convenait pas particulièrement bien. J'ai essayé la solution de gbn sur un grand ensemble de données et je l'ai trouvée terriblement lente (> 45 secondes sur plus de 5 millions d'enregistrements dans SQL Server 2012). En regardant le plan d'exécution, il est évident que le problème est qu'il nécessite une opération SORT qui ralentit considérablement les choses.

Voici une alternative que j'ai extraite du cadre d'entité qui ne nécessite aucune opération SORT et effectue une recherche d'index non clusterisé. Cela réduit le temps d'exécution à <2 secondes sur le jeu d'enregistrements susmentionné.

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]

Maintenant, je suppose que quelque chose n'est pas entièrement spécifié dans la question d'origine, mais si la structure de votre tableau est telle que votre colonne ID est un ID à incrémentation automatique et que DateCreated est défini sur la date actuelle à chaque insertion, alors même sans exécuter ma requête ci-dessus, vous pourriez réellement obtenir un gain de performances appréciable pour la solution de gbn (environ la moitié du temps d'exécution) simplement en commander sur ID au lieu de commander sur DateCreated car cela fournira un ordre de tri identique et plus rapide Trier.

9
Clint

Mon code pour sélectionner le top 1 de chaque groupe

sélectionnez un. * dans #DocumentStatusLogs un où 
 Date de création dans (sélectionnez la première des données à partir de #DocumentStatusLogs b 
 où 
 a.documentid = b.documentid 
 ordre par date-créé desc 
).
5
AnuPrakash

C'est l'une des questions les plus faciles à trouver sur le sujet. Je souhaitais donc donner une réponse moderne à la question (à la fois pour ma référence et pour aider les autres). En utilisant over et first valeur, vous pouvez simplifier la requête ci-dessus:

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

Cela devrait fonctionner dans SQL Server 2008 et versions ultérieures. La première valeur peut être considérée comme un moyen d'accomplir la sélection top 1 lors de l'utilisation d'une clause over. Over permet le regroupement dans la liste de sélection. Ainsi, au lieu d'écrire des sous-requêtes imbriquées (comme le font de nombreuses réponses existantes), cette opération est plus lisible. J'espère que cela t'aides.

3
Randall

Vérification de la réponse géniale et correcte de Clint vue du dessus:

La performance entre les deux requêtes ci-dessous est intéressante. 52% étant le premier. Et 48% étant le deuxième. Une amélioration de 4% des performances en utilisant DISTINCT au lieu de ORDER BY. Mais ORDER BY a l'avantage de trier sur plusieurs colonnes.

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]

M $ 's Management Studio: Après avoir sélectionné et exécuté le premier bloc, mettez en surbrillance les options 1 et 2, faites un clic droit -> [Afficher le plan d'exécution estimé]. Puis lancez la chose entière pour voir les résultats.

Option 1 Résultats:

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 Résultats:

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

Remarque:

J'ai tendance à utiliser APPLY lorsque je souhaite une jointure de 1 à 1.

J'utilise un JOIN si je veux que la jointure soit un à plusieurs ou plusieurs à plusieurs.

J'évite les CTE avec ROW_NUMBER () sauf si j'ai besoin de faire quelque chose de avancé et que la pénalité de performance est acceptable.

J'évite également les sous-requêtes EXISTS/IN de la clause WHERE ou ON, car cela a provoqué de terribles plans d'exécution. Mais le kilométrage varie. Passez en revue le plan d'exécution et les performances du profil où et quand vous en aurez besoin!

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 ;

Si vous souhaitez renvoyer uniquement l'ordre des documents récents par DateCreated, il ne renverra que le premier document par DocumentID.

2
cho

Dans les scénarios dans lesquels vous souhaitez éviter l'utilisation de row_count (), vous pouvez également utiliser une jointure gauche:

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 

Pour l'exemple de schéma, vous pouvez également utiliser un "pas dans la sous-requête", qui compile généralement à la même sortie que la jointure gauche: 

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)

Notez que le modèle de sous-requête ne fonctionnerait pas si la table n'avait pas au moins une clé/contrainte/index unique à une colonne, dans ce cas la clé primaire "Id".

Ces deux requêtes ont tendance à être plus "coûteuses" que la requête row_count () (mesurée par l'analyseur de requêtes). Cependant, vous pouvez rencontrer des scénarios dans lesquels ils renvoient les résultats plus rapidement ou activent d'autres optimisations.

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

Voici 3 approches distinctes du problème à résoudre ainsi que les meilleurs choix d’indexation pour chacune de ces requêtes (veuillez essayer les index vous-mêmes et voir la lecture logique, le temps écoulé, le plan d’exécution. J'ai fourni les suggestions de mon expérience sur ces requêtes sans s'exécuter pour ce problème spécifique).

approche 1: utilisation de ROW_NUMBER (). Si rowstore index ne peut pas améliorer les performances, vous pouvez essayer un index columnstore non clusterisé/en cluster, comme pour les requêtes avec agrégation et regroupement, et pour les tables ordonnées dans des colonnes différentes en tout temps. Index columnstore est généralement le meilleur choix.

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

approche 2: utilisation de FIRST_VALUE. Si rowstore index ne peut pas améliorer les performances, vous pouvez essayer un index columnstore non clusterisé/en cluster, comme pour les requêtes avec agrégation et regroupement, et pour les tables ordonnées dans des colonnes différentes en tout temps. Index columnstore est généralement le meilleur choix.

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;

approche: Utilisation de CROSS APPLY. La création d'un index rowstore sur la table DocumentStatusLogs couvrant les colonnes utilisées dans la requête devrait suffire à couvrir la requête sans avoir besoin d'un index columnstore.

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

Essaye ça:

        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