Interrogazione per selezionare il valore massimo sul join


13


Ho una tabella di utenti:

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

e livelli

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

E sto cercando una query per ottenere il livello per ogni utente. Qualcosa sulla falsariga di:

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

In modo tale che i risultati sarebbero:

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

Qualcuno ha idee o suggerimenti su come potrei farlo senza ricorrere ai cursori?

Risposte:


15

La tua query esistente è vicina a qualcosa che potresti usare ma puoi ottenere facilmente il risultato apportando alcune modifiche. Modificando la query per utilizzare l' APPLYoperatore e l'implementazione CROSS APPLY. Questo restituirà la riga che soddisfa i tuoi requisiti. Ecco una versione che potresti usare:

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;

Ecco un violino SQL con una demo . Questo produce un risultato:

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

3

La seguente soluzione utilizza un'espressione di tabella comune che esegue la scansione della Levelstabella una volta. In questa scansione, il livello di punti "successivo" viene trovato usando la LEAD()funzione finestra, quindi hai MinPoints(dalla riga) e MaxPoints(il prossimo MinPointsper l'attuale UserType).

Dopodiché, puoi semplicemente unire l'espressione di tabella comune lvls, on UserTypee range MinPoints/ MaxPoints, in questo modo:

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;

Il vantaggio dell'utilizzo della funzione finestra è l'eliminazione di ogni tipo di soluzione ricorsiva e il miglioramento delle prestazioni in modo significativo. Per prestazioni ottimali, utilizzare il seguente indice nella Levelstabella:

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

Grazie per la risposta rapida. La tua query mi dà il risultato esatto di cui ho bisogno, ma sembra essere un po 'più lento della risposta di bluefeet sopra usando "CROSS APPLY". Per il mio set di dati specifico, l'utilizzo del CTE richiede circa 10 secondi senza un indice e 7 secondi con l'indice suggerito sui livelli, mentre la query Cross Apply sopra richiede poco meno di 3 secondi (anche senza un indice)
Lambo Jayapalan,

@LamboJayapalan Questa query sembra che dovrebbe essere almeno efficiente come quella di bluefeet. Hai aggiunto questo indice esatto (con il INCLUDE)? Inoltre, hai un indice attivo Users (UserType, Points)? (potrebbe aiutare)
ypercubeᵀᴹ

E quanti utenti (righe nella tabella Users) ci sono e quanto è larga quella tabella?
ypercubeᵀᴹ

2

Perché non farlo usando solo le operazioni rudimentali, INNER JOIN, GROUP BY e 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

Penso che tu possa usare INNER JOIN-come un problema di prestazioni che puoi anche usare LEFT JOINinvece- con una ROW_NUMBER()funzione come questa:

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;

Demo SQL Fiddle

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.