• 分库后如何分页


    前言

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

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

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

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

    有一个文章表 user_article

    其中有一个文章的发表时间 publish_date. 这个时间用户是可以修改的.

    按照 ID 取模分到了两个表中.

    user_article_1 user_article_0

    现在有这样一个需求:

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

    单表

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

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

    # 取第三页的数据
    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.

    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

    第一步, 分别取各表数据

    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', 代表各个查询结果的最小值.

    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.

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

    第三步, 返回结果

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

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

    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为:

    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的时候, 请添加辅助字段排序.

  • 相关阅读:
    简单计算器(栈和队列的应用)
    01背包问题(dfs+剪枝)
    PAT1057 stack(分块思想)
    算法笔记求序列A每个元素左边比它小的数的个数(树状数组和离散化)
    求序列A中每个数的左边比它小的数的个数(树状数组)
    ubuntu-创建python虚拟环境
    vue-打包
    django-项目目录结构
    接口自动化-常见工具
    selenium-封装一个browser
  • 原文地址:https://www.cnblogs.com/hujingnb/p/14966879.html
Copyright © 2020-2023  润新知