Використовуючи LIMIT у групі BY, щоб отримати N результатів у групі?


385

Наступний запит:

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 результатів для кожного ідентифікатора:

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 Це гарна стаття - він вводить елегантне, але наївне рішення проблеми "Топ N на групу", а потім поступово покращується на ньому.
danben

ВИБІР * ВІД (ВИБІР рік, ідентифікатор, швидкість ВІД ЧОГО року МЕЖДУ 2000 І 2009 ІД ІД В (ВИБРАТИ позбавитись від таблиці2) ГРУПА ПО id, рік ЗАМОВЛЕННЯ ПО 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 rate ORDER BY rate) у стовпці rate замість стовпця року.

Максимальна довжина рядка, що повертається GROUP_CONCAT, обмежена, тому це добре працює, якщо вам потрібно вибрати кілька записів для кожної групи.


3
Це гарно виконавство, порівняно просте та чудове пояснення; Дуже дякую. До останнього моменту, де можна обчислити розумну максимальну довжину, можна використати SET SESSION group_concat_max_len = <maximum length>;У випадку з ОП непроблема (оскільки за замовчуванням 1024), але, наприклад, група_конку_макс_лен повинна бути не менше 25: 4 (макс. довжина ряду року) + 1 (розділовий символ), раз 5 (перші 5 років). Рядки усічені, а не помилки, тому слідкуйте за попередженнями типу 1054 rows in set, 789 warnings (0.31 sec).
Тимофі Джонс

Якщо я хочу отримати точні 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
Я думаю, що варто згадати, що ключовою частиною є ЗАМОВЛЕННЯ ПО id, оскільки будь-яка зміна значення id перезапустить підрахунок у ранзі.
ruuter

Чому я повинен запускати його двічі, щоб отримати відповідь WHERE rank <=5? Вперше я не отримую 5 рядків від кожного ідентифікатора, але після нього я можу отримати, як ви сказали.
Brenno Leal

@BrennoLeal Я думаю, ви забуваєте SETтвердження (див. Перший запит). Необхідно.
Салман

3
У нових версіях ORDER BYу похідній таблиці можна і часто буде ігноруватися. Це перемагає мету. Ефективна група-навхрест знаходяться тут .
Рік Джеймс

1
+1 Ваша переписка відповідей є дуже достовірною, оскільки сучасні версії MySQL / MariaDB більше відповідають стандартам ANSI / ISO SQL 1992/1999/2003, де ніколи насправді не було дозволено використовувати ORDER BYу постачанні / підзапросах, подібних до цього. Це причина, чому сучасні версії 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

Ні, ви не можете обмежувати підзапроси довільно (ви можете це робити обмежено в нових MySQL, але не для 5 результатів на групу).

Це запит максимально групового типу, який не тривіально робити в SQL. Існують різні способи вирішити те, що може бути більш ефективним у деяких випадках, але для топ-n в цілому ви хочете подивитися відповідь Білла на аналогічний попередній питання.

Як і в більшості рішень цієї проблеми, вона може повернути більше п'яти рядків, якщо є кілька рядків з однаковим 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;

якщо видалити пункт де в t3, він відображається так:

введіть тут опис зображення

Отримати "TOP N Record" -> додати "rownum <= 3", де пункт (де-пункт t3);

ВИБІР "рік" -> додай "МЕЖДУ 2000 І 2009" у пункті де (де-пункт t3);


Якщо у вас є показники, які повторюються для одного і того ж ідентифікатора, це не буде працювати, оскільки кількість рядківNum зросте вище ви не отримаєте 3 за ряд, ви можете отримати 0, 1 або 2. Чи можете ви придумати будь-яке рішення цього питання?
голодар

@starvator змінить "t1.rate <= t2.rate" на "t1.rate <t2.rate", якщо кращий показник має однакові значення в одному і тому ж ідентифікаторі, усі вони мають однаковий rownum, але не збільшуватимуться вище; як "rate 8 в id p01", якщо він повторюється, використовуючи "t1.rate <t2.rate", обидва "rate 8 в id p01" мають однаковий rownum 0; якщо використовується "t1.rate <= t2.rate", rownum дорівнює 2;
Ван Вень'ан

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: вибір верхнього N запису на групу описує складний спосіб досягнення цього без підзапитів.

Це покращується щодо інших рішень, пропонованих тут:

  • Робити все за один запит
  • Вміння правильно використовувати індекси
  • Уникаючи підзапитів, як відомо, вони створюють погані плани виконання в MySQL

Однак це не дуже. Хорошим рішенням було б домогтися, якщо в 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.