Nhận các bản ghi với <gì> cao nhất / nhỏ nhất cho mỗi nhóm


88

Làm thế nào để làm điều đó?

Tiêu đề trước đây của câu hỏi này là " sử dụng xếp hạng (@Rank: = @Rank + 1) trong truy vấn phức tạp với các truy vấn con - nó có hoạt động không? " Bởi vì tôi đang tìm kiếm giải pháp sử dụng xếp hạng, nhưng bây giờ tôi thấy rằng giải pháp được đăng bởi Bill là tốt hơn nhiều.

Câu hỏi ban đầu:

Tôi đang cố gắng soạn một truy vấn sẽ lấy bản ghi cuối cùng từ mỗi nhóm với một số thứ tự xác định:

SET @Rank=0;

select s.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from Table
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from Table
      order by OrderField
      ) as s 
  on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField

Biểu thức @Rank := @Rank + 1thường được sử dụng cho xếp hạng, nhưng đối với tôi, nó có vẻ đáng ngờ khi được sử dụng trong 2 truy vấn con, nhưng chỉ được khởi tạo một lần. Nó sẽ hoạt động theo cách này?

Và thứ hai, nó sẽ hoạt động với một truy vấn con được đánh giá nhiều lần? Giống như truy vấn con trong mệnh đề where (hoặc have) (một cách khác để viết như trên):

SET @Rank=0;

select Table.*, @Rank := @Rank + 1 AS Rank
from Table
having Rank = (select max(Rank) AS MaxRank
              from (select GroupId, @Rank := @Rank + 1 AS Rank 
                    from Table as t0
                    order by OrderField
                    ) as t
              where t.GroupId = table.GroupId
             )
order by OrderField

Cảm ơn trước!


2
câu hỏi nâng cao hơn tại đây stackoverflow.com/questions/9841093/…
TMS

Câu trả lời:


173

Vì vậy, bạn muốn nhận được hàng có cao nhất OrderFieldmỗi nhóm? Tôi sẽ làm theo cách này:

SELECT t1.*
FROM `Table` AS t1
LEFT OUTER JOIN `Table` AS t2
  ON t1.GroupId = t2.GroupId AND t1.OrderField < t2.OrderField
WHERE t2.GroupId IS NULL
ORDER BY t1.OrderField; // not needed! (note by Tomas)

( CHỈNH SỬA bởi Tomas: Nếu có nhiều bản ghi có cùng OrderField trong cùng một nhóm và bạn cần chính xác một trong số chúng, bạn có thể muốn mở rộng điều kiện:

SELECT t1.*
FROM `Table` AS t1
LEFT OUTER JOIN `Table` AS t2
  ON t1.GroupId = t2.GroupId 
        AND (t1.OrderField < t2.OrderField 
         OR (t1.OrderField = t2.OrderField AND t1.Id < t2.Id))
WHERE t2.GroupId IS NULL

kết thúc chỉnh sửa.)

Nói cách khác, trả về hàng t1mà không có hàng nào khác t2tồn tại với cùng một GroupIdvà lớn hơn OrderField. Khi nào t2.*là NULL, có nghĩa là kết nối bên ngoài bên trái không tìm thấy kết quả phù hợp nào như vậy và do đó t1có giá trị lớn nhất OrderFieldtrong nhóm.

Không có cấp bậc, không có truy vấn phụ. Điều này sẽ chạy nhanh và tối ưu hóa quyền truy cập vào t2 với "Sử dụng chỉ mục" nếu bạn bật chỉ mục kết hợp (GroupId, OrderField).


Về hiệu suất, hãy xem câu trả lời của tôi cho Truy xuất bản ghi cuối cùng trong mỗi nhóm . Tôi đã thử một phương thức truy vấn con và phương thức kết hợp bằng cách sử dụng kết xuất dữ liệu Stack Overflow. Sự khác biệt là đáng chú ý: phương thức tham gia chạy nhanh hơn 278 lần trong thử nghiệm của tôi.

Điều quan trọng là bạn phải có chỉ số phù hợp để đạt được kết quả tốt nhất!

Về phương pháp của bạn bằng cách sử dụng biến @Rank, nó sẽ không hoạt động như bạn đã viết vì các giá trị của @Rank sẽ không đặt lại về 0 sau khi truy vấn đã xử lý bảng đầu tiên. Tôi sẽ chỉ cho bạn một ví dụ.

Tôi đã chèn một số dữ liệu giả, với một trường bổ sung là rỗng ngoại trừ trên hàng mà chúng tôi biết là lớn nhất cho mỗi nhóm:

select * from `Table`;

+---------+------------+------+
| GroupId | OrderField | foo  |
+---------+------------+------+
|      10 |         10 | NULL |
|      10 |         20 | NULL |
|      10 |         30 | foo  |
|      20 |         40 | NULL |
|      20 |         50 | NULL |
|      20 |         60 | foo  |
+---------+------------+------+

Chúng tôi có thể cho thấy rằng xếp hạng tăng lên ba đối với nhóm đầu tiên và sáu đối với nhóm thứ hai và truy vấn bên trong trả về những thứ sau một cách chính xác:

select GroupId, max(Rank) AS MaxRank
from (
  select GroupId, @Rank := @Rank + 1 AS Rank
  from `Table`
  order by OrderField) as t
group by GroupId

+---------+---------+
| GroupId | MaxRank |
+---------+---------+
|      10 |       3 |
|      20 |       6 |
+---------+---------+

Bây giờ hãy chạy truy vấn mà không có điều kiện nối, để buộc một tích Descartes của tất cả các hàng và chúng tôi cũng tìm nạp tất cả các cột:

select s.*, t.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from `Table`
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from `Table`
      order by OrderField
      ) as s 
  -- on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField;

+---------+---------+---------+------------+------+------+
| GroupId | MaxRank | GroupId | OrderField | foo  | Rank |
+---------+---------+---------+------------+------+------+
|      10 |       3 |      10 |         10 | NULL |    7 |
|      20 |       6 |      10 |         10 | NULL |    7 |
|      10 |       3 |      10 |         20 | NULL |    8 |
|      20 |       6 |      10 |         20 | NULL |    8 |
|      20 |       6 |      10 |         30 | foo  |    9 |
|      10 |       3 |      10 |         30 | foo  |    9 |
|      10 |       3 |      20 |         40 | NULL |   10 |
|      20 |       6 |      20 |         40 | NULL |   10 |
|      10 |       3 |      20 |         50 | NULL |   11 |
|      20 |       6 |      20 |         50 | NULL |   11 |
|      20 |       6 |      20 |         60 | foo  |   12 |
|      10 |       3 |      20 |         60 | foo  |   12 |
+---------+---------+---------+------------+------+------+

Chúng ta có thể thấy ở trên rằng thứ hạng tối đa cho mỗi nhóm là đúng, nhưng sau đó @Rank tiếp tục tăng khi nó xử lý bảng dẫn xuất thứ hai, lên 7 và cao hơn. Vì vậy, các thứ hạng từ bảng dẫn xuất thứ hai sẽ không bao giờ trùng lặp với các hạng từ bảng dẫn xuất đầu tiên.

Bạn sẽ phải thêm một bảng dẫn xuất khác để buộc @Rank đặt lại về 0 giữa quá trình xử lý hai bảng (và hy vọng trình tối ưu hóa không thay đổi thứ tự mà nó đánh giá các bảng hoặc nếu không, hãy sử dụng STRAIGHT_JOIN để ngăn điều đó):

select s.*
from (select GroupId, max(Rank) AS MaxRank
      from (select GroupId, @Rank := @Rank + 1 AS Rank 
            from `Table`
            order by OrderField
            ) as t
      group by GroupId) as t 
  join (select @Rank := 0) r -- RESET @Rank TO ZERO HERE
  join (
      select *, @Rank := @Rank + 1 AS Rank
      from `Table`
      order by OrderField
      ) as s 
  on t.GroupId = s.GroupId and t.MaxRank = s.Rank
order by OrderField;

+---------+------------+------+------+
| GroupId | OrderField | foo  | Rank |
+---------+------------+------+------+
|      10 |         30 | foo  |    3 |
|      20 |         60 | foo  |    6 |
+---------+------------+------+------+

Nhưng sự tối ưu hóa của truy vấn này là khủng khiếp. Nó không thể sử dụng bất kỳ chỉ mục nào, nó tạo ra hai bảng tạm thời, sắp xếp chúng theo cách khó và thậm chí sử dụng bộ đệm tham gia vì nó cũng không thể sử dụng một chỉ mục khi tham gia các bảng tạm thời. Đây là ví dụ đầu ra từ EXPLAIN:

+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+
| id | select_type | table      | type   | possible_keys | key  | key_len | ref  | rows | Extra                           |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+
|  1 | PRIMARY     | <derived4> | system | NULL          | NULL | NULL    | NULL |    1 | Using temporary; Using filesort |
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL | NULL    | NULL |    2 |                                 |
|  1 | PRIMARY     | <derived5> | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using where; Using join buffer  |
|  5 | DERIVED     | Table      | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using filesort                  |
|  4 | DERIVED     | NULL       | NULL   | NULL          | NULL | NULL    | NULL | NULL | No tables used                  |
|  2 | DERIVED     | <derived3> | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using temporary; Using filesort |
|  3 | DERIVED     | Table      | ALL    | NULL          | NULL | NULL    | NULL |    6 | Using filesort                  |
+----+-------------+------------+--------+---------------+------+---------+------+------+---------------------------------+

Trong khi giải pháp của tôi bằng cách sử dụng nối ngoài bên trái sẽ tối ưu hóa tốt hơn nhiều. Nó không sử dụng bảng tạm thời và thậm chí cả các báo cáo "Using index", có nghĩa là nó có thể giải quyết kết nối chỉ bằng cách sử dụng chỉ mục mà không cần chạm vào dữ liệu.

+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key     | key_len | ref             | rows | Extra                    |
+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+
|  1 | SIMPLE      | t1    | ALL  | NULL          | NULL    | NULL    | NULL            |    6 | Using filesort           |
|  1 | SIMPLE      | t2    | ref  | GroupId       | GroupId | 5       | test.t1.GroupId |    1 | Using where; Using index |
+----+-------------+-------+------+---------------+---------+---------+-----------------+------+--------------------------+

Có thể bạn sẽ đọc những người đưa ra tuyên bố trên blog của họ rằng "tham gia làm cho SQL chậm", nhưng điều đó vô nghĩa. Tối ưu hóa kém làm cho SQL chậm.


Điều này có thể tỏ ra khá hữu ích (đối với OP nữa), nhưng đáng buồn thay, cả hai câu hỏi đều không trả lời được.
Andriy M

Cảm ơn Bill, đó là một ý tưởng hay để tránh hàng ngũ, nhưng ... việc tham gia sẽ không chậm sao? Tham gia (không có giới hạn mệnh đề where) sẽ có kích thước lớn hơn nhiều so với các truy vấn của tôi. Dù sao, cảm ơn cho ý tưởng! Nhưng tôi cũng rất thú vị trong câu hỏi ban đầu, tức là nếu các cấp bậc sẽ hoạt động theo cách này.
TMS

Cảm ơn vì câu trả lời xuất sắc, Bill. Tuy nhiên, điều gì sẽ xảy ra nếu tôi sử dụng @Rank1@Rank2, một cho mỗi truy vấn con? Điều đó có khắc phục được sự cố không? Điều đó sẽ nhanh hơn giải pháp của bạn?
TMS

Sử dụng @Rank1@Rank2sẽ không có sự khác biệt.
Bill Karwin

2
Cảm ơn vì giải pháp tuyệt vời đó. Tôi đã phải vật lộn rất lâu với vấn đề đó. Đối với những người muốn thêm bộ lọc cho các lĩnh vực khác ví dụ như "foo" bạn cần phải thêm chúng vào điều kiện join ... AND t1.foo = t2.foođể sau đó có được kết quả chính xác choWHERE ... AND foo='bar'
ownking
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.