后端群里隔三差五就会冒出一个经典问题:" 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 来问你性能问题,你可以直接告诉他:别盯着这一行看了,顺着它往后面的条件里查,真正的雷通常埋在那。

更多推荐