TL;DR(30 秒扫完)
- JOIN 六方案:绑定表 / 广播表 / 异构索引表 / 大宽表 / 服务化 JOIN / ES,每种对应不同场景
- JOIN 核心思路:能本地 JOIN 就本地(同分片键 = 绑定表),能广播就广播(字典小表),实在不行才服务化
- 深分页本质:
LIMIT offset, size要扫 offset+size 行,分片场景还要放大 N 倍 - 分页四解:游标分页 / 覆盖索引延迟关联 / 限制翻页深度 / ES
- 顺序原则:能游标就游标 → 不能游标就限制深度 → 最后才是 ES
关键结论
结论 A跨分片 JOIN 是架构限制,中间件做 N×M 笛卡尔积,性能随分片数急剧下降
结论 B绑定表和广播表是分片中间件核心特性(ShardingSphere / MyCat 都支持)
结论 C深分页必须在设计阶段规避,不是出问题再治
结论 D分片键与排序字段尽量对齐,能让分页天然局部
完整讲解(费曼四步)
STEP 1 · 概念
跨分片 JOIN 是分片后 SQL JOIN 无法直接在 MySQL 层完成的替代方案集合;分页是分片场景下数据分散导致LIMIT offset, size 语义变化的应对方案。
STEP 2 · 大白话
JOIN 类比:- 绑定表 = 同一办公室的两份文件,本地翻一下就行
- 广播表 = 每个办公室都备一份字典,谁都能查
- 异构索引表 = 通讯录先查手机号,再打电话确认
- 大宽表 = 把所有信息印在同一张纸上,但改一处要改多份
- 服务化 JOIN = 打两个电话拼数据,快但走网络
- ES = 专门的档案室存副本,主库只做增删
- 普通分页 = 从书架第 1 本开始找,找到第 100001 本拿走 10 本
- 游标分页 = 从上次拿走的最后一本之后接着找 10 本(永远只挪 10 本)
STEP 3 · 底层
六种 JOIN 方案对比
| 方案 | 核心机制 | 一致性 | 适用场景 | 代价 |
|---|---|---|---|---|
| 绑定表 | 主子表同分片键,中间件自动本地 JOIN | 强 | 主子表关联 | 需分片键一致 |
| 广播表 | 小表全量复制到每个分片 | 中(异步) | 字典/配置/类目 | 内存翻倍 |
| 异构索引表 | 主表按 A 分片,索引表按 B 分片 | 强(双写) | 多查询路径 | 双写一致性 |
| 大宽表 | 字段冗余到主表 | 强 | 读多写少 | 写放大 |
| 服务化 JOIN | RPC 跨服务拼接 | 中(缓存) | 跨域强耦合 | 网络开销 |
| ES/ClickHouse | 宽表同步到搜索/OLAP | 弱(异步) | 复杂查询/搜索 | 同步延迟 |
绑定表(Binding Table)
要求:主子表用同一分片键且值相同。-- 主表按 user_id 分片
orders_00(user_id, order_id, status, ...) -- user_id=1001
orders_01(...)
orders_02(...)
-- 明细表也按 user_id 分片
order_items_00(user_id, item_id, ...) -- 同一用户的数据在同一分片
order_items_01(...)
中间件(ShardingSphere / MyCat)识别绑定关系后自动做本地 JOIN,性能接近单库 JOIN。
配置示例:
# ShardingSphere 配置
binding-tables:
- orders, order_items
广播表(Broadcast Table)
原理:字典小表全量复制到每个分片。orders_00 + user_level + product_category + tenant_config
orders_01 + user_level + product_category + tenant_config
orders_02 + user_level + product_category + tenant_config
JOIN 完全本地,性能 = 单库 JOIN。
代价:- 小表更新要广播到所有分片(用 MQ 异步)
- 内存翻倍(但字典小表通常 <1w 行,可忽略)
异构索引表(Secondary Index Table)
原理:主表按 A 分片,异构表按 B 分片。-- 主表按 user_id 分片
orders_00(user_id, order_id, status, ...)
orders_01(...)
-- 异构表按 order_id 分片
order_index_00(order_id, user_id, status)
order_index_01(...)
查订单详情时:- 查异构表按 order_id 定位 → 拿到 user_id
- 用 user_id 回主表精确定位
- 同事务写入主表 + 异构表(推荐)
- 本地消息表异步
- 定时对账修复
- 极端情况允许短暂不一致
大宽表(Denormalization)
原理:把需要的字段直接冗余到主表。-- 冗余 order.status、product.name、user.nickname
orders(user_id, order_id, status, product_name, user_nickname, ...)
代价:写放大(更新要改多份)、字段冗余、维护成本。
适合:读多写少,比如订单列表页展示字段。
服务化 JOIN 的三大优化
跨域强耦合场景(订单查用户服务)必须做:- 批量接口:一次 RPC 拉 N 条而不是 N 次 RPC 拉 1 条
- 多级缓存:本地缓存 + Redis + 远端服务缓存(短时效缓 30 秒,长时效缓 5 分钟)
- 异步加载:主查询走主库,附加数据异步加载,前端分段渲染
电商订单表的典型组合
orders ← 主表按 user_id 分片
order_items ← 绑定表(同 user_id 分片键)
product_category ← 广播表(复制到所有分片)
phone_index ← 异构索引表(按 phone 分片)
es_order_index ← ES 同步,搜索/后台管理走这里
分页
深分页的本质
SELECT * FROM t ORDER BY create_time LIMIT 100000, 10;
MySQL 要扫 100010 行,前 100000 行读回后丢弃。分片场景还要乘以分片数。
分片中间件执行流程:- 向每个分片发
SELECT * FROM t ORDER BY x LIMIT 0, offset+size - 所有分片返回后中间件做内存排序
- 截断前 offset 行,返回后 size 行
四种主流解
解 1:游标分页(Keyset Pagination)-- 第一页
SELECT * FROM t ORDER BY id DESC LIMIT 10;
-- 拿到最大 id = 100
-- 第二页
SELECT * FROM t WHERE id < 100 ORDER BY id DESC LIMIT 10;
-- 拿到最大 id = 90
-- 第三页
SELECT * FROM t WHERE id < 90 ORDER BY id DESC LIMIT 10;
- 优点:走了主键/排序索引,每页恒定性能
- 缺点:不能跳页、不能显示"共 X 页"
- 适用:Feed 流、聊天记录、无限滚动
- 不适用:搜索框切换条件时游标失效
-- 第一步:走覆盖索引,只取 id
SELECT id FROM t ORDER BY create_time LIMIT 100000, 10;
-- 第二步:回表拿详情
SELECT * FROM t WHERE id IN (...);
- 优点:第一步虽扫 100010 行索引,但索引比主表小 10 倍
- 缺点:仍要扫 offset 行
- 适用:能接受一定 offset,想优化深分页
// 业务侧禁止翻到第 100 页后
if (pageNum > 100) {
return "请使用搜索框筛选";
}
- 优点:最简单粗暴,效果最好
- 缺点:功能受限
- 适用:99% 用户不会翻到 20 页,成本远低于架构改造
SearchSourceBuilder ssb = new SearchSourceBuilder()
.from(0)
.size(10)
.searchAfter(new Object[]{lastId, lastTimestamp}); // 游标
- 优点:ES
search_after天然支持游标分页 - 缺点:异步同步有延迟
- 适用:后台管理、报表、搜索场景
分片键与排序字段的对齐
| 分片方式 | 排序字段 | 分页代价 |
|---|---|---|
| 按时间分片 | 按时间排序 | 低(最新数据在最新分片) |
| 按 hash 分片 | 按时间排序 | 高(跨全部分片扫描) |
STEP 4 · 简化
一句话总结:JOIN 走"能本地就本地 → 能异构就异构 → 最后才服务化 → ES 兜底搜索";分页走"能游标就游标 → 不能游标就限制深度 → 最后才 ES"。 记忆口诀:- JOIN 优先级:绑定表 > 广播表 > 异构索引 > 大宽表 > 服务化 > ES
- 分页优先级:游标 > 限制深度 > 覆盖索引 > ES
- 分片键与排序字段对齐 = 分页免费
常见误区
说"分片后所有 JOIN 都慢"
绑定表和广播表可以做到接近单库 JOIN 的性能
说"异构索引表就是分片"
异构索引表是跨分片查询的兜底方案,主表和异构表分片键不同
说"游标分页解决所有分页问题"
游标不能跳页,搜索场景不适合;搜索场景要走 ES
说"深分页问题不大"
分片场景深分页代价放大 N 倍,
LIMIT 100000,10 在分片场景可能几十秒说"限制翻页深度是最懒的做法"
限制翻页深度是最高性价比的做法,99% 用户翻不到 20 页
延伸追问
绑定表要求什么前提条件?分片键不一致怎么办?
主子表分片键必须一致且值相同;不一致时用异构索引表或大宽表兜底
广播表在数据量大时(比如 100w 条字典)还可行吗?
不适合。广播表只适合万级以内;大字典表用异构索引表或服务化查询
异构索引表的双写一致性如何保证?
同事务写入主表 + 异构表 → 本地消息表异步 → 定时对账修复;极端情况允许短暂不一致
LIMIT 100000,10 在 MySQL 里为什么慢?InnoDB 要扫 100010 行,前 100000 行读回后丢弃;如果走二级索引还要回表
游标分页的劣势是什么?哪些场景不适合?
不能跳页、不能显示总页数;搜索框切换条件时游标失效——所以搜索场景不适合
分片中间件做全局分页时如何优化?(如 ShardingSphere)
Hint 强制路由到单分片;聚合查询(COUNT)走异构索引表;预计算排序字段
ES 和 MySQL 主库数据不一致怎么办?
异步同步接受秒级延迟;关键业务用 CDC(Canal)实时同步;读多写少的后台场景可接受
速查表
JOIN 六方案: 绑定表 / 广播表 / 异构索引 / 大宽表 / 服务化 / ES
绑定表: 主子表同分片键,本地 JOIN
广播表: 字典小表全量复制,适合万级以下
异构索引: 主表按 A 分片,索引表按 B 分片,两次查询拼接
大宽表: 反范式,适合读多写少
服务化: 批量接口 + 多级缓存 + 异步加载
深分页本质: LIMIT offset,size 扫 offset+size 行
分页四解: 游标 > 限制深度 > 覆盖索引 > ES
游标分页: WHERE id < :lastId ORDER BY id DESC LIMIT N
ES 分页: search_after(lastId, lastTimestamp)
分片键对齐: 排序字段尽量与分片键一致
Anki 候选卡片
Q: 跨分片 JOIN 的六种方案?
A: 绑定表 / 广播表 / 异构索引表 / 大宽表 / 服务化 JOIN / ES
Q: 绑定表的前提条件?
A: 主子表必须使用同一分片键且值相同
Q: 广播表适合什么规模?
A: 万级以下的字典小表;100w+ 不适合
Q:
LIMIT 100000,10 为什么慢?A: MySQL 要扫 100010 行,前 100000 行读回后丢弃
Q: 游标分页的核心 SQL?
A:
WHERE id < :lastId ORDER BY id DESC LIMIT NQ: 深分页的分片场景放大倍数?
A: 代价 = N 个分片 × 单库深分页耗时
关联题目
- ⚠️ 《分库分表之后的怎么进行join操作》— 2026-09-23 Round 1, ⭐⭐⭐(只答大宽表+服务化,漏绑定/广播/异构)
- ⚠️ 《分库分表后如何进行分页查询?》— 2026-09-23 Round 1, ⭐⭐⭐(漏深分页/游标/覆盖索引)
关联知识
JOIN 走本地优先,分页走游标优先,最后才 ES
图书馆 JOIN:绑定表=同办公室翻文件,广播表=每个办公室备字典,异构索引表=通讯录先查号码;分页:普通=从第 1 本找 10 本,游标=从上次最后一本之后接着找
✦ 记 忆 口 诀 ✦
JOIN 优先级 绑定表 大于 广播表 大于 异构 大于 大宽表 大于 服务化 大于 ES / 分页优先级 游标 大于 限制深度 大于 覆盖索引 大于 ES
关键可视化
六种 JOIN 方案对比
flowchart TB J[跨分片 JOIN 方案] --> A[绑定表 主子表同分片键] J --> B[广播表 字典小表全量复制] J --> C[异构索引表 主表按 A 分片 索引表按 B 分片] J --> D[大宽表 反范式 字段冗余] J --> E[服务化 JOIN RPC 跨服务] J --> F[ES 或 ClickHouse 异步同步] A --> AR[强一致 需分片键一致] B --> BR[异步 内存翻倍 适合万级以下] C --> CR[双写 一致性靠对账修复] D --> DR[写放大 适合读多写少] E --> ER[网络开销 批量接口 多级缓存] F --> FR[弱一致 异步 搜索场景]
绑定表原理
flowchart LR M[主表 orders 按 user_id 分片] --> O0[orders_00 user_id 等于 1001] M --> O1[orders_01] M --> O2[orders_02] S[明细表 order_items 同 user_id 分片] --> I0[order_items_00] S --> I1[order_items_01] S --> I2[order_items_02] O0 -.同一分片.-> I0 O1 -.同一分片.-> I1 O2 -.同一分片.-> I2 X[ShardingSphere 识别绑定] --> Y[自动本地 JOIN] Y --> Z[性能接近单库]
异构索引表
flowchart TB M[主表 按 user_id 分片] --> O0[orders_00 user_id] M --> O1[orders_01] H[异构表 按 order_id 分片] --> I0[order_index_00 order_id user_id] H --> I1[order_index_01] Q[查询订单详情] --> Q1[第一步 查异构表按 order_id 定位] Q1 --> Q2[拿到 user_id] Q2 --> Q3[第二步 回主表精确定位] Q3 --> Q4[返回订单数据]
深分页的本质
flowchart TB S[SELECT limit offset size] --> A[MySQL 扫 offset 加 size 行] A --> B[前 offset 行读回后丢弃] B --> C[分片场景 代价乘以 N] C --> D[例 limit 100000 10 分 8 片] D --> E[代价 8 乘以 单库深分页 100010 行] E --> F[分片中间件归并 top N 截断]
深分页四种主流解
flowchart TB P[深分页解] --> A[游标分页 keyset] A --> AR[WHERE id 小于 lastId ORDER BY id DESC LIMIT N] A --> AB[优点 恒定性能 无 offset] A --> AD[缺点 不能跳页 不能显示总页数] P --> B[覆盖索引加延迟关联] B --> BR[第一步 走覆盖索引取 id] B --> BR2[第二步 回表拿详情] B --> BB[索引比主表小 10 倍] P --> C[限制翻页深度] C --> CR[业务侧禁止翻到第 100 页后] C --> CC[99 百分号用户翻不到 20 页] P --> D[ES search_after] D --> DR[游标模式 天然支持]
电商订单表典型组合
flowchart LR O[电商订单] --> M[主表 orders 按 user_id 分片] O --> I[明细表 order_items 绑定表] O --> C[商品类目表 广播表] O --> P[phone_index 异构索引 按手机号] O --> E[es_order_index 搜索场景] M --> M1[我的订单 90 主查询] I --> I1[订单详情] C --> C1[类目筛选 广播本地] P --> P1[按手机号查订单] E --> E1[搜索 后台管理 报表]
知识关系
🔄 延伸(Extends)
sharding-migration
🎯 概念
📏 规则
⚠️ 误区
🔍 追问
✨ 口诀
共 0 张卡,点击翻面