ใช้ LIMIT ภายใน GROUP BY เพื่อรับผลลัพธ์ N รายการต่อกลุ่มหรือไม่


387

แบบสอบถามต่อไปนี้:

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

อัตราผลตอบแทน:

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

สิ่งที่ฉันต้องการคือผลลัพธ์ 5 อันดับแรกสำหรับแต่ละ 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

มีวิธีทำเช่นนี้โดยใช้ LIMIT เช่นตัวดัดแปลงที่ทำงานภายใน GROUP BY หรือไม่


10
สิ่งนี้สามารถทำได้ใน MySQL แต่ไม่ง่ายเหมือนการเพิ่มส่วนLIMITคำสั่ง นี่คือบทความที่อธิบายปัญหาโดยละเอียด: วิธีเลือกแถวแรก / ต่ำสุด / สูงสุดต่อกลุ่มใน SQLเป็นบทความที่ดี - เขาแนะนำวิธีการแก้ปัญหาที่สง่างามและไร้เดียงสาให้กับปัญหา "Top N ต่อกลุ่ม" แล้วค่อย ๆ ปรับปรุงให้ดีขึ้น
danben

SELECT * FROM (SELECT ปี, id, อัตราจาก h ทุกปีระหว่าง 2000 และ 2009 และ id IN (SELECT กำจัดจาก table2) จัดกลุ่มตาม id, ปี ORDER BY id, อัตรา DESC) จำกัด 5
Mixcoatl

คำตอบ:


115

คุณสามารถใช้ฟังก์ชั่นรวมGROUP_CONCATเพื่อทำให้ทุกปีเป็นคอลัมน์เดียวจัดกลุ่มตามidและเรียงลำดับโดยrate:

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

ผลลัพธ์:

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

จากนั้นคุณสามารถใช้FIND_IN_SETซึ่งจะคืนค่าตำแหน่งของอาร์กิวเมนต์แรกภายในอาร์กิวเมนต์ที่สองเช่น

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

เมื่อใช้การรวมกันของGROUP_CONCATและFIND_IN_SETและการกรองตามตำแหน่งที่ส่งคืนโดย find_in_set คุณสามารถใช้แบบสอบถามนี้ที่ส่งกลับเฉพาะ 5 ปีแรกสำหรับทุก 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;

โปรดดูซอนี่

โปรดทราบว่าหากมีมากกว่าหนึ่งแถวสามารถมีอัตราเดียวกันคุณควรพิจารณาใช้ GROUP_CONCAT (อัตรา DISTINCT อัตรา ORDER BY BY) ในคอลัมน์อัตราแทนที่จะเป็นคอลัมน์ปี

ความยาวสูงสุดของสตริงที่ส่งคืนโดย GROUP_CONCAT นั้นมี จำกัด ดังนั้นจึงใช้งานได้ดีถ้าคุณต้องการเลือกระเบียนสองสามรายการสำหรับทุกกลุ่ม


3
นั่นคือการแสดงที่สวยงามคำอธิบายที่ค่อนข้างเรียบง่ายและยอดเยี่ยม ขอบคุณมาก. จนถึงจุดสุดท้ายของคุณที่สามารถคำนวณความยาวสูงสุดที่เหมาะสมคุณสามารถใช้SET SESSION group_concat_max_len = <maximum length>;ในกรณีของ OP ซึ่งไม่ใช่ปัญหา (ตั้งแต่ค่าเริ่มต้นคือ 1024) แต่ตามตัวอย่าง group_concat_max_len ควรมีอย่างน้อย 25: 4 (สูงสุด ความยาวของสตริงปี) + 1 (อักขระตัวคั่น) คูณ 5 (5 ปีแรก) 1054 rows in set, 789 warnings (0.31 sec)สายจะถูกตัดมากกว่าการขว้างปาข้อผิดพลาดเพื่อให้ดูคำเตือนเช่น
ทิโมธี Johns

ถ้าฉันต้องการที่จะดึงข้อมูลที่แน่นอน 2 แถวมากกว่า 1-5 FIND_IN_SET()กว่าสิ่งที่ฉันควรจะใช้กับ ฉันพยายามFIND_IN_SET() =2แต่ไม่แสดงผลลัพธ์ตามที่คาดไว้
Amogh

FIND_IN_SET ระหว่าง 1 และ 5 จะรับตำแหน่ง 5 ตำแหน่งแรกของ GROUP_CONCAT หากขนาดเท่ากับหรือมากกว่า 5 ดังนั้น FIND_IN_SET = 2 จะรับเฉพาะข้อมูลที่มีตำแหน่งที่ 2 ใน GROUP_CONCAT ของคุณ รับ 2 แถวคุณสามารถลองระหว่าง 1 และ 2 สำหรับตำแหน่งที่ 1 และ 2 สมมติว่าชุดมี 2 แถวที่จะให้
jDub9

วิธีนี้มีประสิทธิภาพที่ดีกว่า Salman สำหรับชุดข้อมูลขนาดใหญ่ ฉันยกนิ้วให้ทั้งสองอย่างเพื่อการแก้ปัญหาที่ฉลาดเช่นนี้ ขอบคุณ !!
tiomno

105

แบบสอบถามเดิมใช้ตัวแปรของผู้ใช้และORDER BYบนโต๊ะมา; ไม่รับประกันพฤติกรรมของนิสัยใจคอทั้งสอง แก้ไขคำตอบดังนี้

ใน MySQL 5.x คุณสามารถใช้อันดับของชายยากจนเหนือพาร์ติชันเพื่อให้ได้ผลลัพธ์ตามที่ต้องการ ด้านนอกเพียงแค่เข้าร่วมตารางด้วยตัวเองและสำหรับแต่ละแถวให้นับจำนวนแถวที่น้อยกว่านั้น ในกรณีข้างต้นแถวที่น้อยกว่าคือแถวที่มีอัตราสูงกว่า:

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

การสาธิตและผลลัพธ์ :

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

โปรดทราบว่าหากอัตรามีความสัมพันธ์เช่น:

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

ข้อความค้นหาด้านบนจะส่งคืน 6 แถว:

100, 90, 90, 80, 80, 80

เปลี่ยนเป็นHAVING COUNT(DISTINCT l.rate) < 5รับ 8 แถว:

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

หรือเปลี่ยนเป็นON t.id = l.id AND (t.rate < l.rate OR (t.rate = l.rate AND t.pri_key > l.pri_key))รับ 5 แถว:

 100, 90, 90, 80, 80

ใน MySQL 8 หรือหลังจากนั้นเพียงแค่ใช้RANK, DENSE_RANKหรือROW_NUMBERฟังก์ชั่น:

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

7
ฉันคิดว่ามันคุ้มค่าที่จะกล่าวถึงว่าส่วนสำคัญคือ ORDER BY id เนื่องจากการเปลี่ยนแปลงใด ๆ ของค่าของ id จะเริ่มนับใหม่ในลำดับ
ruuter

ทำไมฉันจึงควรใช้มันสองครั้งที่จะได้รับการตอบสนองจากWHERE rank <=5? เป็นครั้งแรกที่ฉันไม่ได้รับ 5 แถวจากแต่ละ id แต่หลังจากนั้นฉันก็สามารถทำได้ตามที่คุณพูด
Brenno Leal

@BrennoLeal ฉันคิดว่าคุณลืมSETคำสั่ง (ดูแบบสอบถามแรก) มันจำเป็น.
Salman A

3
ในเวอร์ชันที่ใหม่กว่านั้นORDER BYในตารางที่ได้รับสามารถและมักจะถูกละเว้น นี่คือเป้าหมายที่เอาชนะ ที่มีประสิทธิภาพกลุ่มที่ชาญฉลาดจะพบที่นี่
Rick James

1
+1 การเขียนคำตอบของคุณถูกต้องเนื่องจาก MySQL / MariaDB รุ่นใหม่นั้นเป็นไปตามมาตรฐาน ANSI / ISO SQL 1992/1999/2003 มาตรฐานมากขึ้นซึ่งไม่เคยได้รับอนุญาตให้ใช้จริง ๆORDER BYใน deliverd / subqueries นั่นคือเหตุผลว่าทำไม รุ่น MySQL / MariaDB ที่ทันสมัยเพิกเฉยORDER BYในแบบสอบถามย่อยโดยไม่ใช้LIMITฉันเชื่อว่ามาตรฐาน ANSI / ISO SQL 2008/2011/2016 ทำให้เป็นไปได้ที่จะส่งมอบORDER BY/ แบบสอบถามย่อยทางกฎหมายเมื่อใช้ร่วมกับFETCH FIRST n ROWS ONLY
Raymond Nijland

21

สำหรับฉันบางอย่างเช่น

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

ทำงานได้อย่างสมบูรณ์แบบ ไม่มีการสืบค้นที่ซับซ้อน


ตัวอย่างเช่น: รับอันดับ 1 สำหรับแต่ละกลุ่ม

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;

โซลูชันของคุณทำงานได้อย่างสมบูรณ์ แต่ฉันต้องการดึงปีและคอลัมน์อื่น ๆ จากแบบสอบถามย่อยเราจะทำอย่างไร
MANN

9

ไม่คุณไม่สามารถ จำกัด เคียวรีย่อยโดยพลการ (คุณสามารถทำได้ในขอบเขตที่ จำกัด ใน MySQLs ที่ใหม่กว่า แต่ไม่ใช่สำหรับ 5 ผลลัพธ์ต่อกลุ่ม)

นี่เป็นคิวรีชนิดสูงสุดของกลุ่มที่ไม่น่าทำใน SQL มีหลายวิธีในการจัดการกับสิ่งที่อาจมีประสิทธิภาพมากขึ้นสำหรับบางกรณี แต่สำหรับ top-n โดยทั่วไปคุณจะต้องดูคำตอบของ Billกับคำถามก่อนหน้านี้ที่คล้ายกัน

เช่นเดียวกับวิธีแก้ปัญหาส่วนใหญ่ปัญหานี้สามารถส่งคืนได้มากกว่าห้าแถวหากมีหลายแถวที่มีrateค่าเท่ากันดังนั้นคุณอาจต้องใช้ปริมาณการประมวลผลภายหลังเพื่อตรวจสอบสิ่งนั้น


9

สิ่งนี้ต้องการชุดของแบบสอบถามย่อยเพื่อจัดอันดับค่า จำกัด จากนั้นทำการรวมในขณะที่จัดกลุ่ม

@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

ลองสิ่งนี้:

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
a.type คอลัมน์ที่ไม่รู้จักในรายการเขต
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

ข้อความค้นหาย่อยเกือบจะเหมือนกับแบบสอบถามของคุณ เพิ่มเฉพาะการเปลี่ยนแปลง

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

8
นี่เป็นสิ่งที่ดี แต่ MySQL ไม่มีฟังก์ชั่นหน้าต่าง (เช่นROW_NUMBER())
ypercubeᵀᴹ

3
ในฐานะของ MySQL 8.0 row_number()เป็นใช้ได้
erickg

4

สร้างคอลัมน์เสมือน (เช่น RowID ใน Oracle)

โต๊ะ:

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

ข้อมูล:

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 เช่นนี้

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;

ถ้าลบ clause where ใน t3 มันจะแสดงดังนี้:

ป้อนคำอธิบายรูปภาพที่นี่

รับ "บันทึก N อันดับสูงสุด" -> เพิ่ม "rownum <= 3" ในกรณีที่ข้อ (where-clause ของ t3);

เลือก "the year" -> เพิ่ม "BETWEEN 2000 และ 2009" ในกรณีที่ clause (where-clause ของ t3);


หากคุณมีอัตราที่ทำซ้ำสำหรับรหัสเดียวกันนี้จะไม่ทำงานเพราะจำนวนแถวของคุณจะเพิ่มขึ้นสูงกว่า; คุณจะไม่ได้ 3 ต่อแถวคุณจะได้ 0, 1 หรือ 2 คุณคิดวิธีแก้ปัญหานี้ได้ไหม?
starvator

@starvator เปลี่ยน "t1.rate <= t2.rate" เป็น "t1.rate <t2.rate" หากอัตราที่ดีที่สุดมีค่าเท่ากันในรหัสเดียวกันพวกเขาทั้งหมดมี rownum เดียวกัน แต่จะไม่เพิ่มสูงขึ้น เช่น "rate 8 in id p01" ถ้ามันซ้ำโดยใช้ "t1.rate <t2.rate" ทั้งสอง "rate 8 in id p01" จะมี rownum เหมือนกัน 0; ถ้าใช้ "t1.rate <= t2.rate", rownum คือ 2;
วัง Wen'an

3

ใช้เวลาทำงานบ้าง แต่ฉันคิดว่าวิธีแก้ปัญหาของฉันจะเป็นเรื่องที่ต้องแบ่งปันเพราะมันดูดีและเร็วมาก

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

โปรดทราบว่าตัวอย่างนี้มีการระบุไว้สำหรับจุดประสงค์ของคำถามและสามารถแก้ไขได้ค่อนข้างง่ายสำหรับจุดประสงค์อื่นที่คล้ายคลึงกัน


2

โพสต์ต่อไปนี้: sql: selcting ระเบียน N อันดับสูงสุดต่อกลุ่มอธิบายวิธีการที่ซับซ้อนในการบรรลุเป้าหมายนี้โดยไม่ต้องสืบค้นย่อย

มันปรับปรุงเกี่ยวกับโซลูชั่นอื่น ๆ ที่นำเสนอโดย:

  • ทำทุกอย่างในแบบสอบถามเดียว
  • ความสามารถในการใช้ดัชนีอย่างเหมาะสม
  • หลีกเลี่ยงแบบสอบถามย่อยที่รู้จักกันดีในการสร้างแผนการดำเนินการที่ไม่ดีใน MySQL

อย่างไรก็ตามมันไม่สวย การแก้ปัญหาที่ดีจะเกิดขึ้นได้คือฟังก์ชั่น Window (aka ฟังก์ชั่นการวิเคราะห์) เปิดใช้งานใน MySQL - แต่ไม่ใช่ เคล็ดลับที่ใช้ในโพสต์ดังกล่าวใช้ GROUP_CONCAT ซึ่งบางครั้งก็อธิบายว่า "ฟังก์ชั่นหน้าต่างของคนจนสำหรับ MySQL"


1

สำหรับคนอย่างฉันที่มีคิวรีหมดเวลา ฉันทำข้อตกลงด้านล่างเพื่อใช้ข้อ จำกัด และสิ่งอื่นโดยกลุ่มเฉพาะ

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

มันวนลูปผ่านรายการของโดเมนจากนั้นแทรกเฉพาะแต่ละขีด จำกัด ที่ 200


1

ลองสิ่งนี้:

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

โปรดลองขั้นตอนการจัดเก็บด้านล่าง ฉันได้ตรวจสอบแล้ว ฉันได้รับผลที่เหมาะสม 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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.