Search code examples
mysqljoingroup-bygroup-concat

LEFT JOIN after GROUP BY?


I have a table of "Songs", "Songs_Tags" (relating songs with tags) and "Songs_Votes" (relating songs with boolean like/dislike).

I need to retrieve the songs with a GROUP_CONCAT() of its tags and also the number of likes (true) and dislikes (false).

My query is something like that:

SELECT
    s.*,
    GROUP_CONCAT(st.id_tag) AS tags_ids,
    COUNT(CASE WHEN v.vote=1 THEN 1 ELSE NULL END) as votesUp,
    COUNT(CASE WHEN v.vote=0 THEN 1 ELSE NULL END) as votesDown,
FROM Songs s
    LEFT JOIN Songs_Tags st ON (s.id = st.id_song)
    LEFT JOIN Votes v ON (s.id=v.id_song)
GROUP BY s.id
ORDER BY id DESC

The problem is that when a Song has more than 1 tag, it gets returned more then once, so when I do the COUNT(), it returns more results.

The best solution I could think is if it would be possible to do the last LEFT JOIN after the GROUP BY (so now there would be only one entry for each song). Then I'd need another GROUP BY m.id.

Is there a way to accomplish that? Do I need to use a subquery?


Solution

  • There've been some good answers so far, but I would adopt a slightly different method quite similar to what you described originally

    SELECT
        songsWithTags.*,
        COALESCE(SUM(v.vote),0) AS votesUp,
        COALESCE(SUM(1-v.vote),0) AS votesDown
    FROM (
        SELECT
            s.*,
            COLLATE(GROUP_CONCAT(st.id_tag),'') AS tags_ids
        FROM Songs s
        LEFT JOIN Songs_Tags st
            ON st.id_song = s.id
        GROUP BY s.id
    ) AS songsWithTags
    LEFT JOIN Votes v
    ON songsWithTags.id = v.id_song
    
    GROUP BY songsWithTags.id DESC
    

    In this the subquery is responsible for collating songs with tags into a 1 row per song basis. This is then joined onto Votes afterwards. I also opted to simply sum up the v.votes column as you have indicated it is 1 or 0 and therefore a SUM(v.votes) will add up 1+1+1+0+0 = 3 out of 5 are upvotes, while SUM(1-v.vote) will sum 0+0+0+1+1 = 2 out of 5 are downvotes.

    If you had an index on votes with the columns (id_song,vote) then that index would be used for this so it wouldn't even hit the table. Likewise if you had an index on Songs_Tags with (id_song,id_tag) then that table wouldn't be hit by the query.

    edit added solution using count

    SELECT
        songsWithTags.*,
        COUNT(CASE WHEN v.vote=1 THEN 1 END) as votesUp,
        COUNT(CASE WHEN v.vote=0 THEN 1 END) as votesDown
    FROM (
        SELECT
            s.*,
            COLLATE(GROUP_CONCAT(st.id_tag),'') AS tags_ids
        FROM Songs s
        LEFT JOIN Songs_Tags st
            ON st.id_song = s.id
        GROUP BY s.id
    ) AS songsWithTags
    LEFT JOIN Votes v
    ON songsWithTags.id = v.id_song
    
    GROUP BY songsWithTags.id DESC