Запрос выбора максимального значения при соединении

13


У меня есть таблица пользователей:

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

и уровни

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

И я ищу запрос, чтобы получить уровень для каждого пользователя. Что-то вроде:

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

Так что результаты будут:

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

У кого-нибудь есть идеи или предложения о том, как я могу сделать это, не прибегая к курсорам?

Ламбо Джаяпалан
источник

Ответы:

15

Ваш существующий запрос близок к тому, что вы могли бы использовать, но вы можете легко получить результат, внеся несколько изменений. Путем изменения вашего запроса использовать APPLYоператор и реализовать CROSS APPLY. Это вернет строку, которая соответствует вашим требованиям. Вот версия, которую вы могли бы использовать:

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;

Вот SQL Fiddle с демонстрацией . Это дает результат:

| Username | UserType | Points |  Level |
|----------|----------|--------|--------|
|     John |        A |    250 | Silver |
|     Mary |        A |    150 | Bronze |
|     Anna |        B |    600 | Bronze |
Тарын
источник
3

В следующем решении используется общее табличное выражение, которое сканирует Levelsтаблицу один раз. В этом сканировании «следующий» уровень точек определяется с помощью LEAD()оконной функции, поэтому у вас есть MinPoints(из строки) и MaxPoints(следующий MinPointsдля текущего UserType).

После этого вы можете просто объединить общее табличное выражение lvls, on UserTypeи MinPoints/ MaxPointsrange, например:

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;

Преимущество использования оконной функции заключается в том, что вы исключаете все виды рекурсивных решений и значительно повышаете производительность. Для лучшей производительности вы должны использовать следующий индекс в Levelsтаблице:

CREATE UNIQUE INDEX ... ON Levels (UserType, MinPoints) INCLUDE ([Level]);
Даниэль Хутмахер
источник
Спасибо за быстрый ответ. Ваш запрос дает мне точный результат, который мне нужен, но он, кажется, немного медленнее, чем ответ bluefeet выше, используя "CROSS APPLY". Для моего конкретного набора данных использование вашего CTE занимает около 10 секунд без индекса и 7 секунд с индексом, который вы предложили для уровней, тогда как вышеупомянутый запрос перекрестного применения занимает чуть менее 3 секунд (даже без индекса)
Ламбо Джаяпалан,
@LamboJayapalan Этот запрос выглядит так, как минимум, так же эффективно, как и Bluefeet. Вы добавили этот точный индекс (с INCLUDE)? Кроме того, у вас есть индекс на Users (UserType, Points)? (это может помочь)
ypercubeᵀᴹ
А сколько пользователей (строк в таблице Users) и какова ширина этой таблицы?
ypercubeᵀᴹ
2

Почему бы не сделать это, используя только элементарные операции, INNER JOIN, GROUP BY и 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
источник
2

Я думаю, что вы можете использовать - INNER JOINкак проблему с производительностью, которую вы также можете использовать LEFT JOINвместо этого - с ROW_NUMBER()такой функцией:

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
источник