WHERE 1=1 影响SQL性能吗?实测执行计划告诉你真相
后端群里隔三差五就会冒出一个经典问题:"
where 1=1
会不会影响性能?" 说会影响的人,理由是它改变了SQL结构、增加了计算条件;说不会影响的人,理由是"1=1"又没什么计算量,优化器会自动处理。这两种说法听起来都有道理,但我在 MySQL、PostgreSQL 上都做过实际验证,结论很明确:在成熟的关系型数据库里,常规的
WHERE 1=1
基本不会影响查询性能,优化器会在解析阶段把它当成恒真条件直接处理掉,执行计划和不写它完全一样。但"不影响"不等于"建议用",因为这个写法背后经常跟着一串真正拖慢SQL的坏习惯。
这篇文章我会把执行计划实测数据放出来,讲清楚什么场景下
1=1
才有存在感,再给出工程上更优雅的动态条件写法,最后给你一套三分钟就能上手的慢SQL自查流程。无论你是后端开发、DBA,还是刚开始学SQL的新人,跟着走一遍基本就不会再被这个问题问住了。
1. 先给结论:1=1 不背性能黑锅,但也不值得无脑用
先说清楚一个容易被混淆的点:我们在讨论
WHERE 1=1
的性能问题时,其实在讨论两件不同的事。第一件是"它本身会不会产生额外的计算开销",第二件是"包含它的整条SQL慢,是不是因为它"。前者答案很明确,不会;后者绝大多数情况也不是它的锅。真正让SQL变慢的,是旁边的
OR
条件、函数包裹索引列、字符串值拼接、没有索引的过滤字段这些东西。
1.1 优化器做了一件你没看见的事
数据库拿到一条SQL之后,第一件事不是直接去表里扫描,而是经过解析、预处理、优化这几个阶段。
1=1
是一个常量表达式,优化器在预处理阶段就会做常量折叠(constant folding),把
1=1
直接规约为
TRUE
。一个恒真的条件加到WHERE子句里,等于什么都没加,后续的索引选择、访问路径规划都不会把它当回事。
我用 MySQL 8.0 做过测试,两条SQL:
SELECT * FROM user WHERE status = 1;
SELECT * FROM user WHERE 1=1 AND status = 1;
用
EXPLAIN FORMAT=TREE
查看执行计划,两条SQL的输出完全一致,树形结构里根本看不到
(1 = 1)
这个条件的影子,优化器已经把它吃掉了。PostgreSQL 里用
EXPLAIN
看也一样,Filter 部分不会出现恒真条件。SQL Server 和 Oracle 对这类常量条件的化简能力也都很成熟,所以在这些数据库上纠结
1=1
的"额外计算量",属于想多了。
1.2 网上的说法为什么都对又都不全对
网上最常见的说法是"
where 1=1
会导致全表扫描、会导致索引失效"。这个说法在绝大多数场景下是错的。没有
1=1
,如果查询条件没有可用的索引,一样是全表扫描;有了
1=1
,只要后面的
AND
条件能走索引,执行计划照样走索引。
1=1
本身既不涉及索引列,又是个常量条件,它没有能力让优化器"放弃"一个本来能用的索引。
但网上说"会有影响"的人,其实在某些特定场景下也不算全错。比如到了分库分表中间件、MPP 分析引擎这类环境里,SQL 解析层不一定做了同样的常量消除,SQL 文本变长带来的解析成本、中间件路由成本就会真实存在。另外还有一类衍生问题:很多人写着写着就变成
WHERE 1=1 OR ...
,或者
WHERE 1=1
后面跟着一堆通过字符串拼接塞进去的值,这些情况确实会出性能问题。所以更准确的说法是:
1=1
本身无害,但它经常作为糟糕写法的"掩护",让人们忽略了真正要修的东西。
2. 这行代码是怎么混进生产环境的
要理解
WHERE 1=1
为什么如此常见,得回到动态SQL还靠手动字符串拼接的年代。直到今天,不少老项目的 DAO 层代码里还能看到这样的逻辑。
2.1 没有它,动态条件很难拼接
早期 JDBC 写动态查询,最常见的痛点就是"到底要不要加 AND"。比如按状态和用户名查用户:
String sql = "SELECT * FROM user WHERE 1=1";
if (status != null) {
sql += " AND status = " + status;
}
if (name != null && !name.isEmpty()) {
sql += " AND name = '" + name + "'";
}
先写一个
WHERE 1=1
放在那,后面每个条件就都可以无脑用
AND
开头,不需要去判断"这是不是第一个条件"。如果不用这个技巧,就得多写一堆
if
判断来拼接
WHERE
和
AND
,代码又丑又容易漏。所以
1=1
本质上是一个"省事工具",刚出来的时候确实是解决实际问题的。
2.2 MyBatis 让这个写法彻底流行起来
到了 MyBatis 时代,官方其实已经提供了更优雅的方案——
<where>
标签,它会自动处理条件前面的
AND
或
OR
。但还是有相当多的人习惯写成:
SELECT * FROM user
WHERE 1=1
<if test="status != null">
AND status = #{status}
</if>
原因很简单:很多人是从老 JDBC 代码迁移过来的,肌肉记忆改不掉;还有一部分人觉得
<where>
标签的语法要额外学,而
1=1
零学习成本。结果就是
1=1
从一个"过渡技巧"变成了"长期习惯",甚至在一些新项目里也被当作默认写法沿用下来。
2.3 从"能用"到"约定俗成":真正的问题在这
1=1
本身不背性能黑锅,但它带出来的配套写法值得警惕。我在代码评审里见过太多类似情况:
WHERE 1=1
后面跟了十几个
<if>
条件,每个条件里的值全是
${value}
直接拼进去的;或者条件里有
LIKE '%' + keyword + '%'
这种写法,导致字段上的索引完全失效。这种SQL慢下来,业务方第一反应往往是"是不是
1=1
导致的",实际用执行计划一看,慢的全在
LIKE
条件、全表扫描和临时文件排序上。
所以我的看法是:代码评审里看到
1=1
,不用急着让人改,但一定要接着看后面每一行动态条件是怎么拼的。如果只是普通
AND
加绑定变量,性能上完全没问题;如果看到了字符串拼接、函数套列、
OR
跨条件,那才是要花心思修的地方。
3. 实测验证:执行计划里的 1=1 到底去哪了
光说结论没说服力,我直接在本地 MySQL 里做了个完整实验。环境很简单:MySQL 8.0.32,InnoDB 引擎,一张 50 万行数据的用户表。下面整个流程你可以自己复现。
3.1 测试表和数据准备
USE testdb;
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`status` tinyint NOT NULL DEFAULT 1,
`name` varchar(50) NOT NULL DEFAULT '',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB;
造数我用了一个简单的存储过程,批量插入 50 万行,一半
status=0
,一半
status=1
,
name
用序号区分:
DROP PROCEDURE IF EXISTS insert_users;
DELIMITER $$
CREATE PROCEDURE insert_users(IN total INT)
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
WHILE i < total DO
INSERT INTO `user` (`status`, `name`, `created_at`)
VALUES (i % 2, CONCAT('user_', i), NOW() - INTERVAL i SECOND);
SET i = i + 1;
END WHILE;
COMMIT;
END$$
DELIMITER ;
CALL insert_users(500000);
机器配置一般,插入 50 万行大概花了十几秒,正常范围内的耗时。
3.2 EXPLAIN 对比:加和不加完全一致
接下来对比两条SQL的执行计划:
EXPLAIN SELECT * FROM user WHERE status = 1;
EXPLAIN SELECT * FROM user WHERE 1=1 AND status = 1;
两条结果完全一样:
+----+-------------+-------+------------+------+---------------+------+---------+-------+--------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+-------+--------+----------+-------+
| 1 | SIMPLE | user | NULL | ref | idx_status | idx_status | 1 | const | 249999 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+-------+--------+----------+-------+
type=ref
说明走的是二级索引等值查找,
key=idx_status
用上了
status
索引,
rows=249999
是预估扫描的行数。把
1=1
加进去之后,这些关键字段一个都没变,充分说明优化器在生成执行计划之前就把恒真条件消除了。
3.3 真实耗时差距:基本在误差范围
我还用
SHOW PROFILES
分别统计了两条SQL的真实执行耗时:
SET profiling = 1;
SELECT * FROM user WHERE status = 1;
SELECT * FROM user WHERE 1=1 AND status = 1;
SHOW PROFILES;
多次执行之后看耗时,两条SQL的耗时差异基本在 1 毫秒以内,有时候加
1=1
的版本反而更快,这纯粹是缓存热度和调度波动导致的。真正耗时的部分全在"通过索引定位到 25 万行数据并把结果集传回客户端"上面,跟
1=1
没有任何关系。
3.4 同样的逻辑,在 OR 条件下为什么容易翻车
有同学可能会问,那
WHERE 1=1 OR status = 1
呢?注意,这个写法整体是恒真的,因为
TRUE OR x
永远等于
TRUE
,优化器会发现这个查询根本不需要过滤任何数据,直接返回全表。这看起来"很快",但如果业务本意是"状态等于1或者某种异常状态",写成了
WHERE 1=1 OR status = 1
,那等于把过滤条件彻底废掉了,全表扫一遍。更常见的翻车写法是
WHERE status = 1 OR name = 'xxx'
,
OR
跨字段时索引很难被有效利用,整条SQL可能退化成全表扫描。这类问题的根源不是
1=1
,而是
OR
的语义和动态条件拼接时的逻辑混乱。
4. 真正让 1=1 变危险的,是这些配套场景
虽然常规 OLTP 数据库里
1=1
本身无害,但确实存在一些特殊场景,让它从一个"无所谓的习惯"变成"能感知到的累赘"。下面这几个场景值得单独拎出来说。
4.1 SQL 文本膨胀与硬解析成本
每一条SQL到了数据库都要经历解析(parse)阶段。
1=1
虽然只增加了几个字符,但动态SQL如果越拼越长,几十个
<if>
条件全拼进去之后,SQL 文本可能达到几千甚至上万字节。高并发压力下,数据库解析长SQL的 CPU 消耗、网络上多传的字节数都会真实累加。
更麻烦的是"硬解析"。如果代码里用的是
PreparedStatement
,SQL 文本每次相同,数据库可以复用执行计划;但如果框架是在
1=1
后面把值直接拼进字符串里,比如写成
AND status = 1
而不是
AND status = ?
,那每次SQL文本都不一样,数据库只能反复做硬解析。我在一个支付项目里排查过高 CPU 问题,慢SQL日志一天几万条,全是动态拼接条件,文本千奇百怪,最终改成绑定变量之后数据库 CPU 直接降了一半。这个问题看起来和
1=1
无关,但
1=1
经常是"开始字符串拼接的那个起点",所以我会把它归类到
1=1
习惯引发的连带风险里。
4.2 分库分表中间件与 MPP 引擎并不买账
如果你用了 ShardingSphere、MyCat 这类分库分表中间件,SQL 会先在中间件里做解析、改写和路由。SQL 每多一个无意义的恒真条件,中间件的解析成本就多一点;某些中间件对复杂条件支持的完善程度也不及原生数据库,
1=1
这种"表意无害但没必要存在"的条件,可能让路由结果变得不可控。
大数据分析场景更要注意。Hive 的早期版本对常量条件的化简就不够彻底,
WHERE 1=1
在某些执行引擎里会被当成普通表达式逐行计算,扫描几亿行数据时,每一行都多一次比较操作,纯属浪费。ClickHouse 对这类条件处理得相对好,但为了稳妥,在 OLAP/MPP 引擎、数据湖查询引擎里,我的建议是直接不写
1=1
,让条件列表保持干净。
4.3 1=0 的妙用与误用
既然
1=1
是恒真,那
1=0
就是恒假,它也有实际用途。比如想快速复制一张表的结构:
CREATE TABLE user_copy AS SELECT * FROM user WHERE 1=0;
这条SQL不会扫描数据,秒级创建一张空表,结构完全一致,适合做临时表、测试表。又或者在代码里设置一个"开关条件",想临时让某个查询不返回数据,把恒真条件手工改成
1=0
就行。但这个技巧误用起来也危险,我见过线上事故:某段代码为了"临时关停报表查询",把代码里的条件改成
WHERE 1=0 AND create_time >= ?
,结果上线时忘了改回来,报表整整空跑了两天,没有任何报错和告警。所以我把这类"魔法条件"归为高风险技巧,能不用就不用。
5. 工程上更优雅的动态条件写法
既然
1=1
只是"省事写法",那工程上完全有更清晰、更安全的替代方案。下面这几个是我在实际项目里推荐的做法,照着用就行。
5.1 MyBatis 的
<where>
标签是正统解法
MyBatis 入门教程里都会提到
<where>
标签,它能把条件块里的第一个
AND
或
OR
自动去掉,效果和
1=1
一样,但语义会好很多:
<select id="listUser" resultType="User">
SELECT * FROM user
<where>
<if test="status != null">
AND status = #{status}
</if>
<if test="name != null and name != ''">
AND name = #{name}
</if>
</where>
</select>
当所有条件都不满足时,
<where>
标签会生成没有任何 WHERE 关键字的SQL,也就是整表查询;当有条件存在时,它自动生成
WHERE status = ? AND name = ?
。相比手写
WHERE 1=1
,
<where>
标签更简洁,后续接条件也更不容易出错。
如果你的团队用的是注解式SQL,也可以通过
<script>
标签实现同样的效果:
@Select("<script>" +
"SELECT * FROM user " +
"<where>" +
" <if test='status != null'> AND status = #{status}</if>" +
" <if test='name != null'> AND name = #{name}</if>" +
"</where>" +
"</script>")
List<User> listUser(@Param("status") Integer status, @Param("name") String name);
5.2 用 ORM 的条件构造器彻底告别 SQL 拼接
Spring Data JPA 的
Specification
、MyBatis-Plus 的
LambdaQueryWrapper
,这些条件构造器本质上是帮你在代码层动态生成 WHERE 条件,完全不需要手工拼字符串,更不需要
1=1
。比如 MyBatis-Plus 的写法:
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery()
.eq(status != null, User::getStatus, status)
.like(StringUtils.hasText(name), User::getName, name);
List<User> users = userMapper.selectList(wrapper);
每个方法的第一个参数传"是否启用这个条件",只有当条件为真时才会进入 WHERE。这种写法的好处是:条件越多代码越清晰,也不会出现"忘记加
AND
"的问题,更不会有人写着写着把手拼字符串的
1=1
路线又捡回来。
5.3 服务端拼SQL的三个自检点
不管用不用
1=1
,动态条件拼接完之后,我建议养成三个自检习惯:
-
所有值必须用绑定变量,不允许把参数值直接拼进SQL字符串。在 MyBatis 里就是用
#{}而不是${},JDBC 里就是用PreparedStatement.setXxx()。 -
WHERE 里的条件尽量只用
AND连接。一旦出现OR,要格外小心索引失效,必要时用括号把一组 OR 条件包起来,或者拆分成多条SQL用 UNION 合并。 -
排序和分页字段要有索引支撑。
ORDER BY create_time LIMIT 10如果create_time没索引,数据量大时一定会走文件排序,不管前面写不写1=1都会慢。
把这三个自检点走一遍,比纠结
1=1
有没有影响、要不要删除有意义得多。
6. 三分钟自查:你的SQL到底有没有性能隐患
最后给一套可复用的排查流程。以后不管是自己写的SQL还是别人反馈的慢SQL,按这个顺序查,基本能定位到 90% 的问题,不用再靠猜。
6.1 用 EXPLAIN 看扫描方式对不对
任何一条有性能疑虑的SQL,第一步都是加
EXPLAIN
:
EXPLAIN SELECT * FROM user WHERE 1=1 AND name = 'user_12345';
重点看三列:
type
、
key
、
rows
。
type
从好到差大致是
const
、
eq_ref
、
ref
、
range
、
index
、
ALL
。如果
type
是
ALL
且
rows
很大,说明全表扫描了,接下来去看 WHERE 条件里的字段有没有索引、是不是被函数或前置通配符破坏了索引。如果
key
是
NULL
,也说明没有可用索引,得根据实际查询条件设计索引。
6.2 用 SHOW PROFILE 定位耗时分布
如果 EXPLAIN 看不出明显问题,用 profile 看真实耗时分布:
SET profiling = 1;
SELECT * FROM user WHERE 1=1 AND status = 1;
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
输出会显示各个阶段耗时,比如解析、优化、执行、发送数据各用了多少毫秒。如果耗时集中在发送数据上,说明返回行数太多,优先看是否
SELECT *
导致返回了不需要的大字段;如果耗时集中在拷贝到临时表,就要关注
GROUP BY
、
ORDER BY
和 join 的写法。这一步能把"感觉慢"变成"具体慢在哪"。
6.3 慢SQL排查优先级清单
我把日常排查的顺序整理成一张表,可以直接照着执行:
| 排查维度 | 怎么看 | 常见处理 |
|---|---|---|
| 扫描方式 | EXPLAIN 的 type/key/rows | 加索引、避免函数包裹列、避免前置通配符 |
| 返回数据量 | 执行SQL行数、发送数据耗时 | 避免 SELECT *,只查必要字段,必要时分页 |
| 排序/分组 | 是否有 filesort/temporary | 给排序字段加索引,改写 order by/group by |
| 连接方式 | 多个表的 join/exists 计划 | 小表驱动大表,检查关联字段索引 |
| 数据库负载 | SHOW PROCESSLIST / 监控 | 定位硬解析、锁等待、慢日志 |
回到标题那个问题,
WHERE 1=1
会不会影响性能?单纯从执行计划看,它在主流关系型数据库里基本没有影响;但从工程习惯看,它往往是糟糕SQL拼接的起点,是硬解析、字符串拼接、条件失控的"信号灯"。我个人现在的习惯是:团队规范里不强制禁掉
1=1
,但动态条件必须用
<where>
标签或 ORM 条件构造器;线上真出了慢SQL,先看执行计划,再查参数绑定和索引设计,基本没遇到过哪次慢是因为
1=1
本身导致的。下次再有人拿着
WHERE 1=1
来问你性能问题,你可以直接告诉他:别盯着这一行看了,顺着它往后面的条件里查,真正的雷通常埋在那。
更多推荐



所有评论(0)