Requête SQL pour get le dernier prix

J'ai une table contenant des prix pour beaucoup de "choses" différentes dans une table MS SQL 2005. Il y a des centaines d'loggings par chose par jour et les différentes choses obtiennent des mises à jour des prix à différents moments.

ID uniqueidentifier not null, ThingID int NOT NULL, PriceDateTime datetime NOT NULL, Price decimal(18,4) NOT NULL 

Je dois get les derniers prix d'aujourd'hui pour un groupe de choses. La requête ci-dessous fonctionne mais je récupère des centaines de lignes et je dois les faire défiler et seulement extraire la dernière par ThingID. Comment puis-je (par exemple via un GROUP BY) dire que je veux le dernier par ThingID? Ou devrais-je utiliser des sous-requêtes?

 SELECT * FROM Thing WHERE ThingID IN (1,2,3,4,5,6) AND PriceDate > cast( convert(varchar(20), getdate(), 106) as DateTime) 

UPDATE: Dans une tentative de cacher la complexité, j'ai mis la colonne ID dans un int. Dans la vie réelle, il est GUID (et pas le type séquentiel). J'ai mis à jour la table def ci-dessus pour utiliser uniqueidentifier.

Je pense que la seule solution avec votre structure de table est de travailler avec une sous-requête:

 SELECT * FROM Thing WHERE ID IN (SELECT max(ID) FROM Thing WHERE ThingID IN (1,2,3,4) GROUP BY ThingID) 

(Étant donné le plus haut ID signifie également le prix le plus récent)

Cependant, je vous suggère d'append une colonne "IsCurrent" qui est 0 si ce n'est pas le dernier prix ou 1 si c'est le dernier. Cela appenda le risque possible de données incohérentes, mais cela accélérera beaucoup tout le process lorsque la table s'agrandira (si elle est dans un index). Alors tout ce que vous devez faire est de …

 SELECT * FROM Thing WHERE ThingID IN (1,2,3,4) AND IsCurrent = 1 

METTRE À JOUR

Ok, Markus a mis à jour la question pour montrer que l'ID est un ID unique, pas un int. Cela rend l'écriture de la requête encore plus complexe.

 SELECT T.* FROM Thing T JOIN (SELECT ThingID, max(PriceDateTime) WHERE ThingID IN (1,2,3,4) GROUP BY ThingID) X ON X.ThingID = T.ThingID AND X.PriceDateTime = T.PriceDateTime WHERE ThingID IN (1,2,3,4) 

Je suggère vraiment d'utiliser soit une colonne "IsCurrent" ou aller avec l'autre suggestion trouvée dans les réponses et d'utiliser le tableau "prix actuel" et un tableau séparé "historique des prix" (qui serait finalement le plus rapide, car il maintient le prix table elle-même petite).

(Je sais que le ThingID en bas est redondant. Essayez simplement s'il est plus rapide avec ou sans "WHERE".) Vous ne savez pas quelle version sera la plus rapide après que l'optimiseur a fait son travail.)

Si l'itinéraire de la sous-requête était trop lent, je considérerais le traitement de vos mises à jour de prix comme un journal d'audit et la gestion d'une table ThingPrice – peut-être comme triggersur sur la table des mises à jour des prix:

 ThingID int not null, UpdateID int not null, PriceDateTime datetime not null, Price decimal(18,4) not null 

La key primaire serait juste ThingID et "UpdateID" est le "ID" dans votre table d'origine.

Je voudrais essayer quelque chose comme la sous-requête suivante et oublier de modifier vos data structures.

 SELECT * FROM Thing WHERE (ThingID, PriceDateTime) IN (SELECT ThingID, max(PriceDateTime ) FROM Thing WHERE ThingID IN (1,2,3,4) GROUP BY ThingID ) 

Modifier ce qui précède est ANSI SQL et je devine maintenant avoir plus d'une colonne dans une sous-requête ne fonctionne pas pour T SQL. Marius, je ne peux pas tester ce qui suit mais essayer;

 SELECT p.* FROM Thing p, (SELECT ThingID, max(PriceDateTime ) FROM Thing WHERE ThingID IN (1,2,3,4) GROUP BY ThingID) m WHERE p.ThingId = m.ThingId and p.PriceDateTime = m.PriceDateTime 

une autre option pourrait être de changer la date en string et de la concaténer avec l'identifiant de sorte que vous n'ayez qu'une seule colonne. Ce serait un peu méchant cependant.

Puisque vous utilisez SQL Server 2005, vous pouvez utiliser la nouvelle clause (CROSS | OUTTER) APPLY. La clause APPLY vous permet de join une table avec une fonction de valeur de table.

Pour résoudre le problème, définissez d'abord une fonction de valeur de table afin de récupérer les n premières lignes à partir de Thing pour un ID spécifique, date de la command:

 CREATE FUNCTION dbo.fn_GetTopThings(@ThingID AS GUID, @n AS INT) RETURNS TABLE AS RETURN SELECT TOP(@n) * FROM Things WHERE ThingID= @ThingID ORDER BY PriceDateTime DESC GO 

puis utilisez la fonction pour récupérer les premiers loggings dans une requête:

 SELECT * FROM Thing t CROSS APPLY dbo.fn_GetTopThings(t.ThingID, 1) WHERE t.ThingID IN (1,2,3,4,5,6) 

La magie ici est faite par la clause APPLY qui applique la fonction à chaque ligne du jeu de résultats de gauche puis se joint à l'set de résultats renvoyé par la fonction puis retape l'set de résultats final. (Note: pour faire une jointure à gauche comme applique, utilisez OUTTER APPLY qui returnne toutes les lignes du côté gauche, alors que CROSS APPLY returnne uniquement les lignes qui ont une correspondance dans le côté droit)

BlaM: Parce que je ne peux pas encore postr de commentaires (en raison de points de rept bas) même pas à mes propres réponses ^^, je répondrai dans le corps du message: -la clause APPLY même, si elle utilise des fonctions de table est optimisé en interne par SQL Server de telle sorte qu'il n'appelle pas la fonction pour chaque ligne dans le jeu de résultats de gauche, mais prend le sql interne de la fonction et le convertit en une clause de jointure avec le rest de la requête, donc la performance est équivalente ou même meilleure (si le plan est choisi par sql server et d'autres optimizations peuvent être faites) que la performance d'une requête utilisant des sous-requêtes), et dans mon expérience personnelle APPLY n'a pas de problèmes de performance quand la database est correctement indexé et les statistics sont à jour (tout comme une requête normale avec sous-requêtes se comporte dans de telles conditions)

Cela dépend de la manière dont vos données seront utilisées, mais si les anciennes données de prix ne seront pas utilisées presque aussi régulièrement que datatables de prix actuelles, il peut y avoir un argument ici pour un tableau d'historique des prix. De cette façon, datatables non courantes peuvent être archivées dans la table d'historique des prix (probablement par des triggersurs) au fur et à mesure que les nouveaux prix entrent en vigueur.

Comme je l'ai dit, selon votre model d'access, cela pourrait être une option.

Je convertis l'identifiant unique en un binary afin que je puisse en get un MAX. Cela devrait vous assurer que vous n'obtiendrez pas de duplicates à partir de plusieurs loggings avec des ThingID identiques et PriceDateTimes:

 SELECT * FROM Thing WHERE CONVERT(BINARY(16),Thing.ID) IN ( SELECT MAX(CONVERT(BINARY(16),Thing.ID)) FROM Thing INNER JOIN (SELECT ThingID, MAX(PriceDateTime) LatestPriceDateTime FROM Thing WHERE PriceDateTime >= CAST(FLOOR(CAST(GETDATE() AS FLOAT)) AS DATETIME) GROUP BY ThingID) LatestPrices ON Thing.ThingID = LatestPrices.ThingID AND Thing.PriceDateTime = LatestPrices.LatestPriceDateTime GROUP BY Thing.ThingID, Thing.PriceDateTime ) AND Thing.ThingID IN (1,2,3,4,5,6) 

Comme ID n'est pas séquentiel, je suppose que vous avez un index unique sur ThingID et PriceDateTime, donc un seul prix peut être le plus récent pour un article donné.

Cette requête obtiendra tous les éléments de la list SI ils ont été évalués aujourd'hui. Si vous supprimez la clause where pour PriceDate, vous obtiendrez le dernier prix indépendamment de la date.

 SELECT * FROM Thing thi WHERE thi.ThingID IN (1,2,3,4,5,6) AND thi.PriceDateTime = (SELECT MAX(maxThi.PriceDateTime) FROM Thing maxThi WHERE maxThi.PriceDateTime >= CAST( CONVERT(varchar(20), GETDATE(), 106) AS DateTime) AND maxThi.ThingID = thi.ThingID) 

Notez que j'ai changé ">" en "> =" car vous pourriez avoir un prix juste au début d'une journée

Essayez ceci (à condition que vous n'ayez besoin que du dernier prix , pas de l'identifiant ou du datetime de ce prix)

 SELECT ThingID, (SELECT TOP 1 Price FROM Thing WHERE ThingID = T.ThingID ORDER BY PriceDateTime DESC) Price FROM Thing T WHERE ThingID IN (1,2,3,4) AND DATEDIFF(D, PriceDateTime, GETDATE()) = 0 GROUP BY ThingID 

Il doit fonctionner sans utiliser de colonne PK globale (pour les keys primaires complexes par exemple):

 SELECT t1.*, t2.PriceDateTime AS bigger FROM Prices t1 LEFT JOIN Prices t2 ON t1.ThingID = t2.ThingID AND t1.PriceDateTime < t2.PriceDateTime HAVING t2.PriceDateTime IS NULL 

Peut-être que j'ai mal compris les choses, mais qu'en est-il un:

SELECT ID, ThingID, max(PriceDateTime), Price FROM Thing GROUP BY ThingID