Sử dụng GIỚI HẠN trong NHÓM THEO để nhận N kết quả cho mỗi nhóm?


387

Truy vấn sau đây:

SELECT
year, id, rate
FROM h
WHERE year BETWEEN 2000 AND 2009
AND id IN (SELECT rid FROM table2)
GROUP BY id, year
ORDER BY id, rate DESC

sản lượng:

year    id  rate
2006    p01 8
2003    p01 7.4
2008    p01 6.8
2001    p01 5.9
2007    p01 5.3
2009    p01 4.4
2002    p01 3.9
2004    p01 3.5
2005    p01 2.1
2000    p01 0.8
2001    p02 12.5
2004    p02 12.4
2002    p02 12.2
2003    p02 10.3
2000    p02 8.7
2006    p02 4.6
2007    p02 3.3

Điều tôi muốn chỉ là 5 kết quả hàng đầu cho mỗi id:

2006    p01 8
2003    p01 7.4
2008    p01 6.8
2001    p01 5.9
2007    p01 5.3
2001    p02 12.5
2004    p02 12.4
2002    p02 12.2
2003    p02 10.3
2000    p02 8.7

Có cách nào để thực hiện việc này bằng cách sử dụng một số loại GIỚI HẠN như công cụ sửa đổi hoạt động trong NHÓM B BYNG KHÔNG?


10
Điều này có thể được thực hiện trong MySQL, nhưng nó không đơn giản như việc thêm một LIMITmệnh đề. Dưới đây là một bài viết giải thích chi tiết vấn đề: Cách chọn hàng đầu tiên / tối thiểu / tối đa cho mỗi nhóm trong SQL Đó là một bài viết hay - anh ấy giới thiệu một giải pháp thanh lịch nhưng ngây thơ cho vấn đề "Top N mỗi nhóm", sau đó dần dần cải thiện nó
danben

CHỌN * TỪ (CHỌN năm, id, tỷ lệ TỪ h Ở đâu GIỮA 2000 VÀ 2009 VÀ id IN (CHỌN TỪ bảng 2) NHÓM THEO id, năm ĐẶT HÀNG theo id, tỷ lệ DESC) GIỚI HẠN 5
Mixcoatl

Câu trả lời:


115

Bạn có thể sử dụng hàm tổng hợp GROUP_CONCAT để có được tất cả các năm trong một cột duy nhất, được nhóm theo idvà được sắp xếp theo rate:

SELECT   id, GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
FROM     yourtable
GROUP BY id

Kết quả:

-----------------------------------------------------------
|  ID | GROUPED_YEAR                                      |
-----------------------------------------------------------
| p01 | 2006,2003,2008,2001,2007,2009,2002,2004,2005,2000 |
| p02 | 2001,2004,2002,2003,2000,2006,2007                |
-----------------------------------------------------------

Và sau đó, bạn có thể sử dụng FIND_IN_SET , trả về vị trí của đối số thứ nhất bên trong đối số thứ hai, ví dụ:

SELECT FIND_IN_SET('2006', '2006,2003,2008,2001,2007,2009,2002,2004,2005,2000');
1

SELECT FIND_IN_SET('2009', '2006,2003,2008,2001,2007,2009,2002,2004,2005,2000');
6

Sử dụng kết hợp GROUP_CONCATFIND_IN_SETvà lọc theo vị trí được trả về bởi find_in_set, sau đó bạn có thể sử dụng truy vấn này chỉ trả về 5 năm đầu tiên cho mỗi id:

SELECT
  yourtable.*
FROM
  yourtable INNER JOIN (
    SELECT
      id,
      GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
    FROM
      yourtable
    GROUP BY id) group_max
  ON yourtable.id = group_max.id
     AND FIND_IN_SET(year, grouped_year) BETWEEN 1 AND 5
ORDER BY
  yourtable.id, yourtable.year DESC;

Xin vui lòng xem fiddle ở đây .

Xin lưu ý rằng nếu nhiều hơn một hàng có thể có cùng tỷ lệ, bạn nên cân nhắc sử dụng GROUP_CONCAT (tỷ lệ DISTINCT tỷ lệ ORDER BY) trên cột tỷ lệ thay vì cột năm.

Độ dài tối đa của chuỗi được trả về bởi GROUP_CONCAT bị giới hạn, do đó, chuỗi này hoạt động tốt nếu bạn cần chọn một vài bản ghi cho mỗi nhóm.


3
Đó là cách biểu diễn đẹp , tương đối đơn giản và giải thích tuyệt vời; cảm ơn bạn rất nhiều. Đến điểm cuối cùng của bạn, Trường hợp độ dài tối đa hợp lý có thể được tính toán, người ta có thể sử dụng SET SESSION group_concat_max_len = <maximum length>;Trong trường hợp của OP, một sự cố (vì mặc định là 1024), nhưng bằng ví dụ, group_concat_max_len phải có ít nhất 25: 4 (tối đa độ dài của chuỗi năm) + 1 (ký tự phân cách), lần 5 (5 năm đầu tiên). Các chuỗi được cắt bớt thay vì ném một lỗi, vì vậy hãy theo dõi các cảnh báo như 1054 rows in set, 789 warnings (0.31 sec).
Timothy Johns

Nếu tôi muốn tìm nạp chính xác 2 hàng thay vì 1 đến 5 so với những gì tôi nên sử dụng với FIND_IN_SET(). Tôi đã cố gắng FIND_IN_SET() =2nhưng không hiển thị kết quả như mong đợi.
Amogh

FIND_IN_SET GIỮA 1 và 5 sẽ lấy 5 vị trí đầu tiên của GROUP_CONCAT được đặt nếu kích thước bằng hoặc lớn hơn 5. Vì vậy, FIND_IN_SET = 2 sẽ chỉ lấy dữ liệu với vị trí thứ 2 trong GROUP_CONCAT của bạn. Nhận 2 hàng bạn có thể thử GIỮA 1 và 2 cho vị trí thứ 1 và thứ 2 giả sử tập hợp có 2 hàng để cung cấp.
jDub9

Giải pháp này có hiệu suất tốt hơn nhiều so với Salman cho các bộ dữ liệu lớn. Tôi đã đưa ra một ngón tay cái lên cho cả hai cho các giải pháp thông minh như vậy dù sao. Cảm ơn!!
tiomno

105

Các truy vấn ban đầu sử dụng biến người dùng và ORDER BYtrên bảng có nguồn gốc; hành vi của cả hai quirks không được đảm bảo. Sửa lại câu trả lời như sau.

Trong MySQL 5.x, bạn có thể sử dụng thứ hạng của người nghèo trên phân vùng để đạt được kết quả mong muốn. Chỉ cần tham gia bên ngoài bảng với chính nó và cho mỗi hàng, đếm số lượng hàng ít hơn nó. Trong trường hợp trên, hàng nhỏ hơn là hàng có tỷ lệ cao hơn:

SELECT t.id, t.rate, t.year, COUNT(l.rate) AS rank
FROM t
LEFT JOIN t AS l ON t.id = l.id AND t.rate < l.rate
GROUP BY t.id, t.rate, t.year
HAVING COUNT(l.rate) < 5
ORDER BY t.id, t.rate DESC, t.year

Demo và kết quả :

| id  | rate | year | rank |
|-----|------|------|------|
| p01 |  8.0 | 2006 | 0    |
| p01 |  7.4 | 2003 | 1    |
| p01 |  6.8 | 2008 | 2    |
| p01 |  5.9 | 2001 | 3    |
| p01 |  5.3 | 2007 | 4    |
| p02 | 12.5 | 2001 | 0    |
| p02 | 12.4 | 2004 | 1    |
| p02 | 12.2 | 2002 | 2    |
| p02 | 10.3 | 2003 | 3    |
| p02 |  8.7 | 2000 | 4    |

Lưu ý rằng nếu tỷ lệ có quan hệ, ví dụ:

100, 90, 90, 80, 80, 80, 70, 60, 50, 40, ...

Truy vấn trên sẽ trả về 6 hàng:

100, 90, 90, 80, 80, 80

Thay đổi để HAVING COUNT(DISTINCT l.rate) < 5có 8 hàng:

100, 90, 90, 80, 80, 80, 70, 60

Hoặc thay đổi để ON t.id = l.id AND (t.rate < l.rate OR (t.rate = l.rate AND t.pri_key > l.pri_key))có 5 hàng:

 100, 90, 90, 80, 80

Trong MySQL 8 hay muộn chỉ cần sử dụng RANK, DENSE_RANKhoặcROW_NUMBER chức năng:

SELECT *
FROM (
    SELECT *, RANK() OVER (PARTITION BY id ORDER BY rate DESC) AS rnk
    FROM t
) AS x
WHERE rnk <= 5

7
Tôi nghĩ điều đáng nói là phần quan trọng là ĐẶT HÀNG theo id vì mọi thay đổi của giá trị id sẽ khởi động lại tính theo thứ hạng.
hủy hoại

Tại sao tôi nên chạy nó hai lần để nhận được phản hồi từ WHERE rank <=5? Lần đầu tiên tôi không nhận được 5 hàng từ mỗi id, nhưng sau đó tôi có thể nhận được như bạn đã nói.
Brenno Leal

@BrennoLeal Tôi nghĩ bạn đang quên SETcâu lệnh (xem truy vấn đầu tiên). Nó là cần thiết.
Salman A

3
Trong các phiên bản mới hơn, ORDER BYtrong bảng dẫn xuất có thể, và thường sẽ bị bỏ qua. Điều này đánh bại mục tiêu. Hiệu quả nhóm khôn ngoan được tìm thấy ở đây .
Rick James

1
+1 viết lại câu trả lời của bạn là rất hợp lệ, vì các phiên bản MySQL / MariaDB hiện đại tuân theo các tiêu chuẩn ANSI / ISO SQL 1992/1999/2003 mà không bao giờ thực sự được phép sử dụng ORDER BYtrong phân phối / truy vấn con như thế .. Đó là lý do tại sao Các phiên bản MySQL / MariaDB hiện đại bỏ qua ORDER BYtruy vấn con mà không sử dụng LIMIT, tôi tin rằng ANSI / ISO SQL Standard 2008/2011/2016 làm cho việc phân phối ORDER BY/ truy vấn hợp pháp khi sử dụng kết hợp vớiFETCH FIRST n ROWS ONLY
Raymond Nijland

21

Đối với tôi một cái gì đó như

SUBSTRING_INDEX(group_concat(col_name order by desired_col_order_name), ',', N) 

hoạt động hoàn hảo. Không có truy vấn phức tạp.


ví dụ: nhận top 1 cho mỗi nhóm

SELECT 
    *
FROM
    yourtable
WHERE
    id IN (SELECT 
            SUBSTRING_INDEX(GROUP_CONCAT(id
                            ORDER BY rate DESC),
                        ',',
                        1) id
        FROM
            yourtable
        GROUP BY year)
ORDER BY rate DESC;

Giải pháp của bạn đã hoạt động hoàn hảo, nhưng tôi cũng muốn lấy năm và các cột khác từ truy vấn con, Làm thế nào chúng ta có thể làm điều đó?
MaNn

9

Không, bạn không thể GIỚI HẠN các truy vấn con một cách tùy tiện (bạn có thể thực hiện nó ở một mức độ giới hạn trong các MySQL mới hơn, nhưng không cho 5 kết quả cho mỗi nhóm).

Đây là một truy vấn loại tối đa theo nhóm, không phải là chuyện nhỏ trong SQL. Có nhiều cách khác nhau để giải quyết vấn đề có thể hiệu quả hơn trong một số trường hợp, nhưng đối với top-n nói chung, bạn sẽ muốn xem câu trả lời của Bill cho một câu hỏi tương tự trước đó.

Như với hầu hết các giải pháp cho vấn đề này, nó có thể trả về hơn năm hàng nếu có nhiều hàng có cùng rategiá trị, vì vậy bạn vẫn có thể cần một số lượng xử lý hậu kỳ để kiểm tra xem.


9

Điều này đòi hỏi một loạt các truy vấn con để xếp hạng các giá trị, giới hạn chúng, sau đó thực hiện tổng trong khi nhóm

@Rnk:=0;
@N:=2;
select
  c.id,
  sum(c.val)
from (
select
  b.id,
  b.bal
from (
select   
  if(@last_id=id,@Rnk+1,1) as Rnk,
  a.id,
  a.val,
  @last_id=id,
from (   
select 
  id,
  val 
from list
order by id,val desc) as a) as b
where b.rnk < @N) as c
group by c.id;

9

Thử cái này:

SELECT h.year, h.id, h.rate 
FROM (SELECT h.year, h.id, h.rate, IF(@lastid = (@lastid:=h.id), @index:=@index+1, @index:=0) indx 
      FROM (SELECT h.year, h.id, h.rate 
            FROM h
            WHERE h.year BETWEEN 2000 AND 2009 AND id IN (SELECT rid FROM table2)
            GROUP BY id, h.year
            ORDER BY id, rate DESC
            ) h, (SELECT @lastid:='', @index:=0) AS a
    ) h 
WHERE h.indx <= 5;

1
cột a.type không xác định trong danh sách trường
anu

5
SELECT year, id, rate
FROM (SELECT
  year, id, rate, row_number() over (partition by id order by rate DESC)
  FROM h
  WHERE year BETWEEN 2000 AND 2009
  AND id IN (SELECT rid FROM table2)
  GROUP BY id, year
  ORDER BY id, rate DESC) as subquery
WHERE row_number <= 5

Truy vấn con gần giống với truy vấn của bạn. Chỉ thay đổi là thêm

row_number() over (partition by id order by rate DESC)

8
Điều này là tốt nhưng MySQL không có chức năng cửa sổ (như ROW_NUMBER()).
ypercubeᵀᴹ

3
Tính đến MySQL 8.0, row_number()có sẵn .
erickg

4

Xây dựng các cột ảo (như RowID trong Oracle

bàn:

`
CREATE TABLE `stack` 
(`year` int(11) DEFAULT NULL,
`id` varchar(10) DEFAULT NULL,
`rate` float DEFAULT NULL) 
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`

dữ liệu:

insert into stack values(2006,'p01',8);
insert into stack values(2001,'p01',5.9);
insert into stack values(2007,'p01',5.3);
insert into stack values(2009,'p01',4.4);
insert into stack values(2001,'p02',12.5);
insert into stack values(2004,'p02',12.4);
insert into stack values(2005,'p01',2.1);
insert into stack values(2000,'p01',0.8);
insert into stack values(2002,'p02',12.2);
insert into stack values(2002,'p01',3.9);
insert into stack values(2004,'p01',3.5);
insert into stack values(2003,'p02',10.3);
insert into stack values(2000,'p02',8.7);
insert into stack values(2006,'p02',4.6);
insert into stack values(2007,'p02',3.3);
insert into stack values(2003,'p01',7.4);
insert into stack values(2008,'p01',6.8);

SQL như thế này:

select t3.year,t3.id,t3.rate 
from (select t1.*, (select count(*) from stack t2 where t1.rate<=t2.rate and t1.id=t2.id) as rownum from stack t1) t3 
where rownum <=3 order by id,rate DESC;

nếu xóa mệnh đề where trong t3, nó sẽ hiển thị như sau:

nhập mô tả hình ảnh ở đây

NHẬN "Bản ghi TOP N" -> thêm "rownum <= 3" vào mệnh đề where (mệnh đề where của t3);

CHỌN "năm" -> thêm "GIỮA 2000 VÀ 2009" trong mệnh đề where (mệnh đề where của t3);


Nếu bạn có tỷ lệ lặp lại cho cùng một id, thì điều này sẽ không hoạt động vì số lượng hàng của bạn sẽ tăng cao hơn; bạn sẽ không nhận được 3 mỗi hàng, bạn có thể nhận được 0, 1 hoặc 2. Bạn có thể nghĩ ra giải pháp nào cho việc này không?
chết đói

@starvator thay đổi "t1.rate <= t2.rate" thành "t1.rate <t2.rate", nếu tỷ lệ tốt nhất có cùng giá trị trong cùng một id, tất cả chúng đều có cùng mức tăng nhưng sẽ không tăng cao hơn; như "tỷ lệ 8 trong id p01", nếu nó lặp lại, bằng cách sử dụng "t1.rate <t2.rate", cả hai "tỷ lệ 8 trong id p01" đều có cùng mức 0; nếu sử dụng "t1.rate <= t2.rate", thì rownum là 2;
Wang Wen'an

3

Đã làm một số công việc, nhưng tôi nghĩ rằng giải pháp của tôi sẽ là một cái gì đó để chia sẻ vì nó có vẻ thanh lịch cũng như khá nhanh.

SELECT h.year, h.id, h.rate 
  FROM (
    SELECT id, 
      SUBSTRING_INDEX(GROUP_CONCAT(CONCAT(id, '-', year) ORDER BY rate DESC), ',' , 5) AS l
      FROM h
      WHERE year BETWEEN 2000 AND 2009
      GROUP BY id
      ORDER BY id
  ) AS h_temp
    LEFT JOIN h ON h.id = h_temp.id 
      AND SUBSTRING_INDEX(h_temp.l, CONCAT(h.id, '-', h.year), 1) != h_temp.l

Lưu ý rằng ví dụ này được chỉ định cho mục đích của câu hỏi và có thể được sửa đổi khá dễ dàng cho các mục đích tương tự khác.


2

Bài đăng sau: sql: selcting bản ghi N hàng đầu cho mỗi nhóm mô tả cách phức tạp để đạt được điều này mà không cần truy vấn con.

Nó cải thiện các giải pháp khác được cung cấp ở đây bằng cách:

  • Làm mọi thứ trong một truy vấn duy nhất
  • Có thể sử dụng đúng chỉ mục
  • Tránh các truy vấn con, nổi tiếng là tạo ra các kế hoạch thực hiện xấu trong MySQL

Tuy nhiên nó không đẹp. Một giải pháp tốt sẽ có thể đạt được là Hàm cửa sổ (còn gọi là Hàm phân tích) được bật trong MySQL - nhưng chúng không có. Thủ thuật được sử dụng trong bài viết nói trên sử dụng GROUP_CONCAT, đôi khi được mô tả là "Các chức năng cửa sổ của người nghèo cho MySQL".


1

cho những người như tôi đã hết thời gian truy vấn. Tôi đã thực hiện dưới đây để sử dụng các giới hạn và bất cứ điều gì khác bởi một nhóm cụ thể.

DELIMITER $$
CREATE PROCEDURE count_limit200()
BEGIN
    DECLARE a INT Default 0;
    DECLARE stop_loop INT Default 0;
    DECLARE domain_val VARCHAR(250);
    DECLARE domain_list CURSOR FOR SELECT DISTINCT domain FROM db.one;

    OPEN domain_list;

    SELECT COUNT(DISTINCT(domain)) INTO stop_loop 
    FROM db.one;
    -- BEGIN LOOP
    loop_thru_domains: LOOP
        FETCH domain_list INTO domain_val;
        SET a=a+1;

        INSERT INTO db.two(book,artist,title,title_count,last_updated) 
        SELECT * FROM 
        (
            SELECT book,artist,title,COUNT(ObjectKey) AS titleCount, NOW() 
            FROM db.one 
            WHERE book = domain_val
            GROUP BY artist,title
            ORDER BY book,titleCount DESC
            LIMIT 200
        ) a ON DUPLICATE KEY UPDATE title_count = titleCount, last_updated = NOW();

        IF a = stop_loop THEN
            LEAVE loop_thru_domain;
        END IF;
    END LOOP loop_thru_domain;
END $$

nó lặp qua một danh sách các tên miền và sau đó chỉ chèn một giới hạn 200 mỗi tên miền


1

Thử cái này:

SET @num := 0, @type := '';
SELECT `year`, `id`, `rate`,
    @num := if(@type = `id`, @num + 1, 1) AS `row_number`,
    @type := `id` AS `dummy`
FROM (
    SELECT *
    FROM `h`
    WHERE (
        `year` BETWEEN '2000' AND '2009'
        AND `id` IN (SELECT `rid` FROM `table2`) AS `temp_rid`
    )
    ORDER BY `id`
) AS `temph`
GROUP BY `year`, `id`, `rate`
HAVING `row_number`<='5'
ORDER BY `id`, `rate DESC;

0

Vui lòng thử bên dưới thủ tục lưu trữ. Tôi đã xác minh rồi. Tôi đang nhận được kết quả thích hợp nhưng không sử dụng groupby.

CREATE DEFINER=`ks_root`@`%` PROCEDURE `first_five_record_per_id`()
BEGIN
DECLARE query_string text;
DECLARE datasource1 varchar(24);
DECLARE done INT DEFAULT 0;
DECLARE tenants varchar(50);
DECLARE cur1 CURSOR FOR SELECT rid FROM demo1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

    SET @query_string='';

      OPEN cur1;
      read_loop: LOOP

      FETCH cur1 INTO tenants ;

      IF done THEN
        LEAVE read_loop;
      END IF;

      SET @datasource1 = tenants;
      SET @query_string = concat(@query_string,'(select * from demo  where `id` = ''',@datasource1,''' order by rate desc LIMIT 5) UNION ALL ');

       END LOOP; 
      close cur1;

    SET @query_string  = TRIM(TRAILING 'UNION ALL' FROM TRIM(@query_string));  
  select @query_string;
PREPARE stmt FROM @query_string;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

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