PostgreSQL 详解:高级关系数据库的特性与实践

2024-09-13 后端

目录

  1. PostgreSQL 简介
  2. 数据类型特色
  3. 高级查询功能
  4. 全文搜索
  5. 索引类型
  6. 事务与并发
  7. 扩展生态
  8. 高可用

1. PostgreSQL 简介

PostgreSQL 是一个功能强大的开源对象-关系数据库系统,拥有超过 30 年的活跃开发历史。它以其可靠性、功能丰富性和对 SQL 标准的严格遵守而闻名。

1.1 与 MySQL 的对比

维度 PostgreSQL MySQL
SQL 标准兼容性 高度兼容,几乎完全遵循 SQL:2016 部分兼容,有较多方言特性
ACID 实现 原生 MVCC,完全 ACID InnoDB 引擎支持 ACID,但默认 autocommit
索引类型 B-Tree、Hash、GiST、GIN、SP-GiST、BRIN B-Tree、Hash(Memory 引擎)、R-Tree
JSON 支持 JSONB 二级索引、GIN 索引,性能优异 JSON 函数有限,无原生二进制存储
并发控制 SSI(可序列化快照隔离)、行级锁 MVCC + 间隙锁
扩展性 扩展生态丰富(PostGIS、TimescaleDB 等) 插件较少,主要依赖存储引擎切换
复制 流复制、逻辑复制、级联复制 异步/半同步复制、组复制
全文搜索 内建 tsvector/tsquery + GIN 索引 需全文索引,功能有限
数组类型 原生支持 不支持
窗口函数 完善支持 8.0+ 支持,功能较 PostgreSQL 弱

PostgreSQL 的优势:

  • 更丰富的索引类型和查询优化能力
  • 原生支持 JSONB、数组、范围类型等复杂数据类型
  • 强大的扩展生态(PostGIS 是业界标准的地理空间扩展)
  • 更好的并发控制(SSI、不阻塞的 DDL)
  • 对 SQL 标准的遵循度更高

PostgreSQL 的劣势:

  • 主从复制的配置和管理比 MySQL 复杂
  • 部分运维工具不如 MySQL 生态成熟
  • 在某些简单只读场景下,MySQL InnoDB 的吞吐量更高
  • 社区中文资料相对 MySQL 偏少

1.2 安装(Docker)

PostgreSQL 的安装方式非常灵活,包括源码编译、系统包管理器(apt/yum)、二进制分发包以及 Docker 容器化部署。本节重点介绍使用 Docker 部署的方式,因为它可以跨平台一致地运行,并且非常适合开发和测试环境。

Docker 部署

使用 Docker 快速启动 PostgreSQL 实例:

# 拉取最新 PostgreSQL 镜像
docker pull postgres:16

# 启动 PostgreSQL 容器
docker run -d \
  --name postgres-demo \
  -e POSTGRES_USER=admin \
  -e POSTGRES_PASSWORD=secret123 \
  -e POSTGRES_DB=demodb \
  -p 5432:5432 \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

# 验证连接
docker exec -it postgres-demo psql -U admin -d demodb

在 docker-compose.yml 中使用:

version: '3.8'
services:
  postgres:
    image: postgres:16
    container_name: postgres-demo
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret123
      POSTGRES_DB: demodb
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  pgdata:

1.3 psql 基本使用

psql 是 PostgreSQL 自带的交互式命令行工具,它功能强大,不仅支持 SQL 语句的执行,还提供了丰富的元命令(metacommand)用于数据库管理和维护。熟练掌握 psql 是高效使用 PostgreSQL 的基础。

连接数据库

# 连接数据库
psql -h localhost -p 5432 -U admin -d demodb

# 或者使用连接字符串
psql "postgresql://admin:secret123@localhost:5432/demodb"

常用元命令:

1.4 架构:进程模型、WAL、MVCC

进程模型

PostgreSQL 采用多进程架构(而非 MySQL 的多线程架构),每个客户端连接对应一个独立的操作系统进程。这种设计选择有其深刻的历史和技术原因。

为什么使用多进程而非多线程? PostgreSQL 起源于加州大学伯克利分校的 POSTGRES 项目,诞生于操作系统对多线程支持尚不成熟的年代。多进程架构的优势在于进程隔离——一个客户端连接崩溃不会影响其他连接,内存越界错误不会波及共享内存区域,单个连接的内存泄漏也可以随着进程退出而彻底回收。但这也意味着每个连接占用较多内存资源(每个进程约 5-10MB),因此在处理大量并发连接时,需要配合连接池工具(如 pgpool-II 或 PgBouncer)。

完整的进程结构:

Postmaster(守护进程)
├── Backend Process(每个客户端连接一个)
├── WAL Writer(写入 WAL 日志)
├── Checkpointer(执行检查点)
├── Autovacuum Launcher(自动清理)
├── Archiver(WAL 归档)
├── Stats Collector(统计信息收集)
├── Logical Replication Launcher(逻辑复制)
└── WAL Sender(流复制发送进程)

这种架构的优势在于进程隔离——一个客户端连接崩溃不会影响其他连接,但也意味着每个连接占用较多内存资源。

WAL(Write-Ahead Logging)

WAL 是实现事务持久性和崩溃恢复的核心机制:

-- 查看 WAL 相关配置
SHOW wal_level;              -- replica, logical, minimal
SHOW wal_buffers;            -- WAL 缓冲区大小
SHOW wal_log_hints;          -- 是否启用逻辑复制 hint
SHOW checkpoint_timeout;     -- 检查点间隔
SHOW max_wal_size;           -- WAL 最大大小
SHOW min_wal_size;           -- WAL 最小大小

-- 查看当前 WAL 状态
SELECT * FROM pg_stat_wal;

WAL 工作流程:

  1. 事务开始:事务 ID 分配
  2. 数据修改:将修改写入共享缓冲区
  3. WAL 记录:将 REDO 记录写入 WAL 缓冲区
  4. WAL 刷盘:WAL Writer 进程将 WAL 缓冲区写入磁盘
  5. 事务提交:COMMIT 只有在 WAL 记录落盘后才返回成功
  6. 检查点:Checkpointer 将脏页面写入数据文件,更新控制文件

MVCC(多版本并发控制)

PostgreSQL 的 MVCC 实现是数据库核心中最精妙的设计之一。与 Oracle 使用 undo 段、MySQL InnoDB 使用回滚段来存储旧版本的实现方式不同,PostgreSQL 采用了一种更为纯粹的方法——直接在数据文件中保留多个行版本。

MVCC 的核心思想: 每个事务在开始时会获取一个唯一的事务 ID(XID),并"看到"在该事务开始前已提交的所有数据版本,而看不到其他未提交事务或在该事务之后开始的事务所做的修改。读者永远不会阻塞写者,写者也不会阻塞读者——这是 MVCC 最根本的承诺。

关键系统列:

  • xmin:创建该行版本的事务 ID
  • xmax:删除/更新该行版本的事务 ID(0 表示当前可见)
  • ctid:物理行位置(页面号 + 槽位号)

MVCC 演示

为了直观理解 MVCC 的工作原理,我们通过以下实验来观察行版本的变化:

-- 创建实验表
CREATE TABLE mvcc_demo (
    id SERIAL PRIMARY KEY,
    value TEXT,
    created_at TIMESTAMPTZ DEFAULT now()
);

INSERT INTO mvcc_demo (value) VALUES ('A'), ('B'), ('C');

-- 查看隐藏的系统列
SELECT ctid, xmin, xmax, * FROM mvcc_demo;

MVCC 可视化实验:

-- 会话 1:开启事务并更新数据
BEGIN;
UPDATE mvcc_demo SET value = 'A_updated' WHERE id = 1;
-- 此时不提交,查看元组
SELECT ctid, xmin, xmax, * FROM mvcc_demo;
-- 可以看到 id=1 有两行:旧行 xmax=当前事务ID,新行 xmin=当前事务ID

COMMIT;

-- 清理(vacuum 会清除旧版本)
VACUUM VERBOSE mvcc_demo;

2. 数据类型特色

PostgreSQL 拥有关系数据库中最丰富的数据类型系统之一。

2.1 数组(ARRAY)

PostgreSQL 原生支持数组类型,无需额外表:

数组索引: 使用 GIN 索引加速数组包含和重叠查询:

CREATE INDEX idx_articles_tags ON articles_with_tags USING GIN (tags);

2.2 JSON / JSONB

PostgreSQL 对 JSON 数据的支持可以说是所有关系数据库中最完善的。它提供了两种 JSON 类型:JSON(文本存储,保留完整格式和重复键)和 JSONB(二进制存储,支持索引,更高效)。在实际开发中,JSONB 几乎总是更优的选择,因为它不仅存储效率更高,还支持在 JSON 数据上直接创建索引和进行高效的包含性查询。

JSONB 的核心优势

JSONB 以二进制格式存储,解析速度远快于 JSON 类型。它在插入时会对 JSON 内容进行标准化处理——删除键之间的空白、压缩重复键(只保留最后一个值)、对键进行排序,这使得相同语义的 JSON 数据总是以一致的格式存储,从而可以实现精确的包含检测。

JSONB 与 JSON 的选择:

  • JSONB:一般场景首选。支持索引、删除重复键、高效查询、无空白保留
  • JSON:仅当需要保留键的插入顺序、重复键、原始格式时才使用

2.3 范围类型

PostgreSQL 提供了丰富的范围类型,非常适合处理时间段、价格区间等数据:

2.4 UUID、网络类型、几何类型

UUID

-- 开启 uuid-ossp 扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

CREATE TABLE users_uuid (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    -- 或使用 uuid_generate_v4()
    -- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name TEXT NOT NULL,
    created_at TIMESTAMPTZ DEFAULT now()
);

-- 时间戳排序的 UUID v7(需要 pg_uuidv7 扩展)
-- CREATE EXTENSION IF NOT EXISTS pg_uuidv7;

INSERT INTO users_uuid (name) VALUES ('Alice'), ('Bob'), ('Charlie');
SELECT * FROM users_uuid;

网络类型

CREATE TABLE network_assets (
    id SERIAL PRIMARY KEY,
    device_name TEXT,
    ip_address INET,           -- IPv4/IPv6 地址
    subnet CIDR,               -- CIDR 网段
    mac_address MACADDR        -- MAC 地址
);

INSERT INTO network_assets VALUES
    ('web-server', '192.168.1.100/24', '192.168.1.0/24', '08:00:2b:01:02:03'),
    ('db-server', '10.0.0.50/16', '10.0.0.0/16', '08:00:2b:04:05:06'),
    ('monitor', '172.16.0.1/12', '172.16.0.0/12', '08:00:2b:07:08:09');

-- 网络包含检查
SELECT * FROM network_assets WHERE ip_address << '192.168.1.0/24'::cidr;

-- 获取网络地址
SELECT device_name, host(ip_address) AS host, netmask(ip_address) AS netmask,
       network(ip_address) AS network FROM network_assets;

-- 排序(按 IP 数值)
SELECT * FROM network_assets ORDER BY ip_address;

几何类型

CREATE TABLE locations (
    id SERIAL PRIMARY KEY,
    name TEXT,
    coordinates POINT,         -- 点 (x,y)
    area POLYGON,              -- 多边形
    route PATH                 -- 路径
);

INSERT INTO locations VALUES
    ('公司总部', POINT(116.4074, 39.9042),
     POLYGON '((116.40,39.90),(116.41,39.90),(116.41,39.91),(116.40,39.91))',
     PATH '[(116.40,39.90),(116.41,39.91),(116.42,39.92)]');

-- 计算两点距离
SELECT name, coordinates <-> POINT(116.5, 39.9) AS distance FROM locations;

-- 圆形范围查询
SELECT * FROM locations
WHERE coordinates <@ CIRCLE '((116.4, 39.9), 0.1)';

-- 检查点是否在多边形内
SELECT name, coordinates @> POINT(116.41, 39.905) AS inside_area FROM locations;

2.5 枚举与自定义类型

枚举类型

-- 创建枚举类型
CREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');

-- 使用枚举
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer TEXT NOT NULL,
    status order_status DEFAULT 'pending',
    total NUMERIC(10,2),
    created_at TIMESTAMPTZ DEFAULT now()
);

INSERT INTO orders (customer, status, total) VALUES
    ('张三', 'pending', 299.00),
    ('李四', 'shipped', 1599.00),
    ('王五', 'delivered', 89.90);

-- 枚举排序(按定义顺序)
SELECT * FROM orders ORDER BY status;

-- 枚举值列表
SELECT enum_range(NULL::order_status);

-- 添加新枚举值
ALTER TYPE order_status ADD VALUE 'refunded' AFTER 'cancelled';

-- 注意:枚举一旦创建,不能直接删除值(可用值但不用)

自定义复合类型


3. 高级查询功能

3.1 窗口函数

窗口函数在不合并行的情况下对结果集进行聚合计算,是 PostgreSQL 最强大的功能之一。

3.2 CTE 与递归 CTE

公用表表达式(Common Table Expression,CTE)是 SQL 中一种强大的查询构造工具,它允许你在一个查询中定义临时结果集,然后在后续的查询中引用它。CTE 不仅能提高复杂查询的可读性,还支持递归查询——这是处理树形结构和层次数据的重要手段。

基本 CTE

CTE 使用 WITH 关键字定义,可以在单个查询中定义多个 CTE,彼此用逗号分隔:

-- 基本 CTE
WITH high_value_sales AS (
    SELECT product, SUM(sales_amount) AS total
    FROM sales_data
    GROUP BY product
    HAVING SUM(sales_amount) > 10000
)
SELECT * FROM high_value_sales ORDER BY total DESC;
-- 多 CTE
WITH
monthly_totals AS (
    SELECT
        DATE_TRUNC('month', sale_date) AS month,
        SUM(sales_amount) AS total
    FROM sales_data
    GROUP BY DATE_TRUNC('month', sale_date)
),
top_products AS (
    SELECT product, SUM(sales_amount) AS total
    FROM sales_data
    GROUP BY product
    ORDER BY total DESC
    LIMIT 3
)
SELECT 'monthly' AS type, month::TEXT AS label, total FROM monthly_totals
UNION ALL
SELECT 'product' AS type, product AS label, total FROM top_products;

-- CTE 可写(修改数据)
WITH deleted_old AS (
    DELETE FROM orders
    WHERE created_at < '2024-01-01'
    RETURNING *
)
SELECT count(*) AS deleted_count FROM deleted_old;

递归 CTE

递归 CTE 是 CTE 中最强大的功能之一。它由两个部分组成:基础案例(anchor member)生成初始结果集,递归步骤(recursive member)引用之前的结果进行迭代,直到不再返回新行。递归 CTE 非常适合处理树形结构、组织结构图、物料清单(BOM)等层次数据。

3.3 LATERAL JOIN

LATERAL 允许子查询引用前面表(FROM 子句中靠左的表)的列,实现"对每一行执行子查询"的语义:

3.4 DISTINCT ON

DISTINCT ON 是 PostgreSQL 特有的语法,用于对每组数据只保留一条记录:

-- 每个分类中销售额最高的产品
SELECT DISTINCT ON (category)
    category,
    product,
    sales_amount,
    sale_date
FROM sales_data
ORDER BY category, sales_amount DESC, sale_date;

-- 每个产品的最近一次销售记录
SELECT DISTINCT ON (product)
    product,
    sales_amount,
    sale_date
FROM sales_data
ORDER BY product, sale_date DESC;

-- 每个客户金额最大的订单
SELECT DISTINCT ON (customer_id)
    customer_id,
    amount,
    created_at
FROM transactions
ORDER BY customer_id, amount DESC;

-- 注意:DISTINCT ON 的列必须与 ORDER BY 的起始列匹配
-- ORDER BY 中 DISTINCT ON 列后的额外列决定保留哪一行

3.5 RETURNING 子句

RETURNING 子句可以在 INSERT、UPDATE、DELETE 操作后返回被影响行的数据,避免额外的查询:

-- INSERT ... RETURNING
INSERT INTO products (name, attributes) VALUES
    ('新鼠标', '{"brand": "Logitech", "price": 299}')
RETURNING id, name, created_at;

-- 批量插入并返回所有 ID
INSERT INTO products (name, attributes) VALUES
    ('键盘A', '{"price": 199}'),
    ('键盘B', '{"price": 299}'),
    ('键盘C', '{"price": 399}')
RETURNING id, name;

-- UPDATE ... RETURNING
UPDATE products
SET attributes = attributes || '{"discount": 0.15}'::jsonb
WHERE id = 1
RETURNING id, name, attributes -> 'discount' AS discount;

-- DELETE ... RETURNING
DELETE FROM products
WHERE id = 1
RETURNING id, name, attributes;

-- 结合 CTE 使用
WITH deleted AS (
    DELETE FROM orders WHERE status = 'cancelled' RETURNING *
)
SELECT count(*) AS deleted_count, sum(total) AS refund_total FROM deleted;

4. 全文搜索

PostgreSQL 内建全文搜索功能,无需外部搜索引擎即可实现高效的文本搜索。

4.1 tsvector / tsquery 基础

全文搜索的核心概念是词素(lexeme)——对单词进行标准化处理后的形式。PostgreSQL 的全文搜索分为两个阶段:文档的向量化(将文本转换为 tsvector)和查询的匹配(将搜索词转换为 tsquery)。

tsvector

tsvector 是文本经分词和标准化后得到的有序词素列表,包含每个词素在文档中的位置信息。例如,文本 "PostgreSQL is a powerful database system" 会被转换为类似 'databas':5 'postgresql':1 'power':3 'system':6 这样的结构,其中数字表示词素在原始文本中的位置。

tsquery

tsquery 表示搜索查询,支持布尔操作符:&(AND)、|(OR)、!(NOT)和短语搜索 <->(紧跟)或 <N>(相隔最多 N 个词)。

4.2 全文搜索查询

4.3 GIN 索引

没有索引的全文搜索需要全表扫描,GIN(Generalized Inverted Index)索引可将搜索性能提升几个数量级。GIN 索引的核心原理是倒排索引:它为每个词素维护一个包含该词素的文档列表。当执行 @@ 操作符查询时,PostgreSQL 可以快速定位到包含所有搜索词素的文档,而不需要扫描整个表。

GIN 索引的创建与使用

-- 创建 GIN 索引
CREATE INDEX idx_documents_search ON documents USING GIN (search_vector);

-- 查看索引大小
SELECT pg_size_pretty(pg_relation_size('idx_documents_search'));

-- 带索引的查询(自动使用索引)
EXPLAIN ANALYZE
SELECT id, title
FROM documents
WHERE search_vector @@ to_tsquery('simple', 'PostgreSQL')
ORDER BY ts_rank(search_vector, to_tsquery('simple', 'PostgreSQL')) DESC;

-- GiST 索引(备选,构建较慢但更新更快)
CREATE INDEX idx_documents_search_gist ON documents USING GiST (search_vector);

-- 查询性能对比
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM documents
WHERE search_vector @@ to_tsquery('simple', 'JSON & NoSQL');

4.4 中文分词

PostgreSQL 原生不支持中文分词,这是因为它内置的分词器是为西方语言(英语、德语等)设计的,这些语言中的单词天然以空格分隔。中文文本没有明显的词边界,因此需要专门的中文分词工具才能实现良好的全文搜索效果。

方案一:使用 zhparser(推荐)

zhparser 是基于 SCWS(Simple Chinese Words Segmentation)的 PostgreSQL 扩展,是目前使用最广泛的中文分词方案。

-- 安装 zhparser(需要 root 权限)
-- git clone https://github.com/amutu/zhparser.git
-- cd zhparser && make && make install

CREATE EXTENSION IF NOT EXISTS zhparser;

-- 创建中文文本搜索配置
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);

-- 配置分词模式
ALTER TEXT SEARCH CONFIGURATION chinese
    ADD MAPPING FOR n,v,a,i,e,l WITH simple;

-- 测试中文分词
SELECT to_tsvector('chinese', 'PostgreSQL 是一个强大的开源关系数据库系统');

-- 使用中文分词搜索
SELECT id, title,
       ts_rank(search_vector_zh, query) AS rank
FROM documents,
     to_tsquery('chinese', 'PostgreSQL & 数据库') AS query
WHERE search_vector_zh @@ query
ORDER BY rank DESC;

使用 jieba(Python 方案)

如果 zhparser 不可用,可以使用 Python 的 jieba 分词配合 PostgreSQL:

-- 使用 plpython3u 扩展
CREATE EXTENSION IF NOT EXISTS plpython3u;

-- 创建 jieba 分词函数
CREATE OR REPLACE FUNCTION jieba_segment(text TEXT)
RETURNS TEXT[] AS $$
    import jieba
    return list(jieba.cut(text, cut_all=False))
$$ LANGUAGE plpython3u;

-- 使用自定义分词
SELECT jieba_segment('PostgreSQL 是一个强大的开源关系数据库系统');

4.5 搜索排名优化

-- 综合排名算法
SELECT
    id,
    title,
    -- 综合多个排名指标
    ts_rank(search_vector, query, 32) * 0.6 +   -- 词频权重
    ts_rank_cd(search_vector, query, 32) * 0.4   -- 覆盖密度权重
    AS composite_rank,
    -- 头部高亮
    ts_headline('simple', body, query,
                'StartSel=<b>, StopSel=</b>, MaxWords=50, MinWords=20') AS snippet
FROM documents,
     to_tsquery('simple', 'PostgreSQL & 查询') AS query
WHERE search_vector @@ query
ORDER BY composite_rank DESC
LIMIT 10;

-- 覆盖所有匹配的聚合搜索
SELECT
    COUNT(*) FILTER (WHERE search_vector @@ query) AS total_matches,
    COUNT(*) AS total_docs,
    ROUND(
        COUNT(*) FILTER (WHERE search_vector @@ query) * 100.0 / NULLIF(COUNT(*), 0),
        2
    ) AS match_percentage
FROM documents,
     to_tsquery('simple', 'PostgreSQL') AS query;

5. 索引类型

PostgreSQL 支持多种索引类型,每种索引适用于不同的查询模式。索引是数据库性能调优中最重要也最复杂的领域之一。PostgreSQL 的索引系统非常灵活——你可以在同一列上创建不同类型的索引,查询优化器会根据查询条件自动选择最合适的索引。

5.1 索引概览

在深入每种索引之前,我们先创建一组测试数据来直观对比不同索引的性能特征。PostgreSQL 的索引类型包括:B-Tree(默认)、Hash、GiST、GIN、SP-GiST 和 BRIN,每种索引都有其特定的适用场景和数据类型的操作符类。

-- 准备测试数据(10 万行)
CREATE TABLE index_benchmark (
    id SERIAL PRIMARY KEY,
    int_col INTEGER,
    text_col TEXT,
    range_col INT4RANGE,
    json_col JSONB,
    ts_col TIMESTAMPTZ DEFAULT now(),
    geom_col POINT
);

-- 插入示例数据
INSERT INTO index_benchmark (int_col, text_col, range_col, json_col, geom_col)
SELECT
    floor(random() * 100000)::INT,
    md5(random()::TEXT),
    int4range(floor(random() * 100)::INT, floor(random() * 200 + 100)::INT),
    jsonb_build_object('key', 'val_' || floor(random() * 1000)::INT),
    POINT(random() * 180 - 90, random() * 360 - 180)
FROM generate_series(1, 100000);

5.2 B-Tree 索引

B-Tree 是默认索引类型,适用于等值查询和范围查询:

-- 创建 B-Tree 索引(默认)
CREATE INDEX idx_btree_int ON index_benchmark (int_col);

-- 多列 B-Tree 索引
CREATE INDEX idx_btree_multi ON index_benchmark (int_col, text_col);

-- 降序索引
CREATE INDEX idx_btree_desc ON index_benchmark (int_col DESC, ts_col);

-- NULLS FIRST/LAST
CREATE INDEX idx_btree_nulls ON index_benchmark (text_col NULLS LAST);

-- B-Tree 支持的查询模式
EXPLAIN ANALYZE SELECT * FROM index_benchmark WHERE int_col = 500;           -- 等值
EXPLAIN ANALYZE SELECT * FROM index_benchmark WHERE int_col BETWEEN 100 AND 200; -- 范围
EXPLAIN ANALYZE SELECT * FROM index_benchmark WHERE int_col < 1000;          -- 小于
EXPLAIN ANALYZE SELECT * FROM index_benchmark ORDER BY int_col;              -- 排序
EXPLAIN ANALYZE SELECT * FROM index_benchmark WHERE int_col IN (100, 200, 300); -- IN 列表

5.3 Hash 索引

Hash 索引只适用于等值查询:

-- 创建 Hash 索引
CREATE INDEX idx_hash_text ON index_benchmark USING HASH (text_col);

-- 仅支持等值查询
EXPLAIN ANALYZE SELECT * FROM index_benchmark WHERE text_col = 'abc';  -- 可用

-- 不支持范围查询(不会使用 Hash 索引)
EXPLAIN ANALYZE SELECT * FROM index_benchmark WHERE text_col < 'abc';

-- 注意:PostgreSQL 10+ 后的 Hash 索引是 WAL-logged 且崩溃安全的
-- 但 B-Tree 在等值查询上通常也不差,所以 Hash 索引使用较少

5.4 GiST 索引

GiST(Generalized Search Tree)是一个平衡树结构框架,支持多种数据类型和操作符:

-- GiST 适用于:几何类型、范围类型、全文搜索、FTS

-- 范围类型 GiST 索引
CREATE INDEX idx_gist_range ON index_benchmark USING GiST (range_col);

-- 支持的操作
EXPLAIN ANALYZE SELECT * FROM index_benchmark
WHERE range_col @> 50;                          -- 包含
EXPLAIN ANALYZE SELECT * FROM index_benchmark
WHERE range_col && int4range(10, 30);           -- 重叠
EXPLAIN ANALYZE SELECT * FROM index_benchmark
WHERE range_col << int4range(200, 300);         -- 严格在左

-- 几何类型 GiST 索引
CREATE INDEX idx_gist_point ON index_benchmark USING GiST (geom_col);

-- 最近邻查询(<-> 按距离排序)
EXPLAIN ANALYZE SELECT *
FROM index_benchmark
ORDER BY geom_col <-> POINT(0, 0)
LIMIT 10;

-- 多列 GiST
CREATE INDEX idx_gist_multi ON index_benchmark USING GiST (int_col, range_col);
-- 注意:GiST 多列索引对 int_col 的等值查询支持有限

5.5 GIN 索引

GIN(Generalized Inverted Index)适用于包含多个组成元素的数据类型:

-- GIN 适用于:数组、JSONB、全文搜索

-- JSONB GIN 索引
CREATE INDEX idx_gin_json ON index_benchmark USING GIN (json_col);

-- JSONB 路径索引(jsonb_path_ops 更小更快,但仅支持 @>)
CREATE INDEX idx_gin_json_path ON index_benchmark USING GIN (json_col jsonb_path_ops);

-- 支持的操作
EXPLAIN ANALYZE SELECT * FROM index_benchmark
WHERE json_col @> '{"key": "val_500"}'::jsonb;  -- 包含

-- 全文搜索 GIN 索引
CREATE INDEX idx_gin_fts ON documents USING GIN (search_vector);

-- 数组 GIN 索引
CREATE INDEX idx_gin_array ON articles_with_tags USING GIN (tags);

5.6 SP-GiST 索引

SP-GiST(Space-Partitioned GiST)适用于空间分区数据结构:

-- SP-GiST 适用于:四叉树、k-d 树、trie

-- 几何类型 SP-GiST 索引
CREATE INDEX idx_spgist_point ON index_benchmark USING SPGiST (geom_col);

-- 优点:对某些空间分布比 GiST 更高效
-- 缺点:不支持 <<、>>、~= 等操作符
EXPLAIN ANALYZE SELECT * FROM index_benchmark
WHERE geom_col <@ BOX(POINT(-10, -10), POINT(10, 10));  -- 包含在矩形内

5.7 BRIN 索引

BRIN(Block Range INdex)适用于与物理存储顺序高度相关的大表:

-- BRIN 适用于:时间序列、自增 ID、日志表

-- 创建 BRIN 索引
CREATE INDEX idx_brin_ts ON index_benchmark USING BRIN (ts_col);
CREATE INDEX idx_brin_int ON index_benchmark USING BRIN (int_col) WITH (pages_per_range = 32);

-- BRIN 索引大小对比
SELECT
    pg_size_pretty(pg_relation_size('idx_btree_int')) AS btree_size,
    pg_size_pretty(pg_relation_size('idx_brin_int')) AS brin_size;
-- BRIN 通常比 B-Tree 小 50-100 倍

-- BRIN 扫描
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM index_benchmark
WHERE ts_col BETWEEN '2024-01-01' AND '2024-06-30';

-- 选择 pages_per_range
-- 小值:更精确但索引更大
-- 大值:索引更小但可能返回更多假阳性

5.8 部分索引、覆盖索引、表达式索引

-- 部分索引(Partial Index):只索引满足条件的行
CREATE INDEX idx_partial_high_value ON orders (total)
WHERE total > 1000;

-- 查询时自动使用部分索引
EXPLAIN ANALYZE SELECT * FROM orders WHERE total > 5000;

-- 覆盖索引(Covering/INCLUDE Index):包含额外列避免回表
CREATE INDEX idx_covering ON orders (customer_id) INCLUDE (total, status, created_at);

-- 此查询可完全从索引中获取数据(Index Only Scan)
EXPLAIN ANALYZE
SELECT customer_id, total, status
FROM orders
WHERE customer_id = '张三';

-- 表达式索引(Expression Index)
-- 对函数或表达式结果建立索引
CREATE INDEX idx_expr_lower_email ON contacts (lower(email));
CREATE INDEX idx_expr_json_price ON products (((attributes ->> 'price')::NUMERIC));

-- 查询
EXPLAIN ANALYZE SELECT * FROM contacts WHERE lower(email) = 'user@example.com';
EXPLAIN ANALYZE SELECT * FROM products WHERE (attributes ->> 'price')::NUMERIC > 500;

5.9 索引与 ORDER BY

-- B-Tree 索引可加速排序
CREATE INDEX idx_order_date ON orders (created_at DESC);

-- 索引排序
EXPLAIN ANALYZE
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;

-- 多列排序与索引匹配
CREATE INDEX idx_order_customer_date ON orders (customer_id, created_at DESC);

-- 索引匹配的 ORDER BY
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE customer_id = '张三'
ORDER BY created_at DESC;

-- 索引不匹配的 ORDER BY(需要额外排序)
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE customer_id = '张三'
ORDER BY total DESC;

-- 索引统计信息查看
SELECT * FROM pg_stat_user_indexes WHERE relname = 'orders';

6. 事务与并发

6.1 MVCC 实现细节

PostgreSQL 的 MVCC 通过行版本控制实现,每个元组(行版本)都有事务可见性信息。理解 MVCC 的底层机制对于诊断并发问题和优化数据库性能至关重要。

事务 ID 和可见性规则

每个事务在开始时都会获得一个单调递增的事务 ID(XID),由 txid_current() 函数返回。PostgreSQL 使用 xmin(创建者事务 ID)和 xmax(删除者事务 ID)两个隐藏字段来判断每一行对当前事务的可见性。可见性判断的基本规则如下:

  1. 如果行的 xmin 在当前事务的快照之前已提交 → 该行可见
  2. 如果行的 xmin 在当前事务之后开始 → 该行不可见
  3. 如果行的 xmax 在当前事务的快照之前已提交 → 该行已删除,不可见
  4. 如果行的 xmax 为 0 或等于当前事务 ID → 该行是当前行版本

6.2 序列化快照隔离(SSI)

可序列化快照隔离(Serializable Snapshot Isolation, SSI)是 PostgreSQL 在 9.1 版本中引入的高级事务隔离级别,它通过检测读写依赖关系来发现可序列化冲突,从而完全消除幻读和写偏斜(write skew)等问题。

隔离级别概述

PostgreSQL 支持 SQL 标准定义的所有四种事务隔离级别,但底层实现只有三种不同的行为模式:

  1. 读未提交(READ UNCOMMITTED):在 PostgreSQL 中,此级别行为等同于读已提交,因为 PostgreSQL 不允许脏读
  2. 读已提交(READ COMMITTED):默认级别,每个语句都会获取一个新的快照,因此同一个事务内的不同 SELECT 可能看到不同的数据
  3. 可重复读(REPEATABLE READ):事务开始时获取一个快照,整个事务期间使用该快照,可避免不可重复读和幻读
  4. 可序列化(SERIALIZABLE):最严格的级别,通过 SSI 算法检测串行化冲突,确保事务执行结果等同于某种串行执行顺序

6.3 锁类型与监控

6.4 死锁检测


7. 扩展生态

PostgreSQL 的扩展生态是其最强优势之一,几乎所有功能都可以通过扩展来增强。

7.1 PostGIS(地理空间)

PostGIS 是 PostgreSQL 的地理空间扩展,也是业界公认最强大、最成熟的开源地理空间数据库引擎。它遵循 OpenGIS 规范,支持数百种空间操作函数和多种空间数据类型,被广泛应用于 GIS 系统、位置服务(LBS)、地理数据分析等领域。

PostGIS 的核心功能

  • 空间数据类型:Point、LineString、Polygon、MultiPoint、MultiLineString、MultiPolygon、GeometryCollection 等
  • 空间索引:基于 GiST 的 R-Tree 索引,支持高效的空间查询
  • 空间函数:ST_Distance、ST_Area、ST_Intersects、ST_Within、ST_Buffer 等 300+ 函数
  • 坐标参考系统:支持 EPSG 坐标系,自动进行坐标转换
  • 地理类型:GEOGRAPHY 类型支持基于球面的距离计算(更精确)

7.2 pg_stat_statements

pg_stat_statements 是 PostgreSQL 自带的查询性能分析核心工具。它跟踪所有 SQL 语句的执行统计信息,包括执行次数、总耗时、最小/最大耗时、I/O 统计、缓冲区命中率等。对于定位性能瓶颈和慢查询优化至关重要。

安装与配置

7.3 pg_partman(分区管理)

自动管理表分区的扩展:

7.4 pgBackRest 备份

企业级备份和恢复工具:

# pgbackrest 配置示例(/etc/pgbackrest/pgbackrest.conf)
# [demodb]
# pg1-path=/var/lib/postgresql/16/main
# pg1-port=5432
# pg1-user=postgres
#
# [global]
# repo1-path=/backup/pgbackrest
# repo1-retention-full=4
# repo1-retention-diff=14
# repo1-cipher-type=aes-256-cbc
# repo1-cipher-pass=<secret>
# compress-type=zst
# compress-level=6
# process-max=4
-- 在数据库内检查备份配置
SELECT * FROM pg_settings WHERE name LIKE '%archive%';

-- 查看归档状态
SELECT * FROM pg_stat_archiver;

-- 使用 pgbackrest 的命令行
-- 全量备份
-- pgbackrest --stanza=demodb --type=full backup

-- 增量备份
-- pgbackrest --stanza=demodb --type=incr backup

-- 差异备份
-- pgbackrest --stanza=demodb --type=diff backup

-- 列出备份
-- pgbackrest --stanza=demodb info

-- 恢复到指定时间点
-- pgbackrest --stanza=demodb --type=time "--target=2024-09-13 12:00:00+08" restore

8. 高可用

8.1 流复制(Streaming Replication)

异步和同步流复制是 PostgreSQL 高可用的基础。流复制的核心原理是:主库将 WAL 日志实时传输到从库,从库持续回放这些 WAL 记录,从而与主库保持数据同步。这种方式相比传统的基于文件的日志 shipping 延迟更低、数据一致性更好。

流复制的配置步骤

第一步:主库配置

-- 主库配置(postgresql.conf)
-- listen_addresses = '*'
-- wal_level = replica          -- 或 logical
-- max_wal_senders = 10
-- wal_keep_size = 1024         -- MB
-- hot_standby = on

-- 创建复制用户
CREATE USER replicator WITH REPLICATION LOGIN PASSWORD 'replica_secret';

-- 配置 pg_hba.conf
-- host replication replicator 192.168.1.0/24 scram-sha-256

第二步:从库初始化

-- 从库创建基础备份
-- pg_basebackup -h primary_ip -p 5432 -U replicator -D /var/lib/postgresql/16/standby -Fp -Xs -P -R

-- 从库配置(standby.signal 文件表示这是一个备用节点)
-- touch /var/lib/postgresql/16/main/standby.signal

-- 从库 postgresql.conf
-- primary_conninfo = 'host=primary_ip port=5432 user=replicator password=replica_secret'
-- recovery_target_timeline = 'latest'
-- hot_standby = on

第三步:监控复制状态

8.2 Patroni + etcd 高可用

Patroni 是一个用 Python 编写的 PostgreSQL 高可用框架,它能自动管理故障转移、健康检查和集群拓扑变更。Patroni 结合分布式一致性存储(如 etcd、Consul、ZooKeeper),可以实现真正的自动化主从切换,无需人工干预。

Patroni 的工作原理

Patroni 通过 DCS(分布式配置存储,如 etcd)来协调集群中的各个节点。每个 Patroni 节点定期向 etcd 写入心跳信息,同时从 etcd 读取集群的当前状态。如果主库的心跳超时,Patroni 会通过选举算法从可用的从库中选出新的主库,整个过程通常只需要几十秒。

Patroni 配置文件示例

# 启动 Patroni
patroni /etc/patroni/patroni.yml

# 查看集群状态
patronictl -c /etc/patroni/patroni.yml list

# 手动切换
patronictl -c /etc/patroni/patroni.yml switchover pg_cluster

# 故障转移
patronictl -c /etc/patroni/patroni.yml failover pg_cluster

# 重新初始化失败的节点
patronictl -c /etc/patroni/patroni.yml reinit pg_cluster pg-node2

# 查看集群拓扑
patronictl -c /etc/patroni/patroni.yml topology pg_cluster

8.3 pgpool-II 连接池

pgpool-II 是一个功能强大的 PostgreSQL 中间件,提供连接池、读写分离、负载均衡和自动故障转移等功能。在生产环境中,使用 pgpool-II 可以有效解决 PostgreSQL 多进程架构带来的连接数上限问题。

pgpool-II 的核心功能

  1. 连接池:复用后端数据库连接,减少连接建立的开销,缓解 PostgreSQL 对大量并发连接的支持压力
  2. 读写分离:将 SELECT 查询路由到从库,INSERT/UPDATE/DELETE 路由到主库,充分利用从库资源
  3. 负载均衡:支持基于权重的读请求分配策略,在多从库场景下分散查询压力
  4. 自动故障转移:后端节点故障时自动切换,配合 watchdog 实现 pgpool-II 自身的高可用

pgpool.conf 关键配置

-- 查看 pgpool 节点状态
SHOW pool_nodes;

-- 结果示例
-- node_id | hostname       | port | status | lb_weight | role
-- --------+----------------+------+--------+-----------+--------
-- 0       | 192.168.1.10   | 5432 | up     | 0.3333    | primary
-- 1       | 192.168.1.20   | 5432 | up     | 0.6667    | standby

-- 查看连接池状态
SHOW pool_pools;

-- 查看进程统计
SHOW pool_processes;

-- 验证读写分离
-- 写操作会被路由到主库
INSERT INTO test VALUES (1);

-- 读操作可能被路由到从库(取决于负载均衡策略)
SELECT * FROM test;

-- 强制在主库读取
/*NO LOAD BALANCE*/ SELECT * FROM test;

总结

PostgreSQL 作为世界上最先进的开源关系数据库,凭借其丰富的功能集、强大的扩展生态和对 SQL 标准的严格遵循,已经成为现代应用开发的首选数据库之一。无论是初创公司的 MVP 产品,还是大型企业的核心交易系统,PostgreSQL 都能提供稳定可靠的数据存储服务。

核心优势回顾:

  1. 数据类型丰富:数组、JSONB、范围类型、网络类型、几何类型、枚举和自定义复合类型,可以轻松处理各种结构化数据需求,减少应用层的类型转换工作
  2. 高级查询能力:窗口函数支持复杂的分组统计和排名计算,递归 CTE 能够优雅地处理树形结构和层次数据,LATERAL JOIN 实现逐行子查询,DISTINCT ON 实现每组取 TOP N,RETURNING 子句减少重复查询
  3. 全文搜索:内建 tsvector/tsquery 结合 GIN 索引,支持中文分词和搜索排名,在多数场景下可以替代 Elasticsearch 等专用搜索引擎
  4. 多样索引:B-Tree、Hash、GiST、GIN、SP-GiST、BRIN 六种索引类型配合部分索引、覆盖索引和表达式索引,为各种查询模式提供最优的加速方案
  5. 并发控制:成熟的 MVCC 实现从不阻塞读写,可序列化快照隔离(SSI)解决了写偏斜等并发异常问题,完整的锁监控机制帮助诊断死锁和锁等待
  6. 强大扩展:PostGIS 是地理空间领域的黄金标准,pg_stat_statements 是查询性能分析的必备工具,pg_partman 自动管理表分区,pgBackRest 提供企业级备份恢复
  7. 高可用方案:流复制 + Patroni + pgpool-II 构建生产级高可用架构,自动故障转移确保业务连续性

无论是构建微服务、数据仓库、地理信息系统还是全文搜索应用,PostgreSQL 都能提供企业级的解决方案。随着 PostgreSQL 社区的持续发展和每一年的大版本更新,它正在不断缩小与商业数据库的差距,并在越来越多的领域展现出独特的优势。


本文由 Hermes Agent 撰写,以 PostgreSQL 16 版本为基础。所有示例代码可直接在 PostgreSQL 16+ 环境中运行验证。