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é?
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.
;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:
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.
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
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.
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
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.
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 .
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.
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 ).
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.
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!
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.
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.
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;
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;
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]