来自 GROUP_BY 的两个 LEFT JOIN 的 GROUP_CONCAT 的奇怪重复行为

Posted

技术标签:

【中文标题】来自 GROUP_BY 的两个 LEFT JOIN 的 GROUP_CONCAT 的奇怪重复行为【英文标题】:Strange duplicate behavior from GROUP_CONCAT of two LEFT JOINs of GROUP_BYs 【发布时间】:2017-12-28 06:31:42 【问题描述】:

Here 是我所有表的结构和查询(请关注最后一个 查询,附在下面)。正如您在小提琴中看到的,这是 当前输出:

+---------+-----------+-------+------------+--------------+
| user_id | user_name | score | reputation | top_two_tags |
+---------+-----------+-------+------------+--------------+
| 1       | Jack      | 0     | 18         | css,mysql    |
| 4       | James     | 1     | 5          | html         |
| 2       | Peter     | 0     | 0          | null         |
| 3       | Ali       | 0     | 0          | null         |
+---------+-----------+-------+------------+--------------+

这是正确的,一切都很好。


现在我多了一个名为“类别”的存在。每个帖子只能有一个类别。而且我还想为每个用户获得前两个类别。 here 是我的新查询。正如您在结果中看到的,发生了一些重复:

+---------+-----------+-------+------------+--------------+------------------------+
| user_id | user_name | score | reputation | top_two_tags |   top_two_categories   |
+---------+-----------+-------+------------+--------------+------------------------+
| 1       | Jack      | 0     | 18         | css,css      | technology,technology  |
| 4       | James     | 1     | 5          | html         | political              |
| 2       | Peter     | 0     | 0          | null         | null                   |
| 3       | Ali       | 0     | 0          | null         | null                   |
+---------+-----------+-------+------------+--------------+------------------------+

看到了吗? css,csstechnology, technology。为什么这些是重复的?我刚刚为categories 添加了一个LEFT JOIN,与tags 完全一样。但它不能按预期工作,甚至影响标签。


无论如何,这是预期的结果:

+---------+-----------+-------+------------+--------------+------------------------+
| user_id | user_name | score | reputation | top_two_tags |        category        |
+---------+-----------+-------+------------+--------------+------------------------+
| 1       | Jack      | 0     | 18         | css,mysql    | technology,social      |
| 4       | James     | 1     | 5          | html         | political              |
| 2       | Peter     | 0     | 0          | null         | null                   |
| 3       | Ali       | 0     | 0          | null         | null                   |
+---------+-----------+-------+------------+--------------+------------------------+

有人知道我怎样才能做到这一点吗?


CREATE TABLE users(id integer PRIMARY KEY, user_name varchar(5));
CREATE TABLE tags(id integer NOT NULL PRIMARY KEY, tag varchar(5));
CREATE TABLE reputations(
    id  integer PRIMARY KEY, 
    post_id  integer /* REFERENCES posts(id) */, 
    user_id integer REFERENCES users(id), 
    score integer, 
    reputation integer, 
    date_time integer);
CREATE TABLE post_tag(
    post_id integer /* REFERENCES posts(id) */, 
    tag_id integer REFERENCES tags(id),
    PRIMARY KEY (post_id, tag_id));
CREATE TABLE categories(id INTEGER NOT NULL PRIMARY KEY, category varchar(10) NOT NULL);
CREATE TABLE post_category(
    post_id INTEGER NOT NULL /* REFERENCES posts(id) */, 
    category_id INTEGER NOT NULL REFERENCES categories(id),
    PRIMARY KEY(post_id, category_id)) ;

SELECT
    q1.user_id, q1.user_name, q1.score, q1.reputation, 
    substring_index(group_concat(q2.tag  ORDER BY q2.tag_reputation DESC SEPARATOR ','), ',', 2) AS top_two_tags,
    substring_index(group_concat(q3.category  ORDER BY q3.category_reputation DESC SEPARATOR ','), ',', 2) AS category
FROM
    (SELECT 
        u.id AS user_Id, 
        u.user_name,
        coalesce(sum(r.score), 0) as score,
        coalesce(sum(r.reputation), 0) as reputation
    FROM 
        users u
        LEFT JOIN reputations r 
            ON    r.user_id = u.id 
              AND r.date_time > 1500584821 /* unix_timestamp(DATE_SUB(now(), INTERVAL 1 WEEK)) */
    GROUP BY 
        u.id, u.user_name
    ) AS q1
    LEFT JOIN
    (
    SELECT
        r.user_id AS user_id, t.tag, sum(r.reputation) AS tag_reputation
    FROM
        reputations r 
        JOIN post_tag pt ON pt.post_id = r.post_id
        JOIN tags t ON t.id = pt.tag_id
    WHERE
        r.date_time > 1500584821 /* unix_timestamp(DATE_SUB(now(), INTERVAL 1 WEEK)) */
    GROUP BY
        user_id, t.tag
    ) AS q2
    ON q2.user_id = q1.user_id 
    LEFT JOIN
    (
    SELECT
        r.user_id AS user_id, c.category, sum(r.reputation) AS category_reputation
    FROM
        reputations r 
        JOIN post_category ct ON ct.post_id = r.post_id
        JOIN categories c ON c.id = ct.category_id
    WHERE
        r.date_time > 1500584821 /* unix_timestamp(DATE_SUB(now(), INTERVAL 1 WEEK)) */
    GROUP BY
        user_id, c.category
    ) AS q3
    ON q3.user_id = q1.user_id 
GROUP BY
    q1.user_id, q1.user_name, q1.score, q1.reputation
ORDER BY
    q1.reputation DESC, q1.score DESC ;

【问题讨论】:

试试:... group_concat(distinct q2.tag ...... group_concat(distinct q3.category ... @wchiquito 是,使用distinct 删除重复项并按预期工作。但我认为我必须以另一种方式编写查询。因为我当前的查询似乎有很多废物处理。是不是真的? 很好地提供了一个小提琴 - 和预期的结果 @Strawberry 哈哈哈 .. 最终我听从了你不断对我说的话。 我们可以预期,您添加到旧查询底部以获取新查询的代码不是加入一组独特的字段(例如 PK)。 PS 请在集成之前找到一个刚刚完成最后一步的查询。您可以使用 CTE 或 VIEW。 (mcve 中的 minimal。)另外,为了使您的文章更精美,请像您的表格一样将相关代码(DDL 和查询)作为文本内联。 【参考方案1】:

您的第二个查询格式为:

q1 -- PK user_id
LEFT JOIN (...
    GROUP BY user_id, t.tag
) AS q2
ON q2.user_id = q1.user_id 
LEFT JOIN (...
    GROUP BY user_id, c.category
) AS q3
ON q3.user_id = q1.user_id
GROUP BY -- group_concats

内部 GROUP BY 导致 (user_id, t.tag)(user_id, c.category) 成为键/唯一。除此之外,我不会提及那些 GROUP BY。

TL;DR 当您将 (q1 JOIN q2) 加入到 q3 时,它不在其中一个的键/唯一性上,因此对于每个 user_id,您会为每个可能的标签组合组合获得一行 &类别。因此,最终的 GROUP BY 输入每个(user_id,标签)和每个(user_id,类别)的重复项,并且不恰当地 GROUP_CONCAT 为每个 user_id 重复标签和类别。正确的是 (q1 JOIN q2 GROUP BY) JOIN (q1 JOIN q3 GROUP BY),其中所有连接都在公共键/唯一 (user_id) 上,并且没有虚假聚合。虽然有时您可以撤消这种虚假聚合。

正确的对称 INNER JOIN 方法:LEFT JOIN q1 & q2--1:many--then GROUP BY & GROUP_CONCAT(这是您的第一个查询所做的);然后分别类似地 LEFT JOIN q1 & q3--1:many--then GROUP BY & GROUP_CONCAT;然后在 user_id--1:1 上 INNER JOIN 两个结果。

正确的对称标量子查询方法:从 q1 中选择 GROUP_CONCAT 作为scalar subqueries,每个都有一个 GROUP BY。

正确的累积 LEFT JOIN 方法:LEFT JOIN q1 & q2--1:many--then GROUP BY & GROUP_CONCAT;然后左加入那个 & q3--1:many--然后 GROUP BY & GROUP_CONCAT。

像您的第二个查询这样的正确方法:您首先 LEFT JOIN q1 & q2--1:many。然后你离开那个& q3--many:1:many。它为带有 user_id 的标签和类别的每个可能组合提供一行。然后在你 GROUP BY 你 GROUP_CONCAT 之后 - 重复 (user_id, tag) 对和重复 (user_id, category) 对。这就是为什么你有重复的列表元素。但是将 DISTINCT 添加到 GROUP_CONCAT 会给出正确的结果。 (根据wchiquito 的评论。)

您通常更喜欢根据实际数据/使用情况/统计信息通过查询计划和时间来进行工程权衡。预期重复量的输入和统计信息)、实际查询的时间等。一个问题是 many:1:many JOIN 方法的额外行是否抵消了它对 GROUP BY 的节省。

-- cumulative LEFT JOIN approach
SELECT
   q1.user_id, q1.user_name, q1.score, q1.reputation,
    top_two_tags,
    substring_index(group_concat(q3.category  ORDER BY q3.category_reputation DESC SEPARATOR ','), ',', 2) AS category
FROM
    -- your 1st query (less ORDER BY) AS q1
    (SELECT
        q1.user_id, q1.user_name, q1.score, q1.reputation, 
        substring_index(group_concat(q2.tag  ORDER BY q2.tag_reputation DESC SEPARATOR ','), ',', 2) AS top_two_tags
    FROM
        (SELECT 
            u.id AS user_Id, 
            u.user_name,
            coalesce(sum(r.score), 0) as score,
            coalesce(sum(r.reputation), 0) as reputation
        FROM 
            users u
            LEFT JOIN reputations r 
                ON    r.user_id = u.id 
                  AND r.date_time > 1500584821 /* unix_timestamp(DATE_SUB(now(), INTERVAL 1 WEEK)) */
        GROUP BY 
            u.id, u.user_name
        ) AS q1
        LEFT JOIN
        (
        SELECT
            r.user_id AS user_id, t.tag, sum(r.reputation) AS tag_reputation
        FROM
            reputations r 
            JOIN post_tag pt ON pt.post_id = r.post_id
            JOIN tags t ON t.id = pt.tag_id
        WHERE
            r.date_time > 1500584821 /* unix_timestamp(DATE_SUB(now(), INTERVAL 1 WEEK)) */
        GROUP BY
            user_id, t.tag
        ) AS q2
        ON q2.user_id = q1.user_id 
        GROUP BY
            q1.user_id, q1.user_name, q1.score, q1.reputation
    ) AS q1
    -- finish like your 2nd query
    LEFT JOIN
    (
    SELECT
        r.user_id AS user_id, c.category, sum(r.reputation) AS category_reputation
    FROM
        reputations r 
        JOIN post_category ct ON ct.post_id = r.post_id
        JOIN categories c ON c.id = ct.category_id
    WHERE
        r.date_time > 1500584821 /* unix_timestamp(DATE_SUB(now(), INTERVAL 1 WEEK)) */
    GROUP BY
        user_id, c.category
    ) AS q3
    ON q3.user_id = q1.user_id 
GROUP BY
    q1.user_id, q1.user_name, q1.score, q1.reputation
ORDER BY
    q1.reputation DESC, q1.score DESC ;

【讨论】:

好的,作为咨询,你推荐哪一个?使用DISTINCT?使用LEFT JOIN(喜欢你的答案)?使用 子查询 ? 这里没有可以决定的一般原则。我说过,这是一种权衡你必须在你的确切情况下衡量(包括重要的是你的期望和优化器),就像工程“最佳”(嵌合体)一样。 (不同和累积查询的计划似乎很接近;但数据和统计数据是玩具。我希望优化器实现接近累积或内部连接的子选择查询,因为它们之间有明显的简单转换。我是更怀疑您查询中的所有重复,但我想解决有关连接的问题。) PS 我确信在 dba.stackexchange.com 上会有很多东西。但是到达准备好了

以上是关于来自 GROUP_BY 的两个 LEFT JOIN 的 GROUP_CONCAT 的奇怪重复行为的主要内容,如果未能解决你的问题,请参考以下文章

使用来自多个数据集 SQL 的多个 LEFT JOIN

mysql多表left join联合查询效率问题5

Sql关于两个left join 的问题

DQL查询数据----联表查询(left/right/inner join 区别)

DQL查询数据----联表查询(left/right/inner join 区别)

mysql执行顺序