PostgreSQL 数据访问 offset 的质变 case

本文涉及的产品
云原生数据库 PolarDB 分布式版,标准版 2核8GB
云数据库 RDS SQL Server,基础系列 2核4GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: 背景 offset limit是一个多么场景的需求啊,但是你知道offset的数据可能隐藏着质变吗? 如图 node有30W条数据,其中前100条是满足条件的,然后100条到20W条都是不满足条件的。所以offset 10 limit 10非常的快。但是offset 100 limi

背景

offset limit是一个多么常见的需求啊,但是你知道offset的数据可能隐藏着质变吗?

如图
screenshot
node有30W条数据,其中前100条是满足条件的,然后100条到20W条都是不满足条件的。
所以offset 10 limit 10非常的快。
但是offset 100 limit 10,就要扫描从100到20W条记录,然后再往后才是满足条件的记录。
这就是质变的原因。

例子

生成1000万测试记录。

postgres=# create table tbl(id int primary key, info text);
CREATE TABLE
postgres=# insert into tbl select generate_series(1,10000000),'';
INSERT 0 10000000

更新info字段的数据,分布在前1000条和第500万后的100条。

postgres=# update tbl set info='test' where id<1000 or id between 5000000 and 5000100; 
UPDATE 1100

order by id offset 100 limit 100查询的是前面的记录,非常快。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl where info='test' order by id offset 100 limit 100;
                                                             QUERY PLAN                                                             
------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=49339.42..98678.40 rows=100 width=5) (actual time=0.154..0.343 rows=100 loops=1)
   Output: id, info
   Buffers: shared hit=603
   ->  Index Scan using tbl_pkey on public.tbl  (cost=0.43..329091.45 rows=667 width=5) (actual time=0.019..0.293 rows=200 loops=1)
         Output: id, info
         Filter: (tbl.info = 'test'::text)
         Buffers: shared hit=603
 Planning time: 0.253 ms
 Execution time: 0.386 ms
(9 rows)

如果扫描的是1000条以后的,因为满足条件的记录是500W往后的,所以至少要扫描500万条记录才能拿到结果。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl where info='test' order by id offset 1000 limit 100;
                                                          QUERY PLAN                                                          
------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=169291.40..169291.40 rows=1 width=5) (actual time=952.266..952.330 rows=100 loops=1)
   Output: id, info
   Buffers: shared hit=44260
   ->  Sort  (cost=169289.74..169291.40 rows=667 width=5) (actual time=951.892..952.102 rows=1100 loops=1)
         Output: id, info
         Sort Key: tbl.id
         Sort Method: quicksort  Memory: 100kB
         Buffers: shared hit=44260
         ->  Seq Scan on public.tbl  (cost=0.00..169258.45 rows=667 width=5) (actual time=951.167..951.496 rows=1100 loops=1)
               Output: id, info
               Filter: (tbl.info = 'test'::text)
               Rows Removed by Filter: 9998900
               Buffers: shared hit=44260
 Planning time: 0.105 ms
 Execution time: 952.375 ms
(15 rows)

关闭seqscan则会使用索引扫描,一样的需要扫描一些不满足条件的记录。
removed by filter就是很好的说明

postgres=# set enable_seqscan=off;
SET
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl where info='test' order by id offset 1000 limit 100;
                                                              QUERY PLAN                                                               
---------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=329091.45..329091.45 rows=1 width=5) (actual time=888.400..888.519 rows=100 loops=1)
   Output: id, info
   Buffers: shared hit=38991
   ->  Index Scan using tbl_pkey on public.tbl  (cost=0.43..329091.45 rows=667 width=5) (actual time=0.033..888.267 rows=1100 loops=1)
         Output: id, info
         Filter: (tbl.info = 'test'::text)
         Rows Removed by Filter: 4999000
         Buffers: shared hit=38991
 Planning time: 0.110 ms
 Execution time: 888.632 ms
(10 rows)

or
postgres=# set enable_seqscan=on;
SET
postgres=# set enable_sort=off;
SET
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl where info='test' order by id offset 1000 limit 100;
                                                              QUERY PLAN                                                               
---------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=329091.45..329091.45 rows=1 width=5) (actual time=887.791..887.906 rows=100 loops=1)
   Output: id, info
   Buffers: shared hit=38991
   ->  Index Scan using tbl_pkey on public.tbl  (cost=0.43..329091.45 rows=667 width=5) (actual time=0.040..887.540 rows=1100 loops=1)
         Output: id, info
         Filter: (tbl.info = 'test'::text)
         Rows Removed by Filter: 4999000
         Buffers: shared hit=38991
 Planning time: 0.154 ms
 Execution time: 887.964 ms
(10 rows)

如果把limit加大到超过实际的满足条件的结果,则需要扫完所有的记录。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from tbl where info='test' order by id offset 1000 limit 10000;
                                                               QUERY PLAN                                                               
----------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=329091.45..329091.45 rows=1 width=5) (actual time=898.675..1786.476 rows=100 loops=1)
   Output: id, info
   Buffers: shared hit=74776
   ->  Index Scan using tbl_pkey on public.tbl  (cost=0.43..329091.45 rows=667 width=5) (actual time=0.030..1786.240 rows=1100 loops=1)
         Output: id, info
         Filter: (tbl.info = 'test'::text)
         Rows Removed by Filter: 9998900
         Buffers: shared hit=74776
 Planning time: 0.110 ms
 Execution time: 1786.536 ms
(10 rows)

小结

  1. offset仅仅是偏移量,不是从此位置开始扫描,所以偏移量前的tuple都是需要被扫描到的。
  2. limit的使用也需要注意,如果有断层产生,会额外的扫描更多的块。
  3. offset一种好的优化方法是根据PK来位移。
    例子见我以前写的一批文章。

分页优化手段之一

一位开发的同事给我一个SQL, 问我为什么只改了一个条件, 查询速度居然从毫秒就慢到几十秒了,
如下 :

SELECT *                                                                                
  FROM tbl
  where create_time>='2014-02-08' and create_time<'2014-02-11'
  and x=3
  and id != '123'
  and id != '321'
  and y > 0 order by create_time limit 1 offset 0;

运行结果100毫秒左右.
执行计划 :

Limit  (cost=0.56..506.19 rows=1 width=1038)
   ->  Index Scan using idx on tbl  (cost=0.56..2381495.60 rows=4710 width=1038)
         Index Cond: ((create_time >= '2014-02-08 00:00:00'::timestamp without time zone) AND (create_time < '2014-02-11 00:00:00'::timestamp without time zone))
         Filter: (((id)::text <> '123'::text) AND ((id)::text <> '321'::text) AND (y > 0) AND (x = 3))

改成如下 :

SELECT *                                                                                
  FROM tbl
  where create_time>='2014-02-08' and create_time<'2014-02-11'
  and x=3
  and id != '123'
  and id != '321'
  and y > 0 order by create_time limit 1 offset 10;

运行几十秒.
执行计划如下 :

Limit  (cost=5056.98..5562.62 rows=1 width=1038)
   ->  Index Scan using idx on tbl  (cost=0.56..2382076.78 rows=4711 width=1038)
         Index Cond: ((create_time >= '2014-02-08 00:00:00'::timestamp without time zone) AND (create_time < '2014-02-11 00:00:00'::timestamp without time zone))
         Filter: (((id)::text <> '11622'::text) AND ((id)::text <> '13042'::text) AND (y > 0) AND (x = 3))

我们看到两个SQL执行计划是一样的, 但是走索引扫描的记录却千差万别. 第二个SQL扫描了多少行呢?
我们来看看第二个查询得到的create_time值是多少:

select create_time from tbl 
  where create_time>='2014-02-08' and create_time<'2014-02-11'
  and x=3
  and id != '123'
  and id != '321'
  and y > 0 order by create_time limit 1 offset 10;

结果 :

'2014-02-08 18:38:35.79'

那么它扫描了多少行(或者说多少个数据块)呢? 通过explain verbose可以输出.
当然使用以下查询也可以估算出来 :

select count(*) from tbl where create_time<='2014-02-08 18:38:35.79' and create_time>='2014-02-08';
  count  
---------
 1448081
(1 row)

也就是说本例的SQL中的WHERE条件的数据在create_time这个字段顺序上的分布比较零散, 并且数据量比较庞大.
所以offset 10后, 走create_time这个索引自然就慢了.
仔细的了解了一下开发人员的需求, 是要做类似翻页的需求.

优化方法1,

在不新增任何索引的前提下, 还是走create_time这个索引, 减少重复扫描的数据.
需要得到每次取到的最大的create_time值, 以及可以标示这条记录的唯一ID.
下次取的时候, 不要使用offset 下一页, 而是加上这两个条件.
例如 :

select create_time from tbl 
  where create_time>='2014-02-08' and create_time<'2014-02-11'
  and x=3
  and id != '123'
  and id != '321'
  and pk not in (?)  -- 这个ID是上次取到的create_time最大的值的所有记录的pk值.
  and y > 0 
  and create_time >= '2014-02-08 18:38:35.79'  -- 这个时间是上次取到的数据的最大的时间值.
  order by create_time limit ? offset 0;

如果偏移量本来就是一个PK,则不需要加pk not in (?)的条件

通过这种方法, 可以减少limit x offset y这种方法取后面的分页数据带来的大量数据块离散扫描.
以前写的一些关于分页优化的例子 :
http://blog.163.com/digoal@126/blog/static/163877040201111694355822/
http://blog.163.com/digoal@126/blog/static/1638770402012520105855757/

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
7月前
|
SQL Oracle 关系型数据库
实时计算 Flink版操作报错之往GREENPLUM 6 写数据,用postgresql-42.2.9.jar 报 ON CONFLICT (uuid) DO UPDATE SET 语法有问题。怎么解决
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
|
10天前
|
存储 关系型数据库 数据库
【赵渝强老师】PostgreSQL的数据文件
PostgreSQL的物理存储结构主要包括数据文件、日志文件等。数据文件按oid命名,超过1G时自动拆分。通过查询数据库和表的oid,可定位到具体的数据文件。例如,查询数据库oid后,再查询特定表的oid及relfilenode,即可找到该表对应的数据文件位置。
|
1月前
|
存储 SQL NoSQL
|
6月前
|
消息中间件 Java 关系型数据库
实时计算 Flink版操作报错合集之从 PostgreSQL 读取数据并写入 Kafka 时,遇到 "initial slot snapshot too large" 的错误,该怎么办
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
1017 0
|
6月前
|
DataWorks 安全 关系型数据库
DataWorks产品使用合集之使用Flink CDC读取PostgreSQL数据时如何指定编码格式
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
104 0
|
4月前
|
SQL 关系型数据库 MySQL
mysql 中 case when 的使用
mysql 中 case when 的使用
|
4月前
|
SQL 关系型数据库 MySQL
SQL Server、MySQL、PostgreSQL:主流数据库SQL语法异同比较——深入探讨数据类型、分页查询、表创建与数据插入、函数和索引等关键语法差异,为跨数据库开发提供实用指导
【8月更文挑战第31天】SQL Server、MySQL和PostgreSQL是当今最流行的关系型数据库管理系统,均使用SQL作为查询语言,但在语法和功能实现上存在差异。本文将比较它们在数据类型、分页查询、创建和插入数据以及函数和索引等方面的异同,帮助开发者更好地理解和使用这些数据库。尽管它们共用SQL语言,但每个系统都有独特的语法规则,了解这些差异有助于提升开发效率和项目成功率。
455 0
|
4月前
|
SQL 关系型数据库 HIVE
实时计算 Flink版产品使用问题之如何将PostgreSQL数据实时入库Hive并实现断点续传
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
4月前
|
开发框架 关系型数据库 数据库
在 PostgreSQL 中,解决图片二进制数据,由于bytea_output参数问题导致显示不正常的问题。
在 PostgreSQL 中,解决图片二进制数据,由于bytea_output参数问题导致显示不正常的问题。
|
6月前
|
关系型数据库 Unix 数据库
PostgreSQL开启远程访问
PostgreSQL开启远程访问

相关产品

  • 云原生数据库 PolarDB
  • 云数据库 RDS PostgreSQL 版