Mysql
 sql >> Base de données >  >> RDS >> Mysql

Comportement en double étrange de GROUP_CONCAT de deux LEFT JOIN de GROUP_BY

Votre seconde requête est de la forme :

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

Les GROUP BY internes donnent (user_id, t.tag) &(user_id, c.category) étant des clés/UNIQUES. En dehors de cela, je n'aborderai pas ces GROUP BY.

TL;DR Lorsque vous joignez (q1 JOIN q2) à q3, ce n'est pas sur une clé/UNIQUE de l'un d'entre eux, donc pour chaque user_id, vous obtenez une ligne pour chaque combinaison possible de balise et de catégorie. Ainsi, les entrées GROUP BY finales sont dupliquées par (user_id, tag) et par (user_id, catégorie) et GROUP_CONCATs duplique de manière inappropriée les balises et les catégories par user_id. Correct serait (q1 JOIN q2 GROUP BY) JOIN (q1 JOIN q3 GROUP BY) dans lequel toutes les jointures sont sur la clé commune/UNIQUE (user_id) &il n'y a pas de fausse agrégation. Bien que vous puissiez parfois annuler une telle agrégation fallacieuse.

Une approche INNER JOIN symétrique correcte :LEFT JOIN q1 &q2--1:many--then GROUP BY &GROUP_CONCAT (ce que votre première requête a fait) ; puis séparément de manière similaire LEFT JOIN q1 &q3--1:many--then GROUP BY &GROUP_CONCAT; puis INNER JOIN les deux résultats ON user_id--1:1.

Une approche de sous-requête scalaire symétrique correcte :SÉLECTIONNEZ les GROUP_CONCAT de q1 en tant que sous-requêtes scalaires chacun avec un GROUP BY.

Une approche LEFT JOIN cumulative correcte :LEFT JOIN q1 &q2--1:many--then GROUP BY &GROUP_CONCAT ; puis LEFT JOIN that &q3--1:many--puis GROUP BY &GROUP_CONCAT.

Une approche correcte comme votre 2ème requête :vous avez d'abord LEFT JOIN q1 &q2--1:many. Ensuite, vous avez quitté JOIN that &q3--many:1:many. Il donne une ligne pour chaque combinaison possible d'une balise et d'une catégorie qui apparaissent avec un user_id. Ensuite, après avoir GROUP BY, vous GROUP_CONCAT - sur les paires en double (user_id, tag) et les paires en double (user_id, catégorie). C'est pourquoi vous avez des éléments de liste en double. Mais ajouter DISTINCT à GROUP_CONCAT donne un résultat correct. (Par wchiquito commentaire de.)

Ce que vous préférez est comme d'habitude un compromis d'ingénierie pour être informé par les plans et les délais de requête, par données/utilisation/statistiques réelles. entrée et statistiques pour la quantité de duplication attendue), le moment des requêtes réelles, etc. Un problème est de savoir si les lignes supplémentaires de l'approche JOIN many:1:many compensent sa sauvegarde d'un 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 ;