编写SQL时容易遗漏哪些细节?一份Checklist总结

来源:语言推理作者:霓渡头衔:草根站长
导读:本期聚焦于霓渡创作的《编写SQL时容易遗漏哪些细节?一份Checklist总结》,敬请观看详情。一条看似普通的查询语句,放到千万级数据量下可能直接拖垮接口。多数问题不是数据库引擎能力不足,而是SQL在书写阶段忽略了一些关键细节。本文以一份可落地的Checklist形式,梳理条件判断、NULL处理、索引利用、隐式转换、分页排序、事务边界和注入防护等高频风险点。每个条目都给出错误与正确写法对照,帮助你在提交代码前快速识别全表扫描、结果集偏差、大事务阻塞等隐患。读完这份清单,可以把SQL审查从依赖经验变成按项检查,减少线上慢查询和数据异常的概率。

SQL 本身接近自然语言,读得懂不意味着写得对。同一句查询在不同数据量、不同表结构下表现可能完全不同,而这些问题往往藏在很小的书写习惯里。以下按照实际开发中的排查顺序,整理成一份可对照的 SQL 编写 Checklist。

编写SQL时容易遗漏哪些细节?一份Checklist总结

一、条件判断与 NULL 处理

SQL 中的 NULL 表示未知值,不能参与普通等值比较。如果写成 WHERE status = NULL,条件结果不是 true 也不是 false,而是 unknown,最终不会被返回。正确方式是使用 IS NULL 或 IS NOT NULL。这个差异在单表查询里还好发现,一旦嵌套到子查询或 NOT IN 中,排查成本会成倍增加。

最典型的坑是 NOT IN 子查询。比如想找出没有退款记录的订单,查询订单表的主键不在退款表里。如果退款表的 order_id 中出现 NULL,整个 NOT IN 的结果可能为空。原因是任何值与 NULL 比较都会变成 unknown,外层条件无法确认。更稳妥的做法是使用 NOT EXISTS,或者先在子查询中过滤掉 NULL。

-- 错误:NOT IN 子查询含 NULL 时可能返回空
SELECT id FROM orders
WHERE id NOT IN (SELECT order_id FROM refunds);

-- 正确:过滤掉 NULL
SELECT id FROM orders
WHERE id NOT IN (SELECT order_id FROM refunds WHERE order_id IS NOT NULL);

-- 更推荐:使用 NOT EXISTS
SELECT o.id FROM orders o
WHERE NOT EXISTS (
    SELECT 1 FROM refunds r WHERE r.order_id = o.id
);

聚合函数对 NULL 的处理也不一致。COUNT(*) 会统计所有行,COUNT(column) 会忽略该列值为 NULL 的行。SUM、AVG 也会忽略 NULL,这可能符合业务也可能导致统计偏差。面对可空字段统计时,需要明确是否需要 COALESCE 补默认值。例如 COALESCE(amount, 0) 可以避免求和结果为 NULL 的问题。

-- COUNT(*) 与 COUNT(column) 的区别
SELECT COUNT(*) AS total_rows,
       COUNT(refund_status) AS non_null_status,
       SUM(COALESCE(amount, 0)) AS total_amount
FROM orders;
  • 不要用 = 或 != 判断 NULL
  • 子查询 NOT IN 前先确认结果集是否可能含 NULL
  • 聚合统计时清楚 COUNT(*) 与 COUNT(column) 的差异

二、索引利用与隐式转换

索引列是最容易写出问题的位置。数据库为了使用索引,需要保证条件中的列没有被函数、计算或隐式类型转换包裹。例如 phone 字段是 varchar 类型,查询时传入一个数字,数据库可能将列隐式转换成数字再比较,导致索引失效。字符串字段比较时务必加引号。

-- 错误:phone 是 varchar,传入数字会触发隐式转换
SELECT * FROM users WHERE phone = 13800138000;

-- 正确:统一使用字符串
SELECT * FROM users WHERE phone = '13800138000';

日期和 LIKE 也是高频地雷。对 created_at 使用 DATE() 函数会把索引列转换成函数结果,优化器无法利用索引。应改成范围查询。LIKE 查询如果以 % 开头,B-tree 索引无法定位前缀,会退化为全表扫描。这类需求应优先考虑全文索引或专门的搜索引擎。

-- 错误:对索引列使用函数
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';

-- 正确:使用范围条件
SELECT * FROM orders
WHERE created_at >= '2024-01-01'
  AND created_at < '2024-01-02';

-- LIKE 前导通配符无法走索引
SELECT * FROM products WHERE name LIKE '%手机%';

联合索引遵循最左前缀原则。创建 (user_id, status, created_at) 这种索引后,只查 status 或 created_at 的 SQL 无法命中索引。另外尽量减少 SELECT *,只取必要字段,增加覆盖索引的命中概率,减少回表开销。

  • 索引列不参与函数计算或隐式类型转换
  • 避免 LIKE 前导通配符
  • 联合索引注意最左前缀匹配

三、分页、排序与数据量控制

分页是线上慢查询高发区。使用 LIMIT offset, size 翻到很深的页时,数据库需要扫描并丢弃大量前面的行,offset 越大越慢。比如 LIMIT 100000, 20 会扫描 100020 行。可以通过延迟关联缩小扫描范围,或者改为基于游标的分页,记录上一页最后一条主键 id,用 WHERE id > last_id 继续取下一页。

-- 传统大偏移量分页,越往后越慢
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

-- 延迟关联:先只取主键,再回表取完整数据
SELECT o.*
FROM orders o
JOIN (
    SELECT id FROM orders ORDER BY id LIMIT 100000, 20
) t ON o.id = t.id;

-- 游标分页:记录上一页最后一条 id
SELECT * FROM orders
WHERE id > 123456
ORDER BY id
LIMIT 20;

排序结果不稳定也需要提前处理。如果 ORDER BY 后面只有 create_time,并且这个时间存在大量重复值,分页时可能出现同一条记录出现在两页。最好在排序字段后追加一个唯一键,比如 ORDER BY create_time DESC, id DESC。

大数据量更新和删除不要一次性执行。一条 UPDATE 或 DELETE 影响上百万行,不仅会让事务日志暴增,还可能长时间锁表。可以按 id 或时间分批处理,每批几千条,每次批处理后提交事务。IN 子查询若结果集过大,可改用 EXISTS 或临时表关联。

  • 大偏移量分页优先考虑延迟关联或游标分页
  • 排序字段后追加唯一键保证结果稳定
  • 大批量更新删除分批执行并控制事务大小

四、安全与事务边界

SQL 注入仍然是最严重的安全问题之一。用户输入直接拼接到 SQL 语句中,攻击者可以改变原始语句结构。无论后端框架多成熟,只要存在字符串拼接,就可能出现漏洞。所有用户输入都应该通过参数化查询传递,框架会自动处理转义和类型绑定。

// 错误:拼接用户输入
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

// 正确:使用 PreparedStatement
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();

事务边界要尽可能短,锁范围要尽量小。开启事务后不要包含远程调用、文件读写或人工交互。更新多条数据时注意执行顺序一致,降低死锁概率。UPDATE 和 DELETE 必须带 WHERE 条件,防止误更新全表。必要时在测试环境先通过 SELECT 确认影响范围。动态 SQL 中的表名、排序字段无法参数化,只能使用白名单校验。

-- 转账示例:短事务只包住必要的数据变更
START TRANSACTION;

UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

COMMIT;
  • 所有外部输入使用参数化查询,杜绝字符串拼接
  • 动态表名、排序字段使用白名单校验
  • 事务内只保留必要的数据库操作,避免长事务
  • 更新和删除必须带 WHERE 条件

把这四组检查项放进日常开发流程,不必每次逐句分析执行计划,也能提前规避掉大部分 SQL 正确性和性能问题。更重要的是,这些细节一旦形成习惯,写出的 SQL 会在数据量增长后依然保持稳定表现。

SQL编写SQL优化数据库开发修改时间:2026-09-30 11:40:39

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0930/63807.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。