Kueri untuk memilih nilai maksimum saat bergabung

12


Saya punya tabel Pengguna:

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

dan Level

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

Dan saya mencari kueri untuk mendapatkan level untuk setiap pengguna. Sesuatu di sepanjang garis:

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

Sedemikian rupa sehingga hasilnya akan:

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

Adakah yang punya ide atau saran tentang bagaimana saya bisa melakukan ini tanpa menggunakan kursor?

Lambo Jayapalan
sumber

Jawaban:

14

Permintaan Anda saat ini dekat dengan sesuatu yang bisa Anda gunakan tetapi Anda bisa mendapatkan hasilnya dengan mudah dengan membuat beberapa perubahan. Dengan mengubah kueri Anda untuk menggunakan APPLYoperator dan implementasi CROSS APPLY. Ini akan mengembalikan baris yang memenuhi persyaratan Anda. Ini versi yang bisa Anda gunakan:

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;

Ini SQL Fiddle dengan demo . Ini menghasilkan hasil:

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

Solusi berikut menggunakan ekspresi tabel umum yang memindai Levelstabel sekali. Dalam pemindaian ini, level poin "berikutnya" ditemukan menggunakan LEAD()fungsi jendela, jadi Anda memiliki MinPoints(dari baris) dan MaxPoints(berikutnya MinPointsuntuk saat ini UserType).

Setelah itu, Anda cukup bergabung dengan ekspresi tabel umum lvls,, pada UserTypedan MinPoints/ MaxPointsrange, seperti:

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;

Keuntungan menggunakan fungsi jendela adalah Anda menghilangkan segala macam solusi rekursif dan meningkatkan kinerja secara dramatis. Untuk kinerja terbaik, Anda akan menggunakan indeks berikut di Levelstabel:

CREATE UNIQUE INDEX ... ON Levels (UserType, MinPoints) INCLUDE ([Level]);
Daniel Hutmacher
sumber
Terima kasih atas tanggapan cepatnya. Kueri Anda memang memberi saya hasil persis yang saya butuhkan, tetapi tampaknya sedikit lebih lambat daripada jawaban bluefeet di atas menggunakan "LINTAS BERLAKU". Untuk dataset khusus saya, menggunakan CTE Anda membutuhkan waktu sekitar 10 detik tanpa indeks, dan 7 detik dengan indeks yang Anda sarankan pada Levels, sedangkan permintaan Cross Apply di atas hanya membutuhkan waktu di bawah 3 detik (bahkan tanpa indeks)
Lambo Jayapalan
@LamboJayapalan Permintaan ini sepertinya harus setidaknya seefisien bluefeet. Apakah Anda menambahkan indeks persis ini (dengan INCLUDE)? Juga, apakah Anda memiliki indeks Users (UserType, Points)? (mungkin membantu)
ypercubeᵀᴹ
Dan berapa banyak pengguna (baris dalam tabel Users) di sana dan seberapa lebar tabel itu?
ypercubeᵀᴹ
2

Mengapa tidak hanya menggunakan operasi yang belum sempurna, INNER JOIN, GROUP BY, dan 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
sumber
2

Saya pikir Anda dapat menggunakan INNER JOIN-sebagai masalah kinerja Anda juga dapat menggunakan- LEFT JOINdengan ROW_NUMBER()fungsi seperti ini:

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;

Demo SQL Fiddle

shA.t
sumber