PG中的索引扫描类型
Tags: PGPostgreSQL优化索引
简介
性能是数据库管理系统的基本要素之一。性能低下意味着查询时间过长和前端响应时间过高。提高性能的最有效方法之一是为表列建立适当的索引。索引可以节省大量的数据访问时间,并引导查询以最快的方式收集结果。在PostgreSQL中,有不同的方法可以利用索引来产生最有效的计划。
本文中,我们将回顾以下三种不同的索引扫描类型,这取决于表、查询检索的内容和使用的过滤器:
·位图索引扫描
·索引扫描
·仅索引扫描
构建测试场景
在下面的练习和示例中,我们将使用带有单一索引的表,并回顾扫描策略如何根据查询条件而改变。
定义表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | CREATE OR REPLACE VIEW index_check AS SELECT t.schemaname, t.tablename, c.reltuples::bigint AS num_rows, pg_size_pretty(pg_relation_size(c.oid)) AS table_size, psai.indexrelname AS index_name, pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size, CASE WHEN i.indisunique THEN 'Y' ELSE 'N' END AS "unique", psai.idx_scan AS number_of_scans, psai.idx_tup_read AS tuples_read, psai.idx_tup_fetch AS tuples_fetched, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) as create_index FROM pg_tables t LEFT JOIN pg_class c ON t.tablename = c.relname LEFT JOIN pg_index i ON c.oid = i.indrelid LEFT JOIN pg_stat_all_indexes psai ON i.indexrelid = psai.indexrelid WHERE t.schemaname NOT IN ('pg_catalog', 'information_schema') AND pg_relation_size(i.indexrelid) > 0 ORDER BY 1, 2; 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); ANALYZE person; select * from pg_stat_all_tables where schemaname='public' and relname = 'person'; 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; EXPLAIN SELECT * FROM person; select * from index_check d WHERE d.tablename='person'; |
在这里,我们考虑了三列不同的选择性,即不同值占总行数的比例。下面是从高到低排列的列:
·register_date:我们使用random()函数加载了1000万条记录,所以这三列中,register_date唯一的值是最多的。
·age:当我们加载数据时,也使用了random()函数,但是用floor()函数限制了结果,所以所有不同的值都在1到99之间。
·is_active. 这一列的数据类型为布尔型,因此只有两个不同的值,即true和false。
在规划索引时,必须考虑列的选择性。
例如,在上面的列中,在is_active列上使用单个索引不会增加任何优势,因为在所有的10M行中,只有两个值是可能的,所以如果我们想过滤所有is_active = true的行,优化器将毫无疑问地使用顺序扫描。
验证列的不同值数量的一种方法是查询数据库中的pg_stats视图。在这种情况下,我们运行ANALYZE命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | db1=# ANALYZE person; ANALYZE db1=# db1=# SELECT tablename AS table_name,attname AS column_name,n_distinct AS num_distinct_values db1-# FROM pg_stats db1-# WHERE tablename = 'person' db1-# AND attname IN ('age','register_date','is_active') db1-# ORDER BY num_distinct_values DESC; table_name | column_name | num_distinct_values ------------+---------------+--------------------- person | age | 99 person | is_active | 2 person | register_date | -1 (3 rows) |
可以看到,age列不同的值是99,而is_active列只是2;对于register_date列的值,之所以是-1,是应为analyze认为该列不同的值和所有行数相等。
不带过滤条件,则会顺序扫描(全表扫)
现在我们有了表数据和索引,我们可以测试不同的扫描类型。首先,为了有一个起点,让我们验证一下PostgreSQL将如何解决一个不带过滤器的表所有数据的查询:
1 2 3 4 5 6 7 | db1=# EXPLAIN (ANALYZE) SELECT * FROM person; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------- Seq Scan on person (cost=0.00..304082.18 rows=10000018 width=126) (actual time=0.035..41623.205 rows=10000000 loops=1) Planning Time: 0.187 ms Execution Time: 81347.117 ms (3 rows) |
不出所料,为了从表中获取所有数据,优化器决定进行顺序扫描,获取全部1000万条记录。这是有道理的,因为它是一次性获取所有行。总耗时超过81347.117 ms。
位图索引扫描(Bitmap Index Scan)和位图堆扫描(Bitmap Heap Scan)
当查询请求的数据量足够大,可以利用批量读取的优势(如顺序扫描),但又不至于需要处理整个表时,优化器就会选择这种索引扫描方法。我们可以把位图索引扫描看作是介于顺序扫描和索引扫描之间的一种方法。
关于bitmap index scan:
- 核心是传统的index scan每次从索引中去取一个tuple的指针,然后立马去表中取数据,每一次会造成一次随机io。如果数据量较多的情况下,会比较低效。而bitmap scan一次性将符合条件的tuple-pointers全部取出来,然后在内存中进行地址排序,然后去取出数据,这时的读取数据由于进行的地址排序,读取时就变成了顺序的读。其实就是一个随机读转化为顺序读取的过程,但是取出的数据由于进行了地址的排序,就没有顺序。同时,对于limit这种sql,bitmap index scan这种就不适合,因为它一次会取出所有数据。
- 也就是说bitmap index scan是为了减小index scan扫描同一个页次数过多代价太高而设计的,bitmap index scan适合获取表中的数据量有点大(10%-20%)这样会带了额外的随机扫描开销。有了bitmap index scan就可以先扫描索引,然后符合条件的多条数据如果在同一个page页那么只需要扫描一次页就能获取这些数据,所以index scan一般比bitmap的代价要高,所以优化器优先选择了bitmap index scan。
- 如果人为的降低这个代价,那么优化器又会选择index scan,而人为的修改方法就有降低random_page_cost
位图索引扫描总是和位图堆扫描(Bitmap Heap Scan)一起工作;第一次位图索引扫描找到所有合适的行位置并建立位图,然后第二次使用位图堆扫描逐个扫描堆页面并收集行。