知识卡片

水平分表后join、count()与order by操作的应对方式

普通读书笔记卡

内容

除了[[水平分表的路由算法:范围路由、Hash路由与配置路由的权衡]],水平分表还会让几个原本在单表上很简单的操作变得复杂。join操作:数据分散到多个子表后,如果要和其他表联合查询,只能在业务代码或数据库中间件里对每个子表分别做查询、再把结果合并,不能再依赖数据库原生的一次join完成。count()操作:物理上数据分散在多张表里,但业务逻辑上经常还是要把它们当一张表处理(比如做分页要拿到总记录数),分表前一次count()就能搞定的事,分表后有两种常见处理方式——”count()相加”是对每个子表分别count()再把结果加起来,实现简单但性能较差(比如分成20张表,串行执行20次count(*)可能要几秒才能拿到结果);”记录数表”是新建一张专门记录每个子表行数的表,每次子表插入或删除成功后同步更新这张记录数表,读取时只需一次简单查询,性能明显优于count()相加,但代价是复杂度显著上升——任何一处子表操作忘了同步更新记录数表就会导致数据不一致,而且针对子表的操作和针对记录数表的操作天然无法放进同一个事务,异常情况下完全可能出现”子表操作成功但记录数表更新失败”这种不一致;对不要求记录数实时精确的业务,还可以退而求其次,用后台定时任务通过”count()相加”的方式周期性刷新记录数表,相当于把两种方式结合起来,用牺牲实时性换取写压力和一致性风险的降低。order by操作:数据分散在多个子表后,排序天然无法在数据库层面一次性完成,只能由业务代码或中间件分别查询各子表数据,再把结果汇总后统一排序。分库分表的最终技术实现路径和读写分离一样,也是”程序代码封装”与”中间件封装”两条路线,但实现复杂度明显更高——读写分离只需要识别一条SQL是读还是写(判断SELECT/UPDATE/INSERT/DELETE关键字即可),分库分表除了判断操作类型,还要进一步识别SQL具体操作的是哪张表、有没有用到count这类聚合函数、有没有order by/group by,再针对不同情况分别处理(例如order by要先从各库分别查出数据,再对合并后的结果重新排序一次才能得到最终结果)。

参考来源

- 位置:《从零开始学架构》第15讲《高性能数据库集群:分库分表》"水平分表"之"join操作""count()操作""order by操作""实现方法"(源文件:_epub-src/OEBPS/text00001.html) - 结论依据:原文说明"水平分表后,数据分散在多个表中,如果需要与其他表进行 join 查询,需要在业务代码或者数据库中间件中进行多次 join 查询",并展开count()相加与记录数表两种方案的优劣、order by需要"由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序",以及"分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数……然后再根据不同的操作进行不同的处理",直接支撑本卡片结论。 - 原始内容:count() 相加:具体做法是在业务代码或者数据库中间件中对每个表进行 count() 操作,然后将结果相加……记录数表:具体做法是新建一张表……水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成……分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数……order by、group by 操作等。