问题描述
在执行一个包含多个 UNION ALL 的复杂查询时,报错:
Subquery returns more than 1 row
该查询的核心是多个标量子查询(correlated subquery)用于从字典表 zz_global_dict_item 中取 item_name:
sql
(SELECT d.item_name FROM zz_global_dict_item d
WHERE d.dict_code = 'xxx' AND d.item_id = 主表.字段)
排查过程与发现
-
数据重复检查
执行以下查询均无结果:
sqlSELECT dict_code, item_id, COUNT(*) FROM zz_global_dict_item GROUP BY dict_code, item_id HAVING COUNT(*) > 1;→
(dict_code, item_id)组合唯一,无重复。 -
不可见字符检查
sqlSELECT ... WHERE LENGTH(item_id) != CHAR_LENGTH(item_id);→ 无结果,无隐藏字符(如全角空格、控制字符)。
-
空值/NULL 检查
主表和字典表中空值情况正常,未导致多行匹配。
-
拆分查询验证
将大查询拆成 6 个独立部分(领用、归还、报废、新增、合并等),每个部分单独执行均正常,子查询只返回 0 或 1 行。
-
加 LIMIT 1 测试
在所有子查询中加
LIMIT 1后,整个 UNION ALL 查询正常执行,不再报错。
根本原因
MySQL 优化器 Bug / 执行计划异常
- 每个子查询单独运行时都只返回最多 1 行。
- 但当多个标量子查询放在同一个大
UNION ALL查询中时,MySQL 优化器在解析执行计划阶段误判某些子查询可能返回多行,从而提前抛出 "Subquery returns more than 1 row" 错误(即使实际运行不会)。 - 这属于 MySQL(尤其是 5.7/8.0 某些版本)在复杂 UNION + 标量子查询场景下的已知边缘问题。
- 加
LIMIT 1后,优化器明确知道子查询最多返回一行,于是跳过错误检查,查询正常执行。
解决方案
方案1:短期快速修复(推荐立即使用)
在所有标量子查询中加入以下内容:
sql
AND d.deleted_flag = 0
ORDER BY d.show_order ASC
LIMIT 1
deleted_flag = 0:避免匹配到历史已删除的垃圾数据ORDER BY show_order:确保取到显示顺序正确的记录LIMIT 1:彻底规避优化器误判报错
已验证有效,且结果准确。
方案2:长期最佳实践(强烈推荐)
将所有标量子查询改为 LEFT JOIN
彻底消除子查询,避免优化器问题,同时大幅提升性能。
示例结构(简化):
sql
SELECT ...
d1.item_name AS `来源`,
...
FROM biz_receipt_detail brd
LEFT JOIN zz_global_dict_item d1
ON d1.dict_code = 'DictReceiptType'
AND d1.item_id = brd.receipt_source
AND d1.deleted_flag = 0
...
方案3:其他临时规避(不推荐长期)
- 会话级设置:
SET optimizer_switch = 'derived_merge=off'; - 或
SET SQL_BIG_SELECTS = 1;
结论
- 这不是数据问题,而是 MySQL 优化器在复杂 UNION ALL + 标量子查询下的边缘 Bug。
- 加
LIMIT 1(配合deleted_flag和ORDER BY)是安全、有效的解决方案,已验证可用。 - 建议后续重构为 LEFT JOIN 方式,彻底根治并提升性能。