Posgresql逻辑架构

database cluster
与oracle的多租户等不同,Postgresql允许你通过监听端口来区别数据库簇(database cluster)实例。一个数据库簇,或者叫数据库集群下面可以存放各种数据库(database),数据库簇之间是物理隔离的,即base目录是严格区分的,而一个数据库簇下的各个数据库的物理文件则是按照编号存放在$PGDATA/base目录下的,这个后面的物理架构会详细描述。数据库之间的逻辑是隔离的,也就是说,即使是在同一个数据库簇下,彼此是不能直接调用的,这点和MYSQL不太一样。
schema
PG的schema概念有些像MYSQL的database,你可以通过schema_name + table_name 来进行表调用。不同schema下可以有相同对象名称,这点要特别注意,因为在传统的数据库概念中,命名冲突是大忌,当然你也可以把这点当成一种PG独有的特性。在schema这个层面要提一下命名空间的概念。默认情况下,用户登录数据库如果不指定schema,那么默认使用的是$username,public 这个命名空间,如果你需要调用其他schema下的对象,则需要显式的在对象前加上schema_name,如果想要得知当前会话的命名空间可以使用以下命令。
show search_path ;
┌─────────────────┐
│ search_path │
├─────────────────┤
│ "$user", public │
└─────────────────┘
(1 row)上面提到的命名冲突问题,会在你没有显式指定schema_name后变得糟糕,因为pg会按照命名空间内的schema顺序扫描,只返回第一个找到的对象,这明显不太严谨。尤其是在Postgresql前置pgbouncer这样的连接池,由于pgbouncer遵循Postgresql原生通讯协议,和JDBC并不是完全兼容,JDBC中的current_schema参数会在pgbouncer进行解析时被抛弃,所以最稳妥的方法是在书写SQL的时候,务必在引用对象前面加上schema_name。
table
schema下是有各种数据库对象,表,索引,触发器等等。这些对象在命名的时候加上双引号和不加双引号是两个对象。比如
create table Tb_test1 (id int);
create table "Tb_test1" (id text);
postgres@[local]:postgres=\d+
List of relations
┌────────┬──────────┬───────┬──────────┬────────────┬─────────────┐
│ Schema │ Name │ Type │ Owner │ Size │ Description │
├────────┼──────────┼───────┼──────────┼────────────┼─────────────┤
│ util │ Tb_test1 │ table │ postgres │ 8192 bytes │ │
│ util │ tb_test1 │ table │ postgres │ 0 bytes │ │
└────────┴──────────┴───────┴──────────┴────────────┴─────────────┘
(2 rows)
postgres@[local]:postgres=\d Tb_test1
Table "util.tb_test1"
┌────────┬─────────┬───────────┬──────────┬─────────┐
│ Column │ Type │ Collation │ Nullable │ Default │
├────────┼─────────┼───────────┼──────────┼─────────┤
│ id │ integer │ │ │ │
└────────┴─────────┴───────────┴──────────┴─────────┘
postgres@[local]:postgres= \d "Tb_test1"
Table "util.Tb_test1"
┌────────┬──────┬───────────┬──────────┬─────────┐
│ Column │ Type │ Collation │ Nullable │ Default │
├────────┼──────┼───────────┼──────────┼─────────┤
│ id │ text │ │ │ │
└────────┴──────┴───────────┴──────────┴─────────┘从上面我们可以看出,不加双引号的对象名称,会被pg默认的进行小写处理,也就是说,不加引号的对象对大小写是不敏感的,而加了双引号的对象则会被严格记录,对大小写是敏感的。
表是数据库中最重要的逻辑概念,在表上可以附加序列(依赖于字段上的),索引,触发器,键等等对象,这些对象会在表对象删除的时候被一并移除。不同的对象可能由不同的创建者(owner)创建,那么在表上对象管理的时候,就会有权限和所有者问题出现,这会在权限部分集中讨论。
column
表的下层逻辑对象是字段(column),英文原意是罗马柱。字段又常被叫做列,具有列名、类型、默认值等属性,用于存储每条记录中的各种值。大字段数据存储在另一个名为 TOAST 的表中。每个表都有一个对应的 TOAST 表和 TOAST 索引。
我们在观察表详细信息的时候,往往会看到storage这一列,这列代表字段的存储策略
postgres@[local]:postgres= \d+ tb_test1
Table "util.tb_test1"
Column Type Collation Nullable Default Storage Stats target Description
────── ─────── ───────── ──────── ─────── ─────── ──────────── ───────────
id integer plain PLAIN类型不允许压缩,固定长度或很短的数据会使用这种策略,不开销CPU,但是必须可以放入8KB页面中。
MAIN类型允许压缩后仍必须内联。适合可压缩且通常不太大的数据,避免额外一次 TOAST 取数。
EXTERNAL类型不压缩但可离页。适合已压缩或不可压缩的数据(如 JPEG/ZIP/PDF),减少 CPU 压缩开销,同时把大值移到 TOAST,避免占满数据页。
EXTENDED类型先压缩,不够再离页;通用默认,在 CPU 与 I/O 间折中用,最稳妥。
tablespace
表空间在运维工作中往往是透明般的存在,因为通常会将整个实例放入一块逻辑盘中,出于安全考虑,会将wal日志和审计日志目录分别外移到另外的磁盘上。在IO 压力不高、介质同质化,运维简单性要求高且没有明显的冷热/OLAP 临时峰值的情况下,这种全家桶的处理办法能够有效降低运维工作量。可是在大型项目中,往往不会一块磁盘走天下,表空间的作用就出来了:
存储分层 / 热冷分离
热数据、关键索引放 NVMe/SSD;冷分区放 SATA/HDD。分区表可按月把老分区迁到便宜盘。
在硬盘成本几乎忽略不计的时代,这个最重要的理由似乎也站不住脚了,比起运维复杂度和工作量,磁盘成本往往是可以接受的。
临时文件隔离
创建专用临时表空间,大排序/哈希溢出到该盘,不干扰主数据盘与 WAL,OLAP/ETL 峰值更稳。非常推荐临时文件放入其他磁盘。
SET temp_tablespaces = 'nvme_temp'往往一些看着不起眼的聚合查询语句,会快速的占用临时表空间,如果默认的放在一个表空间会直接影响实例安全的,wal日志写入会受到影响,实例会进入恢复模式,甚至崩溃。所以将临时表空间外移,与实例所在磁盘进行隔离,是基于安全第一的原则进行选择的。
影响优化器的 IO 代价
这一点往往会被忽视。可对表空间级设置代价参数,让放在该表空间的对象更符合真实介质性能:
ALTER TABLESPACE ssd SET (random_page_cost=1.1, seq_page_cost=1.0, effective_io_concurrency=200); ALTER TABLESPACE hdd SET (random_page_cost=2.5, seq_page_cost=1.1, effective_io_concurrency=8);把索引放到
ssd后,优化器会更倾向索引访问。多租户/项目隔离
给不同业务一个表空间,便于统计用量、独立扩容/迁移;配合 OS/卷配额做“软隔离”。这一点似乎也不太能站得住脚,因为通常的技术路径往往是云化或者虚拟化来处理了,而不是在数据库层面做软隔离。
view
视图往往用在业务逻辑与表内容绑定的场景。当视图足够简单时,Postgres 支持更新视图,更新操作会传递到对应的表。 此外,还可以使用 INSTEAD OF 触发器或规则来更新视图,当然这些都是不推荐的,视图往往以预定义的 SQL 查询的形式出现。
关于视图,尽量避免视图之间的调用,这不但会使得业务逻辑耦合从而难以维护,而且会使得性能下降难以优化。
还有一类视图被成为物化视图,物化视图可以预先查询数据,调用时无需重复查询,但更新需要手动刷新。事实上,物化视图和表一样,是需要占用磁盘位置的,这也是和普通视图最大的区别。
创建和刷新物化视图的示例命令:
CREATE MATERIALIZED VIEW m_view AS SELECT * FROM tb;
REFRESH MATERIALIZED VIEW m_view;index
pg的内建索引大致分为6类B-tree,hash,GIST,SP-GIST,GIN,BRIN。索引是重要的运维对象,我们将单独列出一文进行讨论,这里仅作简要的介绍。
row
一行是表中的一条数据记录。行记录往往对应物理结构中的元组(tuple),每一行都有一个行版本,同时拥有系统列xmin和xmax,分别表示创建和删除该行的事务号。删除时,将 xmax 设置为删除事务号,但数据并未真正删除。UPDATE 操作被视为 DELETE 操作后接 INSERT 操作。
上面的xmax和xmin都是pg MVCC(多版本并发控制)的重要概念,后面将详细讨论。