分库分表之后怎么进行 Join 操作?
结论:分片后跨库 JOIN 不能交给数据库,只能前置解决:字段冗余(宽表)、绑定表(Binding Table,同键同规则可本地 JOIN)、广播表(Broadcast Table,小表全量复制)、应用层内存组装,以及把复杂查询下沉到搜索引擎或数仓。原则是「能冗余不 JOIN,能同片不跨片」。
一、方案对比
| 方案 | 做法 | 适用 | 代价 |
|---|---|---|---|
| 字段冗余 | 订单表冗余 user_name、shop_name | 维表字段少、变更少 | 一致性维护 |
| 绑定表 | 订单与明细同分片键同规则 | 主子表 | 要求分片键完全一致 |
| 广播表 | 字典表在每个库各存一份 | 小表、读多写少 | 写放大、同步延迟 |
| 应用层组装 | 先查主表,再按 ID 批量 IN 查从表 | 通用 | 多一次往返、代码量 |
| 搜索引擎 | 宽表同步到 ES | 复杂条件检索 | 同步链路、存储成本 |
| 数仓 | T+1 或准实时同步 | 报表分析 | 实时性差 |
二、绑定表与广播表
绑定表(同分片键 user_id,同规则 %4) 广播表(每库一份)
ds0: t_order_0 + t_order_item_0 ds0: t_dict_city
ds1: t_order_1 + t_order_item_1 ds1: t_dict_city
ds2: t_order_2 + t_order_item_2 ds2: t_dict_city
ds3: t_order_3 + t_order_item_3 ds3: t_dict_city
└── 同库同后缀,可下推为本地 JOIN └── 全量复制,避免跨库
绑定表的三个条件缺一不可:分片键相同、分片算法相同、分片数量相同。少一个,中间件仍然会扇出到全部分片。
三、应用层组装
// 先查订单,再按 userId 批量查用户,避免 N+1
List<Order> orders = orderMapper.selectByUser(userId, page);
Set<Long> userIds = orders.stream()
.map(Order::getUserId)
.collect(Collectors.toSet());
Map<Long, User> userMap = userMapper.selectByIds(userIds).stream()
.collect(Collectors.toMap(User::getId, u -> u));
List<OrderVO> vos = orders.stream().map(o -> {
OrderVO vo = OrderVO.of(o);
User u = userMap.get(o.getUserId());
vo.setUserName(u == null ? "未知用户" : u.getNickname());
return vo;
}).toList();
批量 IN 有两条纪律:单批不超过 500 个 ID,避免 SQL 文本过长与解析开销;跨库查用户表时,若用户表也分片,这批 ID 会命中多个分片,仍需并行查询后合并。
四、ShardingSphere 配置
rules:
sharding:
binding-tables:
- t_order,t_order_item
broadcast-tables:
- t_dict_city
- t_dict_channel
tables:
t_order:
actual-data-nodes: ds$->{0..3}.t_order_$->{0..3}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db_mod
t_order_item:
actual-data-nodes: ds$->{0..3}.t_order_item_$->{0..3}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db_mod
配置生效后,SELECT * FROM t_order o JOIN t_order_item i ON o.id = i.order_id WHERE o.user_id = 42 会被改写成单库单表的本地 JOIN。
五、常见追问
Q1:为什么互联网公司普遍禁止多表 JOIN? 不只是分片问题。JOIN 让优化器的选择空间变大、执行计划不稳定,大表关联容易拖垮实例;同时也会让分库分表、缓存、读写分离难以实施。
Q2:广播表更新怎么办? 由中间件在所有分片执行同一条 DML,本质是分布式写。可靠做法是「写主库 + binlog 同步」或「MQ 广播 + 消费端幂等」,不要依赖中间件的多播事务。
Q3:应用层组装的数据一致性如何保证? 组装只用于展示,不参与判断;涉及金额、库存的决策必须回到主表。允许最终一致,并给冗余字段加版本号或更新时间戳做校验。
小结
- 分片后 JOIN 的关键是把「跨库」变成「同片」或「应用内」。
- 绑定表解决主子表,广播表解决字典表,冗余字段解决维表。
- 复杂检索交给搜索引擎,别在分片数据库上硬扛。