MySQL SELECT le plus fréquent par groupe
-
05-07-2019 - |
Question
Comment puis-je obtenir la catégorie la plus fréquente pour chaque balise dans MySQL? Dans l’idéal, je souhaiterais simuler une fonction agrégée permettant de calculer le mode d'un colonne.
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 |
+-----------------------------+
La solution
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;
Je conviens que c'est un peu trop pour une seule requête SQL. Toute utilisation de GROUP BY
dans une sous-requête me fait grimacer. Vous pouvez le rendre plus simple en utilisant les vues:
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;
Mais, fondamentalement, il fait le même travail dans les coulisses.
Vous dites que vous pourriez facilement effectuer une opération similaire dans le code de l'application. Alors pourquoi tu ne fais pas ça? Faites la requête la plus simple pour obtenir les comptes par catégorie:
SELECT tag, category, COUNT(*) AS count
FROM tags INNER JOIN stuff USING (id)
GROUP BY tag, category;
Et trier le résultat dans le code de l'application.
Autres conseils
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
Sur vos données, cela retourne ce qui suit:
'automotive', 8
'ba', 8
'bamboo', 8
'bananatree', 8
'bath', 9
Voici le script de test:
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);
(Edit: oublié DESC dans ORDER BY)
Facile à faire avec une limite dans la sous-requête. MySQL a-t-il toujours la restriction no-LIMIT-in-sub-subies? L'exemple ci-dessous utilise 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 troisième colonne n'est nécessaire que si vous avez besoin du nombre.
C’est pour des situations plus simples:
Action SELECT, COUNT (action) AS ActionCount
Du journal
GROUPE PAR action
ORDER BY ActionCount DESC;
Voici une approche simpliste qui utilise la fonction d'agrégat max
, car il n'y a pas de fonction d'agrégat de mode dans MySQL (ni dans les fonctions de fenêtrage, etc.) qui permettrait cela:
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;
Fondamentalement, il utilise le fait que nous pouvons trouver le max lexical des comptes de chaque catégorie individuelle.
C’est plus facile à voir avec les catégories nommées:
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');
Dans ce cas, nous ne devrions pas effectuer de conversion d'entier sur la colonne 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 |
+-------------+------------------------+
Et pour approfondir un peu plus ce qui se passe, voici à quoi ressemble la sélection interne de grouped_cats
(j'ai ajouté order by tag, 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 |
+-------------+----------+---+
Et nous pouvons voir comment le maximum de la colonne count (*)
traîne le long de sa catégorie associée si nous omettons le bit sous-chaîne
:
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 |
+-------------+---------------------------+