--创建的索引一定能用上吗?为什么?
--employee_id是主键
select employee_id from employees where employee_id = 12345;
--与
select first_name from employees where employee_id = 12345;
--执行的时间会大体相同吗?如果不同为什么?
--创建索引(first_name)
select * from employees where first_name Like 'F%';
--与
select * from employees where first_name Like '%F%'
--哪个会走索引?为什么?
--创建联合索引(employee_id, date_of_birth)
select * from employees where employee_id = 1000 and date_of_birth between '1987-01-01' and '1987-02-01';
--与
select * from employees where date_of_birth between '1987-01-01' and '1987-02-01'and employee_id = 1000;
--对于联合索引,sql条件字段顺序跟索引字段有关系吗?
--如果同时创建联合索引(employee_id, date_of_birth),会走哪个索引?索引字段顺序对执行计划有影响吗?
--创建索引(last_name)
select * from employees where UPPER(last_name) = UPPER('huayu');
--会走索引吗?
--创建索引(first_name, last_name, date_of_birth)
select * from employees where first_name = 'beijing';
select * from employees where first_name = 'beijing' and last_name = 'huayu';
select * from employees where first_name = 'beijing' and last_name = 'huayu' and date_of_birth = '2009-01-01';
select * from employees where last_name = 'huayu' and date_of_birth = '2009-01-01';
-- 或者任意字段的索引组合,都会走索引吗?有什么不同?
堆(Heap)
存储数据库表数据的地方,最小的存储单位是页,或者叫block,一般是8kb。表的数据会存储在一个个页中,物理上来讲是不连续的。每一个页的头部有pointer,指向本页的中的具体数据
索引存储(Index Storage)
索引是独立的数据,但是只存储关键数据(索引字段),也是用页的形式存储,一个索引数据是有序的
元组标识(TID, Tuple Identifier)
TID是一个6byte的数值,前4byte代表数据页numer,后2byte代表数据在该页中的offset。一个TID就可以唯一标识一个元组的Heap存储位置
元组的概念和数据库中row的概念是一致的,前者是一种抽象,后者是一种具体。就像relation和table一样
select ctid from employees where employee_id = 10000;
select ctid from employees where employee_id < 10000;
索引的数据结构有很多:B-Tree,Hash,GIN,GiST…
索引是数据库独立管理的数据,用来避免出现只需要查询一小部分数据但是却要读取整个table的情况,实质上是无序物理数据的一个逻辑上的有序表示
为什么物理数据从存储不能是有序的?
怎么样创建索引:
Index Leaf Node
双向链表的数据结构,将物理上无序的索引数据页建立双向链表数据结构,便于插入和删除node节点,以及正向和反向遍历index数据。
Index数据和table数据存储的异同:二者页与页之间都是没有关联的,无序的。
table数据页
内部tuple也是无序的,而Index数据页
内部tuple是有序的。整个Index数据的有序是通过Index页内数据的有序和Index Leaf Node双向链表数据结构实现的
索引数据页内tuple排序 + Index Leaf Node这样的数据结构相对于Heap数据排序有什么优点呢?
这个有序的Index Leaf Node是否能够快速的定位某个一数据呢?
Balance Tree
B-Tree是在Index Leaf Node之上建立的平衡树,用来能够快速定位数据所在的页
实质上,为了能够快速的定位数据,数据库做了三个方面的事情:
从Root Node开始,按照平衡树的搜索算法,直到找到叶子节点页
找到对应叶子节点页后,在页中搜索数据
对于非unique的索引,tree traverse之后的index leaf node搜索可能结果很多,这样就可以能跨页来搜索数据,相对于tree traverse来说,这一步的成本可能没有上界
根据得到的index数据,从Heap中load数据
从2和3成本的角度分析,如果索引结果集相对较大,但有不至于走全表扫描的成本,在索引层次有没有什么办法处理来减小查询成本?
rule-based是预先设置好的规则,按照规则生成执行计划。这种一般现在很少用
code-base以成本为依据,选择执行计划成本最小的最为最终的执行方案
优化器(Optimizer)在进行执行计划成本计算(explain)的时候,并没有真正的执行,依靠什么来计算各执行计划的成本呢?
explain vs explain analyze区别:explain只是基于base规则生成执行计划;而explain analyze实际上执行了这个Query
统计信息主要存储在表:pg_class
和pg_statistics
中。
pg_class存储表、索引等创建需要的数据库页数、表行数等信息
SELECT reltuples, relpages FROM pg_class where relname = 'employees_1h';
1e+06 142858
VACUUM employees_1h;
这里的信息是estimation,新创建一个表数据库estimation需要10个数据库页和1000行数据,当插入或者删除数据时,这些信息不回自动更新,需要用命令:VACUUM ${TABLE_NAME}
pg_statistics存储列相关的数据:null值的多少、重复数据的多少、最长出现的数据等等
SELECT attname, null_frac, avg_width, n_distinct, most_common_vals, most_common_freqs, histogram_bounds, correlation FROM pg_stats where tablename = 'employees_1h';
同样,表字段的统计信息也不会在数据插入或者删除的时候自动更新,需要用命令:ANALYZE ${TABLE_NAME}
数据库优化器最终会生成一个树状结构的执行计划,执行计划的叶子节点称为表扫描节点(table scan node),这个节点标识用什么方式扫描表(是否有索引,什么算法等),Postgre支持
的表扫描:
Sequence Scan
explain analyze
select * from employees where employee_id > 1;
Seq Scan on employees (cost=0.00..259361.00 rows=1000000 width=1028) (actual time=0.040..4094.349 rows=999999 loops=1)
Filter: (employee_id > '1'::numeric)
Rows Removed by Filter: 1
Planning time: 34.928 ms
Execution time: 4151.501 ms
顺序扫描一般有两种情况:
顺序扫描发生的是sequence I/O,相对于Random Access I/O,有什么优点?
为什么走索引的成本有时会比走全表扫描要慢?可以先看看Index Scan有哪些问题,再回来
Index Scan
explain analyze
select * from employees where employee_id = 1;
Index Scan using employees_pk on employees (cost=0.42..8.44 rows=1 width=1028) (actual time=0.025..0.026 rows=1 loops=1)
Index Cond: (employee_id = '1'::numeric)
Planning time: 0.333 ms
Execution time: 0.072 ms
存在的问题:Index Scan获取Index Value(TID)之后会从Heap区域获取数据,也就是从某个数据库页的offset Tuple上获取数据,这是一个Random I/O Access的过程,相比较Sequence I/O是比较慢的。所以,如果大量索引扫描Index Value返回的话,性能上可能受较大影响
解释上面问题:为什么走索引的成本有时会比走全表扫描要慢?
Index Scan可以看成走了两个步骤:
Only Index Scan
explain analyze
select employee_id from employees where employee_id = 1;
Index Only Scan using employees_pk on employees (cost=0.42..8.44 rows=1 width=6) (actual time=12.058..12.061 rows=1 loops=1)
Index Cond: (employee_id = '1'::numeric)
Heap Fetches: 1
Planning time: 0.133 ms
Execution time: 12.101 ms
Only Index Scan可以看成走了两个步骤:
Bitmap Scan
如果索引结果集相对较大,但有不至于走全表扫描的成本,在索引层次有没有什么办法处理来减小查询成本?
explain analyze
select * from employees where employee_id > 900000;
Bitmap Heap Scan on employees (cost=3073.28..195346.23 rows=103336 width=1028) (actual time=27.274..77.604 rows=100000 loops=1)
Recheck Cond: (employee_id > '900000'::numeric)
Heap Blocks: exact=23137
-> Bitmap Index Scan on employees_pk (cost=0.00..3047.45 rows=103336 width=0) (actual time=22.243..22.243 rows=100000 loops=1)
Index Cond: (employee_id > '900000'::numeric)
Planning time: 0.194 ms
Execution time: 83.702 ms
Bitmap Scan主要是为了解决Index San有大量的Random I/O Access但是成本又不至于要全表扫描的情况
Bitmap Scan可以看成走了四个步骤:
Bit map的关键作用在于,将分散在不同数据页的数据找出来,避免对同一个数据页的多次Random I/O Access
TID Scan
select ctid from employees where employee_id = 10000;
(143816,2)
EXPLAIN ANALYZE
select * from employees where ctid = '(143816,2)'
Tid Scan on employees (cost=0.00..4.01 rows=1 width=1028) (actual time=245.242..245.244 rows=1 loops=1)
TID Cond: (ctid = '(143816,2)'::tid)
Planning time: 0.169 ms
Execution time: 245.297 ms
可以在表中直接查出来某一行数据的TID,TID作为条件查询时,走元组扫描方法
单字段索引
主键默认会创建索引,并且是唯一索引
当待索引字段没有重复值时,可以创建唯一索引,反之则不能
对于唯一索引来说,其where等于索引字段的性能只和树的高度有关,而非唯一索引就要考虑到Index Leaf Node Search这一相对成本较大的操作
不等于条件不会走索引
联合索引
sql中书写字段的顺序和是否走索引或者走哪个索引没有关系,实质上优化器会尝试调整顺序,预估多种方案来决定走哪个索引或者是否走索引
创建索引时字段的顺序对执行计划有很大的影响
explain analyze
select * from employees where first_name = 'huayu' and date_of_birth between '1978-10-01' and '1998-12-01'
--创建索引(date_of_birth, first_name)
Index Scan using "i_dateOfBirth_firstName" on employees (cost=0.42..5669.83 rows=6 width=1028) (actual time=0.771..15.770 rows=29 loops=1)
Index Cond: ((date_of_birth >= '1978-10-01'::date) AND (date_of_birth <= '1998-12-01'::date) AND ((first_name)::text = 'huayu'::text))
Planning time: 0.290 ms
Execution time: 15.837 ms
--创建索引(first_name, date_of_birth)
Index Scan using "i_firstName_dateOfBirth" on employees (cost=0.42..18.30 rows=6 width=1028) (actual time=0.321..0.377 rows=29 loops=1)
Index Cond: (((first_name)::text = 'huayu'::text) AND (date_of_birth >= '1978-10-01'::date) AND (date_of_birth <= '1998-12-01'::date))
Planning time: 7.725 ms
Execution time: 0.428 ms
索引的过程
postgre官方文档对于联合索引说明,任意的一个索引子集查询原则上都能够走索引,但是left most索引最有效
比如a,b,c三个字段作为索引字段,使用使用where a = ? 或者whre a = ? , b = ? 或者where a = ?, b = ?, c = ? 都是很有效率的索引。实际上使用where a = ? and c = ?也能够走索引,只是效率没有left most组合高
explain analyze
select * from employees where UPPER(first_name) = 'FMZ'
--创建索引(first_name)
Gather (cost=1000.00..254611.00 rows=5000 width=1028) (actual time=271.720..915.371 rows=727937 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on employees (cost=0.00..253111.00 rows=2083 width=1028) (actual time=120.553..615.495 rows=242646 loops=3)
Filter: (upper((first_name)::text) = 'FMZ'::text)
Rows Removed by Filter: 90688
Planning time: 0.547 ms
Execution time: 952.985 ms
--创建索引(UPPER(first_name))
Bitmap Heap Scan on employees (cost=95.17..17867.35 rows=5000 width=1028) (actual time=152.854..465.286 rows=727937 loops=1)
Recheck Cond: (upper((first_name)::text) = 'FMZ'::text)
Rows Removed by Index Recheck: 19
Heap Blocks: exact=51268 lossy=52742
-> Bitmap Index Scan on "i_UPPER_firstName" (cost=0.00..93.92 rows=5000 width=0) (actual time=140.239..140.239 rows=727937 loops=1)
Index Cond: (upper((first_name)::text) = 'FMZ'::text)
Planning time: 0.642 ms
Execution time: 491.250 ms
自定义函数一定是一个确定的函数,也就是说对于同样的输入,一定会返回同样的输出。像年龄这种getNl(Date)
自定义函数,如果函数中使用了now()
当前系统时间,这样的函数索引就不正确。因为函数的返回值会随着时间的变化,不是一个确定性的函数
大于小于、Between都可以走索引
对于Like,如果是select * from table where a like 'TERM%OTHER'
那么解析器会将之转化为大于小于,走索引;如果是select * from table where a like '%TERM%'
全文搜索,就不走索引
explain analyze
select * from employees where first_name like 'A%D';
Index Scan using i_first_name on employees (cost=0.42..1603.35 rows=26 width=1028) (actual time=12.244..12.244 rows=0 loops=1)
Index Cond: (((first_name)::text >= 'A'::text) AND ((first_name)::text < 'B'::text))
Filter: ((first_name)::text ~~ 'A%D'::text)
Rows Removed by Filter: 5596
Planning time: 0.707 ms
Execution time: 12.288 ms
explain analyze
select * from employees where first_name like '%A%D';
Gather (cost=1000.00..253071.93 rows=26 width=1028) (actual time=633.371..633.371 rows=0 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on employees (cost=0.00..252069.33 rows=11 width=1028) (actual time=626.580..626.580 rows=0 loops=3)
Filter: ((first_name)::text ~~ '%A%D'::text)
Rows Removed by Filter: 333333
Planning time: 0.231 ms
Execution time: 641.282 ms
在Postgresql中所谓的部分索引(partial index)指的是只针对某些特定的行来建索引
一个用例是:假设一个表中有mobile字段,mobile字段可以为空值,如果直接对mobile建立唯一索引,那么肯定建议不了,有null值是重复的。这样就可以用partial index把nulll值给过滤掉建立索引,例如:create index table(mobile) where mobile is not null
另一个场景是:消息表中有processed字段、userid字段。如果想查询未处理消息是这样的,select * from t where processed = 'N' and userid = ?
。我们可以通过建一个联合索引的方法加快查询,但是这个索引中有大量的消息是processed=’Y’,如果只是相对未处理的消息加索引,可以这样:create index t(userid) where processed = 'N'
两表或者多表联表查询的时候改变了原来以表为单位各个列之间的关系,使得不同的表的列可以出现再同一个关系结果中。
JOIN的方式有三种:Nested Loop Join、Hash Join和Sorted Merge Join。
无论用什么join算法,有一点是肯定的,就是每次只处理两个表的join,然后基于结果集再join其他表。这样join的顺序就很重要,如果两个表join的结果数据集小,这样再和其他表join的时候,中间过程的笛卡尔积就会少。优化器也会计算各种join order的组合来判断出一个最优的顺序。
#select * from A JOIN B on A.ID < B.ID
For each tuple r in A
For each tuple s in B
If (r.ID < s.ID)
Emit output tuple (r,s)
hash join是为了解决nested loop join的缺陷:在inner query中有很多BTree Traverse。Hash JOIN选择一个候选表hash到内存中,和另外一个表join是能快速的获取数据。因此如果只给join表的关联字段加索引在Hash join算法中提高性能。但是,如果where语句中有独立的字段顾虑时,给这个字段加索引是能够提高性能的。
hash join的步骤:
This algorithm works in two phases:
kkkk
#select * from A JOIN B on A.ID = B.ID
#Build Phase
For each tuple r in inner relation B
Insert r into hash table HashTab with key r.ID
#Probe Phase
For each tuple s in outer relation A
For each tuple r in bucker HashTab[s.ID]
If (s.ID = r.ID)
Emit output tuple (r,s)
#select * from A JOIN B on A.ID = B.ID,用这个前提是A、B都是有序的,并且join条件是等于的情况
For each tuple r in A
For each tuple s in B
If (r.ID = s.ID)
Emit output tuple (r,s)
Break;
If (r.ID > s.ID)
Continue;
Else
Break;
参考: