MySQL
database-schema-dev和backend-dev的MySQL特定专业知识层:
这些代理是引擎无关的,会匹配项目已有的迁移工具/ORM——此技能是它们在引擎实际为MySQL或其近兼容分支MariaDB时调用的MySQL知识。
MariaDB行为不同的地方会特别指出——不要假设高级特性完全对等。
第1步——确认引擎实际是MySQL/MariaDB
检查package.json中是否有mysql2/mysql,ORM方言/提供者(Prisma中的provider = "mysql",Sequelize中的dialect: 'mysql',TypeORM中的type: 'mysql'/'mariadb'),docker-compose.yml中运行mysql/mariadb镜像的服务,或mysql://连接字符串。如果以上都不成立,则停止——使用database-schema-dev/backend-dev的引擎无关默认值,或改用对应的引擎特定技能。
第2步——字符集、排序规则、存储引擎
- 默认字符集是
utf8mb4,而不是utf8——MySQL的utf8是一个最多3字节的别名,无法存放大多数表情符号/星位字符——并显式指定排序规则(MySQL 8+使用utf8mb4_0900_ai_ci;MariaDB没有0900排序规则,对于需要同时兼容两者的场景使用utf8mb4_unicode_ci)。 - 使用
InnoDB,除非有特定理由使用MyISAM(无事务、无行锁、无外键)——自MySQL 5.5/MariaDB 10.x起这就是默认值,因此通常只是确认没有其他设置覆盖它。 _ci排序规则意味着'[email protected]'匹配'[email protected]'——对于必须区分大小写比较的内容(令牌、密钥),使用_bin/_cs(或哈希列)。
第3步——正确的数据类型
- 货币/精确小数:使用
DECIMAL(p,s),绝不使用FLOAT/DOUBLE——二进制浮点数无法精确表示大多数十进制分数。 - 根据实际范围选择合适的整数类型(
TINYINT/SMALLINT/INT/BIGINT),而不是习惯性地全用BIGINT——较小的类型意味着较小的索引。UNSIGNED为ID列提供了额外的空间,而ID列永远不会为负数。 - 对于不需要时区转换的挂钟时间值使用
DATETIME;TIMESTAMP(UTC标准化,按会话时区自动转换,2038年范围限制)仅用于实际的时间点——不要默认使用它。 - 根据实际约束调整
VARCHAR(n)的大小,而不是反射性地使用VARCHAR(255);对于真正无边界的内容使用TEXT/BLOB——两者都不能有默认值或完整(非前缀)索引。 ENUM仅用于固定且很少变化的集合——值按定义顺序排序,而不是按字母顺序(这在ORDER BY中可能是一个意外),在MySQL 8上追加值只是元数据操作,但在MariaDB/较旧MySQL上可能会重写整个表。一旦集合变大或需要元数据,查找表会更具可扩展性。- 原生
JSON(MySQL 5.7.8+/MariaDB 10.2+——MariaDB的JSON实际上是LONGTEXT加上一个CHECK约束,不是二进制类型,因此MySQL的二进制存储/部分更新优势都不适用)仅用于真正无模式的数据——任何需要经常过滤或连接的内容应该放在真实列中。
第4步——索引设计
- 复合索引:先将等值过滤列放在前面,范围/排序列放在最后——
(status, created_at)服务于WHERE status = ? ORDER BY created_at;反向顺序则不行。 - 最左前缀规则:
(a, b, c)上的索引也服务于(a)和(a, b)查找——不要在其旁边添加冗余的(a)索引。 - 覆盖索引(包含所有被选列的索引)允许MySQL仅从索引中回答查询(
EXPLAIN中显示Using index)——值得用于热点、窄查询,但不要默认对每个查询都使用。 - 长
VARCHAR/TEXT列使用前缀索引(col(20))——通过比较COUNT(DISTINCT LEFT(col, n))和COUNT(DISTINCT col)来确定n的大小。 - 深度分页:
LIMIT 20 OFFSET 100000仍然会扫描并丢弃10万行。键集分页(WHERE (created_at, id) < (:last_created, :last_id) ORDER BY created_at DESC, id DESC LIMIT 20)会复用同一个复合索引并直接定位到游标——在无限滚动或深层API分页中使用它代替OFFSET。 - 每个索引都会增加写入时间和磁盘空间——根据实际查询模式添加索引(
database-schema-dev第1步),而不是投机地为每一列添加。
第5步——基于EXPLAIN的查询调优
- 在相信查询快速之前,运行
EXPLAIN(或EXPLAIN ANALYZE,MySQL 8.0.18+/MariaDB 10.4+)——不要仅凭查询形状判断。 type: ALL表示全表扫描;ref/range/const表示实际使用了索引——一个值得优化的查询在表大小超过小规模后不应显示ALL。Extra: Using filesort/Using temporary表示MySQL在索引外进行了排序/物化——通常通过一个匹配ORDER BY/GROUP BY的索引来修复。- 对索引列使用包装函数或隐式类型转换(
WHERE YEAR(created_at) = ...,将字符串列与数字比较)会阻止索引使用——改为写成裸列的范围查询(created_at >= '2024-01-01' AND created_at < '2025-01-01')。 - 在宽表上使用
SELECT *会阻止覆盖索引并拉取未请求的TEXT/BLOB列——只选择实际使用的列。
第6步——事务与锁定
- InnoDB锁定行而不是表,但默认隔离级别
REPEATABLE READ在UPDATE/DELETE/加锁SELECT的范围扫描上还会使用间隙锁——一个范围条件可能会阻塞比匹配行更多的记录。如果不需要可重复读,READ COMMITTED会避免间隙锁。 - 该快照仅覆盖普通
SELECT——同一事务中的加锁读取(SELECT ... FOR UPDATE、UPDATE、DELETE)会读取最新的已提交数据,因此可能看到快照从未展示的行。不要假定快照隔离也能保护加锁读取。 - 保持事务简短——一个打开的事务会持有其锁,并且在
REPEATABLE READ下还会阻止InnoDB清除旧版本的行。 - 使用
SELECT ... FOR UPDATE来锁定在同一个事务中被读取再写入的行(如余额检查后再扣款);在整个代码库中按一致的行顺序获取锁以避免死锁。 - 在真实竞争下死锁(
ERROR 1213,SQLSTATE40001)是预期的——InnoDB会自动回滚一方;捕获该代码并重试,而不是通过延长事务来“修复”它。SHOW ENGINE INNODB STATUS→LATEST DETECTED DEADLOCK会显示每一方持有/等待了什么。 - 每个连接默认开启自动提交——多语句的原子性需要显式事务(或ORM的包装器),而不仅仅是在代码中相邻排列。
第7步——MySQL特定陷阱和MariaDB差异
sql_mode应包含STRICT_TRANS_TABLES(自MySQL 5.7/MariaDB 10.2起默认)——如果没有它,超出范围/错误类型的值会被静默截断而不是报错;确认它没有被放宽。AUTO_INCREMENT值不保证连续(回滚仍然会消耗一个值)——永远不要依赖id是连续的。- 会话
time_zone影响TIMESTAMP转换但不影响DATETIME——一个池中每个连接时区不一致会导致真正的“时间错误”问题;显式固定它。 - MariaDB有自己的扩展,MySQL中没有对应项(如
CREATE SEQUENCE、写入操作的RETURNING)——如果也要支持MySQL,则不要使用它们;窗口函数/CTE需要MySQL 8.0+/MariaDB 10.2+,先检查固定的版本。 - MariaDB的优化器和
EXPLAIN输出与MySQL不是同一个引擎——在实际使用的引擎上重新运行EXPLAIN,而不是相信在另一个引擎上调优的计划。
第8步——报告完成前的自我检查
确认每个新增/修改的列都有有意的类型,并且字符集回退到utf8mb4。确认任何新索引都有实际查询模式支持,并且EXPLAIN输出显示它被使用,任何深度/无边界列表端点使用键集分页而不是OFFSET。确认多语句写操作被包装在显式事务中,并且加锁读取的隔离行为是有意选择的,而不是假定。如果compose文件/CI固定使用MariaDB,确认没有添加仅MySQL-8特有的特性。
不要做什么
- 不要使用
utf8字符集或FLOAT/DOUBLE表示货币——utf8mb4和DECIMAL是正确的默认值。 - 不要在没有查询模式的情况下添加索引,或在不检查
EXPLAIN的情况下称一个查询为“快速”。 - 不要在可以深度分页或无限运行的列表上使用
OFFSET分页——改用键集分页。 - 不要让事务保持打开状态超过其保护的工作所需的时间,尤其是在
REPEATABLE READ的间隙锁下。 - 不要依赖
AUTO_INCREMENT的连续性或隐式类型转换来保证正确性。 - 不要在没有检查固定版本的情况下假定MariaDB目标具有MySQL-8-only特性(原生
JSON、utf8mb4_0900_ai_ci)。 - 不要自己编写引擎无关的迁移/访问层代码——这是
database-schema-dev/backend-dev的职责;此技能是它们应用在其之上的MySQL知识。