mysql建表避坑指南
- 一、建表阶段的坑与规范
-
- [1. 字符集与排序规则](#1. 字符集与排序规则)
-
- [1.1 utf8 和 utf8mb4的区别](#1.1 utf8 和 utf8mb4的区别)
- [1.2 实际影响举例](#1.2 实际影响举例)
- [2. 存储引擎](#2. 存储引擎)
- [3. 主键设计](#3. 主键设计)
- [4. 字段命名](#4. 字段命名)
- [5. 默认值与 NOT NULL](#5. 默认值与 NOT NULL)
- 二、字段类型选择的坑与最佳实践
-
- [1. 整数类型](#1. 整数类型)
- [2. 字符串类型](#2. 字符串类型)
- [3. 时间类型](#3. 时间类型)
- [4. 浮点数与定点数](#4. 浮点数与定点数)
- [5. 枚举与集合](#5. 枚举与集合)
- 三、索引设计的坑与原则
-
- [1. 索引不是越多越好](#1. 索引不是越多越好)
- [2. 联合索引最左前缀原则](#2. 联合索引最左前缀原则)
- [3. 索引冗余](#3. 索引冗余)
- [4. 索引失效场景(常见坑)](#4. 索引失效场景(常见坑))
- [5. 覆盖索引](#5. 覆盖索引)
- 四、查询性能的坑与优化
-
- [1. 切忌 SELECT *](#1. 切忌 SELECT *)
- [2. 分页深翻页](#2. 分页深翻页)
- [3. JOIN 滥用](#3. JOIN 滥用)
- [4. 隐式提交与大事务](#4. 隐式提交与大事务)
- [5. 慢查询日志与 EXPLAIN](#5. 慢查询日志与 EXPLAIN)
- [6. 索引下推与 ICP](#6. 索引下推与 ICP)
- [7. 避免在 WHERE 条件中使用 !=、<>、NOT IN](#7. 避免在 WHERE 条件中使用 !=、<>、NOT IN)
- [8. 使用 UNION 代替 OR](#8. 使用 UNION 代替 OR)
- 五、其他容易忽略的坑
-
- [1. 数据库连接池设置](#1. 数据库连接池设置)
- [2. 大字段与行溢出](#2. 大字段与行溢出)
- [3. 外键约束](#3. 外键约束)
- [4. 自增主键步长](#4. 自增主键步长)
-
- [4.1 变量含义](#4.1 变量含义)
- 典型使用场景
- [3. 设置方法](#3. 设置方法)
- [5. 时区设置](#5. 时区设置)
一、建表阶段的坑与规范
1. 字符集与排序规则
-
坑:使用 latin1 或默认字符集,导致存储中文乱码;或者混用不同字符集导致索引失效、排序结果异常。
-
正确做法:统一使用 utf8mb4 字符集,排序规则用 utf8mb4_general_ci 或 utf8mb4_unicode_ci。MySQL 8.0 默认已改为 utf8mb4,但需确认。
-
CREATE TABLEuser( ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
1.1 utf8 和 utf8mb4的区别
-
utf8 和 utf8mb4 是 MySQL 中两种常见的 UTF-8 字符集,它们的核心区别在于支持的 Unicode 字符范围和存储字节数。简单来说:
-
utf8 是 MySQL 中的一个"别名",但实际上它只实现了 UTF-8 的一部分,最多使用 3 个字节存储一个字符,只能支持 Unicode 基本多语言平面(BMP,即码点范围 U+0000 到 U+FFFF)。
-
utf8mb4 是真正的 UTF-8 完整实现,最多使用 4 个字节 存储一个字符,可以支持所有 Unicode 字符,包括 BMP 以外的字符(如 emoji 表情、生僻汉字、一些特殊符号等)。
详细区别
| 特性 | utf8 | utf8mb4 |
|---|---|---|
| 最大字节数 | 3 字节 | 4 字节 |
| 支持的字符范围 | Unicode 基本多语言平面(BMP) | 全部 Unicode 字符,包括 BMP 和补充平面(如 emoji) |
| 是否支持 emoji | ❌ 不支持(会报错或变成乱码) | ✅ 完全支持 |
| 是否支持生僻汉字(如"𠮷") | ❌ 不支持 | ✅ 支持 |
| MySQL 引入版本 | 很早期就有(但实际为 3 字节 UTF-8) | MySQL 5.5.3 开始引入 |
| 常用排序规则 | utf8_general_ci, utf8_unicode_ci | utf8mb4_general_ci, utf8mb4_unicode_ci(MySQL 8.0 推荐 utf8mb4_0900_ai_ci) |
1.2 实际影响举例
- 插入 emoji 表情(如 "😊")
若表字符集为 utf8,插入 '😊' 会报错:Incorrect string value: '\xF0\x9F\x98\x8A' for column。
若为 utf8mb4,则可以正常存储。
- 生僻汉字(如 "𠮷")
"𠮷" 的 Unicode 码点是 U+20BB7,需要 4 字节编码,utf8 无法存储。
2. 存储引擎
-
坑:使用 MyISAM(不支持事务、行锁,崩溃恢复差)。
-
正确做法:一律使用 InnoDB(支持事务、行级锁、外键、崩溃恢复)。
3. 主键设计
-
坑:使用业务字段作为主键(如身份证号),或使用 UUID 作为主键(导致页分裂、索引膨胀)。
-
正确做法:使用自增整数主键(BIGINT AUTO_INCREMENT)或雪花算法生成的分布式 ID(有序)。
自增主键利于写入性能,页分裂少;但分库分表时需改用分布式 ID。
4. 字段命名
-
坑:使用关键字作为字段名(如 order、group、desc),导致 SQL 必须加反引号,易出错。
-
正确做法:避免使用 MySQL 保留字,字段名用下划线命名法(如 user_name),表名用复数或单数(团队统一)。
5. 默认值与 NOT NULL
-
坑:允许字段为 NULL,导致查询时需要用 IS NULL 或 IS NOT NULL,索引效率低,且聚合函数(如 COUNT)会忽略 NULL。
-
正确做法:尽量设置 NOT NULL,并给一个合理的默认值(如 DEFAULT '' 或 DEFAULT 0)。如果必须存 NULL,需明确业务含义。
二、字段类型选择的坑与最佳实践
1. 整数类型
-
坑:用 INT 存布尔值(浪费空间);用 INT(1) 误以为限制长度(其实只影响显示宽度)。
-
正确做法:
-
布尔值用 TINYINT(1) 或 BOOLEAN(MySQL 中 BOOLEAN 是 TINYINT(1) 的别名)。
-
根据数值范围选择合适类型:TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT。
-
INT(11) 中的数字不影响存储范围,仅用于一些客户端显示,无实际意义。
-
2. 字符串类型
-
坑:滥用 VARCHAR(255) 存储所有字符串,或用 TEXT 存储短文本(TEXT 有单独存储空间,影响排序/索引)。
-
正确做法:
-
长度固定且短的用 CHAR(如手机号、MD5值)。
-
长度不固定但短(<255)用 VARCHAR,但需根据实际最大长度设置,不要随意给 255。
-
超过 5000 字符考虑 TEXT 或 MEDIUMTEXT,但注意 TEXT 类型不能有默认值,且需前缀索引。
-
存储 JSON 用 JSON 类型(MySQL 5.7+),可高效查询 JSON 字段。
-
3. 时间类型
-
坑:用 VARCHAR 存时间戳,或混淆 DATETIME 与 TIMESTAMP。
-
正确做法:
-
日期用 DATE,日期时间用 DATETIME(范围大,不受时区影响)或 TIMESTAMP(受时区影响,2038年问题)。
-
记录创建时间、更新时间建议用 DATETIME,配合 DEFAULT CURRENT_TIMESTAMP 和 ON UPDATE CURRENT_TIMESTAMP。
-
不要用字符串存时间,无法使用时间函数且排序错误。
-
4. 浮点数与定点数
-
坑:用 FLOAT/DOUBLE 存金额,导致精度丢失。
-
正确做法:金额等精确小数用 DECIMAL(如 DECIMAL(10,2))。浮点数只适合科学计算。
5. 枚举与集合
-
坑:用 ENUM 存可变选项(增加新值需 ALTER 表,锁表风险)。
-
正确做法:固定不变的选项可用 ENUM(如性别),会变化的选项用关联表(外键)或 TINYINT 映射。
三、索引设计的坑与原则
1. 索引不是越多越好
-
坑:每个字段都建索引,导致写入慢、占用磁盘空间,且优化器可能选错索引。
-
正确做法:只为查询条件(WHERE)、排序(ORDER BY)、分组(GROUP BY)、关联(JOIN)字段建索引。写多读少的表索引要精简。
2. 联合索引最左前缀原则
-
坑:建立索引 (a, b, c),但查询条件只用 b 或 c,索引失效。
-
正确做法:联合索引按查询频率排序,将最常用、区分度高的列放左边。若查询条件缺失左边列,需另建索引。
3. 索引冗余
-
坑:已有 (a, b) 索引,又建 (a) 索引,后者完全冗余。
-
正确做法:定期用 pt-duplicate-key-checker 等工具检查冗余索引并删除。
4. 索引失效场景(常见坑)
-
对索引列使用函数或计算:WHERE DATE(create_time) = '2023-01-01' → 应改为 create_time >= '2023-01-01' AND create_time < '2023-01-02'。
-
隐式类型转换:字段为字符串,传入数字,导致全表扫描(如 WHERE phone = 13800001111,phone 是 VARCHAR)。
-
模糊查询左模糊:LIKE '%abc' 不走索引,右模糊 LIKE 'abc%' 可走。
-
OR 条件:WHERE a = 1 OR b = 2,如果 a 和 b 没有分别建索引,可能全表扫描。可用 UNION 优化或建复合索引(MySQL 5.0+ 索引合并可能利用,但不如 UNION 稳定)。
5. 覆盖索引
- 优化技巧:查询的字段都在索引中时,只需扫描索引树,无需回表。设计索引时尽量覆盖常用查询。
四、查询性能的坑与优化
1. 切忌 SELECT *
-
坑:SELECT * 返回所有字段,可能包含大字段(如 TEXT),增加网络开销和内存占用,且无法利用覆盖索引。
-
正确做法:只查询需要的字段。
2. 分页深翻页
-
坑:LIMIT 100000, 20 会先扫描 100020 行再丢弃前 100000 行,随着翻页越深越慢。
-
正确做法:
-
使用子查询延迟关联:
SELECT * FROM t WHERE id >= (SELECT id FROM t ORDER BY id LIMIT 100000, 1) LIMIT 20 -
记录上次最大 ID(或排序字段),下一页用 WHERE id > last_id LIMIT 20。
-
若用时间排序,确保时间字段有索引,并用
WHERE time > last_time方式。
-
3. JOIN 滥用
-
坑:多表 JOIN 且无索引,导致临时表、文件排序;或者 JOIN 顺序不当。
-
正确做法:
-
确保 JOIN 的关联字段有索引(类型一致)。
-
小表驱动大表(优化器通常会自动选择,但需检查 EXPLAIN)。
-
避免超过三张表 JOIN,必要时冗余字段或反范式化。
-
4. 隐式提交与大事务
-
坑:在循环中逐条 INSERT/UPDATE,导致多次事务提交,性能差且可能锁范围扩大。
-
正确做法:批量操作
(INSERT INTO ... VALUES (...), (...)),或用START TRANSACTION包裹批量语句,统一提交。
5. 慢查询日志与 EXPLAIN
-
坑:从不开启慢查询日志,问题发生后无从排查。
-
正确做法:
-
开启慢查询日志,设置
long_query_time = 1(或 0.5 秒),定期分析。 -
对每条重要查询使用
EXPLAIN分析执行计划,关注type(ALL 全表扫描需优化)、rows(扫描行数)、Extra(Using filesort 或 Using temporary 需优化)。
-
6. 索引下推与 ICP
- 原理:MySQL 5.6+ 支持索引条件下推(ICP),在存储引擎层过滤数据,减少回表。但前提是联合索引的前缀字段已使用,后置字段可在索引内过滤。
7. 避免在 WHERE 条件中使用 !=、<>、NOT IN
- 这些操作通常无法利用索引,除非数据分布特殊。可考虑改写为范围查询(如 id < 1 OR id > 1 仍无法走索引),或根据业务排除。
8. 使用 UNION 代替 OR
- 示例:
WHERE a = 1 OR b = 2可改为SELECT ... WHERE a = 1 UNION SELECT ... WHERE b = 2,但需确保 a、b 分别有索引。
五、其他容易忽略的坑
1. 数据库连接池设置
-
坑:连接池太小导致请求排队,太大导致数据库负载高。
-
正确做法:根据 QPS、事务耗时合理配置(如 HikariCP 的 maximumPoolSize 一般设为 CPU 核心数 * 2 + 磁盘数,实际需压测)。
2. 大字段与行溢出
-
坑:表中有多个
TEXT/BLOB字段,且频繁查询,导致性能下降(行溢出,需额外 IO)。 -
正确做法:将大字段拆分到附属表,主表只存常用字段。
3. 外键约束
-
坑:在 InnoDB 中使用外键,每次更新/删除都会检查引用完整性,影响性能。
-
正确做法:由应用层保证数据一致性,数据库层面不设外键,但可建索引提高关联查询性能。
4. 自增主键步长
-
坑:使用默认步长 1,但分库分表后可能冲突。
-
正确做法:分布式场景用雪花算法,或设置 auto_increment_increment 和 auto_increment_offset。
这两个是 MySQL 的系统变量,用于控制 AUTO_INCREMENT 列的增长步长和起始偏移量。它们主要用在多主复制、分库分表或分布式 ID 生成场景中,目的是避免不同节点生成的自增主键发生冲突。
4.1 变量含义
-
auto_increment_increment:控制自增值每次增加的步长(间隔)。默认值为 1。
-
auto_increment_offset:控制自增值的起始偏移量(起点)。默认值为 1。
公式:
自增列的值 = auto_increment_offset + N × auto_increment_increment (N 为插入次数)
典型使用场景
假设你有两台 MySQL 服务器(主-主复制或分片),希望它们生成的自增主键全局唯一,不会重复。可以这样配置:
-
节点1:auto_increment_offset = 1,auto_increment_increment = 2 → 生成 ID:1, 3, 5, 7...
-
节点2:auto_increment_offset = 2,auto_increment_increment = 2 → 生成 ID:2, 4, 6, 8...
这样两个节点生成的 ID 交错且唯一。
3. 设置方法
- 全局设置(需要超级权限):
sql
SET GLOBAL auto_increment_increment = 2;
SET GLOBAL auto_increment_offset = 1;
- 会话级设置(仅当前连接):
sql
SET SESSION auto_increment_increment = 2;
SET SESSION auto_increment_offset = 1;
- 配置文件(my.cnf/my.ini):
sql
[mysqld]
auto_increment_increment=2
auto_increment_offset=1
5. 时区设置
-
坑:应用与数据库时区不一致,导致时间错乱。
-
正确做法:统一设置为 +08:00,连接参数加 serverTimezone=Asia/Shanghai。