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

13


J'ai un tableau 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 procéder sans recourir à des curseurs?

Lambo Jayapalan
la source

Réponses:

15

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' APPLYopérateur et l'implémenter 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 |
Taryn
la source
3

La solution suivante utilise une expression de table commune qui analyse la Levelstable une fois. Dans cette analyse, le niveau de points "suivant" est trouvé en utilisant la LEAD()fonction de fenêtre, donc vous avez MinPoints(de la ligne) et MaxPoints(le suivant MinPointspour le courant UserType).

Après cela, vous pouvez simplement joindre l'expression de table commune,, lvlson UserTypeet 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 de 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 Levelstable:

CREATE UNIQUE INDEX ... ON Levels (UserType, MinPoints) INCLUDE ([Level]);
Daniel Hutmacher
la source
Merci pour la réponse rapide. Votre requête me donne le résultat exact dont j'ai besoin, mais il semble être un peu plus lent que la réponse de Bluefeet ci-dessus en utilisant "CROSS APPLY". Pour mon jeu de données spécifique, l'utilisation de votre CTE prend environ 10 secondes sans index et 7 secondes avec l'index que vous avez suggéré sur Levels, tandis que la requête Cross Apply ci-dessus prend un peu moins de 3 secondes (même sans index)
Lambo Jayapalan
@LamboJayapalan Cette requête semble être au moins aussi efficace que celle de Bluefeet. Avez-vous ajouté cet index exact (avec le INCLUDE)? De plus, avez-vous un index sur Users (UserType, Points)? (ça pourrait aider)
ypercubeᵀᴹ
Et combien d'utilisateurs (lignes dans le tableau Users) y a-t-il et quelle est la largeur de ce tableau?
ypercubeᵀᴹ
2

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

Je pense que vous pouvez utiliser un INNER JOINproblème de performance que vous pouvez également utiliser à la LEFT JOINplace avec une ROW_NUMBER()fonction comme celle-ci:

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;

Démo SQL Fiddle

shA.t
la source