分页查询的几种方式及其适用场景

传统偏移量

即 LIMIT OFFSET,OFFSET 子句跳过的行仍需被扫描,数据量较大会存在性能问题。

适用于B端后台数据较小的场景。

基于游标

适用于具有唯一字段且单调的表结构,一般以自增主键id作为游标列。查询时带上上一次返回的cursor,查询第一页数据不用传,后续分页必传。如:select * from table where id < cursor limit 20 ,这将从cursor行开始扫描,大数据量下相较于传统偏移量方式表现更好,特别适合C端应用的滚动分页场景。

一种更好的方式是:查询时多查一条记录,用于判断是否还有更多数据,从而减少一次查询操作。

封装为一个通用的工具类

python 复制代码
class Paginator:
    def __init__(self, query):
        self.query = query
        self.data = None
​
    def paginate(self, page: int, page_size: int, max_per_page=None):
        """分页查询"""
        if page_size is None:
            page_size = 20
​
        if page is None or page <= 0:
            raise ValueError("page or page size is None!")
​
        if max_per_page and page_size > max_per_page:
            raise ValueError("per page size exceeded the max limit!")
​
        offset = page_size * (page - 1)
        self.data = self.query.slice(offset, offset + page_size).all()
        return self
​
    @property
    def total(self):
        return self.query.count()
​
    @property
    def items(self):
        return self.data
​
​
class ScrollPaginator(Paginator):
    """滚动分页(基于游标)"""
​
    def __init__(self, query: Query, model: db.Model):
        """
​
        :param query: 查询对象
        :param model: model对象
        """
        super().__init__(query)
        self.model = model
        self.data = []
        self.has_more = False
        self.last_score = None
​
    def paginate(
        self, last_score: Union[str, int], limit=20, max_limit=100, order_col="id", is_reversed=True
    ):
        """
        基于游标分页
        :param order_col: 排序字段名
        :param last_score: 查询列表中最后一项的id
        :param max_limit: 最大分页限制
        :param limit: 滚动步长,使用limit+1用于判断是否还有更多数据
        :param is_reversed: 按时间倒序
        :return:
        """
        if limit < 1:
            raise ValueError("limit must >= 1")
        if max_limit and limit > max_limit:
            raise ValueError("per page size exceeded the max limit!")
​
        if last_score:  # 非首次查询
            if is_reversed:
                filters = [getattr(self.model, order_col) < int(last_score)]
            else:
                filters = [getattr(self.model, order_col) > int(last_score)]
            self.query = self.query.filter(*filters)
​
        queryset = self.query.limit(limit + 1).all()
        if (length := len(queryset)) >= 1:
            if length > limit:
                self.data = queryset[:-1]
                self.has_more = True
                last = queryset[-2]
            else:
                self.data = queryset
                last = queryset[-1]
​
            self.last_score = getattr(last, order_col)
​
        return self
​
    @property
    def last(self):
        return self.last_score
​
    @property
    def more(self):
        return self.has_more

redis zset

见另一篇文章:juejin.cn/post/732933...

相关推荐
YOU OU5 小时前
Redis基础常识与命令
数据库·redis·缓存
GetcharZp6 小时前
抛弃低效SSH!这套Ansible+Web可视化神器,让你天天准点下班!
后端
Database_Cool_7 小时前
OLTP 和 OLAP 区别详解:分析型数据库和事务型数据库怎么选(附阿里云 AnalyticDB MySQL 选型指南)
数据库·mysql·阿里云
爱写代码的阿森8 小时前
鸿蒙三方库 | harmony-utils之KvUtil键值型数据库操作详解
数据库·华为·harmonyos·鸿蒙·huawei
Database_Cool_8 小时前
单机 MySQL 迁移到分布式数据库方便吗?阿里云 PolarDB-X 100% MySQL 协议兼容零改造平滑迁移
数据库·分布式·mysql
姜太小白9 小时前
【MySQL】 索引优化实战:解决 WHERE 等值 + IS NULL 查询,TEXT 字段报错 1167 的完整指南
数据库·mysql
weixin_446260859 小时前
HACO:面向动态部署环境的对冲式智能计算可靠多智能体调度框架
后端·python·flask
窝子面9 小时前
手搓最简前后端协作-node
javascript·数据库
吳所畏惧9 小时前
宝塔面板Redis密码修改指南:SSH命令修改 vs 面板UI界面修改,哪个更靠谱?
运维·服务器·数据库·redis·缓存·ssh