sql - valores - ¿Usando LIMIT dentro de GROUP BY para obtener N resultados por grupo?
sql promedio varios campos (13)
La siguiente consulta:
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
rendimientos
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
Lo que me gustaría es solo los 5 mejores resultados para cada 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
¿Hay alguna manera de hacer esto usando algún tipo de modificador LIMIT como el que funciona dentro de GROUP BY?
Construye las columnas virtuales (como RowID en Oracle)
mesa:
`
CREATE TABLE `stack`
(`year` int(11) DEFAULT NULL,
`id` varchar(10) DEFAULT NULL,
`rate` float DEFAULT NULL)
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`
datos:
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 como este:
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;
Si elimina la cláusula where en t3, se muestra así:
OBTENGA "TOP N Record" -> agregue el "rownum <= 3" en la cláusula where (la cláusula where de t3);
ELIJA "el año" -> agregue "ENTRE 2000 Y 2009" en la cláusula where (la cláusula where de t3);
Esto requiere una serie de subconsultas para clasificar los valores, limitarlos y luego realizar la suma mientras se agrupan
@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;
La siguiente publicación: sql: selcting top N record por grupo describe la manera complicada de lograr esto sin subconsultas.
Mejora otras soluciones ofrecidas aquí por:
- Haciendo todo en una sola consulta.
- Ser capaz de utilizar correctamente los índices.
- Evitar las subconsultas, se sabe que producen planes de ejecución erróneos en MySQL
Sin embargo, no es bonito. Se podría lograr una buena solución si se habilitaran las funciones de ventana (también conocidas como funciones analíticas) en MySQL, pero no lo están. El truco utilizado en dicha publicación utiliza GROUP_CONCAT, que a veces se describe como "Funciones de la ventana del hombre pobre para MySQL".
No, no puede LIMITAR las subconsultas arbitrariamente (puede hacerlo de forma limitada en MySQL más recientes, pero no para 5 resultados por grupo).
Esta es una consulta de tipo de grupo máximo, que no es trivial de hacer en SQL. Hay varias formas de abordar lo que puede ser más eficiente para algunos casos, pero para top-n en general, querrá ver la respuesta de Bill a una pregunta anterior similar.
Al igual que con la mayoría de las soluciones a este problema, puede devolver más de cinco filas si hay varias filas con el mismo valor de rate
, por lo que es posible que aún necesite una cantidad de procesamiento posterior para verificarlo.
Para aquellos como yo que tuvieron consultas fuera de tiempo. Hice lo siguiente para usar los límites y cualquier otra cosa de un grupo específico.
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 $$
recorre una lista de dominios y luego inserta solo un límite de 200 cada uno
Para mi algo como
SUBSTRING_INDEX(group_concat(col_name order by desired_col_order_name), '','', N)
funciona perfectamente. No hay consulta complicada.
por ejemplo: obtener top 1 para cada grupo
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;
Podría usar la función agregada GROUP_CONCAT para obtener todos los años en una sola columna, agrupados por id
y ordenados por rate
:
SELECT id, GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
FROM yourtable
GROUP BY id
Resultado:
-----------------------------------------------------------
| ID | GROUPED_YEAR |
-----------------------------------------------------------
| p01 | 2006,2003,2008,2001,2007,2009,2002,2004,2005,2000 |
| p02 | 2001,2004,2002,2003,2000,2006,2007 |
-----------------------------------------------------------
Y luego podrías usar FIND_IN_SET , que devuelve la posición del primer argumento dentro del segundo, por ejemplo.
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
Usando una combinación de GROUP_CONCAT
y FIND_IN_SET
, y filtrando por la posición devuelta por find_in_set, podría usar esta consulta que devuelve solo los primeros 5 años para cada 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;
Por favor vea el violín here .
Tenga en cuenta que si más de una fila puede tener la misma tasa, debe considerar el uso de GROUP_CONCAT (tasa DISTINCT rate ORDER BY) en la columna de tarifas en lugar de la columna del año.
La longitud máxima de la cadena devuelta por GROUP_CONCAT es limitada, por lo que esto funciona bien si necesita seleccionar algunos registros para cada grupo.
Por favor intente a continuación el procedimiento almacenado. Ya lo he verificado. Estoy obteniendo el resultado adecuado pero sin usar 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
Prueba esto:
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;
Prueba esto:
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;
Tomé algo de trabajo, pero creo que mi solución sería algo para compartir, ya que parece elegante y bastante rápido.
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
Tenga en cuenta que este ejemplo se especifica para el propósito de la pregunta y se puede modificar muy fácilmente para otros propósitos similares.
La consulta original usaba variables de usuario y ORDER BY
en tablas derivadas; El comportamiento de ambas peculiaridades no está garantizado. Respuesta revisada de la siguiente manera.
Puede usar el rango de hombre pobre sobre la partición para lograr el resultado deseado. Solo une la tabla con ella misma y para cada fila, cuenta el número de filas menor que esta:
SELECT testdata.id, testdata.rate, testdata.year, COUNT(lesser.rate) AS rank
FROM testdata
LEFT JOIN testdata AS lesser ON testdata.id = lesser.id AND testdata.rate < lesser.rate
GROUP BY testdata.id, testdata.rate, testdata.year
HAVING COUNT(lesser.rate) < 5
ORDER BY testdata.id, testdata.rate DESC
Tenga en cuenta que:
- COUNT está basado en cero
- Para la clasificación descendente, la fila menor es la que tiene una tasa más alta.
- Se devuelven todas las filas que empatan para el último lugar.
Resultado:
+------+-------+------+------+
| id | rate | year | rank |
+------+-------+------+------+
| p01 | 8.00 | 2006 | 0 |
| p01 | 7.40 | 2003 | 1 |
| p01 | 6.80 | 2008 | 2 |
| p01 | 5.90 | 2001 | 3 |
| p01 | 5.30 | 2007 | 4 |
| p02 | 12.50 | 2001 | 0 |
| p02 | 12.40 | 2004 | 1 |
| p02 | 12.20 | 2002 | 2 |
| p02 | 10.30 | 2003 | 3 |
| p02 | 8.70 | 2000 | 4 |
+------+-------+------+------+
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
La subconsulta es casi idéntica a su consulta. Solo se agrega cambio
row_number() over (partition by id order by rate DESC)