Abfrage zur Auswahl des Maximalwerts beim Beitritt

12


Ich habe eine Benutzerliste:

|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 irgendwelche Ideen oder Vorschläge, wie ich dies tun könnte, ohne auf Cursor zurückzugreifen?

Lambo Jayapalan
quelle

Antworten:

14

Ihre vorhandene Abfrage liegt 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 ändern, um den APPLYOperator zu verwenden und zu implementieren CROSS APPLY. Dies gibt die Zeile zurück, 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 eine SQL-Geige mit einer Demo . Dies ergibt ein Ergebnis:

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

Die folgende Lösung verwendet einen allgemeinen Tabellenausdruck, der die LevelsTabelle einmal durchsucht . In diesem Scan wird die "nächste" Punktestufe mit der LEAD()Fensterfunktion gefunden, so dass Sie MinPoints(aus der Reihe) und MaxPoints(die nächste) habenMinPoints für die aktuelle UserType) haben.

Danach können Sie einfach den allgemeinen Tabellenausdruck, verbinden lvls, auf UserTypeund MinPoints/ MaxPointsBereich, etwa so:

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 Verwendung der Fensterfunktion besteht darin, dass Sie alle Arten von rekursiven Lösungen eliminieren und die Leistung drastisch verbessern. Für die beste Leistung verwenden Sie den folgenden Index für die LevelsTabelle:

CREATE UNIQUE INDEX ... ON Levels (UserType, MinPoints) INCLUDE ([Level]);
Daniel Hutmacher
quelle
Danke für die schnelle Antwort. Ihre Anfrage liefert mir das genaue Ergebnis, das ich brauche, aber es scheint ein bisschen langsamer zu sein als die Antwort von bluefeet mit "CROSS APPLY". Für meinen spezifischen Datensatz dauert die Verwendung Ihres CTE ungefähr 10 Sekunden ohne Index und 7 Sekunden mit dem von Ihnen vorgeschlagenen Index für Ebenen, wohingegen die obige Cross Apply-Abfrage knapp 3 Sekunden dauert (auch ohne Index)
Lambo Jayapalan
@LamboJayapalan Diese Abfrage sollte mindestens so effizient sein wie die von bluefeet. Haben Sie genau diesen Index (mit dem INCLUDE) hinzugefügt ? Haben Sie auch einen Index Users (UserType, Points)? (Es könnte helfen)
ypercubeᵀᴹ
Und wie viele Benutzer (Zeilen in der Tabelle Users) gibt es und wie breit ist diese Tabelle?
ypercubeᵀᴹ
2

Verwenden Sie dazu nur die 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
;
SlowMagic
quelle
2

Ich denke, Sie können INNER JOINein Leistungsproblem verwenden, das Sie auch LEFT JOINstattdessen mit einer ROW_NUMBER()Funktion wie der folgenden verwenden können:

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

shA.t
quelle