it-swarm.com.de

Abfrage zur Auswahl des Maximalwerts beim Join


Ich habe eine Tabelle mit Benutzern:

|Username|UserType|Points|
|John    |A       |250   |
|Mary    |A       |150   |
|Anna    |B       |600   |

und Ebenen

|UserType|MinPoints|Level  |
|A       |100      |Bronze |
|A       |200      |Silver |
|A       |300      |Gold   |
|B       |500      |Bronze |

Und ich suche nach einer Abfrage, um die Ebene für jeden Benutzer zu erhalten. Etwas in der Art von:

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

So dass die Ergebnisse wären:

|Username|UserType|Points|Level  |
|John    |A       |250   |Silver |
|Mary    |A       |150   |Bronze |
|Anna    |B       |600   |Bronze |

Hat jemand Ideen oder Vorschläge, wie ich dies tun könnte, ohne auf Cursor zurückzugreifen?

13
Lambo Jayapalan

Ihre vorhandene Abfrage befindet sich in der Nähe von etwas, das Sie verwenden könnten, aber Sie können das Ergebnis leicht erhalten, indem Sie einige Änderungen vornehmen. Indem Sie Ihre Abfrage so ändern, dass der Operator APPLY verwendet wird, und CROSS APPLY Implementieren. Dadurch wird die Zeile zurückgegeben, die Ihren Anforderungen entspricht. Hier ist eine Version, die Sie verwenden könnten:

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;

Hier ist ein SQL Fiddle mit einer Demo . Dies erzeugt ein Ergebnis:

| Username | UserType | Points |  Level |
|----------|----------|--------|--------|
|     John |        A |    250 | Silver |
|     Mary |        A |    150 | Bronze |
|     Anna |        B |    600 | Bronze |
15
Taryn

Die folgende Lösung verwendet einen allgemeinen Tabellenausdruck, der die Tabelle Levels einmal durchsucht. Bei diesem Scan wird die "nächste" Punktebene mithilfe der Fensterfunktion LEAD() ermittelt, sodass Sie MinPoints (aus der Zeile) und MaxPoints (die nächste) haben MinPoints für das aktuelle UserType).

Danach können Sie einfach den allgemeinen Tabellenausdruck lvls mit UserType und dem Bereich MinPoints/MaxPoints wie folgt verbinden:

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;

Der Vorteil der Fensterfunktion besteht darin, dass Sie alle Arten von rekursiven Lösungen eliminieren und die Leistung erheblich verbessern. Für eine optimale Leistung würden Sie den folgenden Index für die Tabelle Levels verwenden:

CREATE UNIQUE INDEX ... ON Levels (UserType, MinPoints) INCLUDE ([Level]);
3

Warum nicht nur mit den rudimentären Operationen INNER JOIN, GROUP BY und 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

Ich denke, Sie können einen INNER JOIN Verwenden - als Leistungsproblem können Sie stattdessen auch LEFT JOIN Verwenden - mit ROW_NUMBER() Funktion wie folgt:

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