验证目标:在特定场景下(OR 连接不同字段的条件),将 OR 查询拆分为 UNION ALL 能显著提升查询效率。
通过 Docker 运行 MySQL 服务
1
2
3
4
5
6
7
8
9
10
| $ docker pull mysql:8.0
$ docker run -d \
--name mysql-or-testing \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-e MYSQL_CHARACTER_SET_SERVER=utf8mb4 \
-e MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci \
-v mysql_data:/var/lib/mysql \
--restart=always \
mysql:8.0
|
查看服务运行状态:
1
2
3
| $ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3ec8934547ce mysql:8.0 "docker-entrypoint.s…" 7 seconds ago Up 7 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp mysql-or-testing
|
创建测试表,插入大量数据
进入容器内的 MySQL 终端:
1
2
| $ docker exec -it mysql-or-testing /bin/bash
bash-5.1# mysql -uroot -p123456
|
创建测试数据库
1
2
| CREATE DATABASE IF NOT EXISTS test_optimize;
USE test_optimize;
|
创建用户行为表,模拟真实业务数据:
1
2
3
4
5
6
| CREATE TABLE user_behavior (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
create_time DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
创建一个插入测试数据的存储过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| DELIMITER //
CREATE PROCEDURE insert_test_data()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 1000 DO
INSERT INTO user_behavior (user_id, order_id, create_time)
SELECT
FLOOR(RAND() * 1000000),
FLOOR(RAND() * 10000000),
DATE_ADD('2026-01-01', INTERVAL FLOOR(RAND() * 365) DAY)
FROM information_schema.tables LIMIT 1000;
SET i = i + 1;
END WHILE;
END //
DELIMITER ;
|
调用 insert_test_data 存储过程,并查看插入的记录数
1
2
3
4
5
6
| -- 多次调用,使其记录数尽可能多
CALL insert_test_data();
CALL insert_test_data();
CALL insert_test_data();
SELECT COUNT(*) FROM user_behavior;
|
共插入 990000 条记录。给 user_behavior 表的 user_id 和 order_id 创建索引:
1
2
| CREATE INDEX idx_user_id ON user_behavior(user_id);
CREATE INDEX idx_order_id ON user_behavior(order_id);
|
验证
逻辑:查询 user_id = 929195 或 order_id = 9804737 的记录。
1
2
3
4
5
6
7
8
9
10
11
12
| -- 1. 开启执行时间显示
SET profiling = 1;
-- 2. 执行OR查询
SELECT * FROM user_behavior WHERE user_id = 929195 OR order_id = 9804737;
-- 3. 查看执行计划
EXPLAIN
SELECT * FROM user_behavior WHERE user_id = 929195 OR order_id = 9804737\G;
-- 4. 查看查询耗时
SHOW PROFILES;
|
执行计划的输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| *************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: user_behavior
partitions: NULL
type: index_merge
possible_keys: idx_user_id,idx_order_id
key: idx_user_id,idx_order_id
key_len: 8,8
ref: NULL
rows: 4
filtered: 100.00
Extra: Using union(idx_user_id,idx_order_id); Using where
1 row in set, 1 warning (0.00 sec)
|
类型为索引合并(index_merge)。
执行时间为:0.00763325。
将查询语句调整为 UNION 方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
| -- 1. 执行UNION ALL查询
SELECT * FROM user_behavior WHERE user_id = 929195
UNION ALL
SELECT * FROM user_behavior WHERE order_id = 9804737;
-- 2. 查看执行计划
EXPLAIN
SELECT * FROM user_behavior WHERE user_id = 929195
UNION ALL
SELECT * FROM user_behavior WHERE order_id = 9804737\G;
-- 3. 查看查询耗时(对比OR版本)
SHOW PROFILES;
|
执行计划的输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| *************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: user_behavior
partitions: NULL
type: ref
possible_keys: idx_user_id
key: idx_user_id
key_len: 8
ref: const
rows: 2
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 2
select_type: UNION
table: user_behavior
partitions: NULL
type: ref
possible_keys: idx_order_id
key: idx_order_id
key_len: 8
ref: const
rows: 2
filtered: 100.00
Extra: NULL
2 rows in set, 1 warning (0.00 sec)
|
执行计划显示有两个查询,类型是非唯一索引。
执行时间为:0.00937475s。
总结
UNION/UNION ALL 操作反而略慢于 OR 查询。
在 MySQL 8 及两个过滤字段都有索引的情况下,使用 UNION/UNION ALL 操作并不会明显提升查询速度。原因在于:
- MySQL 5.6 以前无法利用多个索引字段,而之后有“索引合并 index_merge”
- 优化器识别到 OR 条件可以拆分为两个独立索引的查询,自动应用了“索引合并 index_merge”