J'ai une table des utilisateurs:
|Username|UserType|Points|
|John |A |250 |
|Mary |A |150 |
|Anna |B |600 |
et niveaux
|UserType|MinPoints|Level |
|A |100 |Bronze |
|A |200 |Silver |
|A |300 |Gold |
|B |500 |Bronze |
Et je recherche une requête pour obtenir le niveau pour chaque utilisateur. Quelque chose dans le sens de:
SELECT *
FROM Users U
INNER JOIN (
SELECT TOP 1 Level, U.UserName
FROM Levels L
WHERE L.MinPoints < U.Points
ORDER BY MinPoints DESC
) UL ON U.Username = UL.Username
Tels que les résultats seraient:
|Username|UserType|Points|Level |
|John |A |250 |Silver |
|Mary |A |150 |Bronze |
|Anna |B |600 |Bronze |
Quelqu'un a-t-il des idées ou des suggestions sur la façon de faire cela sans recourir à des curseurs?
Votre requête existante est proche de quelque chose que vous pourriez utiliser, mais vous pouvez obtenir le résultat facilement en apportant quelques modifications. En modifiant votre requête pour utiliser l'opérateur APPLY
et en implémentant CROSS APPLY
. Cela retournera la ligne qui répond à vos besoins. Voici une version que vous pourriez utiliser:
SELECT
u.Username,
u.UserType,
u.Points,
lv.Level
FROM Users u
CROSS APPLY
(
SELECT TOP 1 Level
FROM Levels l
WHERE u.UserType = l.UserType
and l.MinPoints < u.Points
ORDER BY l.MinPoints desc
) lv;
Voici un SQL Fiddle avec une démo . Cela produit un résultat:
| Username | UserType | Points | Level |
|----------|----------|--------|--------|
| John | A | 250 | Silver |
| Mary | A | 150 | Bronze |
| Anna | B | 600 | Bronze |
La solution suivante utilise une expression de table commune qui analyse une fois la table Levels
. Dans cette analyse, le niveau de points "suivant" est trouvé en utilisant la fonction de fenêtre LEAD()
, vous avez donc MinPoints
(à partir de la ligne) et MaxPoints
(le prochain MinPoints
pour l'actuel UserType
).
Après cela, vous pouvez simplement joindre l'expression de table commune, lvls
, sur UserType
et la plage MinPoints
/MaxPoints
, comme ceci:
WITH lvls AS (
SELECT UserType, MinPoints, [Level],
LEAD(MinPoints, 1, 99999) OVER (
PARTITION BY UserType
ORDER BY MinPoints) AS MaxPoints
FROM Levels)
SELECT U.*, L.[Level]
FROM Users AS U
INNER JOIN lvls AS L ON
U.UserType=L.UserType AND
L.MinPoints<=U.Points AND
L.MaxPoints> U.Points;
L'avantage de l'utilisation de la fonction fenêtre est que vous éliminez toutes sortes de solutions récursives et améliorez considérablement les performances. Pour de meilleures performances, vous utiliseriez l'index suivant sur la table Levels
:
CREATE UNIQUE INDEX ... ON Levels (UserType, MinPoints) INCLUDE ([Level]);
Pourquoi ne pas le faire en utilisant uniquement les opérations rudimentaires, INNER JOIN, GROUP BY et MAX:
SELECT U1.*,
L1.Level
FROM Users AS U1
INNER JOIN
(
SELECT U2.Username,
MAX(L2.MinPoints) AS QualifyingMinPoints
FROM Users AS U2
INNER JOIN
Levels AS L2
ON U2.UserType = L2.UserType
WHERE L2.MinPoints <= U2.Points
GROUP BY U2.Username
) AS Q
ON U1.Username = Q.Username
INNER JOIN
Levels AS L1
ON Q.QualifyingMinPoints = L1.MinPoints
AND U1.UserType = L1.UserType
;
Je pense que vous pouvez utiliser un INNER JOIN
-En tant que problème de performance, vous pouvez également utiliser LEFT JOIN
À la place- avec la fonction ROW_NUMBER()
comme ceci:
SELECT
Username, UserType, Points, Level
FROM (
SELECT u.*, l.Level,
ROW_NUMBER() OVER (PARTITION BY u.Username ORDER BY l.MinPoints DESC) seq
FROM
Users u INNER JOIN
Levels l ON u.UserType = l.UserType AND u.Points >= l.MinPoints
) dt
WHERE
seq = 1;