mod(id, n)会导致数据倾斜,因真实id不连续、含负值或为字符串,使余数分布不均;应改用crc32哈希后再取模,并对null和字符串做兜底处理。

MOD(id, N) 直接取模为什么会导致数据倾斜?
看似合理:4 台库,就写 MOD(id, 4)。但真实 ID 往往不连续、有空缺、含负值,甚至来自业务发号器(如雪花 ID)。比如 ID 序列是 1, 2, 5, 9, 10, 1000001,MOD(id, 4) 结果是 1, 2, 1, 1, 2, 1 —— 余数 0 和 3 完全没出现,3 台库长期闲置。
根本问题不是 MOD 函数慢,而是输入分布不均 → 输出分布必然不均。MOD 本身无法“打散”原始键的局部聚集性(如按时间插入的订单号天然递增)。
- 负 ID 会返回负余数(
MOD(-7, 4)返回-3),多数分片路由逻辑不处理负值,直接跳过或报错 - ID 字段为
VARCHAR时隐式转换可能失败或截断(如'user_123'转成0),导致所有该类记录落到同一分片 - 字段未建索引 +
WHERE MOD(id, 4) = 2→ 全表扫描,无法走索引
用 CRC32 哈希替代 MOD 提升分布均匀性
对原始 ID 做哈希再取模,比直接对 ID 取模更能打散热点。MySQL 内置 CRC32() 计算快、结果稳定,适合做分片键哈希:
SELECT user_id, MOD(CRC32(CONVERT(user_id AS CHAR)), 4) AS shard_id FROM users;
注意几个关键点:
-
CONVERT(user_id AS CHAR)确保非数字类型(如字符串 ID)被一致编码,避免隐式转换歧义 -
CRC32()返回无符号整数(0–4294967295),再套MOD(..., N)不会出现负余数 - 若字段已为整型(如
BIGINT主键),可省略CONVERT,直接MOD(CRC32(user_id), 4) - 不要用
MD5()或SHA1()—— 计算开销大,且需额外CONV(LEFT(MD5(...), 8), 16, 10)截断转数字,徒增复杂度
MySQL 5.7 及更早版本如何规避窗口函数限制?
MySQL 8.0+ 支持 ROW_NUMBER() OVER (ORDER BY id) 生成稠密序号再取模,但 5.7 不支持窗口函数,也不能在子查询 WHERE 中引用计算列。此时必须换思路:
- 写入时预计算:
INSERT INTO users (id, shard_id, ...) VALUES (123, MOD(CRC32('123'), 4), ...),把shard_id当作冗余字段存下来 - 查询时直查:
SELECT * FROM users WHERE shard_id = 2—— 字段加索引后能走ref或range,性能可控 - 避免应用层拼 SQL 动态算
shard_id:既易引入 SQL 注入,又让连接池无法复用预编译语句 - 如果必须运行时计算,优先用
CRC32()而非MOD(id, N),并在shard_id字段上建索引,而非依赖函数索引(5.7 不支持函数索引)
分片字段为 NULL 或字符串时的兜底处理
MOD() 遇到 NULL 或无法转整数的字符串,一律返回 NULL,而 NULL != 0,这类记录会从所有 WHERE shard_id = ? 查询中消失——静默丢数据。
必须显式兜底:
- 用
COALESCE()或IFNULL()替换空值:MOD(CRC32(COALESCE(user_id, 'fallback')), 4) - 对字符串 ID,先验证是否纯数字:
user_id REGEXP '^[0-9]+$',否则走默认分片或拒绝写入 - 上线前用
SELECT COUNT(*) FROM t WHERE shard_id IS NULL检查异常数据残留 - 永远不要假设业务 ID 是“安全”的——它可能来自用户输入、日志导入或旧系统迁移
真正难的不是写出 MOD() 表达式,而是确认输入数据的分布特征、NULL 边界、字符集兼容性,以及下游路由逻辑能否正确识别你算出的余数值。一个 MOD(CRC32(...), 4) 看似简单,但漏掉 COALESCE 或用错 CONVERT 编码,就足以让 1% 的请求路由错位,且难以监控。











