关于postgresql:Postgresql-内幕探索读书笔记-第一章集簇表空间元组

55次阅读

共计 29390 个字符,预计需要花费 74 分钟才能阅读完成。

《Postgresql 底细摸索》读书笔记 – 第一章:集簇、表空间、元组

引言

集体倡议本章节本人搭建一个 Postgresql 数据库边实战边浏览更容易了解。

思维导图

一、数据库集群的逻辑构造

1.1 人造集群

PostgreSQL 人造集群,多个集群能够组成集簇,有点相似军队的连、团、旅这样的组织规定。对于咱们日常学习应用的单节点则是单个集簇单个集群,本人就是集群。

PostgreSQL 如何治理这种集群规定?答案是通过一个 无符号 4 个字节的标识 进行治理,一个对象就是集群里的一个数据库。

1.2 数据库对象和对象符号标识

数据库对象和对象符号标识能够通过 pg databasepg classs 查问,代表数据库和对象之间映射。

另外集群在物理磁盘中通过文件目录模式展现,一个目录对应一个数据库,也就是一个 base 下子目录中有一个目录就是有一个数据库。

base 目录一个文件对应一个数据库,集体试验的映射如下:
1:template1
14485:template0
14486:postgres

数据库和堆表的 OIDs 别离存储在 pg_database 和 pg_class 中,能够利用上面的 SQL 语句查问 OIDs。

数据库的 OIDs

select * from pg_database;
postgres=# select * from pg_database;

  oid  |  datname  | datdba | encoding | datcollate  |  datctype   | datistemplate | datallowconn | datconnlimit | datlastsysoid | datfrozenxid | datminmxid | dattablespace |             
  datacl                
-------+-----------+--------+----------+-------------+-------------+---------------+--------------+--------------+---------------+--------------+------------+---------------+-------------
------------------------
 14486 | postgres  |     10 |        6 | en_US.UTF-8 | en_US.UTF-8 | f             | t            |           -1 |         14485 |          727 |          1 |          1663 | 
     1 | template1 |     10 |        6 | en_US.UTF-8 | en_US.UTF-8 | t             | t            |           -1 |         14485 |          727 |          1 |          1663 | {=c/postgres
,postgres=CTc/postgres}
 14485 | template0 |     10 |        6 | en_US.UTF-8 | en_US.UTF-8 | t             | f            |           -1 |         14485 |          727 |          1 |          1663 | {=c/postgres
,postgres=CTc/postgres}
(3 rows)

堆表的 OIDs

select relname,oid from pg_class;
postgres=# select relname,oid from pg_class;
                    relname                    |  oid  
-----------------------------------------------+-------
 pg_statistic                                  |  2619
 pg_type                                       |  1247
 pg_toast_1255                                 |  2836
 pg_toast_1255_index                           |  2837
 pg_toast_1247                                 |  4171
 pg_toast_1247_index                           |  4172
 pg_toast_2604                                 |  2830
 pg_toast_2604_index                           |  2831
 pg_toast_2606                                 |  2832
 pg_toast_2606_index                           |  2833
 pg_toast_2612                                 |  4157
 pg_toast_2612_index                           |  4158
 pg_toast_2600                                 |  4159
 pg_toast_2600_index                           |  4160
 pg_toast_2619                                 |  2840
 pg_toast_2619_index                           |  2841
 pg_toast_3381                                 |  3439
 pg_toast_3381_index                           |  3440
 pg_toast_3429                                 |  3430
 pg_toast_3429_index                           |  3431

1.3 所有皆文件

察看 PostgreSQL 的目录构造就能发现和 Linux 有类似的中央,就是 所有细节都藏在物理文件。依据数据库充当一个目录的规定,Postgresql 依据数据目录、配置文件和端口号文件来创立实例。

其中蕴含版本号,日志,索引,事务状态等等所有相干信息,对于 Postgresql 来说都有相干文件进行治理和标识,所以能够说 Postgresql 的底层细节全副展现在数据目录文件当中。

二、数据库集群的物理构造

Postgresql 数据库集群都有叫做 根底目录 的目录,通常在装置 Postgresql 之后执行 initdb 命令能够初始化生成新的数据库集群。

初始化通常生成在 PGDATA 目录。

sudo /usr/pgsql-14/bin/postgresql-14-setup initdb

以 Postgresql-14 版本为例,初始化之后的根底目录生成在上面的地位。

[root@localhost 14]# pwd
/var/lib/pgsql/14

这里应用 ll 察看一下数据文件排列。

[root@localhost 14]# ll data/
total 68
drwx------ 5 postgres postgres    41 Jun 22 02:41 base
-rw------- 1 postgres postgres    30 Jun 22 02:41 current_logfiles
drwx------ 2 postgres postgres  4096 Jun 22 02:44 global
drwx------ 2 postgres postgres    32 Jun 22 02:41 log
drwx------ 2 postgres postgres     6 Jun 22 02:41 pg_commit_ts
drwx------ 2 postgres postgres     6 Jun 22 02:41 pg_dynshmem
-rw------- 1 postgres postgres  4577 Jun 22 02:41 pg_hba.conf
-rw------- 1 postgres postgres  1636 Jun 22 02:41 pg_ident.conf
drwx------ 4 postgres postgres    68 Jun 22 02:46 pg_logical
drwx------ 4 postgres postgres    36 Jun 22 02:41 pg_multixact
drwx------ 2 postgres postgres     6 Jun 22 02:41 pg_notify
drwx------ 2 postgres postgres     6 Jun 22 02:41 pg_replslot
drwx------ 2 postgres postgres     6 Jun 22 02:41 pg_serial
drwx------ 2 postgres postgres     6 Jun 22 02:41 pg_snapshots
drwx------ 2 postgres postgres     6 Jun 22 02:41 pg_stat
drwx------ 2 postgres postgres    63 Jun 22 03:30 pg_stat_tmp
drwx------ 2 postgres postgres    18 Jun 22 02:41 pg_subtrans
drwx------ 2 postgres postgres     6 Jun 22 02:41 pg_tblspc
drwx------ 2 postgres postgres     6 Jun 22 02:41 pg_twophase
-rw------- 1 postgres postgres     3 Jun 22 02:41 PG_VERSION
drwx------ 3 postgres postgres    60 Jun 22 02:41 pg_wal
drwx------ 2 postgres postgres    18 Jun 22 02:41 pg_xact
-rw------- 1 postgres postgres    88 Jun 22 02:41 postgresql.auto.conf
-rw------- 1 postgres postgres 28776 Jun 22 02:41 postgresql.conf
-rw------- 1 postgres postgres    58 Jun 22 02:41 postmaster.opts
-rw------- 1 postgres postgres   103 Jun 22 02:41 postmaster.pid

2.1 数据库集簇的布局

书中的版本为 Postgresql-9,这里用比拟新的 Postgresql-14 版本试验。

PostgreSQL: Documentation: 14: 70.1. Database File Layout

Postgresql-14 的官网文档中的各个文件含意如下。

表 70.1. PGDATA 的内容 `

Item Description 中文简介
PG_VERSION A file containing the major version number of PostgreSQL PostgreSQL 次要版本号
base Subdirectory containing per-database subdirectories 蕴含每个数据库子目录的子目录
current_logfiles File recording the log file(s) currently written to by the logging collector 记录以后由日志收集器写入的日志文件的文件
global Subdirectory containing cluster-wide tables, such as pg_database 蕴含集群范畴的表的子目录,如 pg_database
pg_commit_ts Subdirectory containing transaction commit timestamp data 蕴含事务提交工夫戳数据的子目录
pg_dynshmem Subdirectory containing files used by the dynamic shared memory subsystem 动静共享内存子系统应用的文件的子目录
pg_logical Subdirectory containing status data for logical decoding 逻辑解码的状态数据的子目录
pg_multixact Subdirectory containing multitransaction status data (used for shared row locks) 子目录蕴含多事务状态数据(用于共享行锁)
pg_notify Subdirectory containing LISTEN/NOTIFY status data LISTEN/NOTIFY 状态数据的子目录
pg_replslot Subdirectory containing replication slot data 复制槽数据的子目录
pg_serial Subdirectory containing information about committed serializable transactions 已提交的可序列化事务信息的子目录
pg_snapshots Subdirectory containing exported snapshots 导出的快照的子目录
pg_stat Subdirectory containing permanent files for the statistics subsystem 统计子系统的永恒文件的子目录
pg_stat_tmp Subdirectory containing temporary files for the statistics subsystem 统计子系统的临时文件的子目录
pg_subtrans Subdirectory containing subtransaction status data 子交易状态数据的子目录
pg_tblspc Subdirectory containing symbolic links to tablespaces 表空间符号链接的子目录
pg_twophase Subdirectory containing state files for prepared transactions 筹备好的事务的状态文件的子目录
pg_wal Subdirectory containing WAL (Write Ahead Log) files WAL(提前写入日志)文件的子目录
pg_xact Subdirectory containing transaction commit status data 交易提交状态数据的子目录
postgresql.auto.conf A file used for storing configuration parameters that are set by ALTER SYSTEM 存储由 ALTER SYSTEM 设置的配置参数的文件
postmaster.opts A file recording the command-line options the server was last started with 服务器最初启动时的命令行选项的文件
postmaster.pid A lock file recording the current postmaster process ID (PID), cluster data directory path, postmaster start timestamp, port number, Unix-domain socket directory path (could be empty), first valid listen_address (IP address or *, or empty if not listening on TCP), and shared memory segment ID (this file is not present after server shutdown) 记录以后 postmaster 过程 ID(PID)、集群数据目录门路、postmaster 启动工夫戳、端口号、Unix 域套接字目录门路(能够为空)、第一个无效的 listen_address(IP 地址或 *,如果不在 TCP 上监听则为空)和共享内存段 ID 的锁文件(服务器敞开后此文件不存在)

2.2 数据库布局

依据上表能够晓得数据表存储在 base 目录下。

2.3 表和索引相干的文件的布局

2.3.1 oid 和 relfilenode

大小小于 1GB 的表或索引是独自的文件,存储在它所属的数据库目录下。

数据库外部表和索引作为数据库对象是通过 OID 来治理的,而外面的具体内容则是通过变量 relfilenode 产生关联,大部分状况下 oidrelfilenode 通常会相等,然而也有例外,比方表和索引的 relfilenode 值会被一些命令(例如TRUNCATEREINDEXCLUSTER)所扭转。

比方 TRUNCATE 一个表会重新分配 relfilenode。上面应用案例验证重新分配 relfilenode,咱们先创立一个测试表:

create table db_test(id int primary key, name varchar(50), age int);
postgres=# select * from pg_class where relname ='db_test';
  oid  | relname | relnamespace | reltype | reloftype | relowner | relam | relfilenode | reltablespace | relpages | reltuples | relallvisible | reltoastrelid | relhasindex | relisshared | relpersistence
 | relkind | relnatts | relchecks | relhasrules | relhastriggers | relhassubclass | relrowsecurity | relforcerowsecurity | relispopulated | relreplident | relispartition | relrewrite | relfrozenxid | re
lminmxid | relacl | reloptions | relpartbound 
-------+---------+--------------+---------+-----------+----------+-------+-------------+---------------+----------+-----------+---------------+---------------+-------------+-------------+---------------
-+---------+----------+-----------+-------------+----------------+----------------+----------------+---------------------+----------------+--------------+----------------+------------+--------------+---
---------+--------+------------+--------------
 16384 | db_test |         2200 |   16386 |         0 |       10 |     2 |       16384 |             0 |        0 |        -1 |             0 |             0 | t           | f           | p             
 | r       |        3 |         0 | f           | f              | f              | f              | f                   | t              | d            | f              |          0 |          734 |   
       1 |        |            | 
(1 row)

能够看到这里的 relfilenode 为 16384。上面咱们执行 truncate 命令查看这个值是否扭转。

postgres=# truncate db_test;
TRUNCATE TABLE

postgres=# select * from pg_class where relname ='db_test';
  oid  | relname | relnamespace | reltype | reloftype | relowner | relam | relfilenode | reltablespace | relpages | reltuples | relallvisible | reltoastrelid | relhasindex | relisshared | relpersistence
 | relkind | relnatts | relchecks | relhasrules | relhastriggers | relhassubclass | relrowsecurity | relforcerowsecurity | relispopulated | relreplident | relispartition | relrewrite | relfrozenxid | re
lminmxid | relacl | reloptions | relpartbound 
-------+---------+--------------+---------+-----------+----------+-------+-------------+---------------+----------+-----------+---------------+---------------+-------------+-------------+---------------
-+---------+----------+-----------+-------------+----------------+----------------+----------------+---------------------+----------------+--------------+----------------+------------+--------------+---
---------+--------+------------+--------------
 16384 | db_test |         2200 |   16386 |         0 |       10 |     2 |       16389 |             0 |        0 |        -1 |             0 |             0 | t           | f           | p             
 | r       |        3 |         0 | f           | f              | f              | f              | f                   | t              | d            | f              |          0 |          735 |   
       1 |        |            | 
(1 row)

能够看到 oid 尽管还是 16384,然而 relfilenode 变成了 16389。

2.3.2 内建函数pg_relation_filepath

内建函数 pg_relation_filepath 可能依据 OID 或名称返回关系对应的文件门路。比方上面的案例:

 select oid,relname,relfilenode from pg_class limit 10;
     oid  |       relname       | relfilenode 
    ------+---------------------+-------------
     2619 | pg_statistic        |        2619
     1247 | pg_type             |           0
     2836 | pg_toast_1255       |           0
     2837 | pg_toast_1255_index |           0
     4171 | pg_toast_1247       |           0
     4172 | pg_toast_1247_index |           0
     2830 | pg_toast_2604       |        2830
     2831 | pg_toast_2604_index |        2831
     2832 | pg_toast_2606       |        2832
     2833 | pg_toast_2606_index |        2833

应用 pg_relation_filepath 函数返回关系对应的文件门路:

select pg_relation_filepath('pg_statistic') from pg_class limit 20;
     pg_relation_filepath 
    ----------------------
     base/14486/2619
     base/14486/2619
     base/14486/2619
     base/14486/2619
     base/14486/2619
     base/14486/2619
     base/14486/2619

2.3.3 relfilenode.1 规定

如果一个数据文件的大小超过 1GB,PostgreSQL 会创立并应用一个名为 relfilenode.1 的新文件 ,如新文件再次被写满,则创立下一个名为relfilenode.2 的新文件。留神这条规定实用于索引文件和数据文件,目标是避免单个文件适度扩张。

这个例子须要实操一下,为了在本地验证这一点,这里须要构建一个 千万数据的表

上面构建千万表的材料来自于网络。

  1. 首先创立序列:
CREATE SEQUENCE upms_log_id_seq START 10;

执行后果如下:

postgres=# CREATE SEQUENCE upms_log_id_seq START 10;
CREATE SEQUENCE
  1. 创立测试表
CREATE TABLE "public"."t_user" ("id" int8 NOT NULL DEFAULT nextval( 'upms_log_id_seq' :: regclass),
    "name" VARCHAR (255) COLLATE "pg_catalog"."default",
    "phone" VARCHAR (255) COLLATE "pg_catalog"."default",
    "birthday" TIMESTAMP (6),
    "sex" VARCHAR (64) COLLATE "pg_catalog"."default",
    CONSTRAINT "t_user_pkey" PRIMARY KEY ("id") 
);

同时批改表的归属用户。

ALTER TABLE "public"."t_user" OWNER TO "postgres";
  1. 设置字段随机值:
select substr('abcdefghijklmnopqrstuvwxyz', 1, ( random() * 26 ) :: INTEGER );
  1. hone 应用 11 位字符串:
SELECT lpad(( random() * 138 ) :: INT :: TEXT, 11, '0' );
  1. birthday 应用字符串日期:
SELECT date(generate_series(now(), now() + '1 week', '1 day'));
  1. sex 应用 0,1 示意男女。
SELECT lpad(( random() * 1 ) :: INT :: text, 1, '0' );
  1. 应用 explain 插入数据:
EXPLAIN ANALYZE INSERT INTO t_user 
SELECT  generate_series (1, 10000000),
substr('abcdefghijklmnopqrstuvwxyz', 1, ( random() * 26 ) :: INTEGER ),
lpad(( random() * 138 ) :: INT :: TEXT, 11, '0' ),
DATE (generate_series ( now(), now() + '1 week', '1 day') ),
lpad(( random() * 1 ) :: INT :: TEXT, 1, '0' );

运行后果如下:

postgres=# EXPLAIN ANALYZE INSERT INTO t_user 
postgres-# SELECT  generate_series (1, 10000000),
postgres-# substr('abcdefghijklmnopqrstuvwxyz', 1, ( random() * 26 ) :: INTEGER ),
postgres-# lpad(( random() * 138 ) :: INT :: TEXT, 11, '0' ),
postgres-# DATE (generate_series ( now(), now() + '1 week', '1 day') ),
postgres-# lpad(( random() * 1 ) :: INT :: TEXT, 1, '0' );
                                                               QUERY PLAN                                                                
-----------------------------------------------------------------------------------------------------------------------------------------
 Insert on t_user  (cost=0.00..925000.03 rows=0 width=0) (actual time=53327.150..53327.162 rows=0 loops=1)
   ->  Subquery Scan on "*SELECT*"  (cost=0.00..925000.03 rows=10000000 width=1194) (actual time=0.067..19638.272 rows=10000000 loops=1)
         ->  Result  (cost=0.00..700000.03 rows=10000000 width=104) (actual time=0.059..16112.101 rows=10000000 loops=1)
               ->  ProjectSet  (cost=0.00..50000.03 rows=10000000 width=12) (actual time=0.017..2277.500 rows=10000000 loops=1)
                     ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.003..0.010 rows=1 loops=1)
 Planning Time: 1.396 ms
 Execution Time: 53335.404 ms
(7 rows)
  1. 下面的指令执行之后,咱们先应用函数查找数据文件的门路:
postgres=# select pg_relation_filepath('t_user');
 pg_relation_filepath 
----------------------
 base/14486/16398
(1 row)
  1. 察看 relfilenode.1 文件,发现并没有找到,这里狐疑数据量不够大 又造了一千万数据 。执行之后查看数据目录发现了.1 这个文件。
[root@localhost 14486]# pwd
/var/lib/pgsql/14/data/base/14486


-rw------- 1 postgres postgres 1073741824 Jun 22 17:35 16398
-rw------- 1 postgres postgres  282771456 Jun 22 17:36 16398.1
-rw------- 1 postgres postgres     352256 Jun 22 17:35 16398_fsm
-rw------- 1 postgres postgres      24576 Jun 22 17:35 16398_vm

能够通过启动参数 --with-segsize更改表和索引的最大文件大小。

2.3.4 _fsm_vm 文件

仔细观察目录列表,会发现很多文件都会带有 _fsm and _vm 为后缀的相干文件,这些文件叫做 free space map闲暇空间映射 )和 visibility map 可见性映射)。数据文件或者索引文件存在上面的差异。

数据文件:

  • 闲暇空间映射 free space map:存储 free space capacity(表文件每个页面上的闲暇空间信息)。
  • 可见性映射 visibility map:存储 表文件中每一页的可见性信息。

索引文件:

  • 只有独自的 free space map 闲暇空间映射),没有 可见性映射 visibility map

下面的例子中就有相似的文件产生:

[root@localhost 14486]# pwd
/var/lib/pgsql/14/data/base/14486


-rw------- 1 postgres postgres 1073741824 Jun 22 17:35 16398
-rw------- 1 postgres postgres  282771456 Jun 22 17:36 16398.1
-rw------- 1 postgres postgres     352256 Jun 22 17:35 16398_fsm
-rw------- 1 postgres postgres      24576 Jun 22 17:35 16398_vm

主体数据文件,闲暇空间映射文件,可见性映射文件等这些文件在 Postgresql 的术语中被叫做“分支”,通常这些分支的排布规定如下:

  • 数据文件分支编号为 1。
  • 闲暇空间映射 / 索引数据文件 分支的第一个编号为 1。
  • 可见性映射表为数据文件第二个分支 2。

这些规定概念比较复杂,只须要晓得 1 号分支fsm 保留了 main 分支中闲暇空间的信息,2 号分支 vm 保留了 main 分支中可见性的信息 即可。

此外 3 号分支 init 是很少见的非凡分支,次要存储不被日志记录(unlogged)的表与索引。同时为了避免单个分支文件过大,PostgreSQL 会将过大的分支文件切分为若干段,段的大小为 1GB,也就是相似下面数据文件的 relfilenode 分隔形式。

三、表空间

Postgresql 的表空间能够看作是内部数据文件,和很多常见的 RDBMS 的设计理念不一样。表空间有点相似根底数据的一个映射,在根底数据中建设映射会依照版本和文件夹命名规定建设对应的表空间映射,用于存储根底数据以外的内容。

数据库集簇的表空间结构图如下:

3.1 创立表空间

如何创立表空间?答案是应用 CREATE TABLESPACE 语句,这个语句会在特定的目录上面创立表空间,并且会构建特定的子目录。构建规定如下:

PG_主版本号_目录版本号

构建表空间并且指定特定地位命令如下,须要留神指定地位之前须要确保对应地位存在,同时还须要留神权限问题:

postgres=# create tablespace tbs_test owner postgres location '/opt/postgres/tbs_test';
ERROR:  directory "/opt/postgres/tbs_test" does not exist

postgres=# create tablespace tbs_test owner postgres location '/opt/postgres/tbs_test';
ERROR:  could not set permissions on directory "/opt/postgres/tbs_test": Operation not permitted

创立对应表空间目录以及权限设置:

[root@localhost 14486]# mkdir -p /opt/postgres/tbs_test

[root@localhost 14486]# chown postgres:postgres /opt/postgres/
[root@localhost 14486]# ll /opt
total 0
drwxr-xr-x 3 postgres postgres 22 Jun 22 18:20 postgres

创立实现之后能够在对应的 data 目录上面看到一个新增的目录:

postgres=# create tablespace tbs_test owner postgres location '/opt/postgres/tbs_test';
CREATE TABLESPACE
[root@localhost data]# pwd 
/var/lib/pgsql/14/data
[root@localhost data]# ll
total 72
......

drwx------ 2 postgres postgres    19 Jun 22 18:22 pg_tblspc

.....

新增的目录 pg_tblspc 下有一个连贯文件 16408,指向到/usr/local/pgdata 下,这里用 tree 命令察看后果。

[root@localhost data]# tree pg_tblspc/
pg_tblspc/
└── 16408 -> /opt/postgres/tbs_test

咱们拜访/opt/postgres/tbs_test 查看具体文件内容。

[root@localhost data]# ll /opt/postgres/tbs_test
total 0
drwx------ 2 postgres postgres 6 Jun 22 18:22 PG_14_202107181

这里的文件的确对应下面提到的 PG_主版本号_目录版本号 规定,这里的 202107181 集体认为是公布工夫(最初一个 1 集体认为为版本号)。

3.2 新建表到表空间

特地留神,如果在该表空间内创立一个新表,但新表所属的数据库却创立在根底目录下,那么 PG 会首先在 版本特定的子目录 创立名称与现有数据库 OID 雷同的新目录,而后将新表文件搁置在刚创立的目录下。

比方上面的办法构建一个新表并且表空间指向tbs_test

postgres=# create table newtab(id int) tablespace tbs_test;
CREATE TABLE

通过上面的指令能够看到新表被创立在之前创立的表空间上面。

postgres=# select pg_relation_filepath('newtab');

pg_relation_filepath             
---------------------------------------------
 pg_tblspc/16408/PG_14_202107181/14486/16409
(1 row)

能够查找 pg_class 表有对应的 oid 进一步验证。

postgres=# select relname,oid from pg_class where relname='newtab';
 relname |  oid  
---------+-------
 newtab  | 16409
(1 row)

3.3 删除表空间

删除表空间前必须要删除该表空间下的所有数据库对象,否则会有上面的报错:

ERROR:  tablespace "tbs_test" is not empty

删除数据表对象之后,再删除对应的表空进啊

postgres=# drop table if exists newtab;
DROP TABLE

postgres=# drop tablespace if exists tbs_test;
DROP TABLESPACE

通过上面的命令查看发现数据物理文件曾经被删除了。

[root@localhost data]# ll /opt/postgres/tbs_test/
total 0

四、堆表文件的外部布局

4.1 堆表和索引组织表比照

Postgresql 的数据组织形式和 Mysql 齐全不同,首次接触可能比拟蒙圈。这里简略总结一下两者设计上的区别:

堆表

  • 数据存储在表中,索引存储在索引里,两者离开的。
  • 数据在堆中是无序的,索引让键值有序,但数据还是无序的。
  • 堆表中主键索引和一般索引一样的,都是寄存指向堆表中数据的指针。

索引组织表

  • 数据存储在聚簇索引中,数据依照主键的程序来组织数据,两者合二为一。
  • 主键索引,叶子节点寄存整行数据。
  • 其余索引称为辅助索引(二级索引),叶子节点寄存键值和主键值。

两者数据结构的次要区别为:堆表索引和理论数据离开,索引组织表则通常非叶子节点为索引,叶子节点为数据,所以数据和索引是间接在一块存储的。

4.2 堆表根底构造介绍

在堆表,索引,也包含闲暇空间映射和可见性映射内部结构蕴含上面几项。

  • 页(pages) 或者叫 块(block):默认大小 8192 字节(8KB)
  • 页依照 0 编号,这些数字能够叫做 区块号(block numbers),如果一个区块页面被写满,则会主动追加一个新的空页面来存储增长文件。

上图中蕴含三种类型的数据:

  • 堆元组(heap tuples):也就是数据自身,相似栈构造从底部开始重叠。数据库外部是用 元组标识符(tuple identifier, TID) 标识堆元组。

    • TID 有多个值组成:区块号 + 行指针偏移号。(用于索引)。
  • 行指针(line pointer):也叫做 我的项目指针(item pointer)。每个行指针占用 4 个字节,这些指针都是指向堆元组的。

    • 行指针的构造是简略的线性数组设计,充当堆元组的索引,留神索引是从 1 开始不是 0 开始,这些索引被叫做 偏移号(offset number),偏移号和堆元组意义对应。
  • 首部数据(header data):页面的起始地位是 PageHeaderData 首部数据,固定大小为 24 个字节,首部数据组成如下:

    • pd_lsn:8 字节的无符号整数,代表以后页面最初一次更新 XLOG 记录的 LSN,次要和 WAL 机制无关。
    • pd_checksum:校验和,在 9.3 版本之前存储工夫线标识。
    • pd_lowerpd_upper:别离代表行指针的开端和最新堆元组的起始地位。从结构图能够看出,它用来标识闲暇空间的的范畴。(空余空间称为 闲暇空间(free space) 空洞(hole)
    • pd_special:索引页中会用到该字段(指向非凡空间的起始地位)。而堆表页中则指向页尾。

非凡空间指的是索引应用的非凡区域,具体内容依据索引类型而定,如 B 树,GiST,GiN。

了解堆元组构造对于了解 PostgreSQL 并发管制与 WAL 机制是必须的。

4.3 源码解读

这部分设计能够浏览 postgres/src/include/storage/bufpage.h at master · postgres/postgres · GitHub 源码理解。

4.3.1 根底构造介绍

咱们依据堆表的结构图以及源码正文理解根底构造,首先从头部构造开始:

disk page organization:磁盘页面布局
space management information generic to any page:对任何页面都实用的通用空间治理信息
pd_lsnidentifies xlog record for last change to this page:最近变更对应 xlog 记录的标识。
pd_checksum:如果设置则为校验和。
pd_flags:标记位。
pd_lower:行指针的开端。
pd_upper:最新堆元组的起始地位。
pd_special:堆表页中则指向页尾。索引中代表非凡空间开始地位。
pd_pagesize_version:页面的大小,以及页面布局的版本号
pd_prune_xid:能够修剪的最老的元组中的 XID(MVCC 应用)

上面介绍要害参数的作用。

LSN 值

The LSN is used by the buffer manager to enforce the basic rule of WAL thou shalt write xlog befor data”. A dirty buffer cannot be dumped to disk until xlog has been flushed at least as far as the page’

  1. xlog 至多被刷到该页的 LSN 甚至操作才容许缓冲区脏页刷新到磁盘
  2. 缓冲区管理器应用 LSN 来执行 WAL 的根本规定
校验和 pd_checksum

pd_checksum stores the page checksum, if it has been set for this page; zero is a valid value for a checksum.

  1. 0 是非法的校验和值,pd_checksum 存储着页面的校验和。

If a checksum is not in use then we leave the field unset.

  1. 为了向前兼容,没有应用校验和这个字段不会有值。

This will typically mean the field is zero though non-zero values may also be present if databases have been pg_upgraded from releases prior to 9.3, when the same byte offset was used to store the current timelineid when the page was last updated.

  1. 这样的起因是因为 9.3 版本之前存在非 0 的“校验和”,因为这个字段在 9.3 之前是最初更新时的工夫线标识。

Note that there is no indication on a page as to whether the checksum is valid or not, a deliberate design choice which avoids the problem of relying on the page contents to decide whether to verify it. Hence there are no flag bits relating to checksums

  1. 留神 页面上没有显示校验和是否无效,所以也就没有与校验和无关的标记位,这里成心这样设计是防止依附校验和决定是否验证这一个问题。

    pd_prune_xid is a hint field that helps determine whether pruning will be useful. It is currently unused in index pages.

pd_prune_xid
  1. pd_prune_xid 是一个提醒字段,有助于确定修剪是否有用。(留神索引页临时没有应用此字段)

The page version number and page size are packed together into a single uint16 field. This is for historical reasons: before PostgreSQL 7.3, there was no concept of a page version number, and doing it this way lets us pretend that pre-7.3 databases have page version number zero. We constrain page sizes to be multiples of 256, leaving the low eight bits available for a version number.

  1. 在 PostgreSQL 7.3 之前,没有页面版本号的概念,为了兼容假如版本号为 0。
  2. 页面版本号和页面大小被打包到一个 uint16 字段中。
  3. 束缚页面的尺寸必须为 256 的倍数,留下低 8 位用于页面版本编号。

Minimum possible page size is perhaps 64B to fit page header, opaque space and a minimal tuple; of course, in reality you want it much bigger, so the constraint on pagesize mod 256 is not an important restriction. On the high end, we can only support pages up to 32KB because lp_off/lp_len are 15 bits.

  1. 最小的可行页面大小可能是 64 字节,能放下页的首部,闲暇空间,以及一个最小的元组。
  2. pagesize mod 256 的限度并不是一个重要的限度。
  3. 只能反对最大 32KB 的页面,因为 lp_off/lp_len 是 15 位

4.3.2 PageHeaderData 构造

本局部是接着缓冲页构造介绍的,PageHeaderData 的构造定义网址如下:
postgres/src/include/storage/bufpage.h at master · postgres/postgres · GitHub

typedef struct PageHeaderData
{
    //  XXX LSN 是任何块的成员,不仅是页面组织的成员。/* XXX LSN is member of *any* block, not only page-organized ones */
    
    // 本页面最近变更对应 xlog 记录的标识
    // 用于记录该页的最初一次变动 
    PageXLogRecPtr pd_lsn;        /* LSN: next byte after last byte of xlog
                                 * record for last change to this page */
    // 校验和
    uint16        pd_checksum;    /* checksum */
    
    // 标记位
    uint16        pd_flags;        /* flag bits, see below */
    
    // 闲暇空间起始地位
    LocationIndex pd_lower;        /* offset to start of free space */
    
    // 闲暇空间终止地位
    LocationIndex pd_upper;        /* offset to end of free space */

    // 非凡用处空间的开始地位
    LocationIndex pd_special;    /* offset to start of special space */

    // 页面版本编号(尺寸必须为 256 的倍数,留下低 8 位用于页面版本编号)uint16        pd_pagesize_version;
    
    // 最老的可修剪 XID, 如果没有设置为 0
    TransactionId pd_prune_xid; /* oldest prunable XID, or zero if none */

    // 行指针数组
    ItemIdData    pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* line pointer array */
} PageHeaderData;

typedef PageHeaderData *PageHeader;

4.3.3 ItemIdData 构造

PageHeaderData 单中有一个 ItemIdData 的构造体定义。它的构造如下:

// 缓冲区页中的我的项目指针(item pointer),也被称为行指针(line pointer)
typedef struct ItemIdData     ItemIdData

// 元组偏移量 (绝对页面起始处)
unsigned     lp_off:15

// 行指针的状态
unsigned     lp_flags:2

// 元组的字节长度
// lp_len == 0 示意该行指针没有关联存储。独立于其 lp_flags 的状态
unsigned     lp_len:15

lp_flags 这个字段一共有四种取值:

#define LP_UNUSED       0       // 闲暇行指针 (lp_len 必须始终为 0)
#define LP_NORMAL       1       // 行指针被应用,指向一个元组 (lp_len 必须始终 >0)
#define LP_REDIRECT     2       // HOT 技术标识 (lp_len 必须为 0)
#define LP_DEAD         3       // 行指针对应的元组为死元组

这些内容和得悉呢?因为 ItemIdData 的构造在下面的链接并没有找到任何无关 ItemIdData 的代码,阐明被定义到其余中央。

授人以鱼不如授人以渔,这里解释下这部分源码如何层层递进查找:PostgreSQL Source Code: src/include/storage/itemid.h File Reference

此链接中找到如下页面内容:

咱们点击 ItemIdData 跳转:

点击对应内容咱们会拜访到上面的 Git master 的页面定义。

PostgreSQL Source Code: ItemIdData Struct Reference

从下面的截图能够看到它们各自都有相干参考页面。

  • lp_flags:Referenced by mask_lp_flags().
  • lp_len:Referenced by PageIndexTupleOverwrite().
  • lp_off:Referenced by compactify_tuples(), PageIndexTupleDelete(), PageIndexTupleDeleteNoCompact(), and PageIndexTupleOverwrite().

这里筛选后面介绍的 lp_flags 字段介绍,于是就有了上面的相干源代码,这些代码写的十分工整,依据单词含意不难猜出逻辑:

 /*
  * mask_lp_flags
  *
  * In some index AMs, line pointer flags can be modified on the primary
  * without emitting any WAL record.
  */
 void
 mask_lp_flags(Page page)
 {
     OffsetNumber offnum,
                 maxoff;
     // 获取页面最大偏移量
     maxoff = PageGetMaxOffsetNumber(page);
     // 交易以后的偏移量是否非法
     for (offnum = FirstOffsetNumber;
          offnum <= maxoff;
          offnum = OffsetNumberNext(offnum))
     {
         // 依据偏移量取出对应的编号
         ItemId      itemId = PageGetItemId(page, offnum);
         
        // 查看以后的编号是否被应用,依据后果更新 lp_flags 标记。if (ItemIdIsUsed(itemId))
             itemId->lp_flags = LP_UNUSED;
     }
 }

这里的 LP_UNUSED 能够点击(不得不夸一下 Postgresql 的源码浏览网站做的有点好)

再次点击 “itemid.h” 咱们能够拜访到具体的源代码地位、

 /*
  * A line pointer on a buffer page.  See buffer page definitions and comments
  * for an explanation of how line pointers are used.
  * 缓冲区页面上的一个行指针。对于行指针的应用办法,请参见缓冲区页面的定义和正文。* 
  * In some cases a line pointer is "in use" but does not have any associated
  * storage on the page.  By convention, lp_len == 0 in every line pointer
  * that does not have storage, independently of its lp_flags state.
  * 在某些状况下,行指针是 "应用中"z 状态,但在页面上没有任何相干的存储。* 依据常规,在每一个没有存储空间的行指针中,lp_len == 0。都没有存储空间,这与它的 lp_flags 状态无关。*/
 typedef struct ItemIdData
 {unsigned    lp_off:15,      /* offset to tuple (from start of page)从页面的开始到元组的偏移量 */
                 lp_flags:2,     /* state of line pointer, see below 行指针状态 */
                 lp_len:15;      /* byte length of tuple 元组的字节长度  */
 } ItemIdData;
  
 typedef ItemIdData *ItemId;
  
 /*
  * lp_flags has these possible states.  An UNUSED line pointer is available
  * for immediate re-use, the other states are not.
  */
 #define LP_UNUSED       0       /* unused (should always have lp_len=0) */
 #define LP_NORMAL       1       /* used (should always have lp_len>0) */
 #define LP_REDIRECT     2       /* HOT redirect (should have lp_len=0) */
 #define LP_DEAD         3       /* dead, may or may not have storage */

五、元祖 CRUD 操作详解

5.1 写入形式

假如咱们有一个表,仅仅由一个页面组成,页面只蕴含一个堆元组,此时的 pd_lower 会指向第一个行指针,pd_upper 指向第一个堆元组。

第二个元组会放到第一个元祖前面,第二个行指针被插入到第一个行指针的前面,pd_lower 会改为指向第二个行指针,pd_upper 更改指向第二个堆元组,而后更新头部的 pd_lsnpg_checksumpg_flag 等相干参数。

从下面的步骤能够看到,写入形式比拟好了解,就是在行指针前面插入新的数据,以及在末端元组退出新数据,之后更新指针援用以及更新头部信息即可。

5.2 删除形式

删除形式在源代码中对应办法PageIndexTupleDelete,这里不对源代码做解说,而是次要提一下思路:

  1. 首先 删除行指针 ,而后把前面的地位向前填充补齐空位,如果删除pd_lower 指向地位则不须要挪动,只须要对应更新为上一个行指针即可。
  2. 删除理论的堆元组。对应的也须要进行填补空缺,如果删除 pd_upper 指向地位则不须要挪动,只须要更新为后一个即可。
  3. 数据的存储地位产生挪动,更新数据指针的 offset 属性。

负责删除指定地位的数据,删除数据后,会将须要将闲暇的数据指针和数据进行压缩合并。

/* 负责删除指定地位的数据,删除数据后,会将须要将闲暇的数据指针和数据进行压缩合并 */
void PageIndexTupleDelete(Page    page, OffsetNumber     offnum)    
{PageHeader  phdr = (PageHeader) page;
     char       *addr;
     ItemId      tup;
     Size        size;
     unsigned    offset;
     int         nbytes;
     int         offidx;
     int         nline;
  
     /*
      * As with PageRepairFragmentation, paranoia seems justified.
      */
     if (phdr->pd_lower < SizeOfPageHeaderData ||
         phdr->pd_lower > phdr->pd_upper ||
         phdr->pd_upper > phdr->pd_special ||
         phdr->pd_special > BLCKSZ ||
         phdr->pd_special != MAXALIGN(phdr->pd_special))
         ereport(ERROR,
                 (errcode(ERRCODE_DATA_CORRUPTED),
                  errmsg("corrupted page pointers: lower = %u, upper = %u, special = %u",
                         phdr->pd_lower, phdr->pd_upper, phdr->pd_special)));
  
     nline = PageGetMaxOffsetNumber(page);
     if ((int) offnum <= 0 || (int) offnum > nline)
         elog(ERROR, "invalid index offnum: %u", offnum);
  
     /* change offset number to offset index */
     offidx = offnum - 1;
  
     tup = PageGetItemId(page, offnum);
     Assert(ItemIdHasStorage(tup));
     size = ItemIdGetLength(tup);
     offset = ItemIdGetOffset(tup);
  
     if (offset < phdr->pd_upper || (offset + size) > phdr->pd_special ||
         offset != MAXALIGN(offset))
         ereport(ERROR,
                 (errcode(ERRCODE_DATA_CORRUPTED),
                  errmsg("corrupted line pointer: offset = %u, size = %u",
                         offset, (unsigned int) size)));
      
     /* Amount of space to actually be deleted */
     size = MAXALIGN(size);
      // 首先删除行指针,而后把前面的地位向前填充补齐空位,如果删除 **pd_lower** 指向地位则不须要挪动,只须要对应更新为上一个行指针即可。/*
      * First, we want to get rid of the pd_linp entry for the index tuple. We
      * copy all subsequent linp's back one slot in the array. We don't use
      * PageGetItemId, because we are manipulating the _array_, not individual
      * linp's.
      */
     nbytes = phdr->pd_lower -
         ((char *) &phdr->pd_linp[offidx + 1] - (char *) phdr);
  
     if (nbytes > 0)
         memmove((char *) &(phdr->pd_linp[offidx]),
                 (char *) &(phdr->pd_linp[offidx + 1]),
                 nbytes);
      // 删除理论的堆元组。对应的也须要进行填补空缺,如果删除 **pd_upper** 指向地位则不须要挪动,只须要更新为后一个即可
     /*
      * Now move everything between the old upper bound (beginning of tuple
      * space) and the beginning of the deleted tuple forward, so that space in
      * the middle of the page is left free.  If we've just deleted the tuple
      * at the beginning of tuple space, then there's no need to do the copy.
      */
  
     /* beginning of tuple space */
     addr = (char *) page + phdr->pd_upper;
  
     if (offset > phdr->pd_upper)
         memmove(addr + size, addr, offset - phdr->pd_upper);
  
     /* adjust free space boundary pointers */
     phdr->pd_upper += size;
     phdr->pd_lower -= sizeof(ItemIdData);


     // 3.  数据的存储地位产生挪动,更新数据指针的 offset 属性。/*
      * Finally, we need to adjust the linp entries that remain.
      *
      * Anything that used to be before the deleted tuple's data was moved
      * forward by the size of the deleted tuple.
      */
     if (!PageIsEmpty(page))
     {
         int         i;
  
         nline--;                /* there's one less than when we started */
         for (i = 1; i <= nline; i++)
         {ItemId      ii = PageGetItemId(page, i);
  
             Assert(ItemIdHasStorage(ii));
             if (ItemIdGetOffset(ii) <= offset)
                 ii->lp_off += size;
         }
     }
 }

5.3 批改数据

批改数据办法对应PageIndexTupleOverwrite,它对应的源代码如下:

bool PageIndexTupleOverwrite(Page page, OffsetNumber offnum, Item newtup, Size newsize);

 {PageHeader  phdr = (PageHeader) page;
     ItemId      tupid;
     int         oldsize;
     unsigned    offset;
     Size        alignednewsize;
     int         size_diff;
     int         itemcount;
  
     /*
      * As with PageRepairFragmentation, paranoia seems justified.
      */
     if (phdr->pd_lower < SizeOfPageHeaderData ||
         phdr->pd_lower > phdr->pd_upper ||
         phdr->pd_upper > phdr->pd_special ||
         phdr->pd_special > BLCKSZ ||
         phdr->pd_special != MAXALIGN(phdr->pd_special))
         ereport(ERROR,
                 (errcode(ERRCODE_DATA_CORRUPTED),
                  errmsg("corrupted page pointers: lower = %u, upper = %u, special = %u",
                         phdr->pd_lower, phdr->pd_upper, phdr->pd_special)));
  
     itemcount = PageGetMaxOffsetNumber(page);
     if ((int) offnum <= 0 || (int) offnum > itemcount)
         elog(ERROR, "invalid index offnum: %u", offnum);
  
     tupid = PageGetItemId(page, offnum);
     Assert(ItemIdHasStorage(tupid));
     oldsize = ItemIdGetLength(tupid);
     offset = ItemIdGetOffset(tupid);
  
     if (offset < phdr->pd_upper || (offset + oldsize) > phdr->pd_special ||
         offset != MAXALIGN(offset))
         ereport(ERROR,
                 (errcode(ERRCODE_DATA_CORRUPTED),
                  errmsg("corrupted line pointer: offset = %u, size = %u",
                         offset, (unsigned int) oldsize)));
  
     /*
      * Determine actual change in space requirement, check for page overflow.
      */
     oldsize = MAXALIGN(oldsize);
     alignednewsize = MAXALIGN(newsize);
     if (alignednewsize > oldsize + (phdr->pd_upper - phdr->pd_lower))
         return false;

    // 从新定位现有数据并更新行指针,除非新的元组与旧元组的大小雷同(对齐后), 要从新定位的是指标元组之前的数据
     /*
      * Relocate existing data and update line pointers, unless the new tuple
      * is the same size as the old (after alignment), in which case there's
      * nothing to do.  Notice that what we have to relocate is data before the
      * target tuple, not data after, so it's convenient to express size_diff
      * as the amount by which the tuple's size is decreasing, making it the
      * delta to add to pd_upper and affected line pointers.
      */
     size_diff = oldsize - (int) alignednewsize;
     if (size_diff != 0)
     {char       *addr = (char *) page + phdr->pd_upper;
         int         i;
  
         /* relocate all tuple data before the target tuple */
         memmove(addr + size_diff, addr, offset - phdr->pd_upper);
  
         /* adjust free space boundary pointer */
         phdr->pd_upper += size_diff;
  
         /* adjust affected line pointers too */
         for (i = FirstOffsetNumber; i <= itemcount; i++)
         {ItemId      ii = PageGetItemId(page, i);
  
             /* Allow items without storage; currently only BRIN needs that */
             if (ItemIdHasStorage(ii) && ItemIdGetOffset(ii) <= offset)
                 ii->lp_off += size_diff;
         }
     }
  
     /* Update the item's tuple length without changing its lp_flags field */
     tupid->lp_off = offset + size_diff;
     tupid->lp_len = newsize;
  
     /* Copy new tuple data onto page */
     memcpy(PageGetItem(page, tupid), newtup, newsize);
  
     return true;
 }

下面的逻辑大抵如下:

  • 如果原有数据的大小和新数据雷同,那么间接批改对应的数据指针和理论的数据。
  • 如果不统一,须要先将数据进行删除。
  • 将删除的空间进行压缩合并,并且更新所有数据指针的 offset 属性。最初才实现增加数据。

5.4 罕用读取形式

读取形式分两种:程序扫描 B 树索引扫描

  • 程序扫描:是通过行指针数组遍历,O(1) 的查找速度。
  • BTree 扫描:键存储被索引的 列值 ,值存储的是堆元组的 tid。查找的先依照 Key 搜寻,找到之后依据值的 TID 读取对应堆元祖。TID 这个属性记录堆元组偏移量和长度信息,能够间接通过扫描堆元组找到。

5.5 其余读取形式

除了下面两种经典读取形式之外,Postgresql 还反对上面的读取形式。

  • TID 扫描
  • 仅索引扫描
  • 位图扫描
  • GIN 索引扫描

5.5.1 TID 扫描

TID 扫描是通过应用所需元组的 TID 间接拜访元组的办法。咱们能够通过 explain 命令的 tid scan 确认是否为 tid 扫描。

sampledb=# SELECT ctid, data FROM sampletbl WHERE ctid = '(0,1)';
 ctid  |   data    
-------+-----------
 (0,1) | AAAAAAAAA
(1 row)

sampledb=# EXPLAIN SELECT ctid, data FROM sampletbl WHERE ctid = '(0,1)';
                        QUERY PLAN
----------------------------------------------------------
 Tid Scan on sampletbl  (cost=0.00..1.11 rows=1 width=38)
   TID Cond: (ctid = '(0,1)'::tid)

元组标识符(tuple identifier, TID)蕴含区块号和行指针偏移量

5.5.2 仅索引扫描

和索引组织表的构建思路一样,建设 index 时蕴含的字段汇合囊括了须要查问的字段,这样就只需在索引中取数据,就不用回表了。

仅索引扫描是简直所有的关系型数据库查问的必备形式。

下面的案例剖析,上面是剖析过程:

  1. 咱们假如有上面的表和索引。
    • id – integer
    • name text
    • data text
  2. 索引

    • “tbl_idx” btree (id, name)
  3. 查问语句

    • select id,key from tbl where id between 18 and 19
  4. 元组数据分析
  5. id=18, name = ‘Queen’ 的 Tuple_18 存储在 0 号数据页中。
  6. id=19, name=’BOSTON’ 的 Tuple_19 存储在 1 号数据页中。
  7. 可见性剖析
  8. 0 号页面中的元组永远可见
  9. 可见性映射(visibility map)

    • 可见性映射基本作用是帮忙 VACUUM 确定是否蕴含死元组,进步死元组的扫描效率
  10. 仅索引查问优化
  11. 某一页中存储所有的元组都是可见的,PostgreSQL 就会应用索引元组。
  12. 如果存在不可见元祖,则 PostgreSQL 读取 索引元组指向的数据元组并查看元组可见性

因为存在不可见的元组,所以本查问的仅索引查问优化须要二次查看可见性。

5.5.3 位图扫描

位图扫描最后是为了 Greenplum 的 Bizgres 零碎(业余操作系统)开发,之后被 Postgresql 列入规范实现。

参考:https://wiki.postgresql.org/wiki/Bitmap_Indexes#Index_Scan

bitmap scan 的作用就是通过建设位图的形式,将回表过程中对标拜访随机性 IO 的转换为顺行性行为,从而缩小查问过程中 IO 的耗费。

留神页面位图是为每个查问动态创建的,并在位图索引扫描完结时被抛弃。

位图扫描的过程如下:

  • 扫描满足条件的 TID。
  • TID 依照页面拜访程序构建位图。
  • 读取记录对应的页面只须要读取一次。

相干文章浏览:

位图扫描利用场景不多,具体能够看这篇文章介绍:

  • 第一篇:PostgreSQL 中的位图索引扫描(bitmap index scan)– MSSQL123 – 博客园 (cnblogs.com)
  • 第二篇:PostgreSQL 技术底细(七)索引扫描_数据库_HashData_InfoQ 写作社区
  • 第三篇:PostgreSQL 优化器之从一个对于扫描形式抉择引发的思考 – 掘金 (juejin.cn)

5.5.4 GIN 索引扫描

也叫做 Generalized Inverted Index,通用倒排索引。

GIN 索引特地实用于反对全文搜寻。外部应用了倒排索引的数据结构,存储构造为(key, posting list),意味着 key 是关键字,posting list 是一组呈现过 key 的地位。

GIN 最大的问题是不能频繁插入,并且插入效率很低,因为倒排索引的设计个性,减少一个索引须要更多索引项。

为了优化 GIN 索引插入性能,Postgresql 引入了 插入模式 进行优化,次要思路是将 GIN 索引插入分为两类模式。

  • 失常模式:基表元组产生的新的 GIN 索引立刻插入 GIN 索引。
  • fastupdate(疾速更新)模式:基表元组产生的新的 GIN 索引会以追加的形式被插入到 pending list 列表中。

fastupdate(疾速更新)模式这种优化思路和 Mysql 的插入缓冲相似,就把大量的 GIN 插入合并为一次插入并且一次刷新到磁盘。须要留神 GIN 索引的 pending list 代价要大,因为 posting list 是一组呈现过 key 的地位,一次大批量插入会导致扫描效率低

留神:通过 create index 的 WITH FASTUPDATE = OFF 参数来敞开 fastupdate 模式

为什么 GIN 不应用正排索引?
答案是相似链表模式进行构建,尽管构建索引的形式简略,然而 每次查找最坏须要 O(n)的工夫
倒排索引则记录该文档的 ID 和字符在该文档中呈现的地位状况,只须要扫描一次即可查找到所需的信息。

Postgresql 的 GIN 索引具备肯定的扩展性,代码上只须要实现三个用户定义方法即可。

  1. 比拟两个键(不是被索引项)并且返回一个整数。
int compare(Datum a, Datum b)
  1. 依据参数 inputValue 生成一个键值数组
Datum * extractValue(Datum itemValue, int32 * nkeys, bool ** nullFlags)
  1. 依据参数 query 生成一个用于查问的键值数组,并返回其指针。
** pmatch, Pointer ** extra_data, bool ** nullFlags, int32 * searchMode)

正文完
 0