首页 > 数据库 >PostgreSQL中一个索引的三种不同的扫描类型: 位图、索引和仅索引

PostgreSQL中一个索引的三种不同的扫描类型: 位图、索引和仅索引

时间:2023-07-17 12:56:33浏览次数:45  
标签:rows PostgreSQL person age 扫描 索引 date

性能是数据库管理系统的基本要素之一。性能低下意味着查询时间过长和前端响应时间过高。提高性能的最有效方法之一是为表列建立适当的索引。索引可以节省大量的数据访问时间,并引导查询以最快的方式收集结果。在PostgreSQL中,有不同的方法可以利用索引来产生最有效的计划。

本文中,我们将回顾以下三种不同的索引扫描类型,这取决于表、查询检索的内容和使用的过滤器:

·位图索引扫描

·索引扫描

·仅索引扫描

 

构建测试场景

在下面的练习和示例中,我们将使用带有单一索引的表,并回顾扫描策略如何根据查询条件而改变。

定义表:

create sequence public.person_id_seq start with 1 increment by 1 no minvalue no maxvalue cache 1;CREATE TABLE public.person(id integer DEFAULT nextval('public.person_id_seq'::regclass) NOT NULL,first_name text NOT NULL,last_name text NOT NULL,age integer NOT NULL,email text NOT NULL,register_date timestamp with time zone DEFAULT now() NOT NULL,is_active boolean DEFAULT true NOT NULL);

 

插入1000W行记录:

INSERT INTO public.person SELECT generate_series, md5(random()::text), md5(random()::text), floor(random() * 99)::int, md5(random()::text) || '@gmail.com', now() - (random() * (interval '90 days')), case when random() > 0.5 then true else false end FROM generate_series(1, 10000000);

 

创建索引

create index idx_person_age_date_active on person(age,register_date,is_active);

在这里,我们考虑了三列不同的选择性,即不同值占总行数的比例。下面是从高到低排列的列:

·register_date:我们使用random()函数加载了1000万条记录,所以这三列中,register_date唯一的值是最多的。

·age:当我们加载数据时,也使用了random()函数,但是用floor()函数限制了结果,所以所有不同的值都在1到99之间。

·is_active. 这一列的数据类型为布尔型,因此只有两个不同的值,即true和false。

在规划索引时,必须考虑列的选择性。

例如,在上面的列中,在is_active列上使用单个索引不会增加任何优势,因为在所有的10M行中,只有两个值是可能的,所以如果我们想过滤所有is_active = true的行,优化器将毫无疑问地使用顺序扫描。

验证列的不同值数量的一种方法是查询数据库中的pg_stats视图。在这种情况下,我们运行ANALYZE命令:

db1=# ANALYZE person;ANALYZEdb1=# SELECT tablename AS table_name,attname AS column_name,n_distinct AS num_distinct_values      FROM pg_stats      WHERE tablename = 'person'      AND attname IN ('age','register_date','is_active')      ORDER BY num_distinct_values DESC;table_name |  column_name  | num_distinct_values------------+---------------+---------------------person     | age           |                  99person     | is_active     |                   2person     | register_date |                  -1(3 rows)

可以看到,age列不同的值是99,而is_active列只是2;对于register_date列的值,之所以是-1,是应为analyze认为该列不同的值和所有行数相等。

一个索引,不同的扫描类型

现在我们有了表数据和索引,我们可以测试不同的扫描类型。首先,为了有一个起点,让我们验证一下PostgreSQL将如何解决一个不带过滤器的表所有数据的查询:

db01=# EXPLAIN (ANALYZE) SELECT * FROM person;                                                       QUERY PLAN                                                       ------------------------------------------------------------------------------------------------------------------------ Seq Scan on person  (cost=0.00..304082.00 rows=10000000 width=126) (actual time=0.008..5286.228 rows=10000000 loops=1) Planning Time: 1.672 ms Execution Time: 5716.896 ms(3 rows)db01=# 

不出所料,为了从表中获取所有数据,优化器决定进行顺序扫描,获取全部1000万条记录。这是有道理的,因为它是一次性获取所有行。总耗时超过5716.896 ms。 

 

位图索引扫描

当查询请求的数据量足够大,可以利用批量读取的优势(如顺序扫描),但又不至于需要处理整个表时,优化器就会选择这种索引扫描方法。我们可以把位图索引扫描看作是介于顺序扫描和索引扫描之间的一种方法。

位图索引扫描总是和位图堆扫描一起工作;第一次扫描索引找到所有合适的行位置并建立位图,然后第二次使用位图逐个扫描堆页面并收集行。

下面是一个使用我们之前建立的表和索引进行位图索引扫描的示例:

db01=# EXPLAIN (ANALYZE) SELECT * FROM person WHERE age = 20 ;                                                                       QUERY PLAN                                                                       -------------------------------------------------------------------------------------------------------------------------------------------------------- Gather  (cost=3832.23..217695.48 rows=102667 width=126) (actual time=52.070..847.662 rows=100782 loops=1)   Workers Planned: 2   Workers Launched: 2   ->  Parallel Bitmap Heap Scan on person  (cost=2832.23..206428.78 rows=42778 width=126) (actual time=42.797..797.402 rows=33594 loops=3)         Recheck Cond: (age = 20)         Rows Removed by Index Recheck: 534364         Heap Blocks: exact=17732 lossy=12921         ->  Bitmap Index Scan on idx_person_age_date_active  (cost=0.00..2806.56 rows=102667 width=0) (actual time=40.628..40.629 rows=100782 loops=1)               Index Cond: (age = 20) Planning Time: 6.571 ms Execution Time: 858.086 ms(11 rows)db01=# 

在执行计划的内部节点(首先执行)中,对idx_person_age_date_active索引进行位图索引扫描。它用所有合适的行位置创建位图,并将其传递给它的父节点(在其后执行),

在person表上执行并行位图堆扫描。这第二阶段将逐个检索页,执行过滤条件的重新检查,并返回结果数据集。

为了比较一下,看看同样的操作仅使用顺序扫描是如何执行的:

db01=# START TRANSACTION ;START TRANSACTIONdb01=*# DROP INDEX idx_person_age_date_active;DROP INDEXdb01=*# EXPLAIN (ANALYZE) SELECT * FROM person WHERE age = 20 ;                                                           QUERY PLAN                                                            --------------------------------------------------------------------------------------------------------------------------------- Gather  (cost=1000.00..267432.03 rows=102667 width=126) (actual time=0.227..1284.268 rows=100782 loops=1)   Workers Planned: 2   Workers Launched: 2   ->  Parallel Seq Scan on person  (cost=0.00..256165.33 rows=42778 width=126) (actual time=0.045..1213.880 rows=33594 loops=3)         Filter: (age = 20)         Rows Removed by Filter: 3299739 Planning Time: 0.771 ms Execution Time: 1290.852 ms(8 rows)db01=*# ROLLBACK ;ROLLBACK

考虑到该查询涉及101K行,约占总行数的1%。位图索引扫描利用了顺序扫描方式批量读取有限页的优势,结果比直接顺序扫描更好,速度提高了。

 

索引扫描

当听到"嘿,这个查询做得不错;它使用了索引.... "之类的话时,你可能会想到这种扫描方法。这种方法是通过索引访问数据的基本定义。

索引扫描包括两步,第一步是从索引中获取行位置,第二步是从堆或表页中收集实际数据。因此,每次索引扫描访问都是两次读操作。但是,这仍然是从表中检索数据的最有效方法之一。

当需要检索的行数较少时,优化器会选择这种扫描方法,因此执行两步Index Scan操作比单独处理表页来收集数据成本低和快。

下面是使用测试表进行索引扫描的示例:

db01=# EXPLAIN (ANALYZE) SELECT * FROM person WHERE age = 20 AND register_date = '2023-03-23 19:50:03.22938+00'::timestamp;                                                             QUERY PLAN                                                              ------------------------------------------------------------------------------------------------------------------------------------- Index Scan using idx_person_age_date_active on person  (cost=0.56..8.58 rows=1 width=126) (actual time=0.040..0.040 rows=0 loops=1)   Index Cond: ((age = 20) AND (register_date = '2023-03-23 19:50:03.22938'::timestamp without time zone)) Planning Time: 1.393 ms Execution Time: 0.062 ms(4 rows)db01=#

在我们之前使用的查询中,添加了一个新的过滤表达式: AND register_date = '2023-03-23 19:50:03.22938+00'::timestamp。

register_date列是多列索引idx_person_age_date_active的一部分。由于我们是通过一个单一的值进行过滤,因此只有一个索引条目,所以PostgreSQL从索引中一次读取特定的行位置,然后从表页中读取该位置内的所有行数据。

整个查询耗时0.062 ms,非常快!

 

在上面的示例中,查询通过register_date列的特定时间戳值进行过滤,但是如果行数较少,PostgreSQL仍然会选择索引扫描来获取多条记录,例如在下面的示例中:

db01=# EXPLAIN (ANALYZE) SELECT * FROM person WHERE age = 20 AND register_date BETWEEN '2023-03-23 19:50:00'::timestamp AND '2023-03-23 20:00:00'::timestamp;                                                                                   QUERY PLAN                                                                                   -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Index Scan using idx_person_age_date_active on person  (cost=0.56..8.58 rows=1 width=126) (actual time=0.010..0.011 rows=0 loops=1)   Index Cond: ((age = 20) AND (register_date >= '2023-03-23 19:50:00'::timestamp without time zone) AND (register_date <= '2023-03-23 20:00:00'::timestamp without time zone)) Planning Time: 1.722 ms Execution Time: 0.031 ms(4 rows)db01=# 

 

仅索引扫描

最后,将回顾一下仅索引扫描方法。这是PostgreSQL用来改进标准索引扫描方法的一种非常好的方法。

换句话说,在SELECT和WHERE子句中的列/表达式应该是索引的一部分,这样就可以避免从表页中获取数据的二次读操作,而只从索引读操作中返回结果数据。

在下面的示例中,我们使用了与索引扫描示例中几乎相同的查询,但是我们并没有询问所有的行列(*),而是仅仅检索了我们用来建立多列索引的三列:

db01=# EXPLAIN (ANALYZE) SELECT age,register_date,is_active FROM person WHERE age = 20 AND register_date = '2023-03-23 19:50:03.22938+00'::timestamp;                                                               QUERY PLAN                                                                ----------------------------------------------------------------------------------------------------------------------------------------- Index Only Scan using idx_person_age_date_active on person  (cost=0.56..4.58 rows=1 width=13) (actual time=0.021..0.021 rows=0 loops=1)   Index Cond: ((age = 20) AND (register_date = '2023-03-23 19:50:03.22938'::timestamp without time zone))   Heap Fetches: 0 Planning Time: 0.088 ms Execution Time: 0.048 ms(5 rows)db01=# 

EXPLAIN输出现在显示仅索引扫描,而且,通过Heap Fetches这行确认没有访问堆(表页): 时间甚至比之前的索引扫描更好,只有0.048ms。这种扫描方法可以帮助符合条件的查询获得最佳性能。

请记住,为所有列建立索引,使索引包含与表相同的所有数据,并不是一个好主意。如果是这种情况,PostgreSQL将不会看到使用索引的任何好处,而会选择顺序扫描方法。请看下面的内容:

db01=# START TRANSACTION ;START TRANSACTIONdb01=*# DROP INDEX "idx_person_age_date_active";DROP INDEXdb01=*# CREATE INDEX idx_person_all ON person(id,first_name,last_name,age,email,register_date,is_active);CREATE INDEXdb01=*# ANALYZE person;ANALYZEdb01=*# EXPLAIN (ANALYZE) SELECT * FROM person WHERE age = 20 AND register_date = '2023-03-23 19:50:03.22938+00'::timestamp;                                                         QUERY PLAN                                                         ---------------------------------------------------------------------------------------------------------------------------- Gather  (cost=1000.00..267582.21 rows=1 width=126) (actual time=8496.878..8500.434 rows=0 loops=1)   Workers Planned: 2   Workers Launched: 2   ->  Parallel Seq Scan on person  (cost=0.00..266582.11 rows=1 width=126) (actual time=8464.351..8464.352 rows=0 loops=3)         Filter: ((age = 20) AND (register_date = '2023-03-23 19:50:03.22938'::timestamp without time zone))         Rows Removed by Filter: 3333333 Planning Time: 2.053 ms Execution Time: 8502.746 ms(8 rows)db01=*# ROLLBACK ;ROLLBACKdb01=# 

在上面的示例中,在一个事务中,我们放弃了之前示例中使用的多列索引,并创建了一个包含了表中所有列的新索引,然后刷新了统计信息,并尝试了一个查询,查询特定过滤器上的所有列(*),结果,优化器选择了顺序扫描,它希望通过并行执行操作来提高执行时间。尽管如此,最终的执行时间与我们的预期结果相差甚远。

 

最后的建议

1.在选择性高的列上创建索引

2.尽量查询少量数据

3.只返回你需要的列

4.调优参数random_page_cost,降低这个参数会让优化器更倾向于索引扫描,而不是顺序扫描。固态硬盘可以为随机读取访问提供更好的吞吐量,因此你可以分析调整这个参数。

5.调整有效缓存大小(effective_cache_size)参数。将该参数设置为较高值(如果您的机器专用于PostgreSQL服务,则接近总RAM的75%)将有助于规划器选择索引扫描而不是顺序扫描。

 

标签:rows,PostgreSQL,person,age,扫描,索引,date
From: https://www.cnblogs.com/abclife/p/17558481.html

相关文章

  • PostgreSQL 常用函数(一)
    函数(function)是一些预定义好的代码模块,可以将输入进行计算和处理,最终输出一个结果值。PostgreSQL函数可以分为两类:标量函数(scalarfunction)和聚合函数(aggregationfunction)。标量函数针对每个输入都会返回相应的结果,聚合函数针对一组输入汇总出一个结果。为了便于学习,可以将常见的......
  • 一文搞懂为什么Mysql索引会失效
    在工作中,如果我们想提高一条语句查询速度,通常都会想对字段建立索引。 但是索引并不是万能的。建立了索引,并不意味着任何查询语句都能走索引扫描。稍不注意,可能你写的查询语句是会导致索引失效,从而走了全表扫描,虽然查询的结果没问题,但是查询的性能大大降低。今天就来跟大家盘......
  • 一文搞懂数据库索引原理
    前言写数据库,我第一时间就想到了MySQL、Oracle、索引、存储过程、查询优化等等。不知道大家是不是跟我想得一样,我最想写的是索引,为啥呢?以下这个面试场景,不知道大家熟悉不熟悉:面试官:数据库有几千万的数据,查询又很慢我们怎么办?面试者:加索引。面试官:那索引有哪些数据类型?索引是......
  • 90%的程序员都不知道Mysql索引下推
    索引下推(IndexConditionPushdown)ICP是Mysql5.6之后新增的功能,主要的核心点就在于把数据筛选的过程放在了存储引擎层去处理,而不是像之前一样放到Server层去做过滤。虽然这是一个比较简单的概念,但是可能很多不细心的同学对于索引下推会存在一个小小的误区,至于是什么,请看下文......
  • 遇到了一个需要java生成二维码,支持扫码枪扫描的需求
    1,需求是生产二维码,加入到正文和表单中2,首先能生成二维码,其次就是把二维码插入到表单和正文了,插入到表单设计到一些前端知识暂不分享3,把二维码插入正文,正文是用的word文档,后续分享如何操作 生成二维码的样式应该是pdf417这种码,而不是qrcode普通的二维码#需要引入依赖jar包......
  • MySQL 索引、事务与存储引擎
    目录一、索引1.概念2.作用3.副作用4.创建索引的原则依据5.优化6.分类二、事务1.事务的概念2.事务的特点(1)原子性(2)一致性(3)隔离性(4)持久性3.扩展事务之间的相互影响分为几种4.Mysql及事物隔离级别5.事务控制语句6.使用set设置控制事务三、存储引擎一、索引1.概念是......
  • Mysql进阶篇(二)之索引
    一.索引概述1.介绍索引是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。2.演示表结构及其数据如下:假如我们要执......
  • Mysql进阶篇(二)之索引
    一.索引概述1.介绍索引是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。2.演示表结构及其数据如下:假如我们要执行的SQL语......
  • linux MySQL数据库索引
    索引在数据库中占有重要地位,当高并发时,一条一条的查找数据是很慢的,但是索引可以分流,可以快速定位,解决高并发。目录一、索引概念二、索引作用三、索引的应用四、索引分类五、索引的创建六、索引的管理七、总结    一、索引概念1.索引概念数据库索......
  • 数据库(SQL注入问题、视图、触发器、事务、存储过程、内置函数、流程控制、索引)
    SQL注入问题SQL注入的原因:由于特殊符号的组合会产生特殊的效果 实际生活中,尤其是在注册用户名的时候会非常明显的提示你很多特殊符号不能用,会产生特殊的效果。结论:涉及到敏感数据部分,不要自己拼接,交给现成的方法拼接即可。importpymysql#链接MySQL服务端conn=pymysql.......