MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

PostgreSQL逻辑存储结构解析

2021-07-113.9k 阅读

PostgreSQL逻辑存储结构概述

在PostgreSQL数据库中,逻辑存储结构是理解数据组织和管理的关键。它与物理存储结构相互配合,共同为数据库的高效运行提供支持。逻辑存储结构主要包括数据库、模式、表空间、表、视图、序列等组件。这些组件按照层次化的方式进行组织,使得用户可以方便地管理和操作数据。

数据库

数据库是PostgreSQL中最大的逻辑存储单元。一个PostgreSQL实例可以包含多个数据库。每个数据库都有自己独立的系统目录,用于存储该数据库的元数据信息,如数据类型定义、表结构定义等。不同数据库之间的数据是相互隔离的,这提供了数据安全和多租户支持的基础。

创建数据库的SQL语句如下:

CREATE DATABASE mydatabase;

上述语句创建了一个名为mydatabase的数据库。在创建数据库时,还可以指定一些参数,例如数据库的所有者、编码格式等。如下示例创建一个指定所有者和编码的数据库:

CREATE DATABASE mydatabase
    WITH 
    OWNER = myuser
    ENCODING = 'UTF8'
    TABLESPACE = pg_default
    CONNECTION LIMIT = -1;

这里将数据库mydatabase的所有者设置为myuser,编码设置为UTF8,表空间设置为默认表空间pg_default,并且连接数不限制(CONNECTION LIMIT = -1)。

模式

模式是数据库中的一个逻辑命名空间,它用于组织数据库对象,如表、视图、函数等。一个数据库可以包含多个模式,不同模式中的对象可以同名,只要它们所属的模式不同即可。模式有助于将相关的数据库对象分组,提高数据库的可管理性和安全性。例如,在一个大型项目中,可以为不同的模块创建不同的模式,如public模式用于公共对象,admin模式用于管理相关对象。

创建模式的SQL语句如下:

CREATE SCHEMA myschema;

这将在当前数据库中创建一个名为myschema的模式。也可以在创建模式时指定模式的所有者:

CREATE SCHEMA myschema AUTHORIZATION myuser;

这里将myschema模式的所有者设置为myuser

表空间

表空间是一种将数据库对象物理存储位置抽象化的机制。PostgreSQL允许将不同的数据库对象(如表、索引等)存储在不同的表空间中。这对于管理磁盘空间、提高I/O性能等方面非常有用。例如,可以将频繁访问的表存储在高性能的磁盘设备对应的表空间中,而将不常用的数据存储在普通磁盘对应的表空间中。

创建表空间的步骤如下: 首先,需要确保操作系统层面有合适的目录。例如,在Linux系统下,可以创建一个目录/data/tablespace1

mkdir -p /data/tablespace1
chown -R postgres:postgres /data/tablespace1

然后在PostgreSQL中创建表空间:

CREATE TABLESPACE mytablespace
    OWNER postgres
    LOCATION '/data/tablespace1';

上述语句创建了一个名为mytablespace的表空间,所有者为postgres,物理存储位置为/data/tablespace1

当创建表或索引时,可以指定使用的表空间。例如创建表时指定表空间:

CREATE TABLE mytable (
    id serial,
    name text
) TABLESPACE mytablespace;

这样mytable表就会存储在mytablespace表空间中。

表是PostgreSQL中存储数据的基本结构,它由行(记录)和列(字段)组成。每一行代表一个具体的数据实例,每一列代表数据的一个属性。表的结构定义了列的名称、数据类型、约束等信息。

表的创建

创建表的基本语法如下:

CREATE TABLE tablename (
    column1 datatype [constraint],
    column2 datatype [constraint],
  ...
);

例如创建一个简单的用户表:

CREATE TABLE users (
    user_id serial PRIMARY KEY,
    username varchar(50) NOT NULL,
    email varchar(100) UNIQUE
);

在这个例子中,users表有三个列。user_id列使用serial数据类型,它是一种自动递增的整数类型,并且设置为主键(PRIMARY KEY约束)。username列是varchar类型,长度为50,并且不允许为空(NOT NULL约束)。email列也是varchar类型,长度为100,并且设置为唯一(UNIQUE约束)。

表的数据类型

PostgreSQL支持丰富的数据类型,主要分为以下几类:

  1. 数值类型:包括整数类型(如smallintintegerbigint),用于存储不同范围的整数;小数类型(如numericrealdouble precision),用于存储小数。例如,smallint可以存储 -32768 到 32767 之间的整数,而numeric类型可以存储高精度的小数,适用于财务计算等场景。
  2. 字符类型char(n)固定长度字符串,不足长度会用空格填充;varchar(n)可变长度字符串,最大长度为ntext可变长度字符串,理论上没有长度限制。例如,在存储固定格式的短字符串(如邮编)时可以使用char类型,而存储用户输入的任意长度文本时适合使用text类型。
  3. 日期和时间类型date存储日期(年 - 月 - 日),time存储时间(时:分:秒),timestamp存储日期和时间(年 - 月 - 日 时:分:秒),还有interval类型用于存储时间间隔。比如,在记录用户注册时间时可以使用timestamp类型。
  4. 布尔类型boolean,只有truefalsenull三个值,常用于表示逻辑判断。例如,在表示用户是否激活状态时可以使用boolean类型。
  5. 数组类型:可以存储一组相同数据类型的值。例如,integer[]表示整数数组。在需要存储多个相关值(如用户的多个爱好)时可以使用数组类型。

表的约束

  1. 主键约束(PRIMARY KEY):唯一标识表中的每一行数据。一个表只能有一个主键,它可以由一列或多列组成。例如前面users表中的user_id列就是主键。主键列的值不能为空且必须唯一。
  2. 唯一约束(UNIQUE):确保列中的值唯一,但可以为空值。例如users表中的email列设置了唯一约束,保证每个用户的邮箱地址是唯一的。
  3. 非空约束(NOT NULL):规定列的值不能为空。如users表中的username列设置了非空约束,确保每个用户都有用户名。
  4. 外键约束(FOREIGN KEY):用于建立表与表之间的关联关系。例如有orders表和users表,orders表中的user_id列可以设置为外键,关联到users表中的user_id列,这样就确保了orders表中的user_id值必须存在于users表中,维护了数据的一致性。创建外键约束的示例如下:
CREATE TABLE orders (
    order_id serial PRIMARY KEY,
    user_id integer,
    order_date timestamp,
    FOREIGN KEY (user_id) REFERENCES users(user_id)
);

这里orders表中的user_id列作为外键,引用了users表中的user_id列。

视图

视图是一个虚拟表,它基于一个或多个实际表(基表)的查询结果。视图本身并不存储数据,而是在查询时动态生成结果。视图可以简化复杂的查询,提供数据的不同视角,并且可以用于数据安全控制。

视图的创建

创建视图的基本语法如下:

CREATE VIEW viewname AS
    SELECT column1, column2,...
    FROM tablename
    WHERE condition;

例如,基于前面的users表创建一个只包含活跃用户(假设通过某个状态字段判断)的视图:

CREATE VIEW active_users AS
    SELECT user_id, username, email
    FROM users
    WHERE is_active = true;

这样创建的active_users视图就只包含is_active字段为true的用户信息。

视图的更新

一般情况下,简单视图(基于单个表的简单查询,且不包含聚合函数、分组等操作)是可以更新的。例如上面的active_users视图,如果要更新其中一个用户的邮箱:

UPDATE active_users
SET email = 'newemail@example.com'
WHERE user_id = 1;

但是对于复杂视图(如包含聚合函数、多表连接等的视图),更新操作可能受到限制。例如,如果视图定义如下:

CREATE VIEW user_order_count AS
    SELECT users.user_id, users.username, COUNT(orders.order_id) AS order_count
    FROM users
    LEFT JOIN orders ON users.user_id = orders.user_id
    GROUP BY users.user_id, users.username;

这个视图包含了聚合函数COUNT,对这样的视图进行更新操作会比较复杂,并且在很多情况下是不允许的,因为更新视图可能会导致多个基表的数据不一致。

序列

序列是一种数据库对象,用于生成唯一的数值序列。它通常与表的自增长列一起使用,如前面users表中的user_id列使用serial类型,其本质就是基于序列实现的。

序列的创建

创建序列的基本语法如下:

CREATE SEQUENCE sequencename
    [START WITH start]
    [INCREMENT BY increment]
    [MINVALUE minvalue | NO MINVALUE]
    [MAXVALUE maxvalue | NO MAXVALUE]
    [CYCLE | NO CYCLE];

例如创建一个简单的序列:

CREATE SEQUENCE mysequence
    START WITH 1
    INCREMENT BY 1
    MINVALUE 1
    NO MAXVALUE
    NO CYCLE;

这个序列从1开始,每次递增1,最小值为1,没有最大值,并且不会循环(即达到最大值后不会重新从最小值开始)。

序列的使用

在表中使用序列,以创建一个新表为例:

CREATE TABLE new_table (
    id integer DEFAULT nextval('mysequence'),
    data text
);

这里id列的默认值使用了nextval('mysequence'),表示每次插入新行时,id列的值会从mysequence序列中获取下一个值。如果要手动获取序列的下一个值,可以使用如下语句:

SELECT nextval('mysequence');

如果要获取序列的当前值,可以使用:

SELECT currval('mysequence');

需要注意的是,在使用currval之前,必须先使用nextval获取过值,否则会报错。

索引

索引是一种提高查询性能的数据结构,它类似于书籍的目录,可以帮助数据库快速定位到所需的数据行。PostgreSQL支持多种类型的索引,如B - 树索引、哈希索引、GiST索引、GIN索引等。

B - 树索引

B - 树索引是最常用的索引类型,适用于范围查询和排序操作。例如,对于users表,如果经常根据username进行查询和排序,可以为username列创建B - 树索引:

CREATE INDEX idx_users_username ON users (username);

这样在执行查询语句SELECT * FROM users WHERE username = 'john';时,数据库可以利用这个索引快速定位到满足条件的行,而不需要全表扫描。

哈希索引

哈希索引适用于等值查询,它通过对索引列的值进行哈希计算来快速定位数据。创建哈希索引的语法如下:

CREATE INDEX idx_users_email_hash ON users USING hash (email);

对于查询SELECT * FROM users WHERE email = 'test@example.com';,哈希索引可以快速定位到匹配的行。但哈希索引不支持范围查询和排序,因为哈希值的顺序与原始值的顺序无关。

GiST索引

GiST(Generalized Search Tree)索引适用于处理空间数据、全文搜索等复杂数据类型和查询。例如,在处理地理空间数据时,如果有一个表places存储地点的经纬度信息,并且需要进行空间查询(如查找某个区域内的地点),可以创建GiST索引:

CREATE INDEX idx_places_geom ON places USING gist (geom);

这里geom列假设存储的是地理空间数据。GiST索引可以高效地处理空间关系查询,如包含、相交等。

GIN索引

GIN(Generalized Inverted Index)索引适用于处理多值数据类型,如数组、全文搜索等。例如,如果有一个表documents存储文档内容,并且需要进行全文搜索,可以先对文档内容进行分词处理,然后创建GIN索引:

CREATE INDEX idx_documents_text ON documents USING gin (to_tsvector('english', content));

这里使用to_tsvector函数将content列的文本转换为适合全文搜索的格式,然后创建GIN索引。这样在执行全文搜索查询时,如SELECT * FROM documents WHERE to_tsvector('english', content) @@ tsquery('keyword');,可以快速找到包含指定关键词的文档。

分区表

分区表是将一个逻辑表按照一定的规则(如按时间、按范围等)分割成多个物理分区的技术。这对于管理大型表、提高查询性能和维护效率非常有帮助。

范围分区

范围分区是根据某个列的值的范围进行分区。例如,有一个sales表存储销售记录,其中有一个sale_date列记录销售日期。可以按月份对sales表进行范围分区: 首先创建一个父表:

CREATE TABLE sales (
    sale_id serial,
    sale_date date,
    amount numeric
) PARTITION BY RANGE (sale_date);

然后创建分区表,以2023年1月为例:

CREATE TABLE sales_2023_01 PARTITION OF sales
    FOR VALUES FROM ('2023 - 01 - 01') TO ('2023 - 02 - 01');

这样,插入到sales表中的数据,如果sale_date在2023年1月1日到2023年2月1日之间,就会自动存储到sales_2023_01分区表中。

列表分区

列表分区是根据某个列的具体值列表进行分区。例如,有一个products表存储产品信息,其中有一个product_type列表示产品类型。可以按产品类型进行列表分区: 创建父表:

CREATE TABLE products (
    product_id serial,
    product_type varchar(50),
    product_name text
) PARTITION BY LIST (product_type);

创建分区表,假设产品类型有electronicsclothes

CREATE TABLE products_electronics PARTITION OF products
    FOR VALUES IN ('electronics');

CREATE TABLE products_clothes PARTITION OF products
    FOR VALUES IN ('clothes');

插入数据时,根据product_type的值,数据会被存储到相应的分区表中。

哈希分区

哈希分区是根据某个列的哈希值进行分区。例如,有一个users表,根据user_id进行哈希分区: 创建父表:

CREATE TABLE users (
    user_id serial,
    username varchar(50)
) PARTITION BY HASH (user_id);

创建分区表,假设分为4个分区:

CREATE TABLE users_1 PARTITION OF users
    FOR VALUES WITH (MODULUS 4, REMAINDER 0);

CREATE TABLE users_2 PARTITION OF users
    FOR VALUES WITH (MODULUS 4, REMAINDER 1);

CREATE TABLE users_3 PARTITION OF users
    FOR VALUES WITH (MODULUS 4, REMAINDER 2);

CREATE TABLE users_4 PARTITION OF users
    FOR VALUES WITH (MODULUS 4, REMAINDER 3);

这样,根据user_id的哈希值对4取模的余数,数据会被分配到相应的分区表中。分区表可以显著提高查询性能,特别是在处理大数据量时,通过减少单次查询的数据量,加快查询速度,同时也便于数据的维护和管理,如数据的归档、删除等操作可以在特定分区上进行。