知行札记
专题技术实践数据库实践PostgreSQL 实践

视图、函数、JSONB 与分区

连接视图、数据库程序、审计、结构化扩展和分区限制。

本分册采用 PostgreSQL 18 文档作为语法和行为基线。归档中 PostgreSQL 16 或其他环境的 SQL、计划与耗时保留为历史记录,2026-10-05 整理未重跑这些数据库实验。示例需在独立可丢弃数据库内使用,各章同名表可能有不同结构;先核对 DDL、角色和数据再执行。

第 2 章盖起的书店已经有了图书、顾客和订单,会查询、会建索引。可业务一长大,新麻烦接踵而至:同一条"订单明细"查询在十几张报表里被复制粘贴;每张表的"最后修改时间"全靠应用代码记得去更新,总有人忘;埋点事件、支付回调这类数据字段五花八门,建多少列都追不上变化;订单和支付流水涨到上亿行,连删旧数据都成了苦差事。本章逐一认识 PostgreSQL 为这些问题准备的"进阶武器":视图(View)封装查询,函数(Function)封装逻辑,触发器(Trigger)在关键时刻自动出手,JSONB 装下半结构化数据,分区(Partitioning)切分大表,最后浏览全文检索与扩展生态。

本章示例继续使用第 2 章的 books、customers、orders 三张表,边讲边扩展它们。(第 3 章的六表设计更贴近真实项目;本章为聚焦特性本身沿用简化结构,所讲思路在两套表上同样适用。)所有特性在 PostgreSQL 14~18 上行为一致,可放心照做。

6.1 视图与物化视图:给查询起个名字,或把结果存下来

为什么需要。 后台到处要用"订单明细",每次都要连接 orders、books、customers 三张表,SQL 又长又容易抄错;底层表一改,散落各处的查询全得跟着改。我们希望把这条查询"起个名字",之后像查表一样用它。

是什么。 视图(View)就是一条被保存起来、起了名字的 SELECT 语句。注意它保存的是"查询本身"而不是数据——像一张菜谱卡片:每次查询视图,数据库都现场照着卡片把菜重做一遍,数据始终存在基本表中,视图自身不占数据存储。官方教程把大量使用视图列为良好 SQL 设计的关键一环:它封装复杂连接,也给应用提供了一个不随底层表结构变化的稳定接口。

怎么用。 把三表连接封装成视图:

CREATE VIEW v_order_detail AS
SELECT o.id,
       c.name AS customer,
       b.title,
       o.quantity,
       o.amount,
       o.ordered_at
FROM   orders o
JOIN   books     b ON b.id = o.book_id
LEFT   JOIN customers c ON c.id = o.customer_id;  -- 未登录订单也保留

-- 之后像查普通表一样查询它
SELECT * FROM v_order_detail WHERE amount > 100 ORDER BY ordered_at;

视图并非一律只读。当它的 FROM 里只有一个基本表、且不含 DISTINCT、GROUP BY、聚合、LIMIT、集合运算等成分时,属于简单视图,可以自动更新——对视图的写入会落到基本表上:

CREATE VIEW v_books_in_stock AS
SELECT * FROM books WHERE stock > 0;

UPDATE v_books_in_stock SET price = price * 0.9 WHERE id = 1;  -- 实际修改 books 表

-- 加上 WITH CHECK OPTION,禁止通过视图把行改成"视图看不见"的样子
CREATE OR REPLACE VIEW v_books_in_stock AS
SELECT * FROM books WHERE stock > 0
WITH CHECK OPTION;

UPDATE v_books_in_stock SET stock = -10 WHERE id = 1;
-- 被拒绝:修改后的行不再满足 stock > 0

而带聚合或分组的视图是只读的:

CREATE VIEW v_category_avg AS
SELECT category, round(avg(price), 2) AS avg_price
FROM   books
GROUP  BY category;

UPDATE v_category_avg SET avg_price = 50;
-- 报错:含聚合的视图不可自动更新,确有需要得走 INSTEAD OF 触发器(进阶话题)

物化视图:存下来的查询结果

运营每天都要看"各书销量汇总",每次查询都重新聚合全表太浪费。物化视图(Materialized View)干脆把查询结果物理存成一张实体表:可以建索引、也占磁盘。类比:普通视图是菜谱,物化视图是做好的冷冻成品菜——拿来就能吃,但菜谱更新了它不会自动重做。它和 CREATE TABLE AS 的区别是"记住"了定义查询、可以反复刷新;和普通视图的区别是数据只是创建时刻的快照,不自动反映底层变化。

CREATE MATERIALIZED VIEW mv_book_sales AS
SELECT b.id AS book_id, b.title,
       sum(o.quantity) AS total_qty,
       sum(o.amount)   AS total_revenue
FROM   orders o
JOIN   books b ON b.id = o.book_id
GROUP  BY b.id, b.title;

INSERT INTO orders (book_id, customer_id, quantity, amount)
VALUES (2, 1, 1, 129.00);                          -- 新卖出一本书

SELECT * FROM mv_book_sales;                       -- 结果没变:仍是旧快照
REFRESH MATERIALIZED VIEW mv_book_sales;           -- 手动刷新
SELECT * FROM mv_book_sales;                       -- 现在包含新订单了

刷新有两种模式:默认刷新整体替换内容,结果集大时可能阻塞并发读取该视图的连接;加 CONCURRENTLY 则不阻塞读,但前提是物化视图上至少有一个"只由列名构成"的唯一索引(Unique Index):

CREATE UNIQUE INDEX ON mv_book_sales (book_id);
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_book_sales;

一句版本差异:PG 16 及以前只有物化视图的 owner 能刷新,PG 17 起新增 MAINTAIN 权限,可授权他人执行。

6.2 自定义函数与存储过程:把业务逻辑放进数据库

为什么需要。 会员价规则(金卡八折、银卡九折)散落在十几个查询里,调一次折扣改十几处;应用把几十条 SQL 逐条发到服务器,来回往返也不便宜。我们希望把逻辑封装在数据库端,用一个名字随处调用。

是什么。 函数(Function)由 CREATE FUNCTION 定义,必须声明返回类型,可以在 SELECT 和表达式里直接调用。函数体常用 PL/pgSQL 书写——PostgreSQL 自 9.0 起默认安装的过程语言(Procedural Language),在 SQL 之上补充变量、IF、循环等控制结构,把一串计算搬到服务器端还能减少客户端往返。类比 Excel 的自定义公式:算法封装一次,处处可用。

怎么用。 从最简单的开始,函数体骨架是 DECLARE(声明变量,可省略)加 BEGIN ... END;两侧的 $$ ... $$ 是美元引号定界符——被它包住的文本原样作为字符串处理,不必再操心函数体内部引号的转义,是书写函数体的标准做法:

-- 全场九折
CREATE FUNCTION discount_price(p numeric) RETURNS numeric AS $$
BEGIN
    RETURN round(p * 0.9, 2);
END;
$$ LANGUAGE plpgsql;

SELECT title, price, discount_price(price) FROM books;

加上 IF 就能表达业务规则:

CREATE FUNCTION member_price(p numeric, level text) RETURNS numeric AS $$
BEGIN
    IF level = 'gold' THEN
        RETURN round(p * 0.8, 2);   -- 金卡八折
    ELSIF level = 'silver' THEN
        RETURN round(p * 0.9, 2);   -- 银卡九折
    ELSE
        RETURN p;                   -- 普通会员原价
    END IF;
END;
$$ LANGUAGE plpgsql;

SELECT member_price(100, 'gold');   -- 80.00

函数还能返回整个结果集:RETURNS TABLE 声明列结构,RETURN QUERY 装填数据,这样的表值函数可以直接放进 FROM:

CREATE FUNCTION books_in_category(cat text)
RETURNS TABLE (id bigint, title text, price numeric)
LANGUAGE plpgsql AS $$
BEGIN
    RETURN QUERY
    SELECT b.id, b.title, b.price
    FROM   books b
    WHERE  b.category = cat;
END;
$$;

SELECT * FROM books_in_category('database');

最后分清函数与存储过程(Stored Procedure,PG 11 引入):函数必须声明返回类型、在 SELECT 中调用;过程没有 RETURNS、用 CALL 调用,独有本事是过程体内可以执行 COMMIT/ROLLBACK 自己管理事务。记法:要返回值用函数,要自己管事务用过程。

CREATE PROCEDURE restock(p_book bigint, p_qty int)
LANGUAGE plpgsql AS $$
BEGIN
    UPDATE books SET stock = stock + p_qty WHERE id = p_book;
END;
$$;

CALL restock(1, 50);   -- 补 50 本《SQL 入门经典》

6.3 触发器:让数据库在关键时刻自己动手

为什么需要。 想给 books 加"最后修改时间",靠应用代码记得更新总有人忘;老板还要求"谁在什么时候把价格从多少改成多少"可查,靠人自觉记日志必有遗漏。

是什么。 触发器(Trigger)由两部分组成:CREATE TRIGGER 只声明"哪张表、什么事件(INSERT/UPDATE/DELETE/TRUNCATE)、在操作之前还是之后";真正的逻辑写在触发器函数里——它不接收参数、返回类型必须是 trigger。事件数据通过特殊变量传入:NEW 是即将写入的新行,OLD 是被替换或删除的旧行,TG_OP 告诉你这次是什么操作。时机上,BEFORE 在操作前运行,可以修改 NEW、也可以返回 NULL 悄悄跳过这一行;AFTER 在操作完成后运行,返回值被忽略,最适合审计。粒度上,FOR EACH ROW 每受影响一行触发一次,FOR EACH STATEMENT 每条语句触发一次(默认)。类比:触发器像银行柜员旁边的点钞记录仪,每次动钱都自动留痕。

实战一:自动维护 updated_at

先给 books 补上这一列,再写一个 BEFORE 行级触发器:

ALTER TABLE books ADD COLUMN updated_at timestamptz NOT NULL DEFAULT now();

CREATE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN
    NEW.updated_at := now();   -- 直接修改即将写入的新行
    RETURN NEW;                -- 关键!返回 NULL 会让这一行的更新悄悄失效
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_books_updated_at
BEFORE UPDATE ON books
FOR EACH ROW EXECUTE FUNCTION set_updated_at();

此后任何 UPDATE books 都会自动刷新 updated_at,应用一行代码都不用改。

实战二:审计日志——本章压轴示例

这个示例把三样东西串在一起:触发器捕捉变更,PL/pgSQL 做分支,JSONB 存整行快照。

-- 审计表:一行记录一次变更
CREATE TABLE books_audit (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    action     text NOT NULL,                        -- 'INSERT' / 'UPDATE' / 'DELETE'
    book_id    bigint,
    old_data   jsonb,                                -- 变更前的整行
    new_data   jsonb,                                -- 变更后的整行
    changed_at timestamptz NOT NULL DEFAULT now()
);

CREATE FUNCTION audit_books() RETURNS trigger AS $$
BEGIN
    IF TG_OP = 'DELETE' THEN
        INSERT INTO books_audit (action, book_id, old_data)
        VALUES ('DELETE', OLD.id, row_to_json(OLD)::jsonb);
        RETURN OLD;
    ELSIF TG_OP = 'INSERT' THEN
        INSERT INTO books_audit (action, book_id, new_data)
        VALUES ('INSERT', NEW.id, row_to_json(NEW)::jsonb);
        RETURN NEW;
    ELSE   -- UPDATE:新旧都记,才看得出"从多少改成多少"
        INSERT INTO books_audit (action, book_id, old_data, new_data)
        VALUES ('UPDATE', OLD.id,
                row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb);
        RETURN NEW;
    END IF;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_books_audit
AFTER INSERT OR UPDATE OR DELETE ON books
FOR EACH ROW EXECUTE FUNCTION audit_books();

三种操作各来一遍,再核对审计表:

INSERT INTO books (title, author, category, price, stock)
VALUES ('PostgreSQL 习题集', '赵三', 'database', 39.00, 10);

UPDATE books SET price = 35.00 WHERE title = 'PostgreSQL 习题集';

DELETE FROM books WHERE title = 'PostgreSQL 习题集';

SELECT action, book_id, old_data, new_data FROM books_audit ORDER BY id;
-- 三行记录恰好对应三次操作;UPDATE 那行同时留着旧价 39.00 与新价 35.00

选 AFTER 时机是有讲究的:只有真正写入成功的数据才值得记录。另一个细节:老教程里常见的 EXECUTE PROCEDURE 是已废弃的历史写法(PG 11 起推荐 EXECUTE FUNCTION),两者等价,新代码请用后者。

6.4 JSONB:装下"不太规矩"的数据

为什么需要。 书店的埋点系统源源不断发来"浏览图书""加入购物车""下单"等事件,字段五花八门:今天多一个 device,明天多一个 channel。这类结构不固定的半结构化数据(Semi-structured Data),为每种字段建一列既不现实也永远追不上——第 2 章我们已经在 books.info 里用 jsonb 存过出版社和页数,现在系统地认识它。

是什么。 PostgreSQL 提供两种 JSON 类型:json 保存输入文本的精确副本,每次处理都要重新解析;jsonb 保存解析后的二进制格式,写入稍慢、查询显著更快,而且支持索引。官方建议:除非要保留键的原始顺序这类特殊需求,大多数应用应优先用 jsonb。类比:json 是原件复印件,jsonb 是整理归档进档案柜的资料。要记住 jsonb 会做规范化:不保留空白和键顺序,重复键只留最后一个——{"a":1,"a":2} 存进去取出来是 {"a": 2}。需要逐字回显原文(比如存证报文)时才选 json。

怎么用。 建一张事件表,结构固定的部分用普通列,弹性的部分进 jsonb(第 4 章做索引实验时也建过一张 events 表,两者互不相干;若在同一库中跟做,请先 DROP TABLE events;):

CREATE TABLE events (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    created_at timestamptz NOT NULL DEFAULT now(),
    data       jsonb NOT NULL
);

INSERT INTO events (data) VALUES
    ('{"event":"view_book",   "book_id":2, "tags":["postgres","beginner"]}'),
    ('{"event":"add_to_cart", "book_id":2, "tags":["postgres"], "device":"mobile"}'),
    ('{"event":"checkout",    "tags":["postgres","sale"]}');

最常用的操作符:

SELECT data->'tags'->>0 AS first_tag FROM events;
-- -> 按键取子文档(结果仍是 jsonb),->> 取纯文本,可层层深入

SELECT * FROM events WHERE data @> '{"tags":["postgres"]}';
-- @> 包含判断:最常配合索引的查询写法

SELECT * FROM events WHERE data ? 'device';
-- ? 判断顶层是否存在某键(?| 任一存在、?& 全部存在,都只看顶层,不递归嵌套)

数据量一大就要靠索引。对 jsonb 列建 GIN(Generalized Inverted Index,通用倒排索引)后,上面的 @> 与 ? 查询会从全表顺序扫描(Seq Scan)变成索引扫描——用 EXPLAIN 对比建索引前后,能看到 Seq Scan 变成 Bitmap Index Scan:

CREATE INDEX idx_events_gin ON events USING GIN (data);

两个进阶选项:默认操作符类 jsonb_ops 支持 ?、?|、?&、@> 等;jsonb_path_ops 只支持 @>,但索引通常更小、搜索更精准。若查询总是落在某个子文档上,可用表达式索引:

CREATE INDEX idx_events_tags ON events USING GIN ((data->'tags'));
-- 专门加速 WHERE data->'tags' ? 'sale'

注意一个经典坑:WHERE data->>'book_id' = '2' 这种文本比较不属于 GIN 支持的操作符,索引帮不上忙——应改写成包含查询 data @> '{"book_id":2}'(数字别加引号),或对表达式 (data->>'book_id') 另建普通 B-tree 索引。

关系列还是 JSONB?做个小实验

同一个"出版社"信息,放列上和放 jsonb 里,待遇大不相同:

CREATE TABLE publishers (id int PRIMARY KEY, name text);

ALTER TABLE books ADD COLUMN publisher_id int REFERENCES publishers(id);
-- 可行:关系列上外键、CHECK 约束都能正常工作
-- 而 books.info->>'publisher' 只是 jsonb 里的一个键:对它建不了外键

经验法则:需要强约束(外键、CHECK)、频繁参与连接或作为查询条件的字段,老老实实用关系列;结构有弹性、整体存取的附加信息才用 jsonb。官方还提醒:JSON 文档的结构应相对固定、大小要克制——jsonb 的任何更新都会锁住整行,文档一大,并发就遭殃。

6.5 表分区:把大表切成物理小块

为什么需要。 支付流水每月新增千万行,索引越建越大、越走越慢;想清理两年前的旧数据,DELETE 一跑几小时,还留下空洞等 VACUUM 回收。这时候需要"物理上把大表切开"。

是什么。 表分区(Partitioning)让一张逻辑上的大表按规则拆成多个物理子表:父表只是"目录",本身不存数据,每一行实际落在某个分区里。类比图书馆把档案按年份装盒:查 2026 年 10 月的单据只开 10 月那盒(分区裁剪),清旧档案整盒搬走(DETACH)。官方经验法则:当表大小超过数据库服务器的物理内存时,分区才"通常值得"。还有一条工程事实:分区要在 CREATE TABLE 时声明,已存在的普通表无法直接"升级"为分区表,通常得新建分区表再迁数据。

范围分区(Range Partitioning):时间序列首选

CREATE TABLE payments (
    id       bigint NOT NULL,
    order_id bigint NOT NULL,
    amount   numeric(10, 2) NOT NULL,
    paid_at  timestamptz NOT NULL
) PARTITION BY RANGE (paid_at);

-- 每月一个分区;注意边界规则:下界包含、上界不包含
CREATE TABLE payments_2026_09 PARTITION OF payments
    FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE TABLE payments_2026_10 PARTITION OF payments
    FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');

INSERT INTO payments VALUES (1, 1, 129.00, '2026-11-05');
-- 报错:没有任何分区能接纳 11 月的行

两种补救:提前补建下月分区,或加一个 DEFAULT 分区兜底:

CREATE TABLE payments_2026_11 PARTITION OF payments
    FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');
-- 或者:CREATE TABLE payments_default PARTITION OF payments DEFAULT;

-- 再次执行上面的 INSERT 即可成功

分区最大的红利是分区裁剪(Partition Pruning):查询条件命中分区键时,规划器根据各分区边界自动跳过无关分区——依据是边界而不是索引:

EXPLAIN SELECT count(*) FROM payments
WHERE  paid_at >= '2026-10-01' AND paid_at < '2026-11-01';
-- 计划里只剩 payments_2026_10(若建了 DEFAULT 分区还会带上它),
-- 9 月分区根本不会被扫描

清理旧数据也从 DELETE 的苦役变成秒级操作:

ALTER TABLE payments DETACH PARTITION payments_2026_09;
-- 旧分区"摘下"成为独立表:可改名、导出、确认后整表删除,
-- 完全避开 DELETE + VACUUM;PG 14 起还可用 DETACH CONCURRENTLY,锁更轻

列表分区(List Partitioning)与哈希分区(Hash Partitioning)

地区、类别这类取值可穷举的离散维度,用 LIST 显式枚举。假设书店上线一张按等级分区的会员表:

CREATE TABLE vip_customers (
    id    bigint NOT NULL,
    name  text NOT NULL,
    level text NOT NULL
) PARTITION BY LIST (level);

CREATE TABLE vip_gold   PARTITION OF vip_customers FOR VALUES IN ('gold');
CREATE TABLE vip_silver PARTITION OF vip_customers FOR VALUES IN ('silver', 'bronze');

选型口诀:时间序列与定期归档选 RANGE;地区、类别等可穷举的离散值选 LIST;键值无法穷举、只想把负载均匀打散选 HASH(按 modulus/remainder 哈希分桶)。

两条铁律。其一,唯一约束和主键必须包含全部分区键,否则数据库无法跨分区保证唯一:

ALTER TABLE payments ADD PRIMARY KEY (id);              -- 失败:缺分区键 paid_at
ALTER TABLE payments ADD PRIMARY KEY (id, paid_at);     -- 成功

其二,分区绝不是越多越好:数量过多会拉长查询规划时间、增加内存开销。分区键应选查询 WHERE 里最常出现的列,数量以真实负载测试为准——官方明确提醒"绝不要想当然认为分区越多越好"。

6.6 全文检索:让搜索框聪明起来

为什么需要。 书店搜索框若用 LIKE '%postgres%',有三大短板:不懂语言学——satisfies 和 satisfy 会被当成两个词,词形一变就搜不到;无法按相关性排序;没有索引支持,只能全表扫描。

是什么。 全文检索(Full Text Search)的流水线是:把文本切成词条(token),再用词典归一化成词素(lexeme)——折叠词干、剔除停用词(stop word,即 the、a 这类出现极多、几乎没有区分度的词)——存进 tsvector;查询用 tsquery 表达"要含哪些词、什么关系",用 @@ 操作符匹配。

SELECT to_tsvector('english', 'satisfy satisfies');
-- 两个词归一为同一个词素,因此搜 satisfy 能命中 satisfies

怎么用。 先给 books 补一个简介列并录两段英文样例(PostgreSQL 的内置词典以英语等西方语言为主):

ALTER TABLE books ADD COLUMN description text NOT NULL DEFAULT '';

UPDATE books SET description =
    'A hands-on guide to Postgres indexes, replication and performance tuning.'
WHERE  title = 'PostgreSQL 实战';

UPDATE books SET description =
    'Classic theory of databases: transactions, locking and SQL optimization.'
WHERE  title = '数据库原理';

-- 表达式索引必须用带配置名的两参数形式,单参数形式依赖 default_text_search_config 设置,索引内容不可靠
CREATE INDEX books_fts_idx ON books
    USING GIN (to_tsvector('english', title || ' ' || description));

SELECT id, title FROM books
WHERE  to_tsvector('english', title || ' ' || description)
       @@ to_tsquery('postgres & index');
-- & 且、| 或、! 非、<-> 相邻;命中写着 "Postgres indexes" 的《PostgreSQL 实战》,
-- 却不会命中只谈 transactions 的《数据库原理》

SELECT id, title FROM books
WHERE  to_tsvector('english', title || ' ' || description)
       @@ phraseto_tsquery('english', 'performance tuning');
-- 短语匹配:要求 tuning 紧跟 performance,词序不能乱

两条规矩:查询必须写成与建索引完全相同的表达式——索引里带 'english'、查询里省了它,索引就不命中;也不要把文本直接 cast 成 tsvector('...'::tsvector),那会跳过词干归一化,搜 rat 匹配不到文中的 rats。

中文读者最关心的问题单独交代一句:内置分词按空格切词,对中文不适用(中文没有空格);完整的中文全文检索需要 zhparser、pg_jieba 这类第三方分词扩展,属于进阶话题,本书不展开。若只是中文书名、标题这类短文本的检索,6.7 节的 pg_trgm 模糊匹配(LIKE '%实战%' 走 GIN 索引)通常已经够用。

6.7 扩展生态:一句话安装的新能力

为什么需要。 模糊纠错、加密、地理坐标……这些需求 PostgreSQL 发行版大多自带"选装件",不必重复造轮子。

是什么。 contrib 模块随 PostgreSQL 一起发行但不属于数据库核心;装进某个数据库只需一句 CREATE EXTENSION。其中标记为 trusted 的扩展(pg_trgm、hstore、pgcrypto、uuid-ossp、citext、unaccent 等)只要求 CREATE 权限,普通用户即可安装,无需超级用户。

-- 先看看本机有哪些扩展可用
SELECT name, default_version, installed_version
FROM   pg_available_extensions
ORDER  BY name;

以 pg_trgm 为例。它基于三元组(Trigram):把词拆成所有连续三个字符的"指纹",用指纹重合度衡量相似度:

CREATE EXTENSION pg_trgm;

SELECT show_trgm('word');
-- word 的全部三元组:{"  w"," wo",ord,"rd ",wor}
-- 注意切分前 pg_trgm 会先给词首补两个空格、词尾补一个空格,
-- 所以才有 "  w"、"rd " 这类带空格的三元组——补空格让词头、
-- 词尾的片段也参与比较,相似度计算才不失真

-- 顾客把《PostgreSQL 实战》的"战"打成了"占"?% 筛出相似候选,similarity() 给出相似度
SELECT title, similarity(title, 'PostgreSQL 实占') AS sim
FROM   books
WHERE  title % 'PostgreSQL 实占'
ORDER  BY sim DESC
LIMIT  3;

-- pg_trgm 的 GIN 索引还能加速 B-tree 无能为力的中缀模糊查询
CREATE INDEX idx_books_title_trgm ON books USING GIN (title gin_trgm_ops);
-- 此后 WHERE title LIKE '%实战%' 可走索引而非全表扫描

% 的阈值由 pg_trgm.similarity_threshold 控制,默认 0.3。其他常见扩展速览:pgcrypto 提供加密与哈希函数;hstore 是键值对类型;uuid-ossp 生成 UUID(PG 13 起内置的 gen_random_uuid() 已覆盖多数需求);citext 是大小写不敏感的文本类型。pg_stat_statements 统计最耗时的 SQL,但它要先在 postgresql.conf 的 shared_preload_libraries 中预加载并重启才能创建。PostGIS 则是不属于 contrib 的独立地理空间项目:几何类型、GiST 空间索引和成套 ST_* 函数,把关系数据库变成 GIS 平台,可从 postgis.net 获取安装包(当前稳定版 3.6.3,支持 PostgreSQL 12~18),装好后用 CREATE EXTENSION postgis; 和 postgis_full_version() 验证。

常见误区

  • 把物化视图当实时视图用:它是快照,底层变了必须手动 REFRESH(生产中常配合定时任务);默认 REFRESH 在大结果集上会阻塞并发读,高峰期刷新可能让查询突然卡住。
  • REFRESH ... CONCURRENTLY 前忘建唯一索引:且索引必须"只由列名构成"(非表达式、非部分索引),否则直接报错。
  • BEFORE 行级触发器忘写 RETURN NEW:该行 INSERT/UPDATE 悄悄失效且不报错,是最难排查的触发器 bug;每个分支都要显式 RETURN。
  • 把 jsonb 当"原样存储":空白与键顺序会丢失,重复键只留最后一个;要逐字保留原文请用 json。
  • 建了 GIN 索引却不走索引:col->>'k' = 'x' 不在 GIN 支持的操作符之列,应改写成 @> 包含查询;表达式索引还要求查询表达式与建索引时逐字一致。
  • 分区表只按 id 建主键:主键/唯一键必须包含分区键;忘了建下月分区时插入会报错,应提前建分区或加 DEFAULT 兜底;UPDATE 改动分区键使行跨分区移动时,内部实际按 DELETE+INSERT 执行——AFTER UPDATE 触发器不再触发,取而代之的是 AFTER DELETE/INSERT 触发器(BEFORE UPDATE 仍会先触发)。
  • 以为分区越多越快:分区过多会拉长规划时间、增加内存开销;分区键选 WHERE 最常出现的列,以真实负载测试。
  • 全文检索建索引与查询表达式不一致:索引写 to_tsvector('english', ...)、查询写单参数形式,索引不命中;把文本直接 cast 成 tsvector 会跳过词干归一。
  • 把 JSONB 当万能方案:失去外键、类型约束与统计信息,查询越写越慢;高频查询和关联字段应提升为普通列。
  • 在 PL/pgSQL 里写 SELECT 忘了 INTO:报 "query has no destination for result data";要返回查询结果用 RETURN QUERY。
  • 照老教程写 EXECUTE PROCEDURE:仍兼容但已被官方标记为废弃写法,新代码用 EXECUTE FUNCTION。

参考来源

  1. PostgreSQL 官方文档 §3.2 Views(教程:视图的定义、"视图是良好设计的关键一环"):官方资料
  2. PostgreSQL 官方文档 CREATE VIEW(可更新视图的条件清单、WITH CHECK OPTION、INSTEAD OF 触发器):官方资料
  3. PostgreSQL 官方文档 CREATE MATERIALIZED VIEW 与 REFRESH MATERIALIZED VIEW(快照特性、默认刷新阻塞读、CONCURRENTLY 的唯一索引前提;PG 16 与 17+ 权限差异):官方资料
  4. PostgreSQL 官方文档 第 41 章 PL/pgSQL 概述(默认安装、DECLARE/BEGIN/END、服务器端封装的性能收益)与 CREATE PROCEDURE(CALL、过程内事务控制):官方资料、官方资料
  5. PostgreSQL 官方文档 第 37 章 Triggers(触发器定义、行级/语句级、BEFORE/AFTER/INSTEAD OF、按名称字母序执行)与 CREATE TRIGGER 语法页(EXECUTE FUNCTION 与废弃的 PROCEDURE 写法):官方资料、官方资料
  6. PostgreSQL 官方文档 §8.14 JSON Types(json 与 jsonb 的区别、规范化行为、->/->>/@>/? 操作符、GIN 的 jsonb_ops 与 jsonb_path_ops、表达式索引、"结构相对固定、限制文档大小"的设计建议):官方资料
  7. PostgreSQL 官方文档 §5.12 Table Partitioning(适用规模经验法则、RANGE/LIST/HASH、唯一约束须含分区键、分区裁剪、DETACH/ATTACH、分区数量最佳实践):官方资料
  8. PostgreSQL 官方文档 第 12 章 Full Text Search(LIKE 的三大不足、lexeme、tsvector/tsquery、表达式索引须显式指定配置、生成列方案):官方资料、官方资料
  9. PostgreSQL 官方文档 F.35 pg_trgm(三元组原理、similarity 与 %、GIN/GiST 索引加速 LIKE)与附录 F Additional Supplied Modules(CREATE EXTENSION 机制、trusted 扩展清单、pg_stat_statements 需预加载):官方资料、官方资料
  10. PostgreSQL 官方版本支持政策页(核实 2026-10 当前支持版本 14~18,PG 18 为最新稳定大版本):官方资料
  11. PostgreSQL 14.0 Release Notes(CREATE PROCEDURE/CALL、EXECUTE FUNCTION 为 PG 11 引入;DETACH CONCURRENTLY 为 PG 14 引入):官方资料
  12. PostGIS 官方网站(空间类型、GiST 空间索引、ST_* 函数;当前稳定版 3.6.3,支持 PostgreSQL 12~18):官方资料

最后更新于

本页目录