web-dev-qa-db-ja.com

結合時に最大値を選択するためのクエリ


ユーザーの表があります:

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

カーソルを使わずにこれを行うにはどうすればよいかについて、アイデアや提案がありますか?

13
Lambo Jayapalan

既存のクエリは使用できるものに近いですが、いくつかの変更を加えることで簡単に結果を得ることができます。 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 |
15
Taryn

次のソリューションでは、Levelsテーブルを1回スキャンする共通テーブル式を使用しています。このスキャンでは、「次の」ポイントレベルはLEAD()ウィンドウ関数を使用して検出されるため、MinPoints(行から)とMaxPoints(次のMinPointsは現在のUserType)です。

その後、次のように、lvlsの共通テーブル式UserTypeMinPoints/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]);
3

初歩的な操作である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
SlowMagic

_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

2
shA.t