Count of multiple items in one MySQL query returns an incorrect result

c00000fd :

Say, if I define my tables as such:

create table `usrs` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`fnm` TINYTEXT,
`lnm` TINYTEXT
);

insert into `usrs` (`fnm`, `lnm`)
VALUES
('John', 'Doe'),    -- 1
('Mary', 'Smith'),  -- 2
('Peter', 'Pan'),   -- 3
('Michael', 'Jackson'); -- 4

enter image description here

create table `pmts` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`uid` INT UNSIGNED, -- id in `usrs` table
`amt` DOUBLE,
`flgs` INT UNSIGNED
);

insert into `pmts` (`uid`, `amt`, `flgs`)
VALUES
('3', '0.99', 0),
('1', '1.50', 0x80),
('3', '2',    0x80),
('3', '0.99', 0),
('4', '1.30', 0),
('3', '2.40', 0),
('1', '2.55', 0x80);

enter image description here

create table `downloads` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`uid` INT UNSIGNED, -- id in `usrs` table
`app` TINYTEXT
);

insert into `downloads` (`uid`, `app`)
VALUES
('2', 'Candy Crush'),
('2', 'Skype'),
('3', 'Word'),
('2', 'Calc'),
('4', 'Doom'),
('3', 'Notepad');

enter image description here

Then when I run this query, that among other things counts pmts, downloads and special flgs for each usr:

-- this query contains additional clauses that were used in my production query

SELECT t1.*, 
COUNT(t2.`id`) as cPmts,                        -- count `pmts` for each user
SUM(IF((t2.`flgs`&0x80)<>0, 1, 0)) as cPmtFlgs, -- count `pmts` with `flgs` equal to 0x80 for each user
COUNT(t3.`id`) as cDwds                         -- count `downloads` for each user
FROM `usrs` t1
LEFT JOIN `pmts` t2 ON t1.`id`=t2.`uid`
LEFT JOIN `downloads` t3 ON t1.`id`=t3.`uid`
WHERE t1.`id` > 0   -- just to simulate some condition
GROUP BY t1.`id`
ORDER BY t1.`fnm`, t1.`lnm` ASC
LIMIT 0, 10

the results I receive, marked in red, are wrong:

enter image description here

Why?

Nick :

Your problem is that because Peter Pan has multiple payments and multiple downloads, his rows are being duplicated in the JOIN (you can see this if you remove the GROUP BY and replace the aggregation functions in the query with a simple SELECT * [demo]). The way to work around this is to perform the aggregations in derived tables and JOIN those results instead:

SELECT t1.*, 
COALESCE(t2.cPmts, 0) AS cPmts,                        -- count `pmts` for each user
COALESCE(t2.cPmtFlgs, 0) AS cPmtFlgs, -- count `pmts` with `flgs` equal to 0x80 for each user
COALESCE(t3.cDwds, 0) AS cDwds                         -- count `downloads` for each user
FROM `usrs` t1
LEFT JOIN (SELECT uid, 
           COUNT(*) AS cPmts,
           SUM(IF((`flgs`&0x80)<>0, 1, 0)) AS CpmtFlgs
           FROM `pmts`
           GROUP BY uid) t2 ON t1.`id`=t2.`uid`
LEFT JOIN (SELECT uid, COUNT(*) AS cDwds
           FROM `downloads`
           GROUP BY uid) t3 ON t1.`id`=t3.`uid`
WHERE t1.`id` > 0   -- just to simulate some condition
ORDER BY t1.`fnm`, t1.`lnm` ASC
LIMIT 0, 10

Output:

id  fnm         lnm         cPmts   cPmtFlgs    cDwds
1   John        Doe         2       2           0
2   Mary        Smith       0       0           3
4   Michael     Jackson     1       0           1
3   Peter       Pan         4       1           2

Demo on SQLFiddle

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=395317&siteId=1