SQL作为数据管理的通用语言,看似简单其实处处暗藏玄机。无论是我们初学者还是那些资深大佬,都可能在不经意间踏入各种SQL陷阱,导致SQL查询结果异常、性能瓶颈甚至数据安全隐患。下面我们一起从SQL的基础语法、索引优化、查询技巧、事务处理、表结构设计、JOIN操作、JSON处理、架构运维、安全权限到高级特性,以表格的形式简单地介绍一下86个SQL常见陷阱(后面我们再一起详细拆解)。每个SQL陷阱,我们都给出了核心原因并提供了实用的避坑方法,同时特别关注了不同数据库间的行为差异。掌握了这些知识点,不仅能帮我们避免常见错误,还能帮我们提升SQL编写质量,编写出更健壮、高效、安全的SQL数据库语句,让数据实实在在地为我们的业务加油助力。
一、基础语法与逻辑陷阱
| | | |
---|
| = | 三值逻辑(3VL):NULL = NULL →UNKNOWN ,而非TRUE | 判断NULL 必须用IS NULL /IS NOT NULL |
| NOT IN | col NOT IN (a, b, NULL) ≡col ≠ a AND col ≠ b AND col ≠ NULL → 恒为UNKNOWN ,返回空集 | 我们优先用NOT EXISTS ;若用NOT IN ,子查询必须加WHERE col IS NOT NULL 。 我们注意:多列NOT IN (如:(c1,c2) NOT IN (...) )行为与NOT EXISTS 一致,不受NULL 影响。 |
| COUNT(列名) | COUNT(列名) 忽略NULL ;COUNT(*) 统计所有行(含全NULL 行) | 总记录数 →COUNT(*) ;非空值计数 →COUNT(col) |
| DISTINCT | COUNT(DISTINCT col1, col2) 在MySQL支持,但在PostgreSQL/SQL Server中需(col1, col2) 或子查询 | 跨库兼容方案:用GROUP BY col1, col2 +COUNT(*) |
| GROUP BY | 非聚合列未出现在GROUP BY 中(尤其在关闭ONLY_FULL_GROUP_BY 时) | 启用ONLY_FULL_GROUP_BY 模式,或显式聚合(如:ANY_VALUE(col) ) |
| HAVING | WHERE | |
| | 如:varchar_col = 123 触发列转数字 | 保证类型一致 |
| OR | col = 1 OR NULL →UNKNOWN ,而非col = 1 OR col IS NULL | 我们要显式写出所有条件,避免将NULL 作为布尔表达式 |
| | | |
| CASE | | 所有分支统一类型(如:CAST(... AS VARCHAR) ) |
统一原理提示:以上陷阱(1、2、8)均源于SQL的三值逻辑(TRUE/FALSE/UNKNOWN),NULL
不等于任何值(包括自身),任何与NULL
的比较结果均为UNKNOWN
。
二、索引与性能优化
| | | |
---|
| | 函数破坏索引有序性,如:DATE(created_at) = '2024-01-01' | 函数应用在常量或创建函数索引(如:MySQL的虚拟列索引) |
| | | 将选择性高的列放前面;我们注意:查询条件只需覆盖索引前缀列即可生效(如:索引(a,b) 对WHERE b=? AND a=? 有效,但对WHERE b=? 无效) |
| | | |
| | | |
| LIKE | LIKE '%abc' | |
| | | |
| OR | WHERE a=1 OR b=2 | |
| ORDER BY | | 创建与排序方向一致的索引(如:(a ASC, b DESC) ) |
| | TEXT/BLOB | |
| FORCE INDEX | | 信任优化器(在统计信息准确、查询结构清晰的前提下);仅在统计信息严重失真等临时场景下谨慎使用,并尽快通过更新统计信息或调整索引解决根本问题。 我们强烈建议:避免长期依赖FORCE INDEX ,它会掩盖真正的性能问题。 |
三、查询与子查询
| | | |
---|
| SELECT * | | |
| | | |
| UNION | | |
| | | |
| LIMIT | LIMIT 100000, 10 | 基于游标分页(WHERE id > last_id ORDER BY id LIMIT 10 ) |
| | 多数数据库中CTE默认是语法糖(内联展开): PostgreSQL≥12:支持显式MATERIALIZED ; SQL Server/MySQL:通常内联,仅优化器判断有利时可能物化; 递归CTE(WITH RECURSIVE )必须物化 | |
| | WHERE col = (子查询) | 用IN 或保证子查询返回单行(如:加LIMIT 1 ) |
| EXISTS | 性能取决于外层表大小 vs 子查询结果集大小及索引: 外层小 + 子查询大 →EXISTS (短路); 子查询结果集小(<1k行)→IN (哈希查找) | 优先保证语义正确;性能敏感时,我们用EXPLAIN ANALYZE 验证 |
| | | 转换为JOIN 或LATERAL JOIN (PostgreSQL)/APPLY (SQL Server) |
| | | 复杂查询用EXPLAIN ANALYZE (或EXPLAIN (ANALYZE, BUFFERS) )分析真实性能 |
四、数据修改与事务
| | | |
---|
| | | 拆分为小事务,移除非数据库操作(如:HTTP调用) |
| | | 核心业务用REPEATABLE READ (MySQL)或SERIALIZABLE ,一般查询用READ COMMITTED |
| TRUNCATE | 跨数据库行为不一致: MySQL/Oracle:DDL,隐式提交,不可回滚; PostgreSQL/SQL Server:DML,可回滚 | 需要回滚时我们统一用DELETE ;跨库开发避免依赖TRUNCATE 事务行为 |
| | | 使用原子更新(SET col = col + 1 )、悲观锁(SELECT ... FOR UPDATE )或乐观锁(版本号) |
| INSERT | | |
| UPDATE | | SET 子句中用表别名限定列(如:SET u.status = 1 ) |
| UPSERT | 同时违反多个唯一键时行为不可预测(如:MySQL可能更新错误行) | 避免多唯一键表使用UPSERT ,或明确指定冲突键(如:PostgreSQL的ON CONFLICT (col) ) |
| | | |
| | | 合理分批插入(如:每批1000行),使用LOAD DATA INFILE (MySQL)或COPY (PostgreSQL) |
| | | 实现死锁重试机制(如:最多3次),优化锁顺序(按固定顺序访问资源) |
TRUNCATE
跨数据库行为对比:
五、表结构与设计
| | | |
---|
| ENUM | 频繁变更ENUM 值需ALTER TABLE ,且跨数据库兼容性差 | |
| | ON DELETE CASCADE | |
| | | |
| NOT NULL | | 根据业务逻辑决定是否允许NULL (如:“可选字段”应允许NULL ) |
| | | |
| | CREATE TABLE ... AS SELECT 通常不复制主键、外键、唯一约束、索引、默认值(DEFAULT)等元数据;部分数据库(如:Oracle)可能保留NOT NULL 约束,但行为不一致 | 绝不依赖CTAS复制元数据;用CREATE TABLE ... LIKE 复制结构,再INSERT INTO ... SELECT |
| | 如:用VARCHAR(255) 存储手机号(固定长度) | 选择最合适的类型(如:CHAR(11) 存手机号,TINYINT 存状态) |
| | 无主键导致行标识困难,InnoDB会隐式生成6字节主键,影响性能 | |
| | | 分布式系统使用UUID(注意性能)或雪花算法(Snowflake) |
| | 需用反引号(MySQL)或双引号(PostgreSQL)包裹,易出错 | 避免使用SQL关键字命名(如:order ,group ,user ) |
六、JOIN与关联操作
| | | |
---|
| JOIN | | |
| | | 统一字符集(如:utf8mb4 )和校对规则(如:utf8mb4_unicode_ci ) |
| | 超过6-8表JOIN 性能下降明显,优化器复杂度指数增长 | |
| LEFT JOIN | WHERE 条件过滤右表(如:WHERE b.col = 'x' )导致LEFT JOIN 变INNER JOIN | 右表过滤条件放ON 子句: ✗ LEFT JOIN b ON a.id = b.a_id WHERE b.col = 'x' ✓ LEFT JOIN b ON a.id = b.a_id AND b.col = 'x' |
| | 表自关联条件错误(如:a.id = b.parent_id AND b.id = a.parent_id )导致无限循环 | 我们谨慎设计自关联条件,加深度限制(如:递归CTE中的MAXRECURSION ) |
| | | 小表作为驱动表,大表作为被驱动表(保证被驱动表关联列有索引) |
| NATURAL JOIN | | |
| | | |
| CROSS JOIN | | |
| | 关联列用函数(如:JOIN ... ON UPPER(a.name) = UPPER(b.name) )导致索引失效 | |
七、JSON与特殊类型
| | | |
---|
| | JSON_EXTRACT(col, '$.key') | 使用原生运算符(MySQL:col->>'$.key' ;PostgreSQL:col->>'key' ) |
| | 未按数据库要求创建和使用JSON索引(如:MySQL需虚拟列,PostgreSQL需GIN) | |
| | | |
| | | |
| | | 重要时间字段用TIMESTAMP /DATETIME 类型,单独存储时区信息 |
八、架构与运维
| | | |
---|
| | 大表ALTER TABLE 导致长时间锁表(尤其MySQL 5.6以前) | 使用pt-online-schema-change (MySQL)或pg_repack (PostgreSQL)等工具 |
| | 宽松模式允许不规范操作(如:插入超长字符串被截断),埋下隐患 | 开启严格模式(STRICT_TRANS_TABLES, ONLY_FULL_GROUP_BY, NO_ZERO_DATE 等) |
| | | 开启慢查询日志(long_query_time=1 ),定期用pt-query-digest 分析 |
| SELECT ... INTO OUTFILE | 服务器端创建文件,有安全风险(需FILE权限,路径受限) | 客户端工具导出(如:mysql -e "SELECT ..." > file.csv ) |
| | 默认SQL SECURITY DEFINER 导致权限提升(调用者获得定义者权限) | 创建视图时我们显式指定SQL SECURITY INVOKER |
| | 碎片积累导致性能下降(尤其MyISAM或频繁DELETE 的InnoDB) | 定期执行OPTIMIZE TABLE (MySQL)或VACUUM FULL (PostgreSQL) |
| | | |
| | | 根据服务器配置(CPU、内存)和QPS调整连接池大小 |
| | | 定期备份(全量+增量),并验证备份有效性(恢复演练) |
| | InnoDB的COUNT(*) 在无覆盖索引时需扫描聚簇索引(因MVCC无法使用持久化行数),性能较差 | 维护计数表(通过触发器或应用层更新)或使用缓存(如:Redis) |
九、安全与权限
| | | |
---|
| | | 遵循最小权限原则(仅SELECT ,INSERT ,UPDATE ,DELETE ) |
| | | 存储密码哈希(如:bcrypt、scrypt),绝不存明文或可逆加密 |
| | | 始终使用参数化查询(Prepared Statement)或ORM的安全接口 |
| | | 加密敏感字段(应用层加密或TDE),并控制密钥管理 |
| | | 开启审计日志(如:MySQL Enterprise Audit, pgAudit),记录关键操作(DDL、DML on sensitive tables) |
十、高级特性与行业惯例
| | | |
---|
| | 分区键选择错误(如:不用查询条件中的列)导致全分区扫描 | 按高频查询条件选择分区键(如:created_at ) |
| | | 复杂逻辑放应用层,存储过程仅用在简单封装或批量操作 |
| | | 高并发OLTP场景可在应用层维护外键逻辑;若用外键,保证被引用列有索引 |
| | 不适合短文本(<3字符)或高频更新场景(重建开销大) | 考虑Elasticsearch、Meilisearch等专门搜索引擎 |
| | | |
| | 不同版本功能和行为有差异(如:GROUP BY 默认行为、JSON支持) | 开发、测试、生产环境,我们要使用相同版本,并查阅官方文档 |
阅读原文:原文链接
该文章在 2025/9/28 10:31:55 编辑过