Nhận hồ sơ n hàng đầu cho mỗi nhóm kết quả được nhóm


140

Sau đây là ví dụ đơn giản nhất có thể, mặc dù mọi giải pháp đều có thể mở rộng theo tuy nhiên cần nhiều kết quả hàng đầu:

Đưa ra một bảng như thế bên dưới, với các cột người, nhóm và tuổi, làm thế nào bạn có được 2 người già nhất trong mỗi nhóm? (Các mối quan hệ trong các nhóm không nên mang lại nhiều kết quả hơn, nhưng đưa ra 2 kết quả đầu tiên theo thứ tự bảng chữ cái)

+ -------- + ------- + ----- +
| Người | Nhóm | Tuổi |
+ -------- + ------- + ----- +
| Bob | 1 | 32 |
| Jill | 1 | 34 |
| Bình minh | 1 | 42 |
| Jake | 2 | 29 |
| Paul | 2 | 36 |
| Laura | 2 | 39 |
+ -------- + ------- + ----- +

Bộ kết quả mong muốn:

+ -------- + ------- + ----- +
| Bình minh | 1 | 42 |
| Jill | 1 | 34 |
| Laura | 2 | 39 |
| Paul | 2 | 36 |
+ -------- + ------- + ----- +

LƯU Ý: Câu hỏi này được xây dựng trên câu hỏi trước- Nhận bản ghi với giá trị tối đa cho từng nhóm kết quả SQL được nhóm - để nhận một hàng trên cùng từ mỗi nhóm và nhận được câu trả lời cụ thể về MySQL từ @Bohescent:

select * 
from (select * from mytable order by `Group`, Age desc, Person) x
group by `Group`

Rất thích có thể xây dựng điều này, mặc dù tôi không thấy làm thế nào.



2
Kiểm tra ví dụ này. Nó khá gần với những gì bạn yêu cầu: stackoverflow.com/questions/1537606/
Kẻ

Sử dụng GIỚI HẠN trong NHÓM THEO để nhận N kết quả cho mỗi nhóm? stackoverflow.com/questions/2129693/ cường
Edye Chan

Câu trả lời:


88

Đây là một cách để làm điều này, bằng cách sử dụng UNION ALL(Xem SQL Fiddle with Demo ). Điều này hoạt động với hai nhóm, nếu bạn có nhiều hơn hai nhóm, thì bạn sẽ cần chỉ định groupsố lượng và thêm truy vấn cho mỗi nhóm group:

(
  select *
  from mytable 
  where `group` = 1
  order by age desc
  LIMIT 2
)
UNION ALL
(
  select *
  from mytable 
  where `group` = 2
  order by age desc
  LIMIT 2
)

Có nhiều cách khác nhau để làm điều này, xem bài viết này để xác định tuyến đường tốt nhất cho tình huống của bạn:

http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/

Biên tập:

Điều này cũng có thể làm việc cho bạn, nó tạo ra một số hàng cho mỗi bản ghi. Sử dụng một ví dụ từ liên kết ở trên, điều này sẽ chỉ trả về những bản ghi có số hàng nhỏ hơn hoặc bằng 2:

select person, `group`, age
from 
(
   select person, `group`, age,
      (@num:=if(@group = `group`, @num +1, if(@group := `group`, 1, 1))) row_number 
  from test t
  CROSS JOIN (select @num:=0, @group:=null) c
  order by `Group`, Age desc, person
) as x 
where x.row_number <= 2;

Xem bản demo


52
nếu anh ta có hơn 1 000 nhóm, điều đó có khiến điều này hơi đáng sợ không?
Rừng Charles

1
@CharlesForest có, nó sẽ và đó là lý do tại sao tôi đã nói rằng bạn sẽ phải chỉ định nó cho nhiều hơn hai nhóm. Nó sẽ trở nên xấu xí.
Taryn

1
@CharlesForest Tôi nghĩ rằng tôi đã tìm thấy một giải pháp tốt hơn, xem bản chỉnh sửa của mình
Taryn

1
Một lưu ý cho bất cứ ai đọc điều này: Phiên bản là các biến gần đúng. Tuy nhiên, MySQL không đảm bảo thứ tự đánh giá các biểu thức trong SELECT(và trên thực tế, đôi khi đánh giá chúng không theo thứ tự). Chìa khóa cho giải pháp là đặt tất cả các phép gán biến trong một biểu thức; đây là một ví dụ: stackoverflow.com/questions/38535020/ .
Gordon Linoff

1
@GordonLinoff Cập nhật câu trả lời của tôi, cảm ơn vì đã chỉ ra. Nó cũng mất quá nhiều thời gian để tôi cập nhật nó.
Taryn

63

Trong các cơ sở dữ liệu khác, bạn có thể làm điều này bằng cách sử dụng ROW_NUMBER. MySQL không hỗ trợ ROW_NUMBERnhưng bạn có thể sử dụng các biến để mô phỏng nó:

SELECT
    person,
    groupname,
    age
FROM
(
    SELECT
        person,
        groupname,
        age,
        @rn := IF(@prev = groupname, @rn + 1, 1) AS rn,
        @prev := groupname
    FROM mytable
    JOIN (SELECT @prev := NULL, @rn := 0) AS vars
    ORDER BY groupname, age DESC, person
) AS T1
WHERE rn <= 2

Xem nó hoạt động trực tuyến: sqlfiddle


Chỉnh sửa Tôi chỉ nhận thấy rằng bluefeet đã đăng một câu trả lời rất giống nhau: +1 cho anh ta. Tuy nhiên, câu trả lời này có hai ưu điểm nhỏ:

  1. Nó là một truy vấn duy nhất. Các biến được khởi tạo bên trong câu lệnh SELECT.
  2. Nó xử lý các mối quan hệ như được mô tả trong câu hỏi (thứ tự chữ cái theo tên).

Vì vậy, tôi sẽ để nó ở đây trong trường hợp nó có thể giúp ai đó.


1
Mark- Điều này đang làm việc tốt cho chúng tôi. Cảm ơn vì đã cung cấp một lựa chọn tốt khác để khen ngợi @ bluefeet- được đánh giá cao.
Yarin

+1. Điều này làm việc cho tôi. Thực sự sạch sẽ và câu trả lời điểm. Bạn có thể vui lòng giải thích chính xác làm thế nào điều này hoạt động? Logic đằng sau này là gì?
Aditya Hajare

3
Giải pháp tuyệt vời nhưng có vẻ như nó không hoạt động trong môi trường của tôi (MySQL 5.6) vì mệnh đề theo mệnh đề được áp dụng sau khi chọn để nó không trả về kết quả hàng đầu, hãy xem giải pháp thay thế của tôi để khắc phục vấn đề này
Laurent PELE

Trong khi chạy này, tôi đã có thể xóa JOIN (SELECT @prev := NULL, @rn := 0) AS vars. Tôi có ý tưởng là khai báo các biến rỗng, nhưng có vẻ như ngoại lai đối với MySql.
Joseph Cho

1
Điều này hoạt động rất tốt với tôi trong MySQL 5.7, nhưng thật tuyệt vời nếu ai đó có thể giải thích cách nó hoạt động
George B

41

Thử cái này:

SELECT a.person, a.group, a.age FROM person AS a WHERE 
(SELECT COUNT(*) FROM person AS b 
WHERE b.group = a.group AND b.age >= a.age) <= 2 
ORDER BY a.group ASC, a.age DESC

BẢN GIỚI THIỆU


6
snuffin ra khỏi hư không với giải pháp đơn giản nhất! Đây có phải là thanh lịch hơn so với Ludo / Bill Karwin ? Tôi có thể nhận được một số lời bình luận
Yarin

Hừm, không chắc nó thanh lịch hơn. Nhưng đánh giá từ các phiếu bầu, tôi đoán bluefeet có thể có giải pháp tốt hơn.
hít vào

2
Có một vấn đề với điều này. Nếu có một sự ràng buộc cho vị trí thứ hai trong nhóm, chỉ có một kết quả hàng đầu được trả về. Xem bản demo
Yarin

2
Đó không phải là vấn đề nếu nó muốn. Bạn có thể thiết lập thứ tự của a.person.
Alberto Leal

không, nó không hoạt động trong trường hợp của tôi, DEMO cũng không hoạt động
Choix

31

Làm thế nào về việc sử dụng tự tham gia:

CREATE TABLE mytable (person, groupname, age);
INSERT INTO mytable VALUES('Bob',1,32);
INSERT INTO mytable VALUES('Jill',1,34);
INSERT INTO mytable VALUES('Shawn',1,42);
INSERT INTO mytable VALUES('Jake',2,29);
INSERT INTO mytable VALUES('Paul',2,36);
INSERT INTO mytable VALUES('Laura',2,39);

SELECT a.* FROM mytable AS a
  LEFT JOIN mytable AS a2 
    ON a.groupname = a2.groupname AND a.age <= a2.age
GROUP BY a.person
HAVING COUNT(*) <= 2
ORDER BY a.groupname, a.age DESC;

đưa cho tôi:

a.person    a.groupname  a.age     
----------  -----------  ----------
Shawn       1            42        
Jill        1            34        
Laura       2            39        
Paul        2            36      

Tôi được truyền cảm hứng mạnh mẽ bởi câu trả lời từ Bill Karwin để chọn 10 hồ sơ hàng đầu cho mỗi danh mục

Ngoài ra, tôi đang sử dụng SQLite, nhưng điều này sẽ hoạt động trên MySQL.

Một điều nữa: ở trên, tôi đã thay thế groupcột bằng một groupnamecột cho thuận tiện.

Chỉnh sửa :

Theo dõi bình luận của OP về kết quả cà vạt bị thiếu, tôi đã tăng câu trả lời của snuffin để hiển thị tất cả các mối quan hệ. Điều này có nghĩa là nếu những cái cuối cùng là quan hệ, có thể trả về hơn 2 hàng, như hiển thị bên dưới:

.headers on
.mode column

CREATE TABLE foo (person, groupname, age);
INSERT INTO foo VALUES('Paul',2,36);
INSERT INTO foo VALUES('Laura',2,39);
INSERT INTO foo VALUES('Joe',2,36);
INSERT INTO foo VALUES('Bob',1,32);
INSERT INTO foo VALUES('Jill',1,34);
INSERT INTO foo VALUES('Shawn',1,42);
INSERT INTO foo VALUES('Jake',2,29);
INSERT INTO foo VALUES('James',2,15);
INSERT INTO foo VALUES('Fred',1,12);
INSERT INTO foo VALUES('Chuck',3,112);


SELECT a.person, a.groupname, a.age 
FROM foo AS a 
WHERE a.age >= (SELECT MIN(b.age)
                FROM foo AS b 
                WHERE (SELECT COUNT(*)
                       FROM foo AS c
                       WHERE c.groupname = b.groupname AND c.age >= b.age) <= 2
                GROUP BY b.groupname)
ORDER BY a.groupname ASC, a.age DESC;

đưa cho tôi:

person      groupname   age       
----------  ----------  ----------
Shawn       1           42        
Jill        1           34        
Laura       2           39        
Paul        2           36        
Joe         2           36        
Chuck       3           112      

@ Ludo- Chỉ cần thấy câu trả lời đó từ Bill Karwin - cảm ơn vì đã áp dụng nó ở đây
Yarin

Bạn nghĩ gì về câu trả lời của Snuffin? Tôi đang cố gắng so sánh hai
Yarin

2
Có một vấn đề với điều này. Nếu có một sự ràng buộc cho vị trí thứ hai trong nhóm, chỉ có một kết quả hàng đầu được trả
về-

1
@ Ludo- yêu cầu ban đầu là mỗi nhóm trả về kết quả n chính xác, với bất kỳ mối quan hệ nào được giải quyết theo thứ tự abc
Yarin

Chỉnh sửa để bao gồm các mối quan hệ không làm việc cho tôi. Tôi nhận được ERROR 1242 (21000): Subquery returns more than 1 row, có lẽ là vì GROUP BY. Khi tôi thực hiện SELECT MINtruy vấn con một mình, nó sẽ tạo ra ba hàng: 34, 39, 112và ở đó xuất hiện giá trị thứ hai phải là 36, không phải 39.
verbamour

12

Giải pháp Snuffin có vẻ khá chậm để thực thi khi bạn có nhiều hàng và các giải pháp Mark Byers / Rick James và Bluefeet không hoạt động trên môi trường của tôi (MySQL 5.6) vì thứ tự được áp dụng sau khi thực hiện chọn, vì vậy đây là một biến thể trong số các giải pháp của Marc Byers / Rick James để khắc phục vấn đề này (với một lựa chọn bổ sung thêm):

select person, groupname, age
from
(
    select person, groupname, age,
    (@rn:=if(@prev = groupname, @rn +1, 1)) as rownumb,
    @prev:= groupname 
    from 
    (
        select person, groupname, age
        from persons 
        order by groupname ,  age desc, person
    )   as sortedlist
    JOIN (select @prev:=NULL, @rn :=0) as vars
) as groupedlist 
where rownumb<=2
order by groupname ,  age desc, person;

Tôi đã thử truy vấn tương tự trên một bảng có 5 triệu hàng và nó trả về kết quả sau chưa đầy 3 giây


3
Đây là truy vấn duy nhất đã được làm việc trong môi trường của tôi. Cảm ơn!
herrherr

3
Thêm LIMIT 9999999vào bất kỳ bảng dẫn xuất với một ORDER BY. Điều này có thể ngăn chặn việc ORDER BYbị bỏ qua.
Rick James

Tôi đã chạy một truy vấn tương tự trên một bảng chứa vài nghìn hàng và phải mất 60 giây để trả về một kết quả, vì vậy ... cảm ơn vì bài đăng, đó là một khởi đầu cho tôi. (ETA: giảm xuống còn 5 giây. Tốt!)
Evan

10

Kiểm tra này:

SELECT
  p.Person,
  p.`Group`,
  p.Age
FROM
  people p
  INNER JOIN
  (
    SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`
    UNION
    SELECT MAX(p3.Age) AS Age, p3.`Group` FROM people p3 INNER JOIN (SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`) p4 ON p3.Age < p4.Age AND p3.`Group` = p4.`Group` GROUP BY `Group`
  ) p2 ON p.Age = p2.Age AND p.`Group` = p2.`Group`
ORDER BY
  `Group`,
  Age DESC,
  Person;

Câu đố về SQL: http://sqlfiddle.com/#!2/cdbb6/15


5
Man, những người khác đã tìm thấy các giải pháp đơn giản hơn nhiều ... Tôi chỉ dành 15 phút cho việc này và tự hào về bản thân mình vì đã đưa ra một giải pháp phức tạp như vậy. Đó là hút.
Travesty3

Tôi đã phải tìm một số phiên bản nội bộ ít hơn 1 so với hiện tại - điều này đã cho tôi câu trả lời để làm điều này: max(internal_version - 1)- vì vậy hãy bớt căng thẳng :)
Jamie Strauss

8

Nếu các câu trả lời khác không đủ nhanh Hãy thử mã này :

SELECT
        province, n, city, population
    FROM
      ( SELECT  @prev := '', @n := 0 ) init
    JOIN
      ( SELECT  @n := if(province != @prev, 1, @n + 1) AS n,
                @prev := province,
                province, city, population
            FROM  Canada
            ORDER BY
                province   ASC,
                population DESC
      ) x
    WHERE  n <= 3
    ORDER BY  province, n;

Đầu ra:

+---------------------------+------+------------------+------------+
| province                  | n    | city             | population |
+---------------------------+------+------------------+------------+
| Alberta                   |    1 | Calgary          |     968475 |
| Alberta                   |    2 | Edmonton         |     822319 |
| Alberta                   |    3 | Red Deer         |      73595 |
| British Columbia          |    1 | Vancouver        |    1837970 |
| British Columbia          |    2 | Victoria         |     289625 |
| British Columbia          |    3 | Abbotsford       |     151685 |
| Manitoba                  |    1 | ...

Nhìn vào trang web của bạn - nơi tôi sẽ lấy nguồn dữ liệu cho dân cư của thành phố? TIA và rss.
Vérace

maxmind.com/en/worldcities - Tôi thấy nó thuận tiện cho việc thử nghiệm các tìm kiếm lat / lng , truy vấn, phân vùng, v.v ... Nó đủ lớn để thú vị, nhưng đủ dễ đọc để nhận ra câu trả lời. Tập hợp con Canada là tiện dụng cho loại câu hỏi này. (Ít tỉnh hơn các thành phố của Hoa Kỳ.)
Rick James

2

Tôi muốn chia sẻ điều này bởi vì tôi đã dành một thời gian dài để tìm kiếm một cách dễ dàng để thực hiện điều này trong một chương trình java tôi đang làm việc. Điều này không hoàn toàn mang lại đầu ra mà bạn đang tìm kiếm nhưng nó rất gần. Hàm trong mysql được gọi là GROUP_CONCAT()hoạt động thực sự tốt để xác định có bao nhiêu kết quả trả về trong mỗi nhóm. Sử dụng LIMIThoặc bất kỳ cách ưa thích nào khác để cố gắng làm điều này với tôi COUNTđều không hiệu quả. Vì vậy, nếu bạn sẵn sàng chấp nhận một đầu ra sửa đổi, đó là một giải pháp tuyệt vời. Hãy nói rằng tôi có một bảng gọi là 'sinh viên' với id sinh viên, giới tính của họ và gpa. Hãy nói rằng tôi muốn đứng đầu 5 gpas cho mỗi giới. Sau đó tôi có thể viết truy vấn như thế này

SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5) 
AS subcategories FROM student GROUP BY sex;

Lưu ý rằng tham số '5' cho nó biết có bao nhiêu mục để ghép vào mỗi hàng

Và đầu ra sẽ trông giống như

+--------+----------------+
| Male   | 4,4,4,4,3.9    |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+

Bạn cũng có thể thay đổi ORDER BYbiến và đặt chúng theo một cách khác. Vì vậy, nếu tôi có tuổi học sinh, tôi có thể thay thế 'gpa desc' bằng 'age desc' và nó sẽ hoạt động! Bạn cũng có thể thêm các biến vào nhóm bằng câu lệnh để có thêm các cột trong đầu ra. Vì vậy, đây chỉ là một cách tôi thấy khá linh hoạt và hoạt động tốt nếu bạn ổn với chỉ liệt kê kết quả.


0

Trong SQL Server row_numer()là một chức năng mạnh mẽ có thể nhận được kết quả dễ dàng như dưới đây

select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2

Với 8.0 và 10.2 là GA, câu trả lời này đang trở nên hợp lý.
Rick James

@RickJames 'GA' nghĩa là gì? Các chức năng của cửa sổ ( dev.mysql.com/doc/refman/8.0/en/window-fifts.html ) đã giải quyết vấn đề của tôi rất tốt.
iedmrc

1
@iedmrc - "GA" có nghĩa là "Thường có sẵn". Đó là công nghệ nói cho "sẵn sàng cho thời gian chính" hoặc "phát hành". Họ đang thông qua việc phát triển phiên bản và sẽ tập trung vào lỗi mà họ đã bỏ lỡ. Liên kết đó thảo luận về triển khai của MySQL 8.0, có thể khác với triển khai của MariaDB 10.2.
Rick James

-1

Có một câu trả lời thực sự hay cho vấn đề này tại MySQL - Làm thế nào để có được hàng N hàng đầu cho mỗi nhóm

Dựa trên giải pháp trong liên kết được tham chiếu, truy vấn của bạn sẽ như sau:

SELECT Person, Group, Age
   FROM
     (SELECT Person, Group, Age, 
                  @group_rank := IF(@group = Group, @group_rank + 1, 1) AS group_rank,
                  @current_group := Group 
       FROM `your_table`
       ORDER BY Group, Age DESC
     ) ranked
   WHERE group_rank <= `n`
   ORDER BY Group, Age DESC;

nơi ntop nyour_tablelà tên của bảng.

Tôi nghĩ rằng lời giải thích trong tài liệu tham khảo là thực sự rõ ràng. Để tham khảo nhanh tôi sẽ sao chép và dán nó ở đây:

Hiện tại MySQL không hỗ trợ hàm ROW_NUMBER () có thể gán số thứ tự trong một nhóm, nhưng như một cách giải quyết, chúng ta có thể sử dụng các biến phiên của MySQL.

Các biến này không yêu cầu khai báo và có thể được sử dụng trong truy vấn để thực hiện tính toán và lưu trữ kết quả trung gian.

@cien_country: = country Mã này được thực thi cho mỗi hàng và lưu giá trị của cột quốc gia vào biến @cien_country.

@country_rank: = IF (@cien_country = country, @country_rank + 1, 1) Trong mã này, nếu @cien_country là cùng một thứ hạng, chúng ta sẽ đặt nó thành 1. Đối với hàng đầu tiên @cien_country là NULL, vì vậy xếp hạng là cũng được đặt thành 1.

Để xếp hạng chính xác, chúng ta cần phải ĐẶT HÀNG theo quốc gia, dân số DESC


Vâng, đó là nguyên tắc được sử dụng bởi các giải pháp của Marc Byers, Rick James và của tôi.
Laurent PELE

Khó có thể nói bài đăng nào (Stack Overflow hoặc SQLlines) là bài đầu tiên
Laurent PELE

@LaurentPELE - Của tôi đã được đăng vào tháng 2 năm 2015. Tôi thấy không có dấu thời gian hoặc tên trên SQLlines. Các blog của MySQL đã tồn tại đủ lâu để một số trong số chúng bị lỗi thời và cần được xóa - mọi người đang trích dẫn thông tin sai lệch.
Rick James
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.