MySQL中两个带FIND_IN_SET的LEFT JOIN导致聚合函数结果错误
这个问题和GROUP BY逻辑本身无关,核心原因是两次一对多关联产生了笛卡尔积。
你用FIND_IN_SET做关联本质是一对多匹配:对单条主表记录,关联Beatles表会返回k条匹配记录,关联Nursery表会返回n条匹配记录。当你连续写两个LEFT JOIN时,数据库生成的中间结果集不是k+n条,而是kn条——也就是两个关联表匹配结果的笛卡尔乘积,后续聚合函数是基于这个乘积后的行数计算的,自然会出现计数翻倍、数值虚高的问题。
拿id=4的主表记录举例:它的beatlesIds是1,2,3,4,能匹配到Beatles表4条记录;nurseryIds是1,2,3,能匹配到Nursery表3条记录,两次JOIN后中间结果直接生成43=12行,此时不管是COUNT(N.id)还是COUNT(B.id),统计的都是这12行的非空值,结果自然都是12,和预期值差很多。
只JOIN单张表时不会出现多表匹配结果交叉相乘的情况,所以计算结果完全正常。
另外你测试结果里id=3的记录COUNT(N.id)为0是符合数据逻辑的:你给的Nursery表测试数据只有id=1/2/3三条,主表id=3的nurseryIds是4,根本匹配不到对应记录,计数为0是正确结果,你之前的预期值这里写错了。
不要直接连续关联两个一对多关系的表再做聚合,先分别对单张关联表做聚合计算,再把聚合结果和主表关联,就能避免笛卡尔积问题。
修正后的SQL如下:
SELECT M.id, M.nurseryIds, IFNULL(N.nursery_count, 0) AS COUNT_N_id, M.beatlesIds, IFNULL(B.beatles_count, 0) AS COUNT_B_id FROM main M LEFT JOIN ( SELECT M.id, COUNT(B.id) AS beatles_count FROM main M LEFT JOIN Beatles B ON FIND_IN_SET(B.id, M.beatlesIds) GROUP BY M.id ) B ON M.id = B.id LEFT JOIN ( SELECT M.id, COUNT(N.id) AS nursery_count FROM main M LEFT JOIN Nursery N ON FIND_IN_SET(N.id, M.nurseryIds) GROUP BY M.id ) N ON M.id = N.id
执行后返回的结果完全符合逻辑(除了id=3的Nursery计数为0,是测试数据本身决定的正确结果)。
用逗号分隔字符串存储多ID关联关系本身是反范式设计,FIND_IN_SET函数无法利用索引,查询性能差,也极易出现这类JOIN计数错误的问题。生产环境建议改成标准的关联中间表,通过外键建立关联关系,兼顾查询性能和数据准确性。
内容的提问来源于stack exchange,提问作者Guy Arnon

