mysql - with - sql select group_concat



Comportamento duplicado estranho de GROUP_CONCAT de duas JOINES ESQUERDAS de GROUP_BYs (1)

Here está a estrutura de todas as minhas tabelas e a consulta (por favor, concentre-se na última consulta, anexada abaixo) . Como você vê no violino, aqui está a saída atual:

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

Está correto e tudo bem.

Agora eu tenho mais uma existência chamada "categoria". Cada postagem pode ter apenas uma categoria. E também quero obter duas categorias principais para cada usuário. E here está minha nova consulta. Como você vê no resultado, algumas duplicatas aconteceram:

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

Vejo? css,css , technology, technology . Por que estes são duplicados? Acabei de adicionar mais um LEFT JOIN para categories , exatamente como tags . Mas não funciona como esperado e até afeta as tags também.

De qualquer forma, este é o resultado esperado:

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

Alguém sabe como posso conseguir isso?

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 ;

Sua segunda consulta é da seguinte forma:

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

O GROUP BYs interno resulta em (user_id, t.tag) & (user_id, c.category) sendo chaves / UNIQUEs. Além disso, não vou abordar esses GROUP BYs.

TL; DR Quando você une (q1 JOIN q2) a q3 não está em uma chave / UNIQUE de um deles, então para cada user_id você obtém uma linha para cada combinação possível de tag e categoria. Assim, as entradas finais do GROUP BY duplicam por (user_id, tag) e por (user_id, category) e inapropriadamente GROUP_CONCATs duplicam tags e categorias por user_id. Corrigir seria (q1 JOIN q2 GROUP BY) JOIN (q1 JOIN q3 GROUP BY) em que todas as uniões estão na chave comum / UNIQUE (user_id) e não há nenhuma agregação espúria. Embora às vezes você possa desfazer essa agregação espúria.

Uma abordagem de INNER JOIN simétrica correta: LEFT JOIN q1 & q2--1: muitos - então GROUP BY & GROUP_CONCAT (que é o que sua primeira consulta fez); então separadamente similarmente LEFT JOIN q1 & q3--1: muitos - então GROUP BY & GROUP_CONCAT; então INNER JOIN os dois resultados em user_id - 1: 1.

Uma abordagem de subconsulta escalar simétrica correta: SELECT os GROUP_CONCATs de q1 como subconsultas escalares, cada um com um GROUP BY.

Uma abordagem correta do LEFT JOIN cumulativo: LEFT JOIN q1 & q2--1: muitos - então GROUP BY & GROUP_CONCAT; então LEFT JOIN that & q3--1: many - então GROUP BY & GROUP_CONCAT.

Uma abordagem correta como sua segunda consulta: Você primeiro LEFT JOIN q1 & q2--1: many. Então você LEFT JOIN that & q3 - many: 1: many. Ele fornece uma linha para todas as combinações possíveis de uma tag e uma categoria que aparecem com um user_id. Então, depois de GROUP BY você GROUP_CONCAT - sobre pares duplicados (tag user_id,) e pares duplicados (user_id, category). É por isso que você tem elementos de lista duplicados. Mas adicionar DISTINCT a GROUP_CONCAT fornece um resultado correto. (Por comentário do wchiquito )

Qual você prefere é, como de costume, uma compensação de engenharia a ser informada pelos planos de consulta e horários, por dados / uso / estatísticas reais. entrada e estatísticas para quantidade esperada de duplicação), tempo de consultas reais, etc. Uma questão é se as linhas extras da abordagem: 1: many JOIN compensam a economia de um 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 ;




group-concat