常用扩展(pg_stat_statements / pgcrypto)
PostgreSQL 官方发行版附带的 contrib 扩展包,是「装完数据库就该顺手装上」的一批工具。它们覆盖性能诊断、加密、模糊匹配、UUID 生成、数组加速等日常高频场景,而且大多是 trusted 扩展 —— 普通用户在业务库里就能自己装。
但 contrib 里几十个扩展鱼龙混杂:有的必须写进 shared_preload_libraries 才能用,有的装了之后 pg_dump 行为会变,有的(如 tsearch2)早已废弃却被老教程反复推荐。本篇按场景挑出最常用的一批,逐个给出语法、示例与踩坑提示,让你选型时一眼看清「该装哪个、怎么装、有什么代价」。
前置知识
建议先阅读 CREATE EXTENSION 扩展管理,理解扩展的安装语法、trusted 属性、shared_preload_libraries 与 schema 归属规则。
基础概念
contrib 是什么
contrib 是 PostgreSQL 源码树里的 contrib/ 目录,编译安装后被打包成 postgresql-contrib(Debian/Ubuntu)或 postgresqlXX-contrib(RHEL 系)之类的附加包。这些扩展随官方源码发布、由官方维护,但不在内核里,需要显式安装并 CREATE EXTENSION。
-- 有输出说明 contrib 已安装
SELECT count(*) FROM pg_available_extensions;# Debian / Ubuntu
dpkg -l | grep postgresql-contrib
# RHEL / Rocky / AlmaLinux
rpm -qa | grep postgresql.*contrib
# macOS (Homebrew)
brew list | grep postgresql按场景分类速查
| 场景 | 扩展 | 是否需要预加载 | 是否 trusted |
|---|---|---|---|
| SQL 性能统计 | pg_stat_statements | 必须 | 否 |
| 加密与哈希 | pgcrypto | 否 | 是 |
| UUID 生成 | uuid-ossp / 内置 gen_random_uuid() | 否 | 是 |
| 键值对存储 | hstore | 否 | 是 |
| 模糊/相似匹配 | pg_trgm | 否 | 是 |
| 大小写不敏感文本 | citext | 否 | 是 |
| 音标去重 | unaccent | 否 | 是 |
| 索引类型扩展 | btree_gin / btree_gist | 否 | 是 |
| 数组与集合 | intarray / tablefunc | 否 | 是 |
| 缓存预热 | pg_prewarm | 通常需要 | 否 |
| 索引/数据校验 | amcheck | 否 | 否 |
| 执行计划日志 | auto_explain | 必须 | 否 |
| 缓冲区/页面观察 | pg_buffercache / pageinspect / pg_visibility | 通常需要 | 否 |
| 地理距离(轻量) | earthdistance + cube | 否 | 是 |
| 外部数据包装 | postgres_fdw / file_fdw | 否 | 否 |
| 大对象 / 网络类型 | lo / isn | 否 | 是 |
安装与查看
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS hstore;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS btree_gin;
CREATE EXTENSION IF NOT EXISTS btree_gist;
-- "uuid-ossp" 名字里带连字符,必须加双引号
CREATE EXTENSION uuid-ossp;
-- ERROR: syntax error at or near "-"
CREATE EXTENSION "uuid-ossp";
-- CREATE EXTENSION
-- 查看已安装扩展及版本
SELECT name, default_version, installed_version
FROM pg_available_extensions
WHERE installed_version IS NOT NULL
ORDER BY name;
-- citext 1.6 / hstore 1.8 / pg_trgm 1.6 / pg_stat_statements 1.11 / pgcrypto 1.3 / "uuid-ossp" 1.1语法与用法
pg_stat_statements 的启用流程
这是唯一「必须三步骤」的常用扩展:
# 步骤 1:写配置(在 postgresql.conf 里,或用 ALTER SYSTEM)
psql -c "ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements'"
# 步骤 2:重启(reload 不够!)
pg_ctl restart -D $PGDATA-- 步骤 3:在目标库里创建扩展
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- 验证
SELECT count(*) FROM pg_stat_statements;pgcrypto 的四种能力
CREATE EXTENSION IF NOT EXISTS pgcrypto;
SELECT encode(digest('hello', 'sha256'), 'hex'); -- 1. 单向哈希
SELECT encode(hmac('message', 'secret-key', 'sha256'), 'hex'); -- 2. HMAC 签名
SELECT crypt('my_password', gen_salt('bf', 12)); -- 3. 密码哈希(加盐 + 慢哈希)
SELECT pgp_sym_encrypt('机密内容', 'passphrase'); -- 4. 对称加密pg_trgm 的相似度查询
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SELECT similarity('postgresql', 'postgres'); -- 相似度打分(0~1)
SELECT 'postgresql' % 'postgres'; -- true(% 即阈值判断)
SELECT 'postgresql' % 'mysql'; -- false
-- 模糊搜索 + 索引
CREATE INDEX idx_users_name_trgm ON users USING gin (name gin_trgm_ops);
SELECT * FROM users WHERE name % 'jon' ORDER BY similarity(name, 'jon') DESC LIMIT 10;hstore 的键值对操作
CREATE EXTENSION IF NOT EXISTS hstore;
SELECT 'a=>1, b=>2'::hstore; -- 构造
SELECT hstore(ARRAY['a','b'], ARRAY['1','2']); -- 从数组构造
SELECT ('a=>1'::hstore) -> 'a'; -- 取值
SELECT ('a=>1'::hstore) ? 'a'; -- 是否存在键
SELECT ('a=>1, b=>2'::hstore) ?& ARRAY['a','b']; -- 是否同时存在
SELECT hstore_to_json('a=>1, b=>2'::hstore); -- 转 JSON详细说明
pg_stat_statements:语句统计的完整视角
这是生产环境第一个该装的扩展。它聚合所有被执行的 SQL,给出调用次数、累计耗时、I/O、WAL 等指标。注意 PostgreSQL 13 起 total_time 已改名为 total_exec_time,规划耗时拆到 total_plan_time。
关键列:
| 列 | 含义 |
|---|---|
userid / dbid | 执行者 / 数据库的 OID |
toplevel | 是否顶层语句(非函数内嵌套) |
queryid / query | 归一化后的语句指纹 / 文本 |
calls | 执行次数 |
total_plan_time / mean_plan_time | 规划总耗时 / 平均耗时(毫秒) |
total_exec_time / mean_exec_time / min_exec_time / max_exec_time | 执行耗时统计(毫秒) |
rows | 返回或影响的行数 |
shared_blks_hit / shared_blks_read / shared_blks_written | 共享缓冲区计数 |
temp_blks_read / temp_blks_written | 临时文件块读写 |
wal_records / wal_fpi / wal_bytes | WAL 相关 |
jit_* | JIT 编译统计 |
最常用的「找最耗资源的 SQL」查询,以及按「平均耗时高且调用多」「产生大量临时文件」两种口径筛选:
-- 1) 总耗时 top 20
SELECT calls,
round(total_exec_time::numeric, 2) AS total_ms,
round(mean_exec_time::numeric, 2) AS mean_ms,
round(100 * total_exec_time / sum(total_exec_time) OVER (), 2) AS pct,
rows, left(query, 80) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 20;
-- 2) 平均耗时高且调用多
SELECT calls, round(mean_exec_time::numeric, 2) AS mean_ms, left(query, 80)
FROM pg_stat_statements
WHERE calls > 100
ORDER BY mean_exec_time DESC LIMIT 20;
-- 3) 产生临时文件(work_mem 可能不够)
SELECT calls, temp_blks_read + temp_blks_written AS temp_blks, left(query, 80)
FROM pg_stat_statements
WHERE temp_blks_read + temp_blks_written > 0
ORDER BY temp_blks DESC LIMIT 20;pg_stat_statements_reset(userid Oid, dbid Oid, queryid bigint, minmax_only boolean) 可整体或部分清零:
SELECT pg_stat_statements_reset(); -- 全部清零
SELECT pg_stat_statements_reset(minmax_only => true); -- 只清 min/max 统计重要安全限制:只有超级用户和 pg_read_all_stats 角色能看到别人的 SQL 文本;其他用户看到的同类语句,query 列会被替换成 <insufficient privilege>,可这样授权:
GRANT pg_read_all_stats TO app_developer;跟踪规模相关 GUC(改这些同样需要重启):
pg_stat_statements.max 默认 5000,内存中保留的语句数
pg_stat_statements.track 默认 top,可选 all / none
pg_stat_statements.track_utility 是否跟踪 DDL 等工具类语句
pg_stat_statements.track_planning 是否跟踪规划耗时
pg_stat_statements.save 重启后是否保留统计pgcrypto:哈希、加盐与加密
pgcrypto 是 trusted 扩展,普通用户即可安装。
SELECT encode(digest('hello', 'sha256'), 'hex') AS hex;
SELECT encode(digest('hello', 'sha256'), 'base64') AS b64;
SELECT encode(hmac('order_id=123&amount=99', 'my-secret', 'sha256'), 'hex');digest(data, type) 支持的 type:md5、sha1、sha224、sha256、sha384、sha512。密码哈希(推荐做法):
SELECT crypt('user_password', gen_salt('bf', 12));gen_salt(type [, iter_count]) 支持的算法:
| type | 算法 | 说明 |
|---|---|---|
bf | Blowfish | 可调迭代,推荐;默认迭代 6 |
sha256crypt / sha512crypt | SHA-2 crypt | 默认迭代 5000 |
md5 / xdes / des | 老式算法 | 弱,不推荐 |
对称加密与算法选项:
SELECT pgp_sym_decrypt(pgp_sym_encrypt('机密', 'passphrase'), 'passphrase');
SELECT pgp_sym_encrypt(
'信用卡号 4111-1111-1111-1111',
'passphrase',
'cipher-algo=aes256, compress-algo=1, s2k-mode=3'
);| 选项 | 默认 | 可选值 |
|---|---|---|
cipher-algo | aes128 | bf、aes128、aes192、aes256、3des、cast5 |
compress-algo | 0(不压缩) | 1(ZIP)、2(ZLIB) |
s2k-mode | 3 | 0~3 |
ASCII armor(把二进制转文本,便于放进文本字段,用 armor() / dearmor()):
SELECT armor(pgp_sym_encrypt('secret', 'passphrase'));
SELECT pgp_sym_decrypt(dearmor(armored_text), 'passphrase');uuid-ossp 与内置 gen_random_uuid()
PostgreSQL 13 起内置了 gen_random_uuid(),无需任何扩展,这是首选。需要 v1/v5 等版本时才装 uuid-ossp:
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
amount numeric NOT NULL
);需要 v1/v5 等版本时才装 uuid-ossp:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SELECT uuid_generate_v1(); -- 基于 MAC + 时间,可泄露主机信息
SELECT uuid_generate_v1mc(); -- v1 变体,随机多播 MAC
SELECT uuid_generate_v4(); -- 随机
SELECT uuid_generate_v5(uuid_ns_url(), 'https://example.com'); -- 基于名字的 SHA-1| 函数 | 版本 | 随机性 | 是否需要扩展 | 建议 |
|---|---|---|---|---|
gen_random_uuid() | v4 | 强随机 | 否 | 默认选它 |
uuid_generate_v4() | v4 | 强随机 | 是 | 老代码兼容 |
uuid_generate_v5() | v5 | 确定性(同名同值) | 是 | 需要幂等 ID 时用 |
UUID 主键的代价:随机分布导致 B-tree 插入位置随机,写放大明显。高写入量场景可考虑时间有序 UUID。
pg_trgm:模糊匹配与相似检索
pg_trgm 的核心是「三元组(trigram)」:把字符串切成连续的 3 字符片段,用片段交集衡量相似度。
SELECT show_trgm('postgres'); -- {" p"," po",gre,ost,"p ",pos,res,sgr,tgr}
SELECT similarity('postgresql', 'postgres'); -- 约 0.545
SELECT word_similarity('postgres', 'PostgreSQL 18'); -- 词级相似度操作符:
| 操作符 | 含义 |
|---|---|
% | 相似度超过 pg_trgm.similarity_threshold |
<% / %> | 词级相似(以某一侧为参照) |
<<% / %>> | 严格词级相似 |
<-> | 距离(1 - 相似度),可 ORDER BY |
阈值由 GUC 控制(旧版的 set_limit() / show_limit() 已废弃):
SHOW pg_trgm.similarity_threshold; -- 默认 0.3
SHOW pg_trgm.word_similarity_threshold; -- 默认 0.6
SET pg_trgm.similarity_threshold = 0.4;两种索引各有侧重,建好索引后分别这样查:
-- GIN:查询更快、构建更慢,不支持 <-> KNN 排序;适合 name % 'x' 模糊搜索
CREATE INDEX idx_name_gin ON users USING gin (name gin_trgm_ops);
SELECT id, name, similarity(name, 'johnson') AS sim
FROM users WHERE name % 'johnson' ORDER BY sim DESC LIMIT 10;
-- GiST:支持 ORDER BY <-> 近邻排序,精度受 siglen 影响
CREATE INDEX idx_name_gist ON users USING gist (name gist_trgm_ops(siglen=32));
SELECT id, name FROM users ORDER BY name <-> 'johnson' LIMIT 10;hstore:无需 schema 的键值对
hstore 存的是「text → text」的映射,适合「属性数量不固定」的场景,例如商品的自定义标签。
CREATE TABLE products (
id bigserial PRIMARY KEY,
name text NOT NULL,
attrs hstore
);
INSERT INTO products (name, attrs) VALUES
('手机', 'color=>黑色, storage=>256G'),
('笔记本', 'color=>银色, ram=>16G, storage=>512G');操作符与函数速查:
| 操作符 | 含义 |
|---|---|
-> | 取单个键的值 |
? | 是否存在某键 |
?& | 是否同时存在所有键 |
?| | 是否存在任一键 |
@> / <@ | 是否包含 / 被包含 |
- | 删除键(可传数组) |
|| | 合并 |
#= | 更新指定键 |
SELECT akeys(attrs), avals(attrs) FROM products WHERE id = 1;
SELECT * FROM each('a=>1, b=>2'::hstore); -- 每行一个键值对
SELECT hstore_to_jsonb(attrs) FROM products WHERE id = 1;
SELECT exist('a=>1'::hstore, 'a'); -- 键是否存在
SELECT defined('a=>NULL'::hstore, 'a'); -- 键存在且值非 NULL
-- GIN:适合 @> 包含查询与 ? 存在查询
CREATE INDEX idx_products_attrs_gin ON products USING gin (attrs);hstore 的边界:只能存文本,没有嵌套结构。经验法则 —— 结构固定且有查询需求 → 建列;结构松散且整体读写 → jsonb;扁平键值对且需要 @> 索引 → hstore。
btree_gin / btree_gist:让组合索引覆盖多类型
问题场景:btree 索引不能直接和 gin 混用。假如要在 (status, tags) 上建索引,status 是 text(适合 btree),tags 是 text[](需要 GIN),单一索引类型满足不了。
CREATE EXTENSION IF NOT EXISTS btree_gin;
CREATE TABLE articles (
id bigserial PRIMARY KEY,
status text,
tags text[],
title text
);
-- GIN 里同时支持标量等值与数组包含
CREATE INDEX idx_articles_gin ON articles USING gin (status, tags);
EXPLAIN ANALYZE
SELECT * FROM articles
WHERE status = 'published' AND tags @> ARRAY['postgresql'];btree_gist 面向 GiST,更重要的能力是支持排他约束(EXCLUDE constraint)与区间类型组合:
CREATE EXTENSION IF NOT EXISTS btree_gist;
-- 会议室预订:同房间同时间段不允许重叠
CREATE TABLE room_bookings (
room_id int,
during tsrange,
EXCLUDE USING gist (room_id WITH =, during WITH &&)
);
INSERT INTO room_bookings VALUES (1, '[2026-01-01 09:00, 2026-01-01 10:00)');
INSERT INTO room_bookings VALUES (1, '[2026-01-01 09:30, 2026-01-01 10:30)');
-- ERROR: conflicting key value violates exclusion constraint这是「会议室/车位/资源预订」类的经典实现,纯内核做不到。
文本处理:citext 与 unaccent
原生 text 比较是大小写敏感的,常见做法是建函数索引:
CREATE UNIQUE INDEX idx_users_email_lower ON users (lower(email));citext 是一个大小写不敏感的字符串类型,让这件事更自然:
CREATE EXTENSION IF NOT EXISTS citext;
CREATE TABLE accounts (
id bigserial PRIMARY KEY,
email citext UNIQUE NOT NULL
);
INSERT INTO accounts (email) VALUES ('Alice@Example.com');
INSERT INTO accounts (email) VALUES ('alice@example.com');
-- ERROR: duplicate key value violates unique constraint
SELECT * FROM accounts WHERE email = 'ALICE@EXAMPLE.COM'; -- 命中代价:每次比较都要调用 lower(),比原生 text 慢,且行为依赖 lc_ctype / 排序规则。
unaccent 处理多语言数据:用户输入 cafe 而数据里是 café。
CREATE EXTENSION IF NOT EXISTS unaccent;
SELECT unaccent('café'); -- cafe
SELECT unaccent('München'); -- Munchen
SELECT unaccent('Ångström'); -- Angstrom它还可以作为文本搜索字典使用:
CREATE TEXT SEARCH DICTIONARY my_unaccent (TEMPLATE = unaccent, RULES = 'unaccent');
CREATE TEXT SEARCH CONFIGURATION my_search (COPY = simple);
ALTER TEXT SEARCH CONFIGURATION my_search
ALTER MAPPING FOR hword, hword_part, word
WITH my_unaccent, simple;
SELECT to_tsvector('my_search', 'café au lait');在索引里用它需要包一层(unaccent 的 volatility 可能不满足函数索引要求):
CREATE OR REPLACE FUNCTION f_unaccent(text)
RETURNS text
LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT AS $$
SELECT public.unaccent('public.unaccent', $1)
$$;
CREATE INDEX idx_places_name_unaccent ON places (f_unaccent(lower(name)));这是在绕过
IMMUTABLE检查,需要你自行保证规则文件不变。
观察与诊断:pg_buffercache / pageinspect / amcheck / pg_prewarm / auto_explain
这一类扩展直接读取物理文件与共享内存,因此大多需要超级用户,且主要用于排障而非业务。
pg_buffercache 看共享缓冲区里缓存了什么:
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
SELECT c.relname, count(*) AS buffers,
pg_size_pretty(count(*) * current_setting('block_size')::bigint) AS size,
round(avg(usagecount), 2) AS avg_usage
FROM pg_buffercache b JOIN pg_class c ON c.relfilenode = b.relfilenode
GROUP BY 1 ORDER BY buffers DESC LIMIT 20;pageinspect 直接读数据页头部,用于验证页结构与诊断损坏;pg_visibility 检查可见性映射(VM)判断 VACUUM 效果;pgrowlocks 看行锁;pg_freespacemap 看页内空闲空间:
CREATE EXTENSION IF NOT EXISTS pageinspect;
CREATE EXTENSION IF NOT EXISTS pg_visibility;
CREATE EXTENSION IF NOT EXISTS pgrowlocks;
SELECT * FROM page_header(get_raw_page('users', 0)); -- lsn / checksum / flags / lower / upper / ...
SELECT * FROM pg_visibility_map_summary('users'); -- 全可见/全冻结页统计
SELECT * FROM pgrowlocks('accounts');amcheck 用来校验索引/堆的物理一致性,是排查「索引损坏」的利器:
CREATE EXTENSION IF NOT EXISTS amcheck;
SELECT bt_index_check('idx_users_name'); -- 结构校验
SELECT bt_index_check('idx_users_name', heapallindexed => true); -- 额外校验堆-索引一致性
SELECT bt_index_parent_check('idx_users_name'); -- 校验父子页一致性遇到损坏时的处理思路:先 REINDEX INDEX ...;若也失败,用 pg_dump 导出重建,并同步检查硬件与日志里的 checksum failure。
pg_prewarm 把表或索引主动读进缓存,避免冷启动时的第一批慢查询;auto_explain 自动把慢查询的执行计划写进日志,无需改应用代码:
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
SELECT pg_prewarm('users'); -- 读进 OS 缓存
SELECT pg_prewarm('users', 'buffer'); -- 读进共享缓冲区# postgresql.conf,两者都需放进 shared_preload_libraries
pg_prewarm.autoprewarm = on
pg_prewarm.autoprewarm_interval = 300s
auto_explain.log_min_duration = '1s'
auto_explain.log_analyze = on
auto_explain.log_buffers = on
auto_explain.log_format = 'json'auto_explain 也可以按会话临时打开(前提是已预加载):
LOAD 'auto_explain';
SET auto_explain.log_min_duration = '0';
SET auto_explain.log_analyze = on;
SELECT count(*) FROM big_table;日志中会得到形如 LOG: duration: 1234.567 ms plan: {...} 的 JSON 计划。
轻量地理:cube 与 earthdistance
不想上 PostGIS 的重量级部署时,cube + earthdistance 可以做「经纬度点之间的距离计算」与「就近检索」。
CREATE EXTENSION IF NOT EXISTS cube;
CREATE EXTENSION IF NOT EXISTS earthdistance;
CREATE TABLE shops (
id bigserial PRIMARY KEY,
name text NOT NULL,
lat double precision NOT NULL,
lon double precision NOT NULL
);
INSERT INTO shops (name, lat, lon) VALUES ('天安门店', 39.9087, 116.3975);
-- ll_to_earth 把经纬度转成 cube 的 3D 点,从而能建 GiST 索引
CREATE INDEX idx_shops_earth ON shops USING gist (ll_to_earth(lat, lon));
-- 查找 5 公里内的门店:earth_box 用索引粗筛,earth_distance 精筛
SELECT name,
round(earth_distance(ll_to_earth(39.9087, 116.3975), ll_to_earth(lat, lon))::numeric, 0) AS meters
FROM shops
WHERE earth_box(ll_to_earth(39.9087, 116.3975), 5000) @> ll_to_earth(lat, lon)
AND earth_distance(ll_to_earth(39.9087, 116.3975), ll_to_earth(lat, lon)) < 5000
ORDER BY meters;这个「先箱后距」的模式是地理检索的通用套路,PostGIS 的 ST_DWithin 内部同理。earthdistance 的局限:只支持「点」,不支持多边形、线、相交判断;距离模型粗糙;没有 SRID 概念。需求一旦超出「点对点距离」,就该换 PostGIS。
数据访问与类型扩展:intarray / tablefunc / isn / tsearch2
这几个扩展或其同类(postgres_fdw、file_fdw、lo)的完整用法超出本篇范围,这里只做定位与最小示例。file_fdw 只读,且只能由超级用户创建外部表(因为能读服务器文件系统);postgres_fdw 用于跨 PostgreSQL 实例访问表,详见 FDW 性能调优;lo 提供早期的大对象客户端接口,新项目如需大对象优先用 bytea(详见 bytea 与 lo 的取舍)。
intarray 为 integer[] 提供集合运算与索引加速:
CREATE EXTENSION IF NOT EXISTS intarray;
SELECT sort(uniq(ARRAY[3,1,2,1])); -- {1,2,3}
SELECT ARRAY[1,2,3] & ARRAY[2,3,4]; -- 交集 {2,3}
SELECT ARRAY[1,2,3] | ARRAY[2,3,4]; -- 并集 {1,2,3,4}
SELECT ARRAY[1,2,3] @> ARRAY[1,2]; -- true
SELECT ARRAY[1,2,3] && ARRAY[3,4]; -- true(有交集)
CREATE INDEX idx_tags_gin ON articles USING gin (tag_ids gin__int_ops);tablefunc 提供交叉表(pivot)等表生成函数:
CREATE EXTENSION IF NOT EXISTS tablefunc;
SELECT *
FROM crosstab(
$$SELECT quarter, region, amount FROM sales ORDER BY 1, 2$$,
$$VALUES ('east'), ('west')$$
) AS ct(quarter text, east numeric, west numeric);
-- quarter | east | west
-- ---------+------+------
-- Q1 | 100 | 150
-- Q2 | 120 | 160isn 提供 ISBN、EAN、UPC、ISMN、ISSN 等标准编号类型,自带校验位验证:
CREATE EXTENSION IF NOT EXISTS isn;
SELECT '978-0-201-89683-8'::isbn;
SELECT isbn13('9780201896838');tsearch2 是 PostgreSQL 8.3 之前的内置全文检索扩展,早已废弃,新老项目都不应引入:内核从 8.3 起就内置了 tsvector / tsquery / GIN,能力完全覆盖且更优,而 tsearch2 的二进制在现代发行版里基本已不存在。
SELECT * FROM pg_available_extensions WHERE name = 'tsearch2';
-- 0 rows(现代版本里已无此扩展)
-- 正确姿势:用内核能力
SELECT to_tsvector('simple', 'PostgreSQL 全文检索 tutorial');
SELECT plainto_tsquery('simple', 'postgresql tutorial');
CREATE INDEX idx_articles_fts
ON articles USING gin (to_tsvector('simple', title || ' ' || body));实战示例
场景一:新项目标准扩展清单
-- 项目初始化脚本 db/00_extensions.sql(可重入)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- 性能诊断,需先预加载并重启
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- 加密
CREATE EXTENSION IF NOT EXISTS pg_trgm; -- 模糊匹配
CREATE EXTENSION IF NOT EXISTS unaccent; -- 音标去重
CREATE EXTENSION IF NOT EXISTS hstore; -- 键值对
CREATE EXTENSION IF NOT EXISTS citext; -- 大小写不敏感
CREATE EXTENSION IF NOT EXISTS btree_gin; -- 混合索引
-- 验证:查已装版本
SELECT name, installed_version FROM pg_available_extensions
WHERE installed_version IS NOT NULL ORDER BY name;场景二:慢 SQL 排查工作流
-- 1. 找总耗时 top 10(含占比)
SELECT calls, round(total_exec_time::numeric, 1) AS total_ms,
round(100 * total_exec_time / sum(total_exec_time) OVER (), 2) AS pct,
left(query, 100) AS query
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;
-- 2. 针对可疑 queryid 看完整语句与详细指标
SELECT * FROM pg_stat_statements WHERE queryid = 1234567890123456789;
-- 3. 重置并观察优化效果
SELECT pg_stat_statements_reset();场景三:用户密码表完整实现
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS citext;
CREATE TABLE accounts (
id bigserial PRIMARY KEY,
email citext UNIQUE NOT NULL,
password_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 注册:校验长度后用 crypt + gen_salt 存慢哈希
CREATE OR REPLACE FUNCTION register_account(p_email text, p_password text)
RETURNS bigint LANGUAGE plpgsql AS $$
DECLARE v_id bigint;
BEGIN
IF length(p_password) < 8 THEN RAISE EXCEPTION 'password too short'; END IF;
INSERT INTO accounts (email, password_hash)
VALUES (p_email, crypt(p_password, gen_salt('bf', 12)))
RETURNING id INTO v_id;
RETURN v_id;
END;
$$;
-- 登录校验:用存的哈希做 salt 重算,相等即通过
CREATE OR REPLACE FUNCTION verify_account(p_email text, p_password text)
RETURNS boolean LANGUAGE sql STABLE AS $$
SELECT EXISTS (SELECT 1 FROM accounts
WHERE email = p_email AND password_hash = crypt(p_password, password_hash));
$$;
SELECT register_account('Alice@Example.com', 's3cretpassword');
SELECT verify_account('alice@example.com', 's3cretpassword'); -- true
SELECT verify_account('alice@example.com', 'wrong'); -- false场景四:会议室预订排他约束
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE rooms (
id int PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE bookings (
id bigserial PRIMARY KEY,
room_id int NOT NULL REFERENCES rooms(id),
during tsrange NOT NULL,
who text NOT NULL,
EXCLUDE USING gist (room_id WITH =, during WITH &&)
);
INSERT INTO rooms VALUES (1, 'A 会议室'), (2, 'B 会议室');
-- 正常预订
INSERT INTO bookings (room_id, during, who)
VALUES (1, '[2026-02-01 09:00, 2026-02-01 10:00)', 'alice');
-- 相邻不重叠,允许
INSERT INTO bookings (room_id, during, who)
VALUES (1, '[2026-02-01 10:00, 2026-02-01 11:00)', 'bob');
-- 重叠,被拒
INSERT INTO bookings (room_id, during, who)
VALUES (1, '[2026-02-01 09:30, 2026-02-01 10:30)', 'carol');
-- ERROR: conflicting key value violates exclusion constraint
-- 换个会议室就允许
INSERT INTO bookings (room_id, during, who)
VALUES (2, '[2026-02-01 09:30, 2026-02-01 10:30)', 'carol');场景五:就近检索(earthdistance)
CREATE EXTENSION IF NOT EXISTS cube;
CREATE EXTENSION IF NOT EXISTS earthdistance;
CREATE TABLE stores (
id bigserial PRIMARY KEY,
name text NOT NULL,
lat double precision NOT NULL,
lon double precision NOT NULL
);
INSERT INTO stores (name, lat, lon) VALUES
('北京国贸店', 39.9087, 116.3975),
('北京中关村店', 39.9836, 116.3164),
('上海外滩店', 31.2397, 121.4900),
('天津和平路店', 39.1285, 117.2015);
CREATE INDEX idx_stores_earth ON stores USING gist (ll_to_earth(lat, lon));
-- 以北京国贸为中心,找 30 公里内的门店
WITH center AS (
SELECT ll_to_earth(39.9087, 116.3975) AS p
)
SELECT s.name,
round(earth_distance(center.p, ll_to_earth(s.lat, s.lon))::numeric / 1000, 2) AS km
FROM stores s, center
WHERE earth_box(center.p, 30000) @> ll_to_earth(s.lat, s.lon)
AND earth_distance(center.p, ll_to_earth(s.lat, s.lon)) < 30000
ORDER BY km;
-- 北京国贸店 0.00 km / 北京中关村店 8.63 km / 天津和平路店 118.65 km注意事项
注意一:pg_stat_statements 只写配置不重启,等于没装。
pg_ctl reload -D $PGDATA # 无效!reload 不会加载共享库
pg_ctl restart -D $PGDATA # 这才有效注意二:tsearch2 早已废弃,不要照抄老教程。 改用内置的 tsvector / tsquery / GIN:
-- 错误!现代 PostgreSQL 里根本没有这个扩展
CREATE EXTENSION tsearch2;
-- ERROR: extension "tsearch2" is not available注意三:set_limit() 已废弃。 旧 API 应替换为 GUC:
-- 不推荐(旧 API)
SELECT set_limit(0.4);
-- 推荐
SET pg_trgm.similarity_threshold = 0.4;注意四:GIN 索引不支持 trigram 的 KNN 排序。 gin_trgm_ops 配 GIN,gist_trgm_ops 配 GiST,写错会直接报错:
-- 错误!GIN 不支持 <-> 排序
CREATE INDEX idx_bad ON users USING gin (name gist_trgm_ops);注意五:pgcrypto 的 crypt() 不能用于通用哈希。 通用哈希用 digest(),密码存储用 crypt(..., gen_salt('bf', 12)):
-- 错误!crypt 是密码哈希格式专用
SELECT crypt('hello', 'sha256');注意六:随机 UUID 做主键会放大写压力。 高写入表不建议直接用全随机 UUID 主键:
-- 不推荐
CREATE TABLE events (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), ...);注意七:citext 只对声明为 citext 的列生效。 列定义成 text 时不会改变比较行为:
-- 错误!列是 text,citext 不会改变比较行为
CREATE TABLE t (email text); -- 应为 citext注意八:unaccent 用于函数索引需要包一层 IMMUTABLE 包装函数(见前文 f_unaccent):
-- 错误!unaccent 的 volatility 可能达不到索引要求
CREATE INDEX idx_bad ON places (unaccent(lower(name)));注意九:pg_buffercache / pageinspect 等观察类扩展需要超级用户。
-- 普通用户执行
SELECT * FROM pg_buffercache;
-- ERROR: permission denied for function pg_buffercache_pages注意十:不要在生产高峰用 pg_prewarm 预热超大表。 会把大量数据挤进缓存、冲击正常业务:
-- 不推荐
SELECT pg_prewarm('huge_table');最佳实践
把标准扩展清单写进项目初始化脚本(
CREATE EXTENSION IF NOT EXISTS),保证新环境一次到位、脚本可重入。pg_stat_statements在生产环境必装,并授权pg_read_all_stats给需要的开发/运维角色。密码存储只认
crypt(..., gen_salt('bf', N)),并用crypt(input, stored_hash)校验;永远不要自己拼哈希。UUID 主键优先用内置
gen_random_uuid(),除非必须 v1/v5 才引入uuid-ossp。模糊搜索用
pg_trgm的 GIN 索引;需要「按相似度排序」时才换 GiST 走 KNN。hstore与jsonb不要混用:二维扁平键值对用hstore,任何需要嵌套/数组/类型保留的场景用jsonb。区间排他约束用
btree_gist+EXCLUDE,而不是在应用层用「先查再插」的竞态逻辑。大小写不敏感只用在真正需要的列上(如 email),单字段用
lower()函数索引往往更可控。定期用
amcheck抽查核心业务的索引,尤其在发生过硬件告警、checksum failure 之后。地理需求做分级:「点对点距离 + 就近排序」用
earthdistance;一旦涉及多边形、线、相交、坐标系转换,直接上 PostGIS。观察类与诊断类扩展只给运维/DBA 使用,不要让业务账号依赖它们的输出。
不要在
template1里预装第三方重型扩展;contrib扩展相对安全,但版本升级仍会波及所有基于模板创建的新库。
下一节
下一篇 PostGIS 地理扩展 会进入第三方扩展的世界,从 geometry / geography 类型的选择,到 ST_DWithin 的就近检索与 <-> 空间 KNN 排序,把地理信息系统的核心用法讲清楚。
参考链接
- PostgreSQL 18 Documentation — Additional Supplied Modules
- PostgreSQL 18 Documentation — pg_stat_statements
- PostgreSQL 18 Documentation — pgcrypto
- PostgreSQL 18 Documentation — pg_trgm
- PostgreSQL 18 Documentation — hstore
- PostgreSQL 18 Documentation — btree_gin / btree_gist
- PostgreSQL 18 Documentation — citext
- PostgreSQL 18 Documentation — unaccent
- PostgreSQL 18 Documentation — earthdistance
- PostgreSQL 18 Documentation — amcheck
- PostgreSQL 18 Documentation — auto_explain