\l列出整个集群上所有数据库,带 owner、编码、collation、权限。psql 进去两眼一抹黑时第一条敲的命令。
⚠ 常见坑: 加 + 才能看到库体积(\l+)。库特别多时算 size 要扫一遍,可能卡住连接,卡了就退回 \l。
\l
\l+
\l app_* -- pattern match
PostgreSQL 速查表,80+ 命令和函数,psql、JSONB、CTE、窗口函数、索引、分区、高级扩展。
\l列出整个集群上所有数据库,带 owner、编码、collation、权限。psql 进去两眼一抹黑时第一条敲的命令。
⚠ 常见坑: 加 + 才能看到库体积(\l+)。库特别多时算 size 要扫一遍,可能卡住连接,卡了就退回 \l。
\l
\l+
\l app_* -- pattern match
\c dbname [user]在同一个 psql 会话里切到另一个库(可选另一个用户),不用退出重开。
⚠ 常见坑: 切库会丢会话状态。临时表、prepared statement、search_path 配置都跟旧连接一起没了。
\c production
\c app_db readonly_user
\c - postgres -- same db, new user
\dt [pattern]列当前库所有表,可加 pattern 过滤。\dt *.* 包含系统表,\dt+ 多出体积和描述。
\dt
\dt+
\dt public.user*
\dt myschema.*
\d table描述一张表:列、类型、默认值、索引、外键、触发器、check 约束、继承关系。psql 里用得最多的一条。
⚠ 常见坑: \d 不带参数会列所有 relation 包括序列和视图,通常不是你想要的。要看表用 \dt。
\d users
\d+ users -- with storage details
\d users_id_seq -- describe a sequence
\du列所有角色(用户 + 组)和属性:是不是 superuser、能不能建库 / 建角色 / 登录 / 复制、密码是否设置、过期时间。
\du
\du+ -- include description
\du app_*
\q退出 psql。等价于 exit、空行 Ctrl-D 或 \quit。
\q
\i file.sql执行一个 SQL 文件,路径是 psql 客户端机器上的路径,不是服务器上的。
⚠ 常见坑: \i 默认遇错不停(除非 SET ON_ERROR_STOP off)。生产脚本一定要 \set ON_ERROR_STOP on 并包 BEGIN / COMMIT。
\i schema.sql
\ir relative/migration.sql -- relative to current file
\set ON_ERROR_STOP on \i deploy.sql
\timing on开关每条 SQL 的执行耗时显示。"这条快不快?"先开 \timing 比直接 EXPLAIN 来得快。
\timing on
\timing off
\timing -- toggle
\x [on|off|auto]展开显示:宽行的每个列换行单独打印。\x auto 按终端宽度自动切换,窄行保持表格、宽行自动展开。
\x
\x on
\x auto
\! cmd在 psql 里跑 shell 命令,不用退出。\! 单独使用会开一个临时 subshell。
\! ls -la
\! pwd
\! -- enter subshell, type exit to return
\conninfo打印当前连接信息:用户、库、主机、端口、SSL 模式。跑任何破坏性操作前先 \conninfo 确认连对了集群。
\conninfo
\copy table FROM file通过客户端批量导入 / 导出 CSV。和服务器端 COPY 不同,\copy 不需要 superuser,读的是 psql 所在机器的文件。
⚠ 常见坑: 服务器端 COPY 更快(不用每行网络往返),但要 superuser 且文件必须在 DB 服务器上。从本机操作就用 \copy。
\copy users FROM 'users.csv' WITH CSV HEADER
\copy (SELECT * FROM orders WHERE status='paid') TO 'paid.csv' WITH CSV HEADER
\dn / \df\dn 列 schema(namespace)。\df 列用户定义函数(加 + 看源码,加 pattern 过滤)。\df S 包含系统函数。
\dn
\df
\df+ my_func
\df *json*
\e / \ef在 $EDITOR 里打开上一条查询(\e)或函数体(\ef 名字)。存盘退出就执行。改一条敲错的长多行语句最快的办法。
⚠ 常见坑: $EDITOR 要正常退出 psql 才能读回缓冲区。fork 到后台的编辑器(某些 GUI)会让 psql 卡住。默认不对劲就 \setenv PSQL_EDITOR vim。
\e
\ef my_function
\ev my_view -- edit a view definition
\watch [sec]每 N 秒(默认 2)重跑上一条查询。在 psql 里盯复制延迟、活跃连接、长任务进度的简易实时面板。
SELECT count(*) FROM pg_stat_activity; \watch 5
SELECT now(), pg_size_pretty(pg_database_size('app'));
\watch 10\set / \pset\set 定义 psql 变量和行为开关(ON_ERROR_STOP、AUTOCOMMIT)。\pset 控制输出格式(format、border、null 字符串、pager)。常用的写进 ~/.psqlrc。
\set ON_ERROR_STOP on
\pset null '∅' -- show NULL distinctly
\pset format wrapped
\gx / \g file\gx 只对这一次执行用展开模式跑当前查询缓冲区。\g file 把结果写到文件不打屏。都不用永久改显示设置。
SELECT * FROM users WHERE id = 1 \gx
SELECT * FROM orders \g /tmp/orders.txt
\dx / \di / \ds / \dv对象列表族:\dx 已装扩展、\di 索引、\ds 序列、\dv 视图、\dm 物化视图、\dp 表权限。加 + 看体积/细节,加 pattern 过滤。
\dx
\di+ users*
\dp orders -- who can do what on orders
\gexec把当前查询结果的每一行当成一条 SQL 执行。批量生成并跑 DDL(删所有表、重建所有索引)的地道写法,不用外部脚本语言。
⚠ 常见坑: 查询产出什么就原样执行什么,引号写错或字符串可注入就是自伤。先看 SELECT 输出确认无误,再补 \gexec。
SELECT format('REINDEX INDEX CONCURRENTLY %I;', indexname)
FROM pg_indexes WHERE schemaname = 'public'
\gexec\errverbose一条语句失败后,\errverbose 把最近一次错误带完整细节重打:SQLSTATE 码、schema/表/列/约束名、服务器源码位置。解析含糊的约束冲突时很有用。
\errverbose
text / varchar / char三种字符串类型。99% 情况只用 text 就行:无长度限制,存储和性能和 varchar 完全一样。varchar(n) 只多一个长度检查;char(n) 会补空格(基本永远用不到)。
⚠ 常见坑: varchar(20) 改 varchar(40) 只动元数据瞬时完成。varchar(20) 改 varchar(15) 要全表扫。直接用 text,长度约束放在应用层或者 CHECK。
CREATE TABLE users (id bigserial, name text, email text);
ALTER TABLE users ALTER COLUMN bio TYPE text;
int / bigint / smallint定长整数:smallint 2 字节(-3.2 万 ~ 3.2 万),int 4 字节(±21 亿),bigint 8 字节(±920 亿亿)。普通数字用 int,可能长大的表的 ID 列用 bigint。
⚠ 常见坑: ID 列用 int 是头号定时炸弹,21 亿行后写入直接挂。所有主键 day 1 就用 bigint(或 bigserial / bigint generated as identity)。
age smallint
user_id bigint
CREATE TABLE orders (id bigserial PRIMARY KEY);
serial / bigserial / identity自增整数。serial = int + 序列 + 默认值。bigserial 是 bigint 版本。PG 10+ 增加了 SQL 标准写法 GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY,更推荐用它。
⚠ 常见坑: 序列不在事务里。回滚的 INSERT 也消耗 ID,所以 serial 永远不是连续的。要真正连续的单号(比如发票)得另写一套生成器。
id bigserial PRIMARY KEY
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY -- SQL standard
uuid128 位通用唯一 ID,磁盘上 16 字节。PG 13+ 内置 gen_random_uuid(),老版本装 uuid-ossp 扩展。分布式系统客户端生成 ID 时首选。
⚠ 常见坑: UUID 当主键伤 B 树缓存局部性(随机插入序导致页分裂)。UUID v7(按时间有序)能解决,PG 18 才内置;现在自己手动前缀加时间戳。
id uuid DEFAULT gen_random_uuid() PRIMARY KEY
SELECT gen_random_uuid();
timestamp / timestamptztimestamp(即 timestamp without time zone)存裸日期时间,不带时区不做转换。timestamptz 内部存 UTC,I/O 时按会话时区转换。一律用 timestamptz。
⚠ 常见坑: 存 timestamp 然后在应用层算时差是经典时区 bug。一律用 timestamptz;时区只在显示边界 SET TIME ZONE。
created_at timestamptz NOT NULL DEFAULT now()
SELECT created_at AT TIME ZONE 'Asia/Shanghai' FROM orders;
interval时间区间(月、日、微秒)。可以直接和 timestamp 算:now() + interval '7 days',ts1 - ts2 出 interval。
⚠ 常见坑: interval 把月和日分开存,因为它们长度不固定(2 月 28/29 天)。计费别用 interval '1 month',改成显式天数算。
SELECT now() + interval '1 hour';
SELECT now() - interval '7 days';
DELETE FROM sessions WHERE created_at < now() - interval '30 days';
jsonb / json都存 JSON。jsonb 是二进制、可索引、键去重、键排序后的形式,99% 用它。json 保留原始输入字节和空白(审计日志要保留原文时才用)。
⚠ 常见坑: jsonb 不保留键顺序和空白。json 不支持大部分操作符(没有 @>、不能建 GIN 索引)。默认 jsonb,除非有特定理由用 json。
config jsonb NOT NULL DEFAULT '{}'INSERT INTO events (payload) VALUES ('{"type":"click","x":42}'::jsonb);array (text[], int[])PG 原生支持任意类型的数组。小规模、有序、基本只追加的集合用它(标签、角色列表、有序偏好),避免单建 join 表。
⚠ 常见坑: 频繁查询的多对多关系不要用 array 替代关联表。array 内元素不能加外键;更新会重写整个数组;GIN 索引能查包含关系但不能查顺序。
tags text[] DEFAULT '{}'SELECT * FROM posts WHERE tags @> ARRAY['rust'];
UPDATE posts SET tags = array_append(tags, 'wasm') WHERE id = 1;
enum / CREATE TYPE固定枚举值。比 text+CHECK 更省空间也更自文档化,带原生排序。CREATE TYPE status AS ENUM (...)。
⚠ 常见坑: 删除枚举值必须重建整个类型,设计时要慎重。ALTER TYPE ... ADD VALUE 是单向的。
CREATE TYPE order_status AS ENUM ('pending','paid','shipped','refunded');ALTER TYPE order_status ADD VALUE 'cancelled' AFTER 'pending';
numeric / decimal精确任意精度小数。存钱唯一正确的类型,float 会四舍五入丢分。numeric(precision, scale) 限定总位数和小数位;裸 numeric 不限。
⚠ 常见坑: numeric 运算慢(软件实现不走 CPU)。金额要重度聚合就存整数分(bigint),显示时再除。货币永远不用 real/double precision。
price numeric(12, 2) NOT NULL
SELECT 0.1::numeric + 0.2::numeric; -- exactly 0.3
SELECT round(amount, 2) FROM invoices;
booleantrue / false / NULL(三值逻辑)。输入接受多种字面量:TRUE/FALSE、't'/'f'、'yes'/'no'、'1'/'0'、'on'/'off'。存储一个字节。
⚠ 常见坑: 可空 boolean 有三态。WHERE active 会静默丢掉 NULL 行(NULL 不是 true)。要明确就 WHERE active IS NOT FALSE 或 coalesce(active, false)。
is_active boolean NOT NULL DEFAULT true
SELECT * FROM users WHERE is_active IS NOT FALSE;
date / timedate 是日历日(无时间无时区)。time 是一天内的钟点。time with time zone 存在但基本没用(脱离日期的时间没有真实偏移)。优先 date + timestamptz。
birth_date date
SELECT current_date, current_time;
SELECT date_trunc('month', created_at)::date FROM orders;range types (int4range, tsrange, daterange)内置范围类型存一段连续区间,带开闭边界。运算符:@> 包含、&& 重叠、-|- 相邻。配 EXCLUDE 约束可以防止预订时段重叠。
SELECT '[2026-05-01,2026-06-01)'::daterange @> '2026-05-15'::date;
CREATE TABLE bookings ( room_id int, during tsrange, EXCLUDE USING gist (room_id WITH =, during WITH &&) );
inet / cidr / macaddr网络地址类型。inet 存 IPv4/IPv6 主机(可带掩码),cidr 存网段,macaddr 存硬件地址。原生支持包含(<<=)、掩码、缩写。
client_ip inet
SELECT * FROM logs WHERE client_ip <<= '10.0.0.0/8'; -- inside subnet
SELECT '192.168.1.5/24'::inet + 10;
bytea原始二进制块。默认输出 hex 格式(\x...)。适合小二进制(缩略图、哈希、加密块);大文件存对象存储只在库里留 URL。
⚠ 常见坑: bytea 不支持流式读取,读时整块进内存。多 MB 的块会撑大行、拖慢 VACUUM、撑大备份。bytea 的值要小。
SELECT digest('hello', 'sha256'); -- needs pgcrypto, returns byteaSELECT encode(digest('hello', 'sha256'), 'hex');tsvector / tsquery全文搜索类型。tsvector 是文档解析成带位置的归一化词元;tsquery 是搜索表达式。用 @@ 匹配。配 GIN 索引才快。
SELECT to_tsvector('english', 'The quick brown fox') @@ to_tsquery('english', 'quick & fox');CREATE INDEX docs_fts ON docs USING GIN (to_tsvector('english', body));SELECT * FROM docs WHERE to_tsvector('english', body) @@ plainto_tsquery('english', 'brown fox');CREATE TABLE建新表。列 = 类型 + 可选默认值 + 约束(NOT NULL、UNIQUE、PRIMARY KEY、REFERENCES、CHECK)。永远声明主键,PG 不强制但不写迟早后悔。
CREATE TABLE users ( id bigserial PRIMARY KEY, email text UNIQUE NOT NULL, created_at timestamptz NOT NULL DEFAULT now() );
CREATE TABLE IF NOT EXISTS幂等建表,表已存在时静默成功。可能重跑的迁移脚本标配。
⚠ 常见坑: 只检查表存在,不查 schema。如果列变了,你拿到的是旧表,没告警。生产环境用真正的迁移工具。
CREATE TABLE IF NOT EXISTS audit_log (id bigserial PRIMARY KEY, action text);
ALTER TABLE改表:ADD / DROP / ALTER COLUMN、ADD / DROP CONSTRAINT、RENAME。每个子操作各取一次锁;多个子操作合在一条语句里只取一次锁。
⚠ 常见坑: ALTER TABLE ... ADD COLUMN col text DEFAULT 'x' 在 PG ≤10 会重写整张表。PG 11+ 非 volatile 默认值是元数据操作瞬时完成。volatile 默认值(比如 gen_random_uuid())仍然要重写。
ALTER TABLE users ADD COLUMN avatar_url text;
ALTER TABLE users DROP COLUMN deprecated_field;
ALTER TABLE users RENAME COLUMN nickname TO display_name;
ALTER TABLE users ADD CONSTRAINT users_email_lower CHECK (email = lower(email)) NOT VALID;
DROP TABLE [IF EXISTS] [CASCADE]删表。IF EXISTS 可重跑安全。CASCADE 同时删依赖对象(视图、指向此表的外键);不加 CASCADE 有依赖时 PG 拒绝。
⚠ 常见坑: CASCADE 不可逆,静默删掉所有依赖的视图、函数、外键。先用默认的 DROP ... RESTRICT 看看会破坏什么,再决定要不要核弹。
DROP TABLE IF EXISTS old_users;
DROP TABLE users CASCADE; -- careful
CREATE INDEX [CONCURRENTLY]建索引。普通 CREATE INDEX 持 ACCESS EXCLUSIVE 锁,整段时间堵写(老版本也堵读)。CONCURRENTLY 不堵写,耗时 2-3 倍但生产环境的唯一安全选项。
⚠ 常见坑: CREATE INDEX CONCURRENTLY 可能静默失败,留下 INVALID 索引。建完一定查:SELECT * FROM pg_indexes WHERE indexdef LIKE '%INVALID%'。失败就 DROP 重建。
CREATE INDEX CONCURRENTLY users_email_idx ON users (email);
CREATE UNIQUE INDEX CONCURRENTLY orders_uuid_uidx ON orders (uuid);
CREATE VIEW / MATERIALIZED VIEW视图 = 保存的查询,每次 SELECT 重跑。物化视图 = 查询结果落盘,手动 REFRESH MATERIALIZED VIEW 刷新。昂贵且可接受过期的聚合用物化视图。
⚠ 常见坑: REFRESH MATERIALIZED VIEW 会锁住读。要不堵读用 REFRESH MATERIALIZED VIEW CONCURRENTLY(视图上要有 UNIQUE 索引)。
CREATE VIEW active_users AS SELECT * FROM users WHERE deleted_at IS NULL;
CREATE MATERIALIZED VIEW daily_revenue AS
SELECT date_trunc('day', created_at) d, sum(amount) FROM orders GROUP BY 1;
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_revenue;GENERATED column从其他列算出来的列。STORED 落盘;VIRTUAL PG 还不支持(用视图代替)。
ALTER TABLE products ADD COLUMN search_text text GENERATED ALWAYS AS (lower(name) || ' ' || lower(coalesce(brand, ''))) STORED;
foreign key (REFERENCES)强制引用完整性。ON DELETE CASCADE 连带删子行,SET NULL 置空外键,RESTRICT/NO ACTION 阻止。一定给引用列建索引,PG 不会自动给外键列建索引。
⚠ 常见坑: 子表外键列没索引,每次删/改父行都要全表扫子表。这是规范化 schema 里头号隐形性能杀手。
ALTER TABLE orders ADD CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
CREATE INDEX orders_user_id_idx ON orders (user_id); -- index the FK!
ADD CONSTRAINT ... NOT VALID加 CHECK 或外键约束时不扫现有行(瞬时,只闪一下 ACCESS EXCLUSIVE 锁)。它立刻对新行/改动行生效;存量行之后用 VALIDATE CONSTRAINT 验证(只取 SHARE UPDATE EXCLUSIVE 锁)。
⚠ 常见坑: 两步走(先 NOT VALID 再 VALIDATE)是给大忙表加约束不长锁写的正解。漏了 VALIDATE,存量行就永远没被校验。
ALTER TABLE orders ADD CONSTRAINT amount_positive CHECK (amount > 0) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT amount_positive;
COMMENT ON给任意对象挂文档:表、列、函数、索引、约束。注释出现在 \d+、各种工具、pg_description 里。自文档化的 schema 几年后回本。
COMMENT ON COLUMN users.status IS 'one of: pending, active, banned (see app enum)';
COMMENT ON TABLE audit_log IS 'append-only; never UPDATE rows here';
CREATE SEQUENCE独立计数器。nextval() 推进,currval() 读本会话上一个值,setval() 重置。用 OWNED BY 把序列生命周期绑到某列上。
⚠ 常见坑: 序列按会话缓存值(CACHE n)提速,缓存了没用掉的值在断连时丢失,空洞更大。如果(误以为)需要接近连续,设 CACHE 1。
CREATE SEQUENCE invoice_no_seq START 1000 INCREMENT 1;
SELECT nextval('invoice_no_seq');CREATE SEQUENCE s OWNED BY mytable.id;
CREATE SCHEMA一个库内的命名空间,装表/视图/函数。多租户隔离、按环境分隔、按功能分组(比如 analytics.*、staging.*)都用 schema。用 search_path 控制不限定名字的解析。
CREATE SCHEMA IF NOT EXISTS analytics AUTHORIZATION analyst;
SET search_path TO analytics, public;
CREATE TABLE analytics.events (id bigserial PRIMARY KEY);
RENAME / SET SCHEMA重命名表/列/索引,或把对象移到别的 schema,都是元数据操作瞬时完成(短暂 ACCESS EXCLUSIVE 锁)。expand/contract 迁移模式的利器。
⚠ 常见坑: 重命名列会让所有引用旧名的视图、函数、应用查询失效,且没有编译期告警(PG 惰性解析名字)。改名前先全局搜代码。
ALTER TABLE users RENAME TO app_users;
ALTER INDEX users_email_idx RENAME TO app_users_email_idx;
ALTER TABLE events SET SCHEMA analytics;
UNIQUE / EXCLUDE constraintUNIQUE 禁止重复(NULL 默认允许且互不相等;PG 15+ 加了 NULLS NOT DISTINCT)。EXCLUDE 是其广义形式:"不存在两行在这些运算符下冲突",禁止时段重叠就靠它。
ALTER TABLE users ADD CONSTRAINT users_email_uq UNIQUE (email);
CREATE UNIQUE INDEX ON tags (lower(name)) NULLS NOT DISTINCT; -- PG 15+
ALTER TABLE reservations ADD CONSTRAINT no_overlap EXCLUDE USING gist (room WITH =, during WITH &&);
INSERT INTO ... VALUES插入一行或多行。批量 INSERT 比一行一条语句快得多(一次网络往返,一次 WAL flush)。
INSERT INTO users (email, name) VALUES ('a@x.com', 'Ada');INSERT INTO users (email, name) VALUES ('a@x.com','Ada'), ('b@x.com','Bob'), ('c@x.com','Cy');INSERT ... RETURNING取回刚插入的行,包括服务器生成的列(serial ID、默认值、生成列)。省一次 SELECT 往返。
INSERT INTO users (email) VALUES ('a@x.com') RETURNING id, created_at;INSERT INTO orders (user_id, amount) SELECT id, 0 FROM users WHERE created_at > now() - interval '1 day' RETURNING *;
INSERT ... ON CONFLICT (UPSERT)原子 upsert。命中指定 UNIQUE 约束时,DO UPDATE SET col = EXCLUDED.col 用本次想插的值,DO NOTHING 直接跳过。
⚠ 常见坑: 冲突目标必须是 UNIQUE 约束或主键,普通索引不算。冲突时序列仍然前进,serial ID 会出空洞。
INSERT INTO users (email, name) VALUES ('a@x.com', 'Ada')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;INSERT INTO event_dedupe (event_id) VALUES ('e123')
ON CONFLICT DO NOTHING;UPDATE ... RETURNING改行并返回新值。FROM 子句可以在 UPDATE 里 join 其他表(PG 对标准 SQL 的扩展)。
⚠ 常见坑: UPDATE 不带 WHERE 改全表。先跑对应的 SELECT count 看看,或者测试时 BEGIN / ROLLBACK 包起来。
UPDATE users SET name = 'Ada Lovelace' WHERE id = 1 RETURNING *;
UPDATE orders o SET status = 'paid' FROM payments p WHERE o.id = p.order_id AND p.confirmed_at IS NOT NULL RETURNING o.id;
DELETE ... RETURNING删行并返回被删的内容(适合审计日志或 "撤销" 功能)。
⚠ 常见坑: DELETE 不带 WHERE 清空表。大批量删除要分批(用 CTE 实现 DELETE WHERE id IN (...) LIMIT N 模式);一次性大 DELETE 产生海量 WAL 还让表膨胀。
DELETE FROM sessions WHERE expires_at < now() RETURNING id;
WITH victims AS ( SELECT id FROM sessions WHERE expires_at < now() ORDER BY id LIMIT 10000 ) DELETE FROM sessions WHERE id IN (SELECT id FROM victims) RETURNING id;
TRUNCATE [CASCADE]瞬时清空表,不写逐行 WAL。加 RESTART IDENTITY 才重置序列。CASCADE 连带清空有外键引用的子表。
⚠ 常见坑: TRUNCATE 持 ACCESS EXCLUSIVE 锁,堵一切。某些复制拓扑里不能回滚到 savepoint。触发器默认不触发(除非显式 BEFORE TRUNCATE)。
TRUNCATE TABLE staging_imports;
TRUNCATE TABLE users RESTART IDENTITY CASCADE;
INSERT ... SELECT把一个查询的结果批量插入。回填、复制过滤子集、为报表表反规范化的快路径,全在服务器端,行不过网。
INSERT INTO users_archive (id, email, archived_at) SELECT id, email, now() FROM users WHERE deleted_at < now() - interval '90 days';
COPY (server-side)最快的批量导入/导出路径,单条语句流式处理行,没有逐行解析开销。服务器端 COPY ... FROM 'file' 需要 superuser/pg_read_server_files 且文件在 DB 主机上。驱动底层用的是 COPY ... FROM STDIN。
⚠ 常见坑: COPY 在事务内全有或全无。要比逐行 INSERT 快 10 倍:先删非必要索引,COPY,再重建。注意它绕过 rule,只触发已显式定义的触发器。
COPY users (email, name) FROM '/data/users.csv' WITH (FORMAT csv, HEADER true);
COPY (SELECT * FROM orders WHERE created_at >= '2026-01-01') TO STDOUT WITH (FORMAT csv, HEADER true);
MERGESQL 标准的多动作 upsert(PG 15+)。一条语句用 WHEN MATCHED / WHEN NOT MATCHED 分支做 UPDATE、DELETE 或 INSERT。匹配键不是唯一约束时比 ON CONFLICT 更灵活。
⚠ 常见坑: MERGE 没有 ON CONFLICT 那种插入或更新的原子并发保证。并发写入下仍可能报唯一冲突。纯粹按唯一键 upsert 优先用 ON CONFLICT。
MERGE INTO inventory i USING shipments s ON i.sku = s.sku WHEN MATCHED THEN UPDATE SET qty = i.qty + s.qty WHEN NOT MATCHED THEN INSERT (sku, qty) VALUES (s.sku, s.qty);
SELECT ... FOR UPDATE / SKIP LOCKED事务内的行级锁。FOR UPDATE 锁住选中的行,并发写者得等。SKIP LOCKED 跳过已被锁的行,是用 Postgres 当任务队列让 N 个 worker 各抢一行的经典写法。
⚠ 常见坑: 多 worker 下带 ORDER BY 的 FOR UPDATE 会堆积锁等待;加 SKIP LOCKED 让每个 worker 直接拿下一行空闲的,不排队。NOWAIT 则立刻报错不等待。
SELECT id FROM jobs WHERE status = 'queued' ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 1;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
GROUP BY ROLLUP / CUBE / GROUPING SETS一次扫描出多个聚合层级。ROLLUP 给层级小计 + 总计;CUBE 给所有组合;GROUPING SETS 让你精确挑哪些分组。用 GROUPING() 区分小计行和真实行。
SELECT region, product, sum(amount) FROM sales GROUP BY ROLLUP (region, product);
SELECT region, product, sum(amount), grouping(region, product) AS g FROM sales GROUP BY GROUPING SETS ((region), (product), ());
DISTINCT ONPG 扩展:按 ORDER BY 给每个唯一键保留第一行。"每组最新一行" 最简洁的写法,比窗口函数子查询短,常常也更快。
⚠ 常见坑: ORDER BY 最左边的表达式必须和 DISTINCT ON 的列一致,否则报错。打破平局的列加在后面。不写明确的 ORDER BY,留哪行是随机的。
SELECT DISTINCT ON (user_id) user_id, amount, created_at FROM orders ORDER BY user_id, created_at DESC;
LIMIT / OFFSET vs keyset paginationOFFSET 靠跳过行翻页,简单但是 O(offset):翻到第 10000 页要扫掉并丢弃一百万行。Keyset(seek)翻页用 WHERE (排序键) > (上次最后一行),任意深度都是 O(每页大小)。
⚠ 常见坑: OFFSET 翻页在频繁变动的表上还会因为行位移导致重复/漏数据。Keyset 翻页既稳定又快,无限滚动和深翻页优先用它。
SELECT * FROM posts ORDER BY id LIMIT 20 OFFSET 40; -- page 3
SELECT * FROM posts WHERE id > 1234 ORDER BY id LIMIT 20; -- keyset, fast
-> / ->>取 JSONB 值:-> 返回 jsonb(保留类型),->> 返回 text。要拿字符串比较或显示用 ->>。
⚠ 常见坑: 把 ->> 出来的 text 和数字比较要先转型:(data->>'count')::int > 5。不转型走 text 比较,慢且不走索引。
SELECT data->'user'->>'email' FROM events;
SELECT * FROM events WHERE (data->>'count')::int > 5;
#> / #>>路径取值:#> 返回 jsonb,#>> 返回 text。路径是数组字面量,写成 '{user,name}' 或 ARRAY['user','name']。
SELECT data #> '{user,address,city}' FROM events;SELECT data #>> '{user,name}' FROM events;@>包含运算符:左侧 JSONB 是否包含右侧?可走 GIN 索引,过滤嵌套 key 的最快方式。任何 JSONB 查询负载的支柱。
SELECT * FROM events WHERE data @> '{"type":"click"}';SELECT * FROM users WHERE prefs @> '{"theme":"dark","lang":"zh"}';? / ?| / ?&? 顶层是否存在某 key。?| 是否存在这些 key 任一个。?& 是否同时存在这些 key 全部。配 jsonb_path_ops 可走 GIN 索引。
SELECT * FROM events WHERE data ? 'user_id';
SELECT * FROM events WHERE data ?| ARRAY['email','phone'];
SELECT * FROM events WHERE data ?& ARRAY['ip','ua'];
jsonb_build_object从交替的 key/value 对构造 JSONB 对象。在查询里从头构造比 jsonb_set 干净。
SELECT jsonb_build_object('id', id, 'email', email, 'created', created_at) FROM users;jsonb_set改 JSONB 指定路径的值。最后一个参数控制 key 不存在时是否创建(默认 true)。返回新 JSONB。
⚠ 常见坑: 对 NULL JSONB 跑 jsonb_set 静默返回 NULL。先 coalesce(col, '{}') 再 set,避免更新丢失。
UPDATE users SET prefs = jsonb_set(prefs, '{theme}', '"dark"') WHERE id = 1;UPDATE users SET prefs = jsonb_set(coalesce(prefs, '{}'::jsonb), '{lang}', '"zh"', true);jsonb_path_querySQL/JSON 路径查询(PG 12+):类似 JSONPath 的迷你语言,用于遍历和过滤 JSON。返回 jsonb 集合。
SELECT jsonb_path_query(data, '$.items[*] ? (@.price > 100)') FROM orders;
SELECT * FROM orders WHERE jsonb_path_exists(data, '$.items[*] ? (@.sku == "X1")');
jsonb_array_elements / _text把 JSONB 数组展开成每个元素一行。jsonb_array_elements 返回 jsonb,_text 变体返回 text。把 JSON 数组当成表来 join 的标准做法。
SELECT e.value->>'sku' AS sku FROM orders, jsonb_array_elements(data->'items') AS e;
SELECT jsonb_array_elements_text('["a","b","c"]'::jsonb);jsonb_each / jsonb_object_keysjsonb_each 把 JSONB 对象展开成 (key, value) 行;jsonb_object_keys 只返回 key。用来遍历事先不知道键名的动态对象。
SELECT key, value FROM events, jsonb_each(data) WHERE id = 1;
SELECT jsonb_object_keys(prefs) FROM users WHERE id = 1;
jsonb_agg / jsonb_object_agg把行聚合回 JSONB。jsonb_agg 建数组,jsonb_object_agg(k, v) 建对象。一条查询返回父行带嵌套 JSON 子行就靠它。
SELECT u.id, jsonb_agg(o.* ORDER BY o.created_at) AS orders FROM users u JOIN orders o ON o.user_id = u.id GROUP BY u.id;
SELECT jsonb_object_agg(code, name) FROM countries;
jsonb || / - / #-JSONB 修改运算符:|| 合并/拼接(键冲突右边赢,只浅合并),- 删一个键或数组元素,#- 删指定路径。都不原地改,每个都返回新值。
⚠ 常见坑: || 是浅合并,嵌套对象整体替换不深度合并。要深度合并得自己递归(或写辅助函数)。很容易不小心丢掉嵌套数据。
SELECT '{"a":1,"b":2}'::jsonb || '{"b":9,"c":3}'::jsonb; -- {a:1,b:9,c:3}UPDATE users SET prefs = prefs - 'legacy_flag' WHERE id = 1;
SELECT data #- '{user,temp}' FROM events;to_jsonb / row_to_json把 SQL 值转成 JSON。to_jsonb(任意值) 是现代万能版(处理行、数组、标量,保留类型)。row_to_json 是更老的只接行的版本。在 SQL 里拼 API 响应体的基石。
SELECT to_jsonb(u) FROM users u WHERE id = 1;
SELECT to_jsonb(array_agg(name)) FROM tags;
WITH cte AS (...) SELECTCTE:给子查询命名再复用。PG 12+ 默认 inline(更快);要老的 "只执行一次缓存结果" 语义就加 MATERIALIZED。
⚠ 常见坑: PG 12 之前 CTE 是 "优化围墙",planner 不能下推谓词。老代码靠 CTE 当 hint,到 PG 12+ 这个 hint 没了,执行计划可能突然变。
WITH recent AS ( SELECT * FROM orders WHERE created_at > now() - interval '7 days' ) SELECT user_id, count(*) FROM recent GROUP BY user_id;
WITH RECURSIVE递归 CTE:遍历树和图。anchor SELECT 起步;recursive SELECT 自引用。UNION ALL 保留重复,UNION 去重。
⚠ 常见坑: 一定要有终止条件。无界递归 CTE 在循环图上一直跑到 OOM。加 WHERE depth < 100 或跟踪已访问节点。
WITH RECURSIVE org AS (
SELECT id, manager_id, name, 1 AS depth FROM employees WHERE id = 1
UNION ALL
SELECT e.id, e.manager_id, e.name, org.depth + 1
FROM employees e JOIN org ON e.manager_id = org.id
WHERE org.depth < 100
)
SELECT * FROM org;WITH writable CTECTE 可以包 INSERT / UPDATE / DELETE 配 RETURNING,"一条语句把行从一张表搬到另一张" 的经典用法。
WITH moved AS ( DELETE FROM events WHERE created_at < now() - interval '30 days' RETURNING * ) INSERT INTO events_archive SELECT * FROM moved;
CTE batched DELETE / UPDATE loop用 CTE 包一个自限子查询,把巨表按有界分块处理,每个事务都短、WAL 也平。反复跑到影响 0 行为止。在线清理或回填上亿行的安全办法。
⚠ 常见坑: CTE 里不写 ORDER BY ... LIMIT,单条语句还是会动整张表。LIMIT 才是每次的边界,大表上绝不能省。
WITH batch AS (
SELECT id FROM events
WHERE processed = false ORDER BY id LIMIT 5000 FOR UPDATE SKIP LOCKED
)
UPDATE events SET processed = true
WHERE id IN (SELECT id FROM batch) RETURNING id;MATERIALIZED / NOT MATERIALIZED显式控制优化围墙(PG 12+)。MATERIALIZED 强制 CTE 只执行一次并缓存(昂贵且被多次引用、或有副作用时有用)。NOT MATERIALIZED 让 planner 内联并下推谓词。
WITH heavy AS MATERIALIZED ( SELECT * FROM big_join_view ) SELECT * FROM heavy WHERE x = 1 UNION ALL SELECT * FROM heavy WHERE y = 2;
recursive graph cycle detection在递归 CTE 里记录已访问路径,遇环就停。PG 14+ 有原生 CYCLE col SET is_cycle USING path 语法;更早版本自己带一个已访问 id 数组,过滤 WHERE NOT id = ANY(path)。
WITH RECURSIVE g AS (
SELECT id, parent_id, ARRAY[id] AS path FROM nodes WHERE id = 1
UNION ALL
SELECT n.id, n.parent_id, g.path || n.id
FROM nodes n JOIN g ON n.parent_id = g.id
WHERE NOT n.id = ANY(g.path)
)
SELECT * FROM g;ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)在每个分区内按 ORDER BY 给每行一个唯一连续编号。"每组前 N 行" 经典写法:外层套一层过滤 rn <= N。
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t WHERE rn <= 3;RANK() / DENSE_RANK()都在分区内排名。RANK 同分后跳号(1, 1, 3);DENSE_RANK 不跳(1, 1, 2)。"前 3 不同分数" 用 DENSE。
SELECT name, score, RANK() OVER (ORDER BY score DESC) FROM players;
SELECT name, score, DENSE_RANK() OVER (ORDER BY score DESC) FROM players;
LAG() / LEAD()不用自连接直接拿同一分区内的前一行(LAG)或后一行(LEAD)。第二个参数是偏移(默认 1),第三个是缺省值。
SELECT day, revenue,
revenue - LAG(revenue, 1, 0) OVER (ORDER BY day) AS day_over_day
FROM daily_revenue;NTILE(n) OVER (ORDER BY ...)按 ORDER BY 把行均匀切成 n 个桶。用于四分位、分位段、A/B 测试分组。
SELECT user_id, spend, NTILE(4) OVER (ORDER BY spend DESC) AS quartile FROM users;
sum() OVER (ROWS BETWEEN ... AND ...)帧子句:聚合看哪几行。ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW = 累计;ROWS BETWEEN 6 PRECEDING AND CURRENT ROW = 7 日滑动求和。
SELECT day, revenue,
SUM(revenue) OVER (ORDER BY day ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total,
AVG(revenue) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7
FROM daily_revenue;FIRST_VALUE / LAST_VALUE取窗口帧里第一个 / 最后一个值。LAST_VALUE 要显式 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,默认帧到当前行就停。
SELECT user_id,
FIRST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS first_order,
LAST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_order
FROM orders;WINDOW w AS (...)命名窗口:OVER 子句定义一次多个聚合复用。比把 PARTITION BY x ORDER BY y 复制粘贴三次干净。
SELECT user_id,
SUM(amount) OVER w,
AVG(amount) OVER w,
COUNT(*) OVER w
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY created_at);PERCENT_RANK() / CUME_DIST()相对位置窗口函数。PERCENT_RANK = (rank - 1) / (总数 - 1),范围 0..1。CUME_DIST = 小于等于当前值的行所占比例。做百分位面板不用分桶。
SELECT name, score, round((percent_rank() OVER (ORDER BY score))::numeric, 3) FROM students;
percentile_cont / percentile_disc求百分位的有序集聚合。percentile_cont(0.5) 插值(真中位数,可能不在数据里);percentile_disc(0.5) 返回集合里实际存在的值。求 p50/p95/p99 延迟的正确姿势。
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY latency_ms) AS p50, percentile_cont(0.95) WITHIN GROUP (ORDER BY latency_ms) AS p95, percentile_cont(0.99) WITHIN GROUP (ORDER BY latency_ms) AS p99 FROM requests;
gaps-and-islands (group runs)用 "row_number 减去值" 技巧把连续段折叠成区间:同一段不间断的行差值是常数,拿它当分组键。统计连续天数、会话、连续区间的经典模式。
SELECT user_id, min(day) AS streak_start, max(day) AS streak_end, count(*) AS len
FROM (
SELECT user_id, day,
day - (row_number() OVER (PARTITION BY user_id ORDER BY day))::int AS grp
FROM active_days
) t
GROUP BY user_id, grp;FILTER (WHERE ...) on aggregates不用 CASE 的条件聚合。count(*) FILTER (WHERE status = 'paid') 只数匹配行。比 sum(CASE WHEN ... THEN 1 ELSE 0 END) 干净也快,同样能当窗口聚合用。
SELECT count(*) AS total, count(*) FILTER (WHERE status = 'paid') AS paid, count(*) FILTER (WHERE status = 'refunded') AS refunded FROM orders;
CREATE INDEX (B-tree)默认索引类型。等值和范围查询首选。联合索引遵守最左前缀:(a, b) 帮 WHERE a=? 和 WHERE a=? AND b=?,但不帮 WHERE b=?。
CREATE INDEX users_email_idx ON users (email);
CREATE INDEX orders_user_created_idx ON orders (user_id, created_at DESC);
CREATE INDEX ... USING GIN通用倒排索引,适合 "数据本身是复合的,要查它的部件" 场景。JSONB 包含、全文 tsvector、array 包含、pg_trgm 模糊匹配。
⚠ 常见坑: GIN 写入慢(要改写整个 posting list)。写多的 JSONB 列考虑部分 GIN 或者只对热点 key 建函数索引。
CREATE INDEX events_data_gin ON events USING GIN (data jsonb_path_ops);
CREATE INDEX posts_tags_gin ON posts USING GIN (tags);
CREATE INDEX users_name_trgm ON users USING GIN (name gin_trgm_ops); -- needs pg_trgm
CREATE INDEX ... USING GiST通用搜索树,几何和范围类型用。PostGIS 空间索引、range 排他约束、相似度搜索。
CREATE INDEX locations_geom_gist ON locations USING GiST (geom);
CREATE INDEX bookings_period_gist ON bookings USING GiST (period); -- daterange
partial index (WHERE)只对满足条件的行建索引。当大部分查询只命中子集(活跃行、最近行、status='pending')时,比全表索引小得多也快得多。
CREATE INDEX orders_pending_idx ON orders (created_at) WHERE status = 'pending';
CREATE INDEX users_active_email_idx ON users (email) WHERE deleted_at IS NULL;
expression / functional index索引表达式的结果,不是裸列。查询里列被函数包了就必须用(WHERE lower(email) = ? 要 lower(email) 的索引)。
CREATE INDEX users_email_lower_idx ON users (lower(email));
CREATE INDEX events_date_idx ON events (date_trunc('day', created_at));covering index (INCLUDE)PG 11+ 特性:把额外的非键列也存到索引叶子页,planner 可以纯走索引(index-only scan),不回表。
⚠ 常见坑: index-only scan 跳过 heap,要可见性图是新的。批量导入后要跑 VACUUM 才能看到提速。
CREATE INDEX users_email_inc_idx ON users (email) INCLUDE (name, created_at);
CREATE INDEX ... USING BRIN块范围索引,每个块范围只存 min/max 的极小索引。超大表 + 天然有序(时序、追加日志)首选。
CREATE INDEX events_created_brin ON events USING BRIN (created_at);
EXPLAIN (ANALYZE, BUFFERS)真跑一遍 SQL 并显示实际执行计划带真实行数、耗时、buffer 命中。"为什么这条慢?" 第一招:看大表上的 Seq Scan、Rows Removed by Filter 巨大、读盘数高。
⚠ 常见坑: EXPLAIN ANALYZE 跑 UPDATE / DELETE / INSERT 会真写数据。包在 BEGIN; EXPLAIN ANALYZE ...; ROLLBACK; 里只看计划不改数据。
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE email = $1;
BEGIN; EXPLAIN (ANALYZE) UPDATE users SET name = 'x' WHERE id = 1; ROLLBACK;
CREATE INDEX ... USING hash哈希索引,只支持等值(=),不支持范围和排序。PG 10 起崩溃安全且写 WAL。对大随机键偶尔比 B 树更小更快,但 99% 情况 B 树是更稳的默认。
CREATE INDEX sessions_token_hash ON sessions USING hash (token);
REINDEX [CONCURRENTLY]重建膨胀或损坏的索引。REINDEX INDEX CONCURRENTLY(PG 12+)不持排他锁重建,生产安全选项。索引被频繁改动拖低效率,或要修 INVALID 索引时用。
⚠ 常见坑: 普通 REINDEX 对表持 ACCESS EXCLUSIVE 锁,活表上绝不能不带 CONCURRENTLY 跑。并发形式失败会留一个无效索引,删掉重试即可。
REINDEX INDEX CONCURRENTLY users_email_idx;
REINDEX TABLE CONCURRENTLY orders;
pg_stat_user_indexes (unused indexes)每个索引都拖慢写入、占磁盘。这个视图显示 idx_scan(planner 用过每个索引几次)。跑满一个完整流量周期后 idx_scan = 0 的索引是死重,删掉。
SELECT schemaname, relname, indexrelname, idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0 ORDER BY pg_relation_size(indexrelid) DESC;EXPLAIN (FORMAT JSON) / GENERIC_PLANEXPLAIN (FORMAT JSON) 输出给工具用的机器可读计划。EXPLAIN (GENERIC_PLAN)(PG 16+)对带 $1 占位符的参数化查询出计划且不执行,终于能直接 EXPLAIN 一条 prepared statement。
EXPLAIN (FORMAT JSON) SELECT * FROM users WHERE id = 1;
EXPLAIN (GENERIC_PLAN) SELECT * FROM users WHERE email = $1;
pg_stat_statements: finding the slow query真正找到瓶颈的流程:pg_stat_statements 按总耗时排,不是平均耗时。一条 5 ms 但被调 1000 万次的查询比跑一次的 2 秒报表更伤。mean_exec_time 找最慢的单条;total_exec_time 找总负载最重的。
SELECT calls, round(mean_exec_time::numeric, 2) AS mean_ms,
round(total_exec_time::numeric, 0) AS total_ms, query
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;SELECT pg_stat_statements_reset(); -- zero the counters before a test
PARTITION BY RANGE按值范围切表,最常见按日期。每个分区是独立的物理表,删一个分区等于毫秒级删掉一整个月数据。
CREATE TABLE events (id bigserial, created_at timestamptz NOT NULL, payload jsonb)
PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_05 PARTITION OF events
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');PARTITION BY LIST按显式枚举值切表。多租户按客户分区、按地区分表都用它。
CREATE TABLE orders (id bigserial, region text, amount numeric)
PARTITION BY LIST (region);
CREATE TABLE orders_us PARTITION OF orders FOR VALUES IN ('us-east','us-west');
CREATE TABLE orders_eu PARTITION OF orders FOR VALUES IN ('eu-west','eu-central');PARTITION BY HASH对分区键做哈希,行均匀分到 N 个分区。没有天然的 range / list 但想分散写入压力时用。
CREATE TABLE messages (id bigint, user_id bigint, body text) PARTITION BY HASH (user_id); CREATE TABLE messages_p0 PARTITION OF messages FOR VALUES WITH (MODULUS 4, REMAINDER 0); CREATE TABLE messages_p1 PARTITION OF messages FOR VALUES WITH (MODULUS 4, REMAINDER 1);
ATTACH / DETACH PARTITION把一张表 attach / detach 到分区表。DETACH 是 "便宜地删掉一个月数据" 的关键操作。PG 14+ 的 CONCURRENTLY 避免短暂的 access exclusive 锁。
⚠ 常见坑: ATTACH PARTITION 默认会全表扫验证分区范围,除非表上已经有匹配的 CHECK 约束。先加 CHECK 再 ATTACH 就能跳过扫描。
ALTER TABLE events ATTACH PARTITION events_2026_06 FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');ALTER TABLE events DETACH PARTITION events_2025_01 CONCURRENTLY;
DEFAULT partition一个兜底分区,接住不落入任何其他分区范围的行。没有它,落在所有已定义范围之外的 INSERT 会报 "no partition found"。加一个当安全网,但要盯它的体积。
⚠ 常见坑: 再 attach 一个范围与已落在 DEFAULT 分区里的行重叠的新分区时,会扫 default 把行搬过去,有冲突就失败。提前定义好分区,让 default 接近空。
CREATE TABLE events_default PARTITION OF events DEFAULT;
sub-partitioning一个分区本身可以再分区,形成两级方案(比如按月 RANGE,每个月内再按租户 HASH)。超大多租户时序适用,但增加规划开销,分区总数控制在几千以内。
CREATE TABLE metrics (tenant_id int, ts timestamptz, val numeric)
PARTITION BY RANGE (ts);
CREATE TABLE metrics_2026_05 PARTITION OF metrics
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01')
PARTITION BY HASH (tenant_id);partition pruningplanner 跳过不可能匹配 WHERE 的分区,这才是分区的意义。看 EXPLAIN 计划是否只碰相关分区来确认生效。运行时剪枝(PG 11+)还能在执行时按参数值剪枝。
⚠ 常见坑: 剪枝只在 WHERE 直接引用分区键时生效。把它包进函数(WHERE date_trunc('day', ts) = ...)或跨类型比较都会让剪枝失效,扫所有分区。
EXPLAIN SELECT * FROM events WHERE created_at >= '2026-05-15' AND created_at < '2026-05-16';
pg_dump数据库的逻辑备份:默认 SQL 文本,可选 custom (-Fc) / directory (-Fd) / tar (-Ft) 格式支持并行恢复。任何客户端都能跑;跨集群迁移用 --no-owner / --no-privileges。
⚠ 常见坑: pg_dump 是单事务快照一致的,但多 TB 库就慢。超大集群用 pg_basebackup(物理备份)+ WAL 归档,不用 pg_dump。
pg_dump -Fc -f app.dump app
pg_dump -Fd -j 4 -f app_dir app # 4-way parallel dump
pg_dump --schema-only -f schema.sql app
pg_restore从 pg_dump 归档(-Fc / -Fd / -Ft 格式)恢复。-j 并行恢复,-t 按表过滤,--no-owner 跨用户迁移。
pg_restore -d app_new -j 4 app.dump
pg_restore -d app -t users app.dump
pg_restore --schema-only -d app schema.dump
pg_basebackup整个集群的物理基础备份,服务器照常运行的同时按字节复制数据目录。流复制和 PITR 的基础。
pg_basebackup -h primary.db -D /var/lib/postgresql/replica -U replicator -R -P
streaming replication备库连主库实时回放 WAL。备库配 primary_conninfo,主库建复制槽(replication slot)保证 WAL 留到备库消费完为止。
⚠ 常见坑: 不建复制槽,主库可能在备库读到之前回收 WAL,备库永久落后。建了槽,备库挂了主库磁盘就被撑爆。监控 pg_replication_slots.confirmed_flush_lsn。
SELECT pg_create_physical_replication_slot('replica1');SELECT slot_name, active, confirmed_flush_lsn FROM pg_replication_slots;
logical replication跨 PG 版本复制指定的表(不是整个集群)。基于 publication(源)和 subscription(目标)。大版本在线升级的必备工具。
CREATE PUBLICATION mypub FOR TABLE users, orders;
CREATE SUBSCRIPTION mysub CONNECTION 'host=src dbname=app user=repl' PUBLICATION mypub;
pg_dumpall导出整个集群,含 pg_dump 跳过的全局对象:角色、表空间、授权。--globals-only 只导角色/表空间,配合按库 pg_dump 构成完整且可并行的备份。
pg_dumpall --globals-only -f globals.sql
pg_dumpall -f full_cluster.sql
Point-in-Time Recovery (PITR)在基础备份上回放归档 WAL 恢复到任意精确时刻。需要 archive_mode = on + 可用的 archive_command。在恢复配置里设 recovery_target_time,PG 回放到那一刻停下。
⚠ 常见坑: PITR 的可靠性等于 archive_command 的可靠性。WAL 归档一直在静默失败,你的恢复窗口就有个洞。监控 pg_stat_archiver.last_failed_time,定期演练恢复。
# in postgresql.conf archive_mode = on archive_command = 'test ! -f /arch/%f && cp %p /arch/%f'
# in recovery target config recovery_target_time = '2026-05-29 14:30:00+00'
synchronous_commit / replicassynchronous_commit 在持久性和延迟之间权衡。on(默认)COMMIT 返回前把 WAL 刷盘。remote_apply 等同步备库应用完。off 在本地刷盘前就返回,快但崩溃会丢最近的提交。
⚠ 常见坑: 全局设 synchronous_commit = off 提速写入,会在崩溃时静默丢数据。更好的做法:只对不关键的批量导入按事务设置,关键写入保持持久。
SET LOCAL synchronous_commit = off; -- this transaction only
SELECT application_name, state, sync_state FROM pg_stat_replication;
monitoring replication lag在主库按字节量每个备库落后多少;在备库按秒衡量延迟。字节差揪出慢网络;时间差(now() - 上次回放)揪出卡住的应用。两个都要告警。
SELECT application_name,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS bytes_behind
FROM pg_stat_replication;SELECT now() - pg_last_xact_replay_timestamp() AS replay_lag; -- run on the standby
CREATE ROLE / CREATE USERPG 里 USER 和 GROUP 是一回事:都是 ROLE。CREATE USER = CREATE ROLE ... LOGIN。角色可以授予其他角色(PG 风格的组成员关系)。
CREATE ROLE app_readonly NOLOGIN;
CREATE USER app_service WITH PASSWORD 'xxx';
GRANT app_readonly TO app_service;
GRANT / REVOKE授予 / 收回数据库、schema、表、列、序列、函数权限。新建的表不会自动继承权限,用 ALTER DEFAULT PRIVILEGES 让授权对未来对象也生效。
⚠ 常见坑: GRANT SELECT ON ALL TABLES IN SCHEMA 只覆盖现在存在的表,以后建的没权限。ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_readonly 才让未来建的表也有权限。
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_readonly;
REVOKE ALL ON SCHEMA public FROM PUBLIC;
pg_hba.confhost 认证配置:把 (连接类型, 数据库, 用户, 地址) 映射到认证方法。顺序敏感,命中第一条就停。方法:trust(不需要密码!)、md5、scram-sha-256、peer、cert、ldap。
⚠ 常见坑: 一条漏写的 host all all 0.0.0.0/0 trust 让全网随便登录任何用户。所有集群审计 pg_hba.conf,任何能从网络到达的行不准用 trust。
# TYPE DATABASE USER ADDRESS METHOD
host all all 10.0.0.0/8 scram-sha-256
hostssl app app 0.0.0.0/0 scram-sha-256
local all all peer
ROW LEVEL SECURITY (RLS)行级安全策略,数据库自己保证 "用户 X 只看到自己的行"。表上启用后 CREATE POLICY。
⚠ 常见坑: RLS 默认对表 owner 和 superuser 不生效。要 FORCE ROW LEVEL SECURITY 才对 owner 也生效。否则迁移和以 owner 身份连接的应用都绕过你的安全。
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
ALTER TABLE notes FORCE ROW LEVEL SECURITY;
CREATE POLICY my_notes ON notes
USING (user_id = current_setting('app.user_id')::bigint);SET ROLE / SESSION AUTHORIZATIONSET ROLE 把当前权限上下文切到你所属的某个角色(RESET ROLE 还原)。安全连接池的基础:以可信池角色连接,再 SET ROLE 到每个请求的用户。
SET ROLE app_readonly;
RESET ROLE;
SET SESSION AUTHORIZATION 'reporting';
connection limits & timeouts防御性的角色/会话设置:CONNECTION LIMIT 限制每个角色的并发连接;statement_timeout 杀失控查询;idle_in_transaction_session_timeout 杀掉占着事务不放、堵 VACUUM 的连接。
⚠ 常见坑: 交互会话里一个忘了收尾的 BEGIN 能全集群堵 autovacuum 几个小时。给每个交互/管理角色设 idle_in_transaction_session_timeout 限制损害。
ALTER ROLE app_service CONNECTION LIMIT 50;
ALTER ROLE app_service SET statement_timeout = '30s';
ALTER ROLE admin SET idle_in_transaction_session_timeout = '60s';
predefined roles (pg_read_all_data)PG 自带可 GRANT 的内置角色,不用直接给 superuser:pg_read_all_data / pg_write_all_data(PG 14+)、pg_monitor(看统计)、pg_signal_backend(非 superuser 也能取消/终止别人的查询)。
GRANT pg_read_all_data TO analyst;
GRANT pg_monitor TO observability;
pg_stat_statements每条 SQL 的执行统计:总耗时、平均耗时、调用次数、返回行数、I/O。生产集群第一个装的扩展,没有它 "找慢 SQL" 全靠猜。
CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- needs shared_preload_libraries
SELECT query, calls, mean_exec_time, total_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;
pg_trgm基于 trigram 的模糊文本匹配。让 LIKE '%foo%'、ILIKE 和相似度搜索(% 运算符)可走 GIN 索引。Search-as-you-type 的关键扩展。
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX users_name_trgm ON users USING GIN (name gin_trgm_ops);
SELECT * FROM users WHERE name % 'Adda'; -- fuzzy match
postgis地理空间数据和查询:geometry / geography 类型、空间索引、距离 / 包含 / 相交运算符。严肃 GIS 工作负载选 PostgreSQL 的核心原因。
CREATE EXTENSION IF NOT EXISTS postgis;
SELECT name FROM places WHERE ST_DWithin(geog, ST_MakePoint(-122.4, 37.8)::geography, 1000); -- within 1km
uuid-osspUUID 生成函数(v1、v3、v4、v5)。PG 13+ 自带 gen_random_uuid() 覆盖 v4,只在需要 v1(时间戳)或 v5(命名空间)时才装 uuid-ossp。
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SELECT uuid_generate_v1();
SELECT uuid_generate_v5(uuid_ns_url(), 'https://x.com/user/42');
hstore / pgcrypto / citexthstore:简单键值对(jsonb 之前的产物,新代码用 jsonb)。pgcrypto:crypt()、gen_salt()、digest()、pgp_sym_encrypt()。citext:不区分大小写的 text 类型,比到处写 lower() 更友好可索引。
CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO users (password) VALUES (crypt('hunter2', gen_salt('bf')));SELECT * FROM users WHERE password = crypt('hunter2', password);CREATE EXTENSION IF NOT EXISTS citext;
CREATE TABLE accounts (email citext UNIQUE);
pgvector为嵌入向量做相似度搜索(vector 类型)。支持 L2(<->)、内积(<#>)、余弦(<=>)距离,配 HNSW 和 IVFFlat 近似最近邻索引。Postgres 能当向量库用的原因。
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE docs (id bigserial, embedding vector(1536));
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
SELECT id FROM docs ORDER BY embedding <=> $1 LIMIT 5;
postgres_fdw外部数据包装器:把另一台 PostgreSQL 服务器上的表当本地表查。定义 server + 用户映射,导入外部 schema,就能跨库 JOIN。内置的联邦查询方案。
⚠ 常见坑: 跨服务器 JOIN 在 planner 下推不了过滤时,可能把整张外部表拉过网。看 EXPLAIN 的 "Foreign Scan" 是否带下推的 WHERE;否则开 use_remote_estimate 并 analyze 外部表。
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
CREATE SERVER src FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'db2', dbname 'app');
IMPORT FOREIGN SCHEMA public LIMIT TO (orders) FROM SERVER src INTO remote;
btree_gist / btree_gin给 GiST/GIN 索引补上 B 树式运算符(=、<、>)。多列 EXCLUDE 约束(比如 room WITH =, during WITH &&)和把标量等值与 JSONB/数组搜索合进一个 GIN 索引的关键。
CREATE EXTENSION IF NOT EXISTS btree_gist;
ALTER TABLE bookings ADD CONSTRAINT no_double_book EXCLUDE USING gist (room_id WITH =, during WITH &&);
CREATE / ALTER / DROP EXTENSION扩展生命周期。CREATE EXTENSION ... WITH SCHEMA x 把对象装进指定 schema。ALTER EXTENSION ... UPDATE 升级到新版本。可用的在 pg_available_extensions,已装的在 pg_extension。
SELECT name, default_version, installed_version FROM pg_available_extensions WHERE installed_version IS NOT NULL;
ALTER EXTENSION postgis UPDATE;
DROP EXTENSION IF EXISTS hstore CASCADE;
NULL is not equal to NULLSQL 里 NULL 表示 "未知"。NULL = NULL 结果是 NULL(不是 TRUE),NULL != NULL 也是 NULL。要用 IS NULL / IS NOT NULL,或者 IS DISTINCT FROM 把 NULL 当可比较的值。
⚠ 常见坑: WHERE col = NULL 永远零行。NOT IN (子查询) 只要子查询里有一个 NULL 整体返空。改 NOT EXISTS 或者先过滤掉 NULL。
SELECT * FROM users WHERE deleted_at IS NULL;
SELECT * FROM users WHERE id IS DISTINCT FROM other_id; -- treats NULL as a value
SELECT * FROM orders WHERE NOT EXISTS (SELECT 1 FROM refunds WHERE refunds.order_id = orders.id);
VACUUM bloatPG 用 MVCC:UPDATE = 插入新行 + 标记旧行 dead。VACUUM 回收 dead 行;不跑表会一直涨且查询变慢。autovacuum 默认开,但高写入表可能要调参。
⚠ 常见坑: 长事务(忘了关的 psql、卡死的连接)会全集群堵 vacuum。SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction' 查一下,杀掉老会话。
VACUUM ANALYZE users;
VACUUM FULL users; -- rewrites table, exclusive lock — avoid in production
SELECT pid, age(now(), xact_start) AS age, query FROM pg_stat_activity WHERE state = 'idle in transaction' ORDER BY age DESC;
big-table ALTER多数 ALTER 持 ACCESS EXCLUSIVE 锁,整段时间堵读堵写。生产忙表上锁一秒就能级联出几千个卡住的查询。永远要预估锁时间。
⚠ 常见坑: PG 11+ ADD COLUMN 非 volatile 默认值只动元数据。volatile 默认值(gen_random_uuid())仍会重写表。ALTER COLUMN TYPE 基本都会重写。要提前规划。
SET lock_timeout = '5s'; -- never block forever
ALTER TABLE users ADD COLUMN x text; -- safe, metadata only
ALTER TABLE users ADD COLUMN id_v2 uuid DEFAULT gen_random_uuid(); -- REWRITES TABLE — danger
sequence drift序列在事务外。回滚的 INSERT 也消耗 ID。COPY 批量导入可能让序列值落后于实际 max(id),下次 INSERT 报主键冲突。
⚠ 常见坑: 显式带 id 列批量导入后,跑 SELECT setval('users_id_seq', (SELECT max(id) FROM users)) 把序列拨回去。
SELECT setval('users_id_seq', (SELECT max(id) FROM users));SELECT last_value, is_called FROM users_id_seq;
WAL filling the diskWAL(预写日志)会堆积:archive_command 失败、复制槽没人消费、wal_keep_size 太大都会。WAL 卷写满直接挂服务器。监控 pg_wal 目录大小。
⚠ 常见坑: 永远不要手动 rm pg_wal 里的文件,会直接腐烂数据库。用 pg_archivecleanup 或者干掉惹事的复制槽(SELECT pg_drop_replication_slot('slot_name'))。
SELECT slot_name, active, restart_lsn, pg_size_pretty(pg_current_wal_lsn() - restart_lsn) AS lag FROM pg_replication_slots;
SELECT pg_drop_replication_slot('dead_replica');timezone surprisestimestamp without time zone 只存裸 y-m-d h:m:s,不带时区。应用和数据库时区不一致时,每次读都静默把时间理解错。一律 timestamptz;时区只在展示边界 SET TIME ZONE。
SHOW timezone;
SET TIME ZONE 'Asia/Shanghai';
SELECT created_at AT TIME ZONE 'UTC' FROM orders; -- explicit conversion
search_path securityPG 解析没限定 schema 的名字时走 search_path。如果 search_path 包含 public,攻击者能在 public 里建对象 shadow 你的函数和表。一律 set search_path = "$user", pg_catalog,SECURITY DEFINER 函数里所有名字都全限定。
⚠ 常见坑: SECURITY DEFINER 函数不固定 search_path 就是 CVE 制造机。永远写:CREATE FUNCTION ... SECURITY DEFINER SET search_path = pg_catalog, pg_temp。
SHOW search_path;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
CREATE FUNCTION sensitive() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$ BEGIN /* ... */ END $$;
implicit cast & index miss把列和不同类型的值比较,可能触发隐式转型,让索引失效。bigint 列和文本字面量比、varchar 与 text 不匹配,都会静默把索引扫变成全表扫。
⚠ 常见坑: 永远按列的精确类型传参。应用代码里整数就绑整数别绑字符串。看 EXPLAIN 有没有意外的 Seq Scan,或者索引列外面被套了一层转型。
-- bad: '5' is text, may skip a bigint index SELECT * FROM orders WHERE id = '5';
SELECT * FROM orders WHERE id = 5; -- typed correctly
deadlocks from lock ordering两个事务以相反顺序锁同样的行就死锁;PG 会检测到并杀掉其中一个,报 "deadlock detected"。解法是纪律:永远以一致的顺序加锁(更新行),比如按主键排序。
⚠ 常见坑: 死锁是应用逻辑 bug,不是 PG 的 bug。重试被杀的事务通常能成,但真正的修法是给写入排序。多行 UPDATE 前先把 id 排好序。
UPDATE accounts SET bal = bal - 10 WHERE id = LEAST(1, 2); UPDATE accounts SET bal = bal + 10 WHERE id = GREATEST(1, 2);
count(*) is not freePG 的 MVCC 让 SELECT count(*) 必须扫可见行,不像 MyISAM 有存好的计数器。大表上很慢。要近似的实时行数,读 pg_class 的 reltuples(由 ANALYZE/VACUUM 更新)。
⚠ 常见坑: 在巨大忙表上做一个显示精确总数的 UI 会把 DB 打爆。用近似计数、缓存计数表,或者只对有索引的过滤子集 count(*)。
SELECT reltuples::bigint AS approx_rows FROM pg_class WHERE relname = 'events';
SELECT count(*) FROM orders WHERE status = 'open'; -- ok if status is indexed & selective
transaction wraparound (frozen XIDs)PG 事务 ID 是 32 位会回卷。如果 autovacuum 来不及冻结老行(被长事务堵住、或卡在巨表上),集群逼近回卷,PG 会强制停机保护数据。
⚠ 常见坑: 监控每个库的 age(datfrozenxid) 对照 autovacuum_freeze_max_age(默认 2 亿)。逼近这个数就是紧急情况,去找堵住 vacuum 的元凶(长事务、没消费的复制槽、失败的 autovacuum)。
SELECT datname, age(datfrozenxid) FROM pg_database ORDER BY age(datfrozenxid) DESC;
connection exhaustion (use a pooler)每个 PG 连接是一个完整的操作系统进程带独立内存,大多数硬件上几百个就到顶。每请求开一个连接的应用在高负载下会耗尽 max_connections。前面放 PgBouncer(事务级池化)。
⚠ 常见坑: 把 max_connections 调到几千并不能扩容,只是把瓶颈推到内存和上下文切换。正解是连接池,不是更大的数字。Serverless/Lambda 尤其需要事务级池化。
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
SHOW max_connections;
OR defeats indexes (use UNION/ANY)跨不同列的 OR 常逼出全表扫,因为没有单个索引能覆盖两个分支。改写成两个带索引查询的 UNION,或者同一列时用 col = ANY(ARRAY[...])。
⚠ 常见坑: PG 14+ 有时能跨两个索引做 Bitmap Or,但不保证。看 EXPLAIN,如果该走索引的地方出现 Seq Scan,OR 通常就是元凶。
-- may seq scan: SELECT * FROM users WHERE email = $1 OR phone = $2;
SELECT * FROM users WHERE email = $1 UNION SELECT * FROM users WHERE phone = $2;
可搜索的 PostgreSQL 速查表,覆盖后端、DBA、数据岗日常真在 psql 里敲的 80+ 条命令和函数,不是凑数的入门 SELECT 列表。 十三大分类:psql 元命令(\l \dt \d \du \q \i \timing \x \! \conninfo \copy \dn \df),数据类型(text 与 varchar 怎么 选、int / bigint / serial / bigserial / identity、uuid、 timestamp 与 timestamptz、interval、jsonb 与 json、array、 enum、range),DDL(CREATE / ALTER / DROP,IF EXISTS 与 CASCADE,生成列,表继承),DML(INSERT、UPDATE、DELETE、 RETURNING、INSERT ... ON CONFLICT 原子 UPSERT),JSONB 操作(-> ->> #> #>> @> ? ?| ?& || jsonb_build_object jsonb_set jsonb_path_query,配合 GIN 索引),CTE(WITH ... AS,RECURSIVE 递归遍树,MATERIALIZED 提示),窗口函数 (ROW_NUMBER、RANK、DENSE_RANK、LAG、LEAD、NTILE、ROWS 与 RANGE 帧,FIRST_VALUE,命名 WINDOW 子句),索引(B 树、 GIN、GiST、BRIN,部分索引,表达式 / 函数索引,覆盖索引 INCLUDE,CREATE INDEX CONCURRENTLY),表分区(PARTITION BY RANGE / LIST / HASH,attach / detach,分区裁剪),复制与备 份(pg_dump、pg_restore、pg_basebackup,流复制,逻辑复制, WAL),角色与权限(CREATE ROLE、GRANT、REVOKE、pg_hba.conf 认证方式、search_path、行级安全 RLS),常用扩展(pg_stat_ statements、pg_trgm、postgis、uuid-ossp、hstore、pgcrypto、 citext),以及真烧钱的坑(NULL 比较、vacuum bloat、大表 ALTER、序列空洞、WAL 写满磁盘、时区惊喜、search_path 安全 问题)。每条都附中英说明、1-3 条可直接粘到 psql 跑的真实 例子、一行"常见坑"。搜索框跨命令 / 说明 / 例子 / 坑四个 字段一起搜,分类胶囊缩范围。完全在浏览器里跑,不连库,不 上传,不发任何网络请求。配合 SQL Formatter、SQL 速查搭配 Docker / kubectl / Regex 速查覆盖整条技术栈。
把内容粘贴或拖入工具面板。
点击按钮,在浏览器内本地处理,文件不上传。
一键复制结果或下载到本地。
适合穿插在写代码、查问题、做 Review、上线前的小任务里。
这些入口会把当前任务接到更完整的工具链里。
凌晨两点一条查询卡住,你想不起是 \x 还是 \timing 切竖排 输出。搜 「psql」 或 「\d」,用 \conninfo 确认连的是哪台 主机,\x auto 看宽行,再查 pg_stat_activity 找堵塞的 PID。不用切到 40 个标签页的文档站,也不用划过一堆入门 SELECT 教程。
你要用 INSERT ... ON CONFLICT,但 serial 列是发票号来源。 搜 「upsert」 或 「ON CONFLICT」,坑提示行警告冲突依然消 耗序列,会出空洞。于是你把发票号改成独立的无空洞生成 器,代理键继续走 serial,在设计阶段就堵掉这个 bug,而不 是等审计时翻车。
产品要在事件表上加 30 个可选属性。筛 「jsonb」,读 @>、 ->>、GIN 索引那几条加经验法则:同一个 key 索引五次就提 成真列。于是你把 event_type 和 user_id 留成真实列,其余 塞进一个带 GIN 索引的 jsonb payload,避开 30 列的稀疏 表。
5000 万行的 orders 表要在 created_at 上加索引,团队怕停 机。搜 「CONCURRENTLY」,索引章直接讲清普通 CREATE INDEX 持 ACCESS EXCLUSIVE 大表能堵几分钟写,而 CREATE INDEX CONCURRENTLY 在线建,代价是两遍全表扫。于是你在高峰期 就敢跑在线建索引,零下单被堵。
把 serial 列当无空洞单号用在发票号或订单号上。回滚的 INSERT 或 ON CONFLICT 照样消耗 ID,得换独立的无空洞生成器。
用 ->> 过滤 JSONB 还指望走索引。GIN 索引服务的是 @> 包含查询,不是 ->> 等值。要么在 (payload->>'key') 上建函数索引,要么把这个 key 提成真实列。
在生产大表上直接跑普通 CREATE INDEX。它持 ACCESS EXCLUSIVE 堵写好几分钟。生产环境永远用 CREATE INDEX CONCURRENTLY,哪怕它不能放在事务里。
这份速查就是一个静态页。你输入的搜索词只在浏览器里对内置的 命令数组做内存匹配,不发往任何服务器,也不写进 URL。没有数 据库连接,没有埋点,没有任何可泄露的东西。一边输入一边看 DevTools 的 Network,零请求,所以堡垒机后面、公司代理后面、 气隙网络里都能放心用。
做你这行的人, 还会一起用这些。