Consulta para seleccionar el valor máximo en unirse

13


Tengo una tabla de usuarios:

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

y niveles

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

Y estoy buscando una consulta para obtener el nivel para cada usuario. Algo en la línea 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

Tal que los resultados serían:

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

¿Alguien tiene alguna idea o sugerencia sobre cómo podría hacer esto sin recurrir a los cursores?

Lambo Jayapalan
fuente

Respuestas:

15

Su consulta existente está cerca de algo que podría usar, pero puede obtener el resultado fácilmente haciendo algunos cambios. Alterando su consulta para usar el APPLYoperador e implementando CROSS APPLY. Esto devolverá la fila que cumpla con sus requisitos. Aquí hay una versión que podría usar:

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;

Aquí hay un SQL Fiddle con una demostración . Esto produce un resultado:

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

La siguiente solución usa una expresión de tabla común que escanea la Levelstabla una vez. En esta exploración, el nivel de "siguiente" punto se encuentra utilizando la LEAD()función de ventana, por lo que tiene MinPoints(desde la fila) y MaxPoints(el siguiente MinPointspara el actual UserType).

Después de eso, simplemente puede unir la expresión de tabla común,, lvlson UserTypey MinPoints/ MaxPointsrange, así:

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;

La ventaja de usar la función de ventana es que elimina todo tipo de soluciones recursivas y mejora drásticamente el rendimiento. Para un mejor rendimiento, usaría el siguiente índice en la Levelstabla:

CREATE UNIQUE INDEX ... ON Levels (UserType, MinPoints) INCLUDE ([Level]);
Daniel Hutmacher
fuente
Gracias por la rápida respuesta. Su consulta me da el resultado exacto que necesito, pero parece ser un poco más lento que la respuesta anterior de bluefeet usando "APLICACIÓN CRUZADA". Para mi conjunto de datos específico, usar su CTE toma alrededor de 10 segundos sin un índice y 7 segundos con el índice que sugirió en los niveles, mientras que la consulta de aplicación cruzada anterior toma poco menos de 3 segundos (incluso sin un índice)
Lambo Jayapalan
@LamboJayapalan Parece que esta consulta debería ser al menos tan eficiente como la de bluefeet. ¿Agregaste este índice exacto (con el INCLUDE)? Además, ¿tienes un índice Users (UserType, Points)? (podría ayudar)
ypercubeᵀᴹ
¿Y cuántos usuarios (filas en la tabla Users) hay y qué ancho tiene esa tabla?
ypercubeᵀᴹ
2

¿Por qué no hacerlo utilizando solo las operaciones rudimentarias, INNER JOIN, GROUP BY y 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
fuente
2

Creo que puede usar un INNER JOIN-como un problema de rendimiento que también puede usar en su LEFT JOINlugar- con ROW_NUMBER()funciones como esta:

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;

Demostración de violín de SQL

shA.t
fuente