web-dev-qa-db-fra.com

Requête pour sélectionner la valeur maximale lors de la jointure


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?

13
Lambo Jayapalan

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

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]);
3
Daniel Hutmacher

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

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;

SQL Fiddle Demo

2
shA.t