前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >分库后如何分页

分库后如何分页

作者头像
烟草的香味
发布2021-07-23 12:41:17
7280
发布2021-07-23 12:41:17
举报
文章被收录于专栏:烟草的香味烟草的香味

前言

在实际应用中, 为了降低单表的数据量, 会对较大的表进行水平切分, 将单表的数据切分到多表多库中.

既然要切分, 就要有一个切分的依据, 比如说按照 ID 取模等. 那么多张表联合分页是如何做到的呢?

如果分表的依据是字段 A, 但是需要根据字段 B 进行分页查询, 针对这种情况应该如何处理呢?

为了后面方便说明, 这里举个例子.

有一个文章表 user_article 其中有一个文章的发表时间 publish_date. 这个时间用户是可以修改的. 按照 ID 取模分到了两个表中. user_article_1 user_article_0

现在有这样一个需求:

按照文章的发表时间进行排序分页

单表

先来看在单表的时候, 我们是如何查询的, 之后再扩展到多表.

代码语言:javascript
复制
select * from `user_article` order by `publish_date` offset 0 limit 10;

单表查询很简单, 一条 sql 搞定.

多表

对于多表的情况, 将其抽象一下, 就是有两个有序列表:

[1, 5, 7, 8, 9] [2, 3, 4, 6, 10]

现在要取合并后的有序列表第 n 页的数据.

方案一

想到的第一个方案, 将两个列表合并之后, 就可以退化为单列表的情况了.

对应到 sql查询中, 如果要取第三页的数据, 可以肯定的是, 每个列表都不会读到第四页, 所以我们可以将每个表的前三页拿出来, 在内存中进行合并后, 就可以拿到全局的第三页了.

代码语言:javascript
复制
# 取第三页的数据
select * from `user_article_0` order by `publish_date` offset 0 limit 3*10;
select * from `user_article_1` order by `publish_date` offset 0 limit 3*10;

这种方案确实可以获取到分页的数据, 但是查询的数据量随着页数的增大而增大. 如果查询第200页的数据, 那每张表都要返回2000条数据, 性能太低.

方案二

如果说, 我们按照页数依次获取, 中间不跳页的话, 那么就可以将上一次查询的终点保存下来, 供下一次查询使用.

比如, 上一次查询, 最后一条数据是8, 那么, 下一次查询从各个列表中取出大于8的10条数据, 内存排序后取前10条, 同时将最后一条的值存下来供下一次查询使用.

对应到sql查询中, 就需要有一个全局的searchId.

代码语言:javascript
复制
select * from `user_article_0` where `publish_date` > 'searchId' order by `publish_date` limit 10;
select * from `user_article_1` where `publish_date` > 'searchId' order by `publish_date` limit 10;

每页查询数量固定, 效率较高. 但同时, 这种功能方案不能做到跳页, 如果要查询第 n 页的数据, 前提是查询了 n-1页. 同时, 此方案要求 publish_date字段无重复值, 如果有20条相同的publish_date, 在翻页的时候就会丢失部分数据.

方案三

那么, 有没有一种方案, 即能跳页查询, 查询数量也能保持在常量级呢?

说明

来了, 为了方便说明, 先从数组开始:

[1, 3, 5, 7, 11, 18, 23, 32, 41] [2, 8, 9, 15, 17, 22, 27, 51, 60]

此时, 如果想获取全局: offset 4 limit 4 的数据. 因为我们不知道全局偏移量4在各个数组中的各自偏移量. 所以在方案一中需要进行大量的查询, 如果我们知道了, 问题不就解决了么.

第一步, 分别取各列表半个偏移量的数据

先分别取各个数据中offset 2 limit 4的数据. 结果如下:

[5, 7, 11, 18] [9, 15, 17, 22]

注意: 这里的offset 2, 是通过全局偏移量/表个数算出来的.

拿到这个数据之后, 我们得到了什么? 其中的最小值5, 全局偏移量必定>=2.

  • 如果数据1中小于5的值大于2个, 那么第一次查询时结果必定不同
  • 如果数据2中存在小于5的值, 那么5的全局偏移量只会增加.

第二步, 获取最小值的全局偏移量

通过第一步的分析, 如果我们能够知道数据2中存在多少个小于5的值, 那么我们就能够计算出5的全局偏移量. 进而得到全局偏移量为4的数据.

查询数据2中, data > 5 and data < 9的数据, 结果如下:

[8]

前面, 我们知道9数据2中的偏移量为2. 同时 数据2中, >5 and <9的数据, 个数为1, 计算可得, 数据2中小于5的数据个数为: 2-1=1

又因, 5数据1中的偏移量为2, 进而可得, 5的全局偏移量为: 2+1=3. (加上数据2中小于5的数量)

第三步, 整合数据并返回

我们将前后查询的结果整合一下, 得到如下数据:

[5, 7, 11, 18] [8, 9, 15, 17, 22]

再将两数组合并为一个数组:

[5, 7, 8, 9, 11, 15, 17, 18, 22]

已知, 5的全局偏移量为3, 则偏移量为4的数据为7. 我们从7开始, 向后拿4个, 就是全局的offset 4 limit 4的数据了.

[7, 8, 9, 11]

问题

到这里, 你以为已经完成了么? 看下面这组数据:

[1, 2, 3, 4, 5, 6, 7, 8] [9, 10, 11, 12, 13, 14, 15, 16]

很明显, 这组数据分布十分不均匀, 按照上面的操作获取分页数据offset 4 limit 4

第一步折半查询结果offset 2 limit 4

[3, 4, 5, 6] [11, 12, 13, 14]

然后拿到3的全局偏移量2. 得到偏移量4的数据为5. 组合后返回结果为:

[5, 6, 9, 10]

这, 明眼人一看, 就知道结果应该是[5, 6, 7, 8].

很明显, 因为数据都在一张表上, 所以导致第一次获取数据没有取完. 但是, 到这一步, 我们获取到的偏移量是没有问题的.

也就是说, 全局偏移量为4的数据为5. 那么, 我们就可以退回到 方案二 进行处理了.

当然, 如果对数据的精度要求没有那么高, 或者确信数据分布不会出现这种极限情况, 可以忽略.

貌似网上将这种方法称为二次查询, 没有找到文章提到这个问题, 难道说实际应用中不会遇到么?

sql

将上面方案转为 sql, 取第4页的数据, 既offset 30 limit 10

第一步, 分别取各表数据

代码语言:javascript
复制
select * from `user_article_0` order by `publish_date` offset 30/2 limit 10;
select * from `user_article_1` order by `publish_date` offset 30/2 limit 10;

我们在这一步, 统计出查询结果的最小值 M

第二步, 最小值全局偏移量

下方sql中的 M', 代表各个查询结果的最小值.

代码语言:javascript
复制
select * from `user_article_0` where `publish_date`>M and `publish_date`<"M'" order by `publish_date`;
select * from `user_article_1` where `publish_date`>M and `publish_date`<"M'" order by `publish_date`;

此时, 通过计算, 我们已经得到M的全局偏移量了. 同时, 也拿到了偏移量30的值S.

如果数据分布十分不均匀, 在这一步, 极端情况会将前面所有数据都拿出来.

第三步, 返回结果

如果确信不会出现前面提到的极限情况, 这里直接组合结果并返回即可.

否则, 继续执行下面查询并返回. 此时, 排序字段不可重复.

代码语言:javascript
复制
select * from `user_article_0` where `publish_date` > S order by `publish_date` limit 10;
select * from `user_article_1` where `publish_date` > S order by `publish_date` limit 10;

此方案执行了三倍的数据库查询, 但优点是查询效率恒定与页数无关, 且支持跳页.

方案四

因为我们的数据是按照 ID 取模, 根据概率分布, 两个库的数据分布应该是一样的. 那是不是可以每个表取半页数据, 拿回来拼上.

这样的话, 取第3页数据的 sql为:

代码语言:javascript
复制
select * from `user_article_0` order by `publish_date` offset 20/2 limit 5;
select * from `user_article_1` order by `publish_date` offset 20/2 limit 5;

这样的话, 数据不太准确, 拿到的数据顺序可能不对, 但重点是查询效率高啊. 应该是有对顺序精度没什么要求的场景吧. 想到了这种方案, 但是暂时没有想到应用场景.

如果是针对分表字段排序的话, 那么数据分布均匀, 此方案完美.

最后

具体业务应该如何选择分页方式呢?

  • 如果不需要跳页, 直接选择 方案二
  • 如果对顺序精度没什么要求, 直接选择 方案四
  • 如果只需要查询前 n 页数据, 且 n 比较小. 那么方案一 可能更适合你
  • 如果需要查询所有数据, 且需要跳页, 可以选择 方案一 和 方案三 结合的方式. 优先选择当前效率更高的.
    • 对于访问频率较高的前几页数据, 选择 方案一
    • 对于页数比较大的数据, 选择 方案三

当然了, 前面说的情况, 排序字段与分表字段不同, 数据分布可能不均匀. 如果是相同的字段, 那就没这么多事了, 数据都是均匀分布的, 参考 方案四

最后, 对于排序使用的字段, 最好能够保证其唯一性, 如果不能, order by的时候, 请添加辅助字段排序.

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-07-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 烟草的香味 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 单表
  • 多表
    • 方案一
      • 方案二
        • 方案三
          • 说明
          • sql
        • 方案四
        • 最后
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档