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


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, на, UserTypeі MinPoints/ MaxPointsдіапазону, наприклад:

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]);

Дякуємо за швидку відповідь. Ваш запит дає мені точний результат, який мені потрібен, але він, здається, трохи повільніше, ніж відповідь синього фета вище, використовуючи "CROSS APPLY". Для мого конкретного набору даних, використання вашого CTE займає близько 10 секунд без індексу, і 7 секунд з індексом, який ви запропонували на рівнях, тоді як вищезазначений запит на крос-застосунок займає трохи менше 3 секунд (навіть без індексу)
Lambo Jayapalan

@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
;

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.