【问题标题】:MySQL SELECT most frequent by groupMySQL SELECT 按组最频繁
【发布时间】:2010-11-27 07:43:15
【问题描述】:

如何获取 MySQL 中每个标签最常出现的类别?理想情况下,我想模拟一个聚合函数来计算列的mode

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

【问题讨论】:

  • 只是想我会在几年后更聪明地提到 - 不要像这样组织标签,这是一种反模式。使用 many2many 表来定义标签和项目之间的关系。也就是说,我仍然希望 MySQL 中有一个 MODE 聚合函数。
  • 问题中无需将stuff缩写为s。在这种情况下,更难发现 category 属于 s 而不是 t 表,因为它们都是单个字符。

标签: mysql group-by greatest-n-per-group frequency


【解决方案1】:

这是一种利用 max 聚合函数的 hacky 方法,因为 MySQL 中没有模式聚合函数(或窗口函数等)允许这样做:

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;

基本上,它利用了我们可以找到每个单独类别计数的词汇最大值这一事实。

使用命名类别更容易看到:

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');

在这种情况下,我们不应该在 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                  |
+-------------+------------------------+

为了更深入地了解正在发生的事情,这是grouped_cats 内部选择的样子(我添加了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 |
+-------------+----------+---+

如果我们省略substring 位,我们可以看到count(*) 列的最大值是如何沿其关联类别拖动的:

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

【讨论】:

    【解决方案2】:

    这适用于更简单的情况:

    SELECT action, COUNT(action) AS ActionCount FROM log GROUP BY action ORDER BY ActionCount DESC;

    【讨论】:

      【解决方案3】:

      (编辑:在 ORDER BYs 中忘记了 DESC)

      在子查询中使用 LIMIT 很容易。 MySQL 是否仍然有 no-LIMIT-in-subqueries 限制?下面的例子是使用 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)
      

      仅当您需要计数时才需要第三列。

      【讨论】:

        【解决方案4】:
        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;
        

        我同意这对于单个 SQL 查询来说太过分了。在子查询中使用GROUP BY 会让我畏缩。您可以使用视图使其看起来更简单:

        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;
        

        但它基本上在幕后做同样的工作。

        您评论说您可以在应用程序代码中轻松执行类似操作。那你为什么不这样做呢?执行更简单的查询以获取每个类别的计数:

        SELECT tag, category, COUNT(*) AS count
        FROM tags INNER JOIN stuff USING (id)
        GROUP BY tag, category;
        

        并在应用程序代码中对结果进行排序。

        【讨论】:

        • 我一直很难让它工作.. 做一个聚合函数 MOST_FREQUENT() 似乎更好.. 我要看看这是否在我的技能水平范围内...
        • 对不起,我误解了你的架构。我仔细查看并模拟了一个测试数据库,因此我可以确定查询有效。试试上面的编辑版本。
        • 这似乎行得通。不过有点难以下咽……而且有两个子选择,而不仅仅是一个。我希望只有一个内置的聚合函数 MEAN() 或其他东西:-P。我大概可以用 C 语言在 5 分钟内写出来。
        • 在分隔线之后查看我的附加内容。
        【解决方案5】:
        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
        

        在您的数据上,这将返回以下内容:

        'automotive',  8
        'ba',          8
        'bamboo',      8
        'bananatree',  8
        'bath',        9
        

        这是测试脚本:

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

        【讨论】:

        • 这应该是正确的答案。它避免了通过查询自行加入组,并且应该更有效率。它执行一个 group by 并对其进行迭代,标记我们需要的行,然后将它们过滤掉。聪明!
        猜你喜欢
        • 2016-11-27
        • 1970-01-01
        • 2013-11-27
        • 1970-01-01
        • 1970-01-01
        • 2021-07-06
        • 2020-09-20
        • 2021-05-16
        • 1970-01-01
        相关资源
        最近更新 更多