1. 复合查询的本质与价值
MySQL中的复合查询(Compound Query)是指通过逻辑运算符组合多个SELECT语句的查询方式。这就像厨师做一道复杂菜品时,需要把几种基础食材按特定方式组合——单看每种食材都很普通,但组合后却能产生全新的风味。
复合查询的核心价值在于它能解决三类典型问题:
- 数据分散在不同表但需要合并展示(如连锁店销售汇总)
- 需要对数据集进行前后对比(如月度销售增长率)
- 复杂业务规则下的数据筛选(如VIP客户且最近三个月有消费)
工作中我常遇到这样的场景:市场部门需要一份"既有A行为又有B行为"的用户名单。如果分开查两次再人工比对,不仅效率低还容易出错。而用UNION或INTERSECT一次搞定,代码简洁且性能更好。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 复合查询的四种武器库
2.1 UNION:数据合并术
UNION就像把两份Excel表格上下粘贴在一起。最近做用户画像分析时,我需要合并APP端和Web端的日活用户:
sql复制SELECT user_id FROM app_daily_active
WHERE date = '2023-06-01'
UNION
SELECT user_id FROM web_daily_active
WHERE date = '2023-06-01';
这里有个实战经验:UNION默认会去重,如果明确需要保留重复记录(比如统计UV),要用UNION ALL。有次我漏了ALL导致DAU数据少算了15%,排查半天才发现是去重惹的祸。
2.2 INTERSECT:数据交集定位器
MySQL官方其实没有直接实现INTERSECT,但我们可以用INNER JOIN模拟:
sql复制SELECT a.user_id
FROM (
SELECT user_id FROM behavior_A
WHERE date > '2023-05-01'
) a
INNER JOIN (
SELECT user_id FROM behavior_B
WHERE date > '2023-05-01'
) b ON a.user_id = b.user_id;
这种查询特别适合做用户行为分析。上个月我们通过这种方式找出同时点击广告和完成购买的客户,精准优化了投放策略。
2.3 EXCEPT:数据差异过滤器
同样需要技巧性实现,用LEFT JOIN配合NULL判断:
sql复制SELECT a.user_id
FROM registered_users a
LEFT JOIN banned_users b ON a.user_id = b.user_id
WHERE b.user_id IS NULL;
实际使用时要注意:LEFT JOIN的性能在大数据量时可能变差。我有次在百万级用户表操作,这个查询跑了20分钟。后来给user_id加了联合索引,优化到8秒完成。
2.4 子查询:查询中的查询
子查询就像俄罗斯套娃,最经典的例子是找比平均工资高的员工:
sql复制SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
但新手常犯的错误是在SELECT子句里放关联子查询,导致每行都要执行一次子查询。有次我这样写导致查询时间从0.1秒暴增到12秒,后来改用JOIN重写才解决。
3. 性能优化实战技巧
3.1 索引的正确打开方式
复合查询涉及多表时,索引设计尤为关键。建议:
- 所有JOIN字段必须建立索引
- WHERE条件中的高频过滤字段建索引
- 避免在索引列上使用函数(如DATE(create_time))
上周我优化一个UNION查询,原执行时间4.7秒。给三个关联字段加索引后降到0.3秒,效果立竿见影。
3.2 执行计划解读指南
EXPLAIN是诊断查询性能的X光机。重点关注:
- type列:最好看到eq_ref或const,避免ALL
- rows列:估算扫描行数越少越好
- Extra列:出现"Using temporary"或"Using filesort"要警惕
有次发现个查询用了临时表,调整成UNION ALL后性能提升6倍。执行计划就像汽车仪表盘,不看它就是在盲开。
3.3 大数据量分页策略
当UNION查询需要分页时,绝对不要这样写:
sql复制(SELECT * FROM table1)
UNION
(SELECT * FROM table2)
LIMIT 10 OFFSET 10000;
这会先合并所有数据再分页,超级浪费资源。正确的做法是各子查询先分页再合并:
sql复制(SELECT * FROM table1 LIMIT 10010)
UNION
(SELECT * FROM table2 LIMIT 10010)
LIMIT 10 OFFSET 10000;
4. 真实业务场景案例
4.1 电商用户行为分析
找出加购但未购买的用户(转化漏斗分析):
sql复制SELECT cart.user_id
FROM shopping_cart cart
LEFT JOIN orders o ON cart.user_id = o.user_id
AND cart.product_id = o.product_id
WHERE o.order_id IS NULL
AND cart.add_time > '2023-06-01';
这个查询帮助我们发现了支付流程的BUG,修复后转化率提升了22%。
4.2 多平台数据汇总报表
合并微信小程序和APP的订单数据:
sql复制SELECT 'WeChat' as platform, COUNT(*) as order_count
FROM wechat_orders
WHERE create_date = CURDATE()
UNION ALL
SELECT 'APP' as platform, COUNT(*)
FROM app_orders
WHERE create_date = CURDATE();
注意这里用UNION ALL是因为我们需要保留各平台的独立统计,不需要去重。
4.3 权限管理系统查询
查找有查看权限但无编辑权限的用户:
sql复制SELECT user_id
FROM user_permissions
WHERE permission = 'view'
AND user_id NOT IN (
SELECT user_id
FROM user_permissions
WHERE permission = 'edit'
);
这种NOT IN子查询在数据量大时会变慢,超过10万条记录建议改用LEFT JOIN方案。
5. 避坑指南与经验分享
5.1 数据类型一致性陷阱
UNION查询时,各SELECT语句的列数据类型必须兼容。有次我遇到个报错,发现是一个查询返回INT,另一个返回VARCHAR:
sql复制-- 会报错
SELECT 123 AS number
UNION
SELECT '456' AS number;
-- 正确写法
SELECT 123 AS number
UNION
SELECT CAST('456' AS SIGNED) AS number;
5.2 排序与LIMIT的微妙关系
ORDER BY作用于整个UNION结果,不是单个查询:
sql复制-- 错误:只排序第一个查询
(SELECT name FROM employees ORDER BY salary DESC)
UNION
(SELECT name FROM contractors);
-- 正确:整体排序
(SELECT name, salary FROM employees)
UNION
(SELECT name, salary FROM contractors)
ORDER BY salary DESC;
5.3 临时表优化技巧
对于复杂复合查询,可以分步用临时表存储中间结果:
sql复制CREATE TEMPORARY TABLE temp_qualified_users AS
SELECT user_id FROM users WHERE score > 80;
-- 后续查询直接使用临时表
SELECT * FROM temp_qualified_users
JOIN user_behavior ON ...;
临时表在会话结束会自动删除,适合处理中间结果。但要注意内存消耗,超过tmp_table_size会转磁盘影响性能。
