Consulta para selecionar o valor máximo na associação

12


Eu tenho uma tabela de usuários:

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

e Níveis

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

E estou procurando uma consulta para obter o nível de cada usuário. Algo ao longo das linhas 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

De tal forma que os resultados seriam:

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

Alguém tem alguma idéia ou sugestão de como eu poderia fazer isso sem recorrer a cursores?

Lambo Jayapalan
fonte

Respostas:

14

Sua consulta existente está próxima de algo que você poderia usar, mas você pode obter o resultado facilmente fazendo algumas alterações. Alterando sua consulta para usar o APPLYoperador e implementando CROSS APPLY. Isso retornará a linha que atende aos seus requisitos. Aqui está uma versão que você pode 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;

Aqui está um SQL Fiddle com uma demonstração . Isso produz um resultado:

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

A solução a seguir usa uma expressão de tabela comum que verifica a Levelstabela uma vez. Nesta varredura, o nível de "próximo" ponto é encontrado usando a LEAD()função de janela, então você tem MinPoints(da linha) e MaxPoints(o próximo MinPointsda corrente UserType).

Depois disso, você pode simplesmente associar a expressão de tabela comum,, lvlson UserTypee MinPoints/ MaxPointsrange, assim:

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;

A vantagem de usar a função window é que você elimina todos os tipos de soluções recursivas e melhora drasticamente o desempenho. Para obter o melhor desempenho, você usaria o seguinte índice na Levelstabela:

CREATE UNIQUE INDEX ... ON Levels (UserType, MinPoints) INCLUDE ([Level]);
Daniel Hutmacher
fonte
Obrigado pela resposta rápida. Sua consulta me fornece o resultado exato de que preciso, mas parece ser um pouco mais lento que a resposta da bluefeet acima usando "CROSS APPLY". Para o meu conjunto de dados específico, o uso do seu CTE leva cerca de 10 segundos sem índice e 7 segundos com o índice que você sugeriu nos Níveis, enquanto a consulta Cross Apply acima leva pouco menos de 3 segundos (mesmo sem índice)
Lambo Jayapalan
@LamboJayapalan Esta consulta parece ser pelo menos tão eficiente quanto a bluefeet. Você adicionou este índice exato (com o INCLUDE)? Além disso, você tem um índice Users (UserType, Points)? (que poderia ajudar)
ypercubeᵀᴹ
E quantos usuários (linhas na tabela Users) existem e qual a largura dessa tabela?
ypercubeᵀᴹ
2

Por que não fazê-lo usando apenas as operações rudimentares, 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
;
SlowMagic
fonte
2

Eu acho que você pode usar um INNER JOIN- como um problema de desempenho, você também pode usar LEFT JOIN- com uma ROW_NUMBER()função 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;

Demonstração do SQL Fiddle

shA.t
fonte