前言
堆表(heap table)和索引组织表(Index Oragnization Table,简称IOT)是两种数据表的存储结构。pg中的表是堆表。mysql Innodb引擎中的表是索引组织表。oracle中既支持堆表,也支持索引组织表。
在具体介绍堆表和索引组织表之前,我们先看下pg中index scan和index only scan。
Index Scan: 也即普通索引扫描,对于给定的查询,我们先扫描一遍索引,从索引中找到符合要求的记录的位置(指针),再定位到表中具体的Page去取。等于是两次I/O,先走索引,再取表记录。
Index only scan: 建立index时,所包含的字段集合,囊括了我们需要查询的字段,这样就只需在索引中取数据,就不必访问表了。
从两者的定义我们看出,pg中索引和表数据是分开存储的,索引中存储了数据行的指针,当使用普通索引查找数据时,需要先扫描索引树,找到对应的行指针,再去表中找到相应的tuple。
而index only scan可以极大的提高性能。因为不需要再去表中查找数据了。 这里我们思考一个问题:
Index only scan依靠存储在索引中的冗余数据,消除了去访问堆表的操作。如果我们将这个概念进一步扩大,并将所有列放在索引中,我们还需要堆表吗?
其实这也就引出了索引组织表的概念,索引组织表的数据是按照主键顺序被存储到一个B+树索引中的,索引就是数据,数据就是索引,二者合二为一。当使用主键去查询一个索引组织表时,不需要再访问表,能从索引中获取到表的全部数据。这也是mysql中的聚簇索引的概念,数据行存储在索引的叶子节点中。在mysql中除了聚簇索引外,还有非聚簇索引(也叫二级索引)。非聚簇索引索引它的叶子节点存的是键值和主键值。 从上面的分析我们也可以看到索引组织表有些明显的好处,一是节约了磁盘空间,二是降低了IO,提高了查询的性能。尤其是当我们的数据几乎总是通过主键来进行搜索时,查询效率的提升将会很显著。
那索引组织表有什么不好的地方呢?
上面提到了索引组织表的好处,其实当索引组织表上有二级索引,并且频繁使用二级索引进行访问时,它的缺点也很明显了,那就是二级索引需要回表,它的效率要比堆表直接使用行指针访问数据的效率要低的。
还有一点堆表相对于索引组织表来说,因为不需要考虑排序,所以堆表的存储速度要更快一点。
Postgresql 数据目录
Postgresql的数据存储使用heap方式实现,当我们创建数据库后,pg在指定路径下(initdb -D 设置的数据路径)生成一个新的目录,用来存储创建的数据信息,pg的不同数据库之间是物理隔离的。
sql
select * from pg_database;
oid | datname | datdba | encoding | datcollate | datctype | datistemplate | datallowconn | datconnlimit | datlastsysoid | datfrozenxid | datminmxid | dattablespace | datacl
-------+-----------+--------+----------+------------+----------+---------------+--------------+--------------+---------------+--------------+------------+---------------+-------------------------------------
1 | template1 | 10 | 0 | C | C | t | t | -1 | 12625 | 478 | 1 | 1663 | {=c/postgres,postgres=CTc/postgres}
12625 | template0 | 10 | 0 | C | C | t | f | -1 | 12625 | 478 | 1 | 1663 | {=c/postgres,postgres=CTc/postgres}
32777 | postgres | 10 | 0 | C | C | f | t | -1 | 12625 | 478 | 1 | 1663 |
32804 | test | 10 | 0 | C | C | f | t | -1 | 12625 | 478 | 1 | 1663 |
查看上面的数据库对应的物理文件,数据库oid和文件夹名相同
sql
create table t1(a int, b int);
select pg_relation_filepath('t1');
pg_relation_filepath
----------------------
base/32804/32805
select relname, oid, relfilenode from pg_class where relname ='t1';
relname | oid | relfilenode
---------+-------+-------------
t1 | 32805 | 32805
可以发下表的物理文件大小为0,因为表中没有插入数据
ls -l 32804/32805
-rw------- 1 postgres postgres 0 Jul 22 06:20 32804/32805
sql
insert into t1 values(1,2);
select * from t1;
a | b
---+---
1 | 2
ll 32804/32805
-rw------- 1 postgres postgres 16384 Jul 22 06:32 32804/32805
如果我们查看整个 数据库 32804
的目录,则能够看到一些表文件为前缀的 _fsm
以及 _vm
文件,它们分别是管理整个表空闲空间映射以及可见性映射的持久化文件,分别用于为表数据从表文件中分配存储空间以及事务读写场景进行数据可见性检查的。
heap表
pg的数据组织方式就是采用的heap(堆)表,数据在文件中是无序的。
heap表结构
堆表文件组织方式,如下图:
- 统一性,放在同一个 main fork 中的数据一定是同一表的数据,但可能有多个 segment,而且一个 segment只服务一个表,不会出现不同表的数据混在同一个文件中;
- 不跨页,不会存在一个元组的前部分数据在一个页面中,另一半的数据在另一个页面中;
page中有相应的数据组织方式,如下图:
一般来说,数据表数据物理存储在非易失性存储设备上面,PG也不例外。如下图所示,数据表中的数据存储在N个数据文件中,每个数据文件有N个Page(大小默认为8K,可在编译安装时指定)组成。Page为PG的最小存取单元。
数据页(Page)
数据页Page由页头、数据指针数组(ItemIdData)、可使用的空闲空间(Free Space)、实际数据(Items)和特殊空间(Special Space)组成。
- 页头存储LSN号、校验位等元数据信息,占用24Bytes;
- 数据指针数组存储指向实际数据的指针,数组中的元素ItemId可理解为相应数据行在Page中的实际开始偏移,数据行指针ItemID由三部分组成,前15位为Page内偏移,中间2位为标志,后面15位为长度;
- 空闲空间为未使用可分配的空间,ItemID从空闲空间的头部开始分配,Item(数据行)从空闲空间的尾部开始;
- 实际数据为数据的行数据(每种数据类型的存储格式后续再解析;
- 特殊空间用于存储索引访问使用的数据,不同的访问方法数据不同;
指针指向实际数据的存储位置(Tuple),根据指针能找到元组在什么地方,根据偏移量的位置去取指定长度数据,然后将数据转换成对应的元组数据结构,这样就取出了实际的数据; 指针数据是从前向后放,对应的元组是从后向前放,这样做的好处是因为元组的指针是固定的大小,从前往后顺着 pager header 往下读,可以读取所有的元组指针的,然后跟着元组指针的信息从后往前取 ,方便在页面中去找元组;
相关数据结构
PageHeaderData
HeapTupleData
HeapTupleHeaderData
详解 PageHeaderData
sql
select * from t1;
a | b
---+--------
1 | ni hao
-- 查看t1表对应的物理数据文件位置
select pg_relation_filepath('t1');
pg_relation_filepath
---------------------
base/32825/32958
-
hexdump查看数据文件
hexdump -C /opt/pgsql/data/base/32825/32958
-
pg_lsn (8bytes)
hexdump -C /opt/pgsql/data/base/32825/32958 -s 0 -n 8
数据文件的8个Bytes存储的是LSN,其中最开始的4个Bytes是TimelineID,在这里是\x0000 0000(即数字0),后面的4个Bytes是\x29453a18,组合起来LSN为0/29453a18
使用pageinspect插件查看对应的页面信息进行简单验证:
select * from page_header(get_raw_page('t1',0));
- pg_linp
-
- lp_off:取低15位
hexdump -C /opt/pgsql/data/base/32825/32958 -s 24 -n 2
echo ((0xbfd8 & ~((1 << 15))))
- lp_off:取低15位
-
- lp_len:取高15位
hexdump -C /opt/pgsql/data/base/32825/32958 -s 26 -n 2
echo $((0x0046 >> 1))
- lp_len:取高15位
使用pageinspect插件查看对应的页面信息进行简单验证:
select * from heap_page_items(get_raw_page('t1',0));