MySQL SELECT más frecuente por grupo
-
05-07-2019 - |
Pregunta
¿Cómo obtengo la categoría más frecuente para cada etiqueta en MySQL? Idealmente, me gustaría simular una función agregada que calcularía el modo de un columna.
SELECT
t.tag
, s.category
FROM tags t
LEFT JOIN stuff s
USING (id)
ORDER BY tag;
+------------------+----------+
| tag | category |
+------------------+----------+
| automotive | 8 |
| ba | 8 |
| bamboo | 8 |
| bamboo | 8 |
| bamboo | 8 |
| bamboo | 8 |
| bamboo | 8 |
| bamboo | 10 |
| bamboo | 8 |
| bamboo | 9 |
| bamboo | 8 |
| bamboo | 10 |
| bamboo | 8 |
| bamboo | 9 |
| bamboo | 8 |
| banana tree | 8 |
| banana tree | 8 |
| banana tree | 8 |
| banana tree | 8 |
| bath | 9 |
+-----------------------------+
Solución
SELECT t1.*
FROM (SELECT tag, category, COUNT(*) AS count
FROM tags INNER JOIN stuff USING (id)
GROUP BY tag, category) t1
LEFT OUTER JOIN
(SELECT tag, category, COUNT(*) AS count
FROM tags INNER JOIN stuff USING (id)
GROUP BY tag, category) t2
ON (t1.tag = t2.tag AND (t1.count < t2.count
OR t1.count = t2.count AND t1.category < t2.category))
WHERE t2.tag IS NULL
ORDER BY t1.count DESC;
Estoy de acuerdo en que esto es demasiado para una sola consulta SQL. Cualquier uso de GROUP BY
dentro de una subconsulta me hace estremecer. Puede hacer que parezca más simple usando las vistas:
CREATE VIEW count_per_category AS
SELECT tag, category, COUNT(*) AS count
FROM tags INNER JOIN stuff USING (id)
GROUP BY tag, category;
SELECT t1.*
FROM count_per_category t1
LEFT OUTER JOIN count_per_category t2
ON (t1.tag = t2.tag AND (t1.count < t2.count
OR t1.count = t2.count AND t1.category < t2.category))
WHERE t2.tag IS NULL
ORDER BY t1.count DESC;
Pero básicamente está haciendo el mismo trabajo detrás de escena.
Comentas que podrías realizar una operación similar fácilmente en el código de la aplicación. Entonces, ¿por qué no haces eso? Haga la consulta más sencilla para obtener los recuentos por categoría:
SELECT tag, category, COUNT(*) AS count
FROM tags INNER JOIN stuff USING (id)
GROUP BY tag, category;
Y ordena el resultado en el código de la aplicación.
Otros consejos
SELECT tag, category
FROM (
SELECT @tag <> tag AS _new,
@tag := tag AS tag,
category, COUNT(*) AS cnt
FROM (
SELECT @tag := ''
) vars,
stuff
GROUP BY
tag, category
ORDER BY
tag, cnt DESC
) q
WHERE _new
En sus datos, esto devuelve lo siguiente:
'automotive', 8
'ba', 8
'bamboo', 8
'bananatree', 8
'bath', 9
Aquí está el script de prueba:
CREATE TABLE stuff (tag VARCHAR(20) NOT NULL, category INT NOT NULL);
INSERT
INTO stuff
VALUES
('automotive',8),
('ba',8),
('bamboo',8),
('bamboo',8),
('bamboo',8),
('bamboo',8),
('bamboo',8),
('bamboo',10),
('bamboo',8),
('bamboo',9),
('bamboo',8),
('bamboo',10),
('bamboo',8),
('bamboo',9),
('bamboo',8),
('bananatree',8),
('bananatree',8),
('bananatree',8),
('bananatree',8),
('bath',9);
(Editar: olvidé DESC en ORDER BYs)
Fácil de hacer con un LÍMITE en la subconsulta. ¿MySQL todavía tiene la restricción de no-LIMIT-in-subqueries? El siguiente ejemplo es usar PostgreSQL.
=> select tag, (select category from stuff z where z.tag = s.tag group by tag, category order by count(*) DESC limit 1) AS category, (select count(*) from stuff z where z.tag = s.tag group by tag, category order by count(*) DESC limit 1) AS num_items from stuff s group by tag;
tag | category | num_items
------------+----------+-----------
ba | 8 | 1
automotive | 8 | 1
bananatree | 8 | 4
bath | 9 | 1
bamboo | 8 | 9
(5 rows)
La tercera columna solo es necesaria si necesitas el recuento.
Esto es para situaciones más simples:
SELECT action, COUNT (action) AS ActionCount
DE log
GRUPO POR ACCIÓN
ORDER BY ActionCount DESC;
Este es un enfoque intrépido para esto que utiliza la función agregada max
ya que no hay una función agregada de modo en MySQL (o funciones de ventanas, etc.) que permita esto:
SELECT
tag,
convert(substring(max(concat(lpad(c, 20, '0'), category)), 21), int)
AS most_frequent_category
FROM (
SELECT tag, category, count(*) AS c
FROM tags INNER JOIN stuff using (id)
GROUP BY tag, category
) as grouped_cats
GROUP BY tag;
Básicamente, utiliza el hecho de que podemos encontrar el máximo léxico de los conteos de cada categoría individual.
Esto es más fácil de ver con las categorías con nombre:
create temporary table tags (id int auto_increment primary key, tag character varying(20));
create temporary table stuff (id int, category character varying(20));
insert into tags (tag) values ('automotive'), ('ba'), ('bamboo'), ('bamboo'), ('bamboo'), ('bamboo'), ('bamboo'), ('bamboo'), ('bamboo'), ('bamboo'), ('bamboo'), ('bamboo'), ('bamboo'), ('bamboo'), ('bamboo'), ('banana tree'), ('banana tree'), ('banana tree'), ('banana tree'), ('bath');
insert into stuff (id, category) values (1, 'cat-8'), (2, 'cat-8'), (3, 'cat-8'), (4, 'cat-8'), (5, 'cat-8'), (6, 'cat-8'), (7, 'cat-8'), (8, 'cat-10'), (9, 'cat-8'), (10, 'cat-9'), (11, 'cat-8'), (12, 'cat-10'), (13, 'cat-8'), (14, 'cat-9'), (15, 'cat-8'), (16, 'cat-8'), (17, 'cat-8'), (18, 'cat-8'), (19, 'cat-8'), (20, 'cat-9');
En este caso, no deberíamos hacer una conversión de enteros en la columna most_frequent_category
:
SELECT
tag,
substring(max(concat(lpad(c, 20, '0'), category)), 21) AS most_frequent_category
FROM (
SELECT tag, category, count(*) AS c
FROM tags INNER JOIN stuff using (id)
GROUP BY tag, category
) as grouped_cats
GROUP BY tag;
+-------------+------------------------+
| tag | most_frequent_category |
+-------------+------------------------+
| automotive | cat-8 |
| ba | cat-8 |
| bamboo | cat-8 |
| banana tree | cat-8 |
| bath | cat-9 |
+-------------+------------------------+
Y para profundizar un poco más en lo que está pasando, esto es lo que parece la selección interna grouped_cats
(he añadido orden por etiqueta, c desc
) :
+-------------+----------+---+
| tag | category | c |
+-------------+----------+---+
| automotive | cat-8 | 1 |
| ba | cat-8 | 1 |
| bamboo | cat-8 | 9 |
| bamboo | cat-10 | 2 |
| bamboo | cat-9 | 2 |
| banana tree | cat-8 | 4 |
| bath | cat-9 | 1 |
+-------------+----------+---+
Y podemos ver cómo el máximo de la columna count (*)
se arrastra a lo largo de su categoría asociada si omitimos el bit subcadena
:
SELECT
tag,
max(concat(lpad(c, 20, '0'), category)) AS xmost_frequent_category
FROM (
SELECT tag, category, count(*) AS c
FROM tags INNER JOIN stuff using (id)
GROUP BY tag, category
) as grouped_cats
GROUP BY tag;
+-------------+---------------------------+
| tag | xmost_frequent_category |
+-------------+---------------------------+
| automotive | 00000000000000000001cat-8 |
| ba | 00000000000000000001cat-8 |
| bamboo | 00000000000000000009cat-8 |
| banana tree | 00000000000000000004cat-8 |
| bath | 00000000000000000001cat-9 |
+-------------+---------------------------+