入门
使用 psql 连接
psql 是 PostgreSQL 的命令行客户端。元命令以反斜杠 (\) 开头。用 \l 列出数据库,\d 描述表结构,\? 查看帮助。连接字符串 (postgresql://) 很方便且被大多数驱动支持。用 \i filename.sql 执行 SQL 文件。设置 PGPASSWORD 环境变量或 ~/.pgpass 文件可避免重复输入密码。
# connect to a database
psql -h localhost -p 5432 -U postgres -d mydb
# -h host, -p port, -U user, -d database
# connect via connection string
psql "postgresql://user:pass@localhost:5432/mydb"
# common psql meta-commands
\l # list databases
\c mydb # connect to mydb
\dt # list tables
\d users # describe table
\dn # list schemas
\df # list functions
\du # list roles/users
\q # quit
\? # help on meta-commands
\timing # toggle query timing创建和删除数据库
CREATE DATABASE 创建新数据库,可指定所有者、编码、区域设置和模板。template0 是不含区域数据的干净模板;template1(默认)可自定义。DROP DATABASE 要求无活动连接——WITH (FORCE)(PG13+)会先断开连接。不能删除当前连接的数据库。ALTER DATABASE RENAME 只更新名称,不改变磁盘目录。
# create a database
CREATE DATABASE mydb
WITH OWNER alice
ENCODING 'UTF8'
LC_COLLATE 'en_US.UTF-8'
LC_CTYPE 'en_US.UTF-8'
TEMPLATE template0
CONNECTION LIMIT 100;
# create from a template (clone)
CREATE DATABASE mydb_copy TEMPLATE mydb;
# drop a database (must disconnect first)
DROP DATABASE IF EXISTS mydb;
# drop with force (PostgreSQL 13+)
DROP DATABASE IF EXISTS mydb WITH (FORCE);
# rename a database
ALTER DATABASE mydb RENAME TO newdb;模式与搜索路径
模式是数据库对象的命名空间——类似表的文件夹。默认模式是 'public'。search_path 决定引用未限定名称时搜索哪些模式。用模式来组织多租户应用、分离关注点或管理版本。CASCADE 级联删除依赖对象;RESTRICT(默认)在有对象时拒绝删除。在单个连接内,模式比数据库更灵活地实现多租户。
# create a schema
CREATE SCHEMA IF NOT EXISTS app_schema AUTHORIZATION alice;
# set the search path (schema lookup order)
SET search_path TO app_schema, public;
SHOW search_path;
# create a table in a specific schema
CREATE TABLE app_schema.users (id serial PRIMARY KEY);
# move a table between schemas
ALTER TABLE public.users SET SCHEMA app_schema;
# list objects in a schema
SELECT * FROM information_schema.tables
WHERE table_schema = 'app_schema';
# drop a schema (and its objects)
DROP SCHEMA IF EXISTS app_schema CASCADE;配置与设置
PostgreSQL 设置存储在 postgresql.conf 中。SHOW 读取当前值;SET 修改会话级值;SET LOCAL 仅修改当前事务;ALTER DATABASE/ROLE 设置持久化默认值。部分设置(shared_buffers、max_connections)需要重启服务器;其他(work_mem、statement_timeout)立即生效。用 pg_reload_conf() 应用配置文件变更而无需重启。用 log_min_duration_statement 监控慢查询。
# view a setting
SHOW shared_buffers;
SHOW max_connections;
# set a parameter for the session
SET work_mem = '64MB';
SET statement_timeout = '30s';
# set a parameter for a transaction
BEGIN;
SET LOCAL work_mem = '256MB';
-- heavy sort here
COMMIT; -- reverts to previous value
# set at database or role level (persists)
ALTER DATABASE mydb SET log_min_duration_statement = 100;
ALTER ROLE alice SET search_path TO app_schema, public;
# reload config without restart
SELECT pg_reload_conf();
# view config file location
SHOW config_file;psql 脚本与变量
psql 支持变量脚本 (\set)、文件包含 (\i)、输出重定向 (\o) 和 Shell 执行 (\!)。变量用 :varname 替换。用 \set 编写循环和条件脚本。-f 标志执行文件后退出,适合批处理和迁移。对于复杂脚本,考虑用 PL/pgSQL 函数或外部工具如 sqitch、Flyway 管理迁移。
# run a SQL file
psql -d mydb -f script.sql
psql -d mydb < script.sql
# inside psql
\i /path/to/script.sql
# use psql variables
\set table_name 'users'
SELECT * FROM :table_name;
# prompt for input
\echo -n 'Enter user ID: ' \set user_id `read x && echo $x`
SELECT * FROM users WHERE id = :user_id;
# output to file
\o results.txt
SELECT * FROM users;
\o
# execute shell command
\! date表与约束
创建表
serial 自动创建序列来实现自增整数;新表建议使用 GENERATED ALWAYS AS IDENTITY(SQL 标准,PG10+ 首选)。约束包括:PRIMARY KEY(唯一+非空)、UNIQUE、NOT NULL、CHECK、REFERENCES(外键)。ON DELETE CASCADE 在父行删除时级联删除依赖行;SET NULL 将外键设为 NULL。TEMP 表在会话结束后消失——适合中间结果。
CREATE TABLE users (
id serial PRIMARY KEY,
username varchar(50) UNIQUE NOT NULL,
email varchar(255) UNIQUE NOT NULL,
password text NOT NULL,
age integer CHECK (age >= 0 AND age <= 150),
role varchar(20) NOT NULL DEFAULT 'user',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
# create with a foreign key
CREATE TABLE posts (
id serial PRIMARY KEY,
user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title text NOT NULL,
body text,
published boolean DEFAULT false
);
# create a temporary table (session-scoped)
CREATE TEMP TABLE temp_stats AS
SELECT user_id, count(*) FROM posts GROUP BY user_id;标识列 (PG10+)
GENERATED ALWAYS AS IDENTITY 是 serial 的 SQL 标准替代——它将序列绑定到列,删除列时序列一并删除(serial 会留下孤立序列)。ALWAYS 防止手动插入 ID(用 OVERRIDING SYSTEM VALUE 强制插入)。BY DEFAULT 行为类似 serial(允许手动插入)。新表用标识列;已有表用 serial 也没问题。底层都使用序列。
# preferred over serial (SQL standard)
CREATE TABLE products (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
# allow manual override (like serial behavior)
CREATE TABLE products (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
# restart a sequence
ALTER TABLE products ALTER COLUMN id RESTART WITH 1000;
# view identity info
SELECT * FROM pg_sequences WHERE sequencename = 'products_id_seq';修改表
ALTER TABLE 是无需重建表即可演进 schema 的方式。PG11+ 添加带默认值的列很快(无需全表重写)。修改列类型可能需要全表重写和 USING 子句进行转换。CASCADE 级联删除依赖对象(视图、外键)——谨慎使用。为约束命名便于后续管理。用 IF EXISTS/IF NOT EXISTS 使脚本幂等。
# add a column
ALTER TABLE users ADD COLUMN bio text;
ALTER TABLE users ADD COLUMN score numeric DEFAULT 0;
# drop a column
ALTER TABLE users DROP COLUMN IF EXISTS bio;
ALTER TABLE users DROP COLUMN bio CASCADE; -- drop dependent objects
# rename a column
ALTER TABLE users RENAME COLUMN username TO handle;
# change column type
ALTER TABLE users ALTER COLUMN age TYPE smallint USING age::smallint;
# set/drop default
ALTER TABLE users ALTER COLUMN role SET DEFAULT 'member';
ALTER TABLE users ALTER COLUMN role DROP DEFAULT;
# add constraint
ALTER TABLE users ADD CONSTRAINT email_check
CHECK (email ~ '^[^@]+@[^@]+\.[^@]+$');约束
约束在数据库层面强制数据完整性——始终优先于应用层检查。复合键适合关联表。ON UPDATE CASCADE 将主键变更传播到外键;ON DELETE SET NULL 将外键置空(外键列必须可空)。DEFERRABLE 约束在提交时 检查(而非逐语句),适合循环引用。为约束命名便于管理——自动生成的名称难看且跨数据库不一致。
# named constraint for easy management
ALTER TABLE users
ADD CONSTRAINT positive_age CHECK (age >= 0);
# composite primary key
CREATE TABLE enrollments (
student_id integer REFERENCES students(id),
course_id integer REFERENCES courses(id),
PRIMARY KEY (student_id, course_id)
);
# unique constraint on multiple columns
ALTER TABLE users ADD CONSTRAINT unique_email_domain
UNIQUE (email, domain);
# foreign key with actions
ALTER TABLE posts
ADD FOREIGN KEY (user_id) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE SET NULL;
# drop a constraint
ALTER TABLE users DROP CONSTRAINT positive_age;
# defer a constraint until commit
SET CONSTRAINTS unique_email_domain DEFERRED;表继承
PostgreSQL 的表继承是独有的——子表继承父表的列。查询父表会返回所有子表的行,除非使用 ONLY。这对表分区(声明式分区之前)和分类建模很有用。但继承有局限:外键不会传播,UNIQUE 约束仅适用于单表。新的分区需求请使用声明式分区 (PARTITION BY)——更健壮且支持更好。
# PostgreSQL supports table inheritance
CREATE TABLE people (
id serial PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE employees (
salary numeric,
dept text
) INHERITS (people);
CREATE TABLE customers (
credit_limit numeric
) INHERITS (people);
# query parent sees all children
SELECT * FROM people; -- includes employees and customers
SELECT * FROM ONLY people; -- only people, not children
# insert into a child
INSERT INTO employees (name, salary, dept)
VALUES ('Alice', 50000, 'Engineering');数据类型
数值类型
根据数据大小选择整数类型——smallint 用于小范围,bigint 用于大范围。numeric/decimal 是精确的(对货币至关重要)但比 real/double 慢。绝不要用 float 存货币——舍入误差会累积。serial 是旧式写法;推荐 GENERATED AS IDENTITY。money 类型受区域设置影响且不灵活——改用 numeric(10,2)。序列在回滚/崩溃时可能跳号(这是为性能的设计)。
# integer types (use the smallest that fits)
smallint -- 2 bytes, -32768 to 32767
integer -- 4 bytes, -2B to 2B
bigint -- 8 bytes, very large
# auto-incrementing (use identity, not serial)
id integer GENERATED ALWAYS AS IDENTITY
# decimal/numeric (exact)
numeric(precision, scale) -- e.g. numeric(10,2) for currency
decimal -- alias for numeric
# floating point (inexact, faster)
real -- 4 bytes, 6 decimal digits
double precision -- 8 bytes, 15 decimal digits
# special
serial / bigserial -- legacy auto-increment (prefer identity)
money -- currency (prefer numeric)
# examples
CREATE TABLE products (
price numeric(10,2), -- $99999999.99
weight real, -- 1.5 (kg)
quantity integer DEFAULT 0
);文本与字符类型
在 PostgreSQL 中,varchar、char 和 text 性能完全相同——text 用于无限长文本,varchar(n) 用于需要长度约束时。char(n) 用空格填充且很少有用。修改 varchar 长度是仅元数据操作(很快)。LIKE 使用 % 和 _ 通配符;ILIKE 不区分大小写;~ 是 POSIX 正则。全文搜索请用 tsvector/tsquery。始终用 text/varchar 存储文本,不要用 bytea。
# three text types (all variable-length)
varchar(n) -- varchar with max length
char(n) -- fixed-length, padded with spaces
text -- unlimited length (preferred)
# in practice, all three perform the same
# varchar(n) adds a length check; char(n) pads (rarely useful)
CREATE TABLE articles (
title varchar(200) NOT NULL,
slug varchar(100) UNIQUE,
body text,
summary text DEFAULT ''
);
# string functions
SELECT length(body), char_length(title), octet_length(title)
FROM articles;
# change length limit (fast, no rewrite)
ALTER TABLE articles ALTER COLUMN title TYPE varchar(500);
# pattern matching
SELECT * FROM articles WHERE title LIKE '%postgres%';
SELECT * FROM articles WHERE title ILIKE '%postgres%'; -- case-insensitive
SELECT * FROM articles WHERE slug ~ '^[a-z0-9-]+$'; -- regex日期与时间类型
始终使用 timestamptz(而非 timestamp)存储时间戳——它以 UTC 存储并按会话时区显示。timestamp(不带时区)是朴素的,在多时区应用中会出 bug。interval 对算术运算很强大('1 day'、'2 hours'、'3 months')。date_trunc 向下取整到指定单位。to_char/to_timestamp 格式化字符串很灵活。按会话或角色设置时区——数据始终以 UTC 存储。
# date/time types
date -- date only (4 bytes)
time -- time only (8 bytes)
timetz -- time with time zone (12 bytes)
timestamp -- timestamp without tz (8 bytes)
timestamptz -- timestamp WITH time zone (8 bytes, preferred)
interval -- time span (16 bytes)
# always use timestamptz for timestamps
CREATE TABLE events (
id serial PRIMARY KEY,
created_at timestamptz DEFAULT now(),
scheduled timestamptz,
duration interval
);
# common operations
SELECT now(); -- current timestamp
SELECT current_date; -- today
SELECT created_at + interval '1 hour'; -- add time
SELECT age('2025-01-01', '2024-01-01'); -- interval between
SELECT date_trunc('month', created_at); -- truncate to month
SELECT to_char(created_at, 'YYYY-MM-DD HH:MI:SS');
SELECT to_timestamp('2025/01/15', 'YYYY/MM/DD');
SELECT EXTRACT(YEAR FROM created_at);
# timezone handling
SET timezone = 'UTC';
SET timezone = 'Asia/Shanghai';JSON 与 JSONB
jsonb 是二进制 JSON——查询更快、可索引,但插入略慢且不保留键顺序。所有新项目都用 jsonb。-> 返回 jsonb,->> 返回文本。@>(包含)是最强大的 JSON 运算符且与 GIN 索引配合良好。jsonb_set 修改路径,|| 合并对象,- 删除键。对频繁查询的 JSON 字段,添加 GIN 索引或对提取值建表达式索引。JSONB 非常适合关系数据库中的灵活 schema。
# json stores text; jsonb stores binary (faster, preferred)
CREATE TABLE products (
id serial PRIMARY KEY,
data jsonb
);
# insert JSON
INSERT INTO products (data) VALUES
('{"name": "Widget", "price": 19.99, "tags": ["sale", "new"]}');
# query JSON fields
SELECT data->'name' FROM products; -- returns jsonb
SELECT data->>'name' FROM products; -- returns text
SELECT data->'tags'->0 FROM products; -- first tag (jsonb)
# filter by JSON
SELECT * FROM products WHERE data->>'name' = 'Widget';
SELECT * FROM products WHERE data @> '{"tags": ["sale"]}'; -- containment
# modify JSON
UPDATE products SET data = jsonb_set(data, '{price}', '29.99');
UPDATE products SET data = data || '{"sale": true}'; -- merge
UPDATE products SET data = data - 'sale'; -- remove key
# index JSON (GIN index)
CREATE INDEX idx_products_data ON products USING GIN (data);
CREATE INDEX idx_products_name ON products ((data->>'name'));数组与自定义类型
PostgreSQL 数组从 1 开始索引(与大多数语言不同)。ANY 和 @> 是主要查询运算符;unnest() 将数组展开为行(适合连接)。数组很强大但需谨慎使用——如果频繁查询/过滤数组元素,规范化表可能更好。枚举类型有序且在插入时验证。复合类型允许结构化列。对于复杂的嵌套数据,考虑用 JSONB 替代复合类型数组。
# array columns
CREATE TABLE teams (
id serial PRIMARY KEY,
name text,
members text[] -- array of text
);
INSERT INTO teams (name, members) VALUES
('Engineering', ARRAY['Alice', 'Bob', 'Carol']),
('Sales', '{"Dave", "Eve"}');
# query arrays
SELECT * FROM teams WHERE 'Alice' = ANY(members);
SELECT * FROM teams WHERE members @> ARRAY['Alice']; -- contains
SELECT members[1] FROM teams; -- 1-indexed!
SELECT array_length(members, 1) FROM teams;
SELECT unnest(members) FROM teams WHERE id = 1; -- expand to rows
# custom enum type
CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral');
CREATE TABLE persons (id serial PRIMARY KEY, current_mood mood);
INSERT INTO persons (current_mood) VALUES ('happy');
# composite type
CREATE TYPE address AS (street text, city text, zip text);
CREATE TABLE contacts (id serial, addr address);CRUD 操作
INSERT
RETURNING 是 PostgreSQL 特性,返回插入/更新/删除的行——省去了插入后单独 SELECT 的需要。ON CONFLICT (UPSERT) 很强大:DO UPDATE 用 EXCLUDED 引用待插入行,DO NOTHING 静默跳过。多行插入比逐行插入快得多。INSERT ... SELECT 在表间复制数据。批量加载用 COPY(更快)——它绕过 SQL 解析开销。
# basic insert
INSERT INTO users (username, email, age)
VALUES ('alice', '[email protected]', 30);
# multi-row insert
INSERT INTO users (username, email) VALUES
('bob', '[email protected]'),
('carol', '[email protected]'),
('dave', '[email protected]');
# insert with RETURNING (get auto-generated values)
INSERT INTO users (username, email)
VALUES ('eve', '[email protected]')
RETURNING id, username, created_at;
# insert from a query (INSERT ... SELECT)
INSERT INTO archive_users (username, email)
SELECT username, email FROM users WHERE active = false;
# upsert (insert or update on conflict)
INSERT INTO users (username, email) VALUES ('alice', '[email protected]')
ON CONFLICT (username)
DO UPDATE SET email = EXCLUDED.email
RETURNING id;
# do nothing on conflict
INSERT INTO users (username, email) VALUES ('alice', '[email protected]')
ON CONFLICT (username) DO NOTHING;UPDATE
除非要更新所有行,否则始终加 WHERE。UPDATE ... FROM 允许在更新中进行连接(非标准但很强大)。RETURNING 显示变更内容——审计必备。CASE 表达式可在单条语句中实现条件更新。不改变任何值的更新是空操作(无 WAL、不触发语句级触发器)。大批量更新可能导致表膨胀——分批执行并随后 VACUUM。
# basic update
UPDATE users SET email = '[email protected]' WHERE id = 1;
# update multiple columns
UPDATE users
SET email = '[email protected]', age = 31, updated_at = now()
WHERE id = 1;
# update with RETURNING
UPDATE users SET role = 'admin' WHERE id = 1
RETURNING id, username, role;
# update from another table
UPDATE posts p
SET title = p.title || ' (archived)'
FROM users u
WHERE p.user_id = u.id AND u.role = 'admin';
# conditional update with CASE
UPDATE products
SET price = CASE
WHEN price < 10 THEN price * 1.2
WHEN price < 50 THEN price * 1.1
ELSE price
END;
# update all rows (be careful!)
UPDATE users SET status = 'active';DELETE 与 TRUNCATE
DELETE 逐行删除(触发触发器,在事务中可见)。TRUNCATE 瞬间清空全表(DDL,事务行为不同,RESTART IDENTITY 重置序列)。清空整表用 TRUNCATE——快几个数量级。CASCADE 级联清空有外键引用的表。DELETE 配合 RETURNING 适合审计日志。DELETE 始终加 WHERE——不加则删除全部(事务内安全,提交后灾难性)。
# basic delete
DELETE FROM users WHERE id = 1;
# delete with RETURNING
DELETE FROM users WHERE active = false
RETURNING id, username;
# delete based on a join
DELETE FROM posts
WHERE user_id IN (
SELECT id FROM users WHERE role = 'deleted'
);
# delete using USING (join syntax)
DELETE FROM posts p
USING users u
WHERE p.user_id = u.id AND u.status = 'deleted';
# truncate (fast, resets sequences, DDL-like)
TRUNCATE TABLE posts;
TRUNCATE TABLE posts, comments; -- multiple tables
TRUNCATE TABLE posts RESTART IDENTITY; -- reset sequences
TRUNCATE TABLE posts CASCADE; -- also truncates FK refs
# delete all rows (slower than TRUNCATE, but transactional)
DELETE FROM posts;SELECT 基础
DISTINCT ON 是 PostgreSQL 扩展,返回每组的第一行——对'每个分类最新/最贵'查询很强大(必须先按 DISTINCT ON 列排序)。LIMIT/OFFSET 分页简单但大偏移量时很慢——用键集分页 (WHERE id > last_id) 性能更好。生产环境始终显式指定列(不要 SELECT *)以避免 schema 变化带来的意外。用 FETCH FIRST n ROWS ONLY 是 SQL 标准语法。
# basic select
SELECT id, username, email FROM users;
# filtering
SELECT * FROM users WHERE age >= 18 AND role = 'user';
SELECT * FROM users WHERE role IN ('admin', 'moderator');
SELECT * FROM users WHERE age BETWEEN 18 AND 65;
SELECT * FROM users WHERE email IS NOT NULL;
SELECT * FROM users WHERE username LIKE 'al%';
# sorting and limiting
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
SELECT * FROM users ORDER BY age DESC, username ASC
OFFSET 20 LIMIT 10; -- pagination (or use FETCH)
# distinct
SELECT DISTINCT role FROM users;
SELECT DISTINCT ON (user_id) * FROM posts
ORDER BY user_id, created_at DESC; -- latest post per user
# column aliases and expressions
SELECT
username AS name,
EXTRACT(YEAR FROM created_at) AS join_year,
age * 365 AS age_in_days
FROM users;MERGE (PG15+)
MERGE(SQL 标准,PG15+)比 ON CONFLICT 更强大:可在一条语句中根据匹配条件插入、更新和删除。适合复杂同步操作(ETL、数据仓库合并)。简单 upsert 用 ON CONFLICT 更简洁且广泛支持。MERGE 需要数据源(表或查询)和匹配条件。WHEN MATCHED/NOT MATCHED 子句定义操作。每个分支可加可选 AND 条件实现精细控制。
# MERGE: insert, update, or delete in one statement (PG15+)
MERGE INTO products p
USING new_prices n
ON p.sku = n.sku
WHEN MATCHED AND p.price != n.price THEN
UPDATE SET price = n.price, updated_at = now()
WHEN MATCHED AND n.discontinued = true THEN
DELETE
WHEN NOT MATCHED THEN
INSERT (sku, name, price) VALUES (n.sku, n.name, n.price);
# simpler upsert (still works, often preferred)
INSERT INTO products (sku, name, price) VALUES ('A1', 'Widget', 9.99)
ON CONFLICT (sku)
DO UPDATE SET price = EXCLUDED.price;
# MERGE with a source query
MERGE INTO inventory i
USING (SELECT product_id, sum(qty) AS total FROM orders GROUP BY product_id) o
ON i.product_id = o.product_id
WHEN MATCHED THEN UPDATE SET quantity = i.quantity - o.total;查询与过滤
WHERE 与条件
ILIKE 是不区分大小写的 LIKE(PostgreSQL 扩展)。~ 是 POSIX 正则(区分大小写),~* 不区分,!~ 取反。IS DISTINCT FROM 将 NULL 视为可比较的值(NULL = NULL 的结果是 NULL,不是 true)。ANY/ALL 适用于数组和子查询。复杂文本匹配考虑全文搜索 (tsvector) 或 trigram 索引 (pg_trgm) 提升性能。大表的 WHERE 列始终建索引。
# comparison operators
SELECT * FROM products WHERE price > 100;
SELECT * FROM products WHERE price <> 0; -- not equal
SELECT * FROM products WHERE name IS DISTINCT FROM 'Test';
# logical operators
SELECT * FROM products
WHERE (price > 50 AND category = 'electronics')
OR (price < 10 AND category = 'books');
# IN, BETWEEN, LIKE/ILIKE
SELECT * FROM products WHERE category IN ('books', 'toys');
SELECT * FROM products WHERE price BETWEEN 10 AND 50;
SELECT * FROM products WHERE name LIKE '_o%'; -- second char 'o'
SELECT * FROM products WHERE name ILIKE '%widget%';
# IS NULL / IS NOT NULL
SELECT * FROM products WHERE description IS NULL;
SELECT * FROM products WHERE description IS NOT DISTINCT FROM NULL;
# ANY/ALL with arrays
SELECT * FROM products WHERE id = ANY(ARRAY[1,2,3]);
SELECT * FROM products WHERE price > ALL(ARRAY[10, 20, 30]);
# regex matching
SELECT * FROM products WHERE name ~ '^[A-C]'; -- starts with A-C
SELECT * FROM products WHERE name ~* 'widget'; -- case-insensitive
SELECT * FROM products WHERE name !~ 'test'; -- does not matchORDER BY 与分页
键集分页 (WHERE id > last_id) 比 OFFSET 快得多——OFFSET 要扫描并丢弃行。ORDER BY random() 在大表上很慢;用 TABLESAMPLE 做统计采样。NULLS FIRST/LAST 控制 NULL 排序位置(默认因 ASC/DESC 而异)。分页时始终加 ORDER BY——否则行顺序未定义。稳定分页请按唯一列排序。
# basic sorting
SELECT * FROM users ORDER BY created_at DESC;
SELECT * FROM users ORDER BY last_name ASC, first_name ASC;
# sort by expression
SELECT * FROM products ORDER BY (price * 1.1) DESC;
SELECT * FROM events ORDER BY created_at::date;
# NULLS handling
SELECT * FROM users ORDER BY last_login DESC NULLS LAST;
SELECT * FROM users ORDER BY last_login DESC NULLS FIRST;
# LIMIT/OFFSET pagination (simple but slow for large offsets)
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 0; -- page 1
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 10; -- page 2
# keyset pagination (faster, use for large datasets)
SELECT * FROM users WHERE id > 100 ORDER BY id LIMIT 10;
# FETCH (SQL standard)
SELECT * FROM users ORDER BY id FETCH FIRST 10 ROWS ONLY;
# random rows
SELECT * FROM products ORDER BY random() LIMIT 5; -- slow on large tables
SELECT * FROM products TABLESAMPLE BERNOULLI(1); -- 1% sampleGROUP BY 与聚合
GROUP BY 按分组列折叠行;聚合函数 (count、sum、avg、min、max) 按组计算。HAVING 过滤组(WHERE 在分组前过滤行)。ROLLUP 在每级添加小计;CUBE 添加所有组合;GROUPING SETS 让你精确指定需要哪些分组——都对报表很有用。count(*) 计行数;count(列) 计非空值。用 count(DISTINCT 列) 计唯一值。
# basic grouping
SELECT category, count(*) FROM products GROUP BY category;
SELECT user_id, sum(amount) FROM orders GROUP BY user_id;
# multiple aggregates
SELECT
category,
count(*) AS total,
avg(price) AS avg_price,
min(price) AS min_price,
max(price) AS max_price,
sum(price) AS total_value
FROM products
GROUP BY category;
# HAVING (filter on aggregates, like WHERE for groups)
SELECT user_id, count(*) AS order_count
FROM orders
GROUP BY user_id
HAVING count(*) > 5;
# GROUP BY with multiple columns
SELECT category, status, count(*)
FROM products
GROUP BY category, status
ORDER BY category, status;
# GROUP BY ROLLUP/CUBE (subtotals)
SELECT category, status, count(*)
FROM products
GROUP BY ROLLUP (category, status); -- adds subtotals and grand total
# GROUP BY GROUPING SETS
SELECT category, status, count(*)
FROM products
GROUP BY GROUPING SETS ((category, status), (category), ());DISTINCT 与集合操作
UNION 合并结果并去重(慢);UNION ALL 保留重复(快,在确定无重复或不在意时首选)。INTERSECT 返回共同行;EXCEPT 返回差集。集合操作要求兼容的列类型和数量。DISTINCT ON 是 PostgreSQL 的瑰宝:基于 ORDER BY 返回每组第一行——完美解决'每分类最新/最贵'查询。所有集合操作可链式使用,但用括号控制优先级。
# distinct rows
SELECT DISTINCT category FROM products;
SELECT DISTINCT category, status FROM products;
# DISTINCT ON (first row per group)
SELECT DISTINCT ON (category) *
FROM products
ORDER BY category, price DESC; -- most expensive per category
# UNION (combine, remove duplicates)
SELECT 'user' AS type, username FROM users
UNION
SELECT 'admin' AS type, username FROM admins;
# UNION ALL (faster, keeps duplicates)
SELECT username FROM active_users
UNION ALL
SELECT username FROM inactive_users;
# INTERSECT (common rows)
SELECT product_id FROM orders_2024
INTERSECT
SELECT product_id FROM orders_2025;
# EXCEPT (rows in first but not second)
SELECT product_id FROM all_products
EXCEPT
SELECT product_id FROM discontinued_products;
# set operations with ORDER BY (applies to the whole result)
SELECT username FROM users
UNION
SELECT username FROM admins
ORDER BY username;CASE 与条件逻辑
CASE 表达式为 SQL 添加 if/then/else 逻辑。简单 CASE 匹配值;搜索 CASE 评估条件。FILTER 子句 (PostgreSQL) 比聚合内的 CASE 更简洁,适合条件计数。CASE 可出现在 SELECT、WHERE、ORDER BY 甚至 GROUP BY 中。用于数据透视、条件格式化和属于数据库的业务逻辑。复杂多分支逻辑考虑用函数或视图。
# simple CASE
SELECT
name,
CASE category
WHEN 'book' THEN 'Media'
WHEN 'laptop' THEN 'Electronics'
ELSE 'Other'
END AS category_group
FROM products;
# searched CASE (more flexible)
SELECT
name,
CASE
WHEN price < 10 THEN 'Cheap'
WHEN price < 50 THEN 'Moderate'
WHEN price < 100 THEN 'Expensive'
ELSE 'Luxury'
END AS price_tier
FROM products;
# CASE in aggregate (pivot-like)
SELECT
count(*) AS total,
count(*) FILTER (WHERE status = 'active') AS active_count,
count(*) FILTER (WHERE status = 'inactive') AS inactive_count
FROM users;
# CASE for conditional sorting
SELECT * FROM products
ORDER BY
CASE WHEN featured THEN 0 ELSE 1 END,
name;连接
内连接与外连接
INNER JOIN 只返回匹配行。LEFT JOIN 保留所有左表行(无匹配时右表列为 NULL)——最常用的'有则包含关联数据'连接。RIGHT JOIN 很少用(改写为 LEFT)。FULL OUTER JOIN 保留所有内容。LEFT JOIN 后 WHERE p.id IS NULL 是'反连接'模式,查找无匹配的行。始终用表别名限定列名 (u.id、p.user_id) 避免歧义。
# INNER JOIN (only matching rows)
SELECT u.username, p.title
FROM users u
INNER JOIN posts p ON u.id = p.user_id;
# LEFT JOIN (all left rows, matching right or NULL)
SELECT u.username, p.title
FROM users u
LEFT JOIN posts p ON u.id = p.user_id;
# RIGHT JOIN (all right rows, matching left or NULL)
SELECT u.username, p.title
FROM users u
RIGHT JOIN posts p ON u.id = p.user_id;
# FULL OUTER JOIN (all rows from both, NULLs where no match)
SELECT u.username, p.title
FROM users u
FULL OUTER JOIN posts p ON u.id = p.user_id;
# find rows with no match (anti-join pattern)
SELECT u.username
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE p.id IS NULL; -- users with no posts多表连接
PostgreSQL 可以连接很多表,但每个连接都增加成本——确保连接使用索引。自连接适合层次数据(但深层级考虑递归 CTE)。与聚合连接时,LEFT JOIN + count(p.id)(不是 count(*))为无帖子的用户返回 0。优化器选择连接顺序,但写清晰的查询有帮助。复杂多表查询考虑创建视图。
# join three tables
SELECT u.username, p.title, c.text AS comment
FROM users u
JOIN posts p ON u.id = p.user_id
JOIN comments c ON p.id = c.post_id;
# join with different conditions
SELECT
u.username,
p.title,
l.name AS liked_by
FROM posts p
JOIN users u ON p.user_id = u.id
LEFT JOIN likes l ON p.id = l.post_id AND l.user_id != u.id;
# self-join (e.g., employees and their managers)
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
# join with aggregation
SELECT u.username, count(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.username
ORDER BY post_count DESC;交叉连接与 LATERAL
CROSS JOIN 产生笛卡尔积(A 的每行与 B 的每行)——很少是你想要的,但与 generate_series 配合生成数据很有用。LATERAL 很强大:让子查询引用外查询的列,类似逐行函数。非常适合'每组 Top N'查询(比窗口函数更简洁)。LEFT JOIN LATERAL ... ON true 包含无匹配的行。
# CROSS JOIN (Cartesian product — every combination)
SELECT u.username, c.name
FROM users u
CROSS JOIN categories c;
-- same as: SELECT ... FROM users, categories;
# useful for generating combinations
SELECT generate_series(1, 5) AS n, c.name
FROM categories c
CROSS JOIN generate_series(1, 5);
# LATERAL (subquery can reference outer query)
SELECT u.username, recent.title
FROM users u
CROSS JOIN LATERAL (
SELECT title FROM posts
WHERE user_id = u.id
ORDER BY created_at DESC
LIMIT 3
) recent;
# LATERAL with LEFT JOIN (include users with no posts)
SELECT u.username, recent.title
FROM users u
LEFT JOIN LATERAL (
SELECT title FROM posts WHERE user_id = u.id
ORDER BY created_at DESC LIMIT 3
) recent ON true;连接策略 (EXPLAIN)
PostgreSQL 根据表大小、索引和统计信息选择连接策略。Nested Loop 是 O(N*M)——适合小表或索引查找。Hash Join 在较小输入上建哈希表再探测——适合大型无序连接。Merge Join 要求两端已排序——索引提供排序时很好。生产中不要强制策略;让优化器决定。用 EXPLAIN (ANALYZE) 查看实际耗时,按需调整索引/统计信息。
# see how PostgreSQL joins tables
EXPLAIN SELECT * FROM users u JOIN posts p ON u.id = p.user_id;
# join strategies you'll see:
# Nested Loop: for each left row, scan right (good for small/indexed)
# Hash Join: hash one table, probe with other (good for large unsorted)
# Merge Join: both sorted, merge (good for sorted/indexed large tables)
# force a join strategy (for testing, not production)
SET enable_nestloop = off;
SET enable_hashjoin = off;
SET enable_mergejoin = off;
# check actual execution stats
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM users u JOIN posts p ON u.id = p.user_id;
# join removal: PostgreSQL can remove unnecessary joins
# (e.g., LEFT JOIN to a table only used for FK validation)
EXPLAIN SELECT u.* FROM users u LEFT JOIN posts p ON u.id = p.user_id;连接性能技巧
最有影响的连接优化是索引外键列——PostgreSQL 不会自动为外键创建索引。用 ANALYZE 保持统计新鲜(autovacuum 会做,但批量加载后手动 ANALYZE 有帮助)。避免在连接列上包函数(会阻止使用索引)。只选需要的列。超大表考虑分区让优化器裁剪分区。用 pg_stat_statements 监控慢连接。
# always index foreign key columns
CREATE INDEX idx_posts_user_id ON posts(user_id);
# index join columns on both sides
CREATE INDEX idx_orders_product_id ON orders(product_id);
CREATE INDEX idx_products_id ON products(id); -- PK already indexed
# composite index for multi-column joins
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at);
# analyze tables for accurate statistics
ANALYZE users;
ANALYZE posts;
# avoid joining on expressions (prevents index use)
-- BAD: JOIN ON lower(a.email) = lower(b.email)
-- GOOD: JOIN ON a.email = b.email (normalize case on insert)
# limit columns selected (reduces I/O)
SELECT u.username, p.title -- not SELECT *
FROM users u JOIN posts p ON u.id = p.user_id;
# partition large tables for join pruning
SELECT * FROM orders o
JOIN products p ON o.product_id = p.id
WHERE o.order_date >= '2025-01-01'; -- prunes partitions聚合与窗口函数
聚合函数
PostgreSQL 有丰富的聚合函数,远超 count/sum/avg。string_agg 和 array_agg 将值拼接为字符串/数组——避免 N+1 查询。bool_or/bool_and 聚合布尔值。percentile_cont 计算精确百分位(中位数 = 0.5)——对分析非常有用。mode() 返回最频繁值。FILTER 子句 (count(*) FILTER (WHERE...)) 比 CASE 更简洁。聚合内 ORDER BY 控制输出顺序。
# standard aggregates
SELECT
count(*) AS total_rows,
count(distinct category) AS unique_categories,
sum(price) AS total_value,
avg(price) AS avg_price,
min(price) AS min_price,
max(price) AS max_price,
stddev(price) AS std_dev,
variance(price) AS variance
FROM products;
# string aggregation
SELECT
user_id,
string_agg(tag, ', ' ORDER BY tag) AS tags
FROM post_tags
GROUP BY user_id;
# array aggregation
SELECT
user_id,
array_agg(title ORDER BY created_at DESC) AS recent_titles
FROM posts
GROUP BY user_id;
# boolean aggregation
SELECT
bool_or(published) AS any_published,
bool_and(published) AS all_published
FROM posts;
# statistical aggregates
SELECT
percentile_cont(0.5) WITHIN GROUP (ORDER BY price) AS median,
percentile_cont(0.95) WITHIN GROUP (ORDER BY price) AS p95,
mode() WITHIN GROUP (ORDER BY price) AS most_common
FROM products;窗口函数
窗口函数在与当前行相关的一组行上计算——不像 GROUP BY 那样折叠行。PARTITION BY 定义窗口组;ORDER BY 定义组内顺序。ROW_NUMBER 始终唯一;RANK 在并列后跳号;DENSE_RANK 不跳号。LAG/LEAD 访问其他行——完美用于时间序列分析。帧子句 (ROWS BETWEEN ...) 控制窗口包含哪些行。窗口函数在 WHERE/GROUP BY/HAVING 之后评估。
# ROW_NUMBER: unique sequential number
SELECT
title,
price,
ROW_NUMBER() OVER (ORDER BY price DESC) AS rank
FROM products;
# RANK and DENSE_RANK (handle ties)
SELECT
title,
category,
price,
RANK() OVER (PARTITION BY category ORDER BY price DESC) AS cat_rank,
DENSE_RANK() OVER (PARTITION BY category ORDER BY price DESC) AS dense_rank
FROM products;
# LAG and LEAD (compare to previous/next row)
SELECT
date,
revenue,
LAG(revenue, 1) OVER (ORDER BY date) AS prev_day,
revenue - LAG(revenue, 1) OVER (ORDER BY date) AS daily_change,
LEAD(revenue, 1) OVER (ORDER BY date) AS next_day
FROM daily_sales;
# running total
SELECT
date,
revenue,
SUM(revenue) OVER (ORDER BY date) AS running_total,
AVG(revenue) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7d
FROM daily_sales;窗口帧与 NTILE
窗口帧控制窗口函数能看到哪些行。聚合的默认帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(累计求和)。对 LAST_VALUE,必须将帧扩展到 UNBOUNDED FOLLOWING,否则返回当前行(默认帧到当前行结束)。NTILE 将行分为 N 个桶——适合四分位/十分位。FIRST_VALUE/LAST_VALUE 返回帧边界行的值。理解帧是掌握窗口函数的关键。
# frame types
# ROWS: exact row count
# RANGE: logical range (e.g., same value)
# GROUPS: groups of peer rows
SELECT
date,
revenue,
# running sum from start to current
SUM(revenue) OVER (ORDER BY date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative,
# 7-day moving average
AVG(revenue) OVER (ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS avg_7d,
# sum of all rows (no frame = entire partition)
SUM(revenue) OVER () AS grand_total
FROM daily_sales;
# NTILE: divide rows into N equal buckets
SELECT
name,
price,
NTILE(4) OVER (ORDER BY price DESC) AS price_quartile
FROM products;
# FIRST_VALUE / LAST_VALUE
SELECT
name,
category,
price,
FIRST_VALUE(name) OVER (PARTITION BY category ORDER BY price DESC) AS most_expensive,
LAST_VALUE(name) OVER (
PARTITION BY category ORDER BY price DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS least_expensive
FROM products;FILTER 子句
FILTER 子句是 PostgreSQL 扩展,比聚合内的 CASE 更简洁且通常更快。特别适合需要多个条件聚合的透视报表。查询只扫描表一次并在单遍中计算所有聚合——比多个子查询高效得多。FILTER 是 SQL 标准的一部分(不像许多 PG 扩展),其他数据库也支持。
# FILTER (PostgreSQL extension, cleaner than CASE)
SELECT
count(*) AS total,
count(*) FILTER (WHERE status = 'active') AS active,
count(*) FILTER (WHERE status = 'inactive') AS inactive,
count(*) FILTER (WHERE status = 'banned') AS banned,
sum(amount) FILTER (WHERE status = 'active') AS active_revenue
FROM orders
GROUP BY user_id;
# equivalent with CASE (more verbose)
SELECT
count(*) AS total,
count(CASE WHEN status = 'active' THEN 1 END) AS active,
sum(CASE WHEN status = 'active' THEN amount ELSE 0 END) AS active_revenue
FROM orders
GROUP BY user_id;
# FILTER with multiple aggregates in one pass (efficient)
SELECT
category,
count(*) FILTER (WHERE price > 100) AS expensive_count,
avg(price) FILTER (WHERE price > 100) AS expensive_avg,
count(*) FILTER (WHERE price <= 100) AS cheap_count,
avg(price) FILTER (WHERE price <= 100) AS cheap_avg
FROM products
GROUP BY category;公用表表达式 (CTE)
CTE(WITH 子句)提升查询可读性并允许复用子查询。PostgreSQL 12+ 默认内联非递归 CTE(性能与子查询相同);12 之前 CTE 是优化屏障。递归 CTE 对层次/树形数据很强大——锚定查询播种,递归部分增长。配合 RETURNING 的 CTE 可在一条语句中实现复杂数据移动(从一表删除、归档到另一表)。用 CTE 将复杂查询拆分为可读的步骤。
# basic CTE (WITH clause)
WITH active_users AS (
SELECT id, username FROM users WHERE active = true
)
SELECT au.username, count(p.id) AS post_count
FROM active_users au
LEFT JOIN posts p ON au.id = p.user_id
GROUP BY au.username;
# multiple CTEs (can reference each other)
WITH user_stats AS (
SELECT user_id, count(*) AS post_count FROM posts GROUP BY user_id
),
top_users AS (
SELECT user_id FROM user_stats WHERE post_count > 10
)
SELECT u.username, us.post_count
FROM users u
JOIN top_users tu ON u.id = tu.user_id
JOIN user_stats us ON u.id = us.user_id;
# recursive CTE (tree traversal)
WITH RECURSIVE org_tree AS (
SELECT id, name, manager_id, 0 AS depth
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, ot.depth + 1
FROM employees e
JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT id, name, depth FROM org_tree ORDER BY depth;
# CTE for DML (data modification)
WITH deleted AS (
DELETE FROM users WHERE active = false RETURNING id
)
INSERT INTO archived_users (user_id)
SELECT id FROM deleted;索引
B-Tree 索引
B-tree 是默认且最常见的索引类型——支持等值、范围和排序。复合索引从左到右搜索,所以等值列放前面、范围/排序列放后面。部分索引 (WHERE 子句) 节省空间且更快——用于常见过滤查询(如 WHERE active = true)。表达式索引可对函数结果建索引。CONCURRENTLY 构建时不阻塞写(较慢但生产安全)。始终索引外键!
# create a B-tree index (default)
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_created ON posts(created_at DESC);
# unique index (enforces uniqueness)
CREATE UNIQUE INDEX idx_users_username ON users(username);
# composite index (order matters!)
CREATE INDEX idx_posts_user_created
ON posts(user_id, created_at DESC);
# partial index (only rows matching the condition)
CREATE INDEX idx_active_users ON users(last_login)
WHERE active = true;
# expression index (index on a function)
CREATE INDEX idx_users_lower_email ON users(lower(email));
# query must use the same expression:
# SELECT * FROM users WHERE lower(email) = '[email protected]'
# check index usage
SELECT * FROM pg_stat_user_indexes WHERE relname = 'users';
# drop an index
DROP INDEX IF EXISTS idx_users_email;
DROP INDEX CONCURRENTLY idx_users_email; -- no lock, slowerGIN 与 GiST 索引
GIN 索引适合每行有多个索引值的情况(JSONB 键、数组元素、文本词元)。支持 @>(包含)、?(键存在)和全文 @@ 运算符。GIN 索引较大但能高效处理这些查询。GiST 用于几何数据(点、多边形)和范围类型——支持重叠 (&&)、包含 (@>)、被包含 (<@)。GIN 和 GiST 构建/更新比 B-tree 慢,但能实现 B-tree 无法完成的查询。JSONB 和全文用 GIN;空间和范围查询用 GiST。
# GIN (Generalized Inverted Index) — for multi-value columns
# great for JSONB, arrays, full-text search
CREATE INDEX idx_products_data ON products USING GIN (data); -- JSONB
CREATE INDEX idx_posts_tags ON posts USING GIN (tags); -- array
CREATE INDEX idx_articles_search ON articles USING GIN (search_vector); -- full-text
# query patterns that use GIN
SELECT * FROM products WHERE data @> '{"tags": ["sale"]}';
SELECT * FROM posts WHERE tags && ARRAY['python'];
SELECT * FROM articles WHERE search_vector @@ to_tsquery('postgres & index');
# GiST (Generalized Search Tree) — for geometric/range data
CREATE INDEX idx_locations ON places USING GIST (location); -- point/polygon
CREATE INDEX idx_events ON events USING GIST (during); -- tsrange
# geometric queries
SELECT * FROM places
WHERE location <@ box '(0,0),(10,10)';
# range queries
SELECT * FROM events
WHERE during && tsrange('2025-01-01', '2025-02-01');BRIN 与其他索引类型
BRIN(块范围索引)极其紧凑(KB 而非 GB)——它存储每个块范围的 min/max。在物理行序与查询序匹配时有效(如追加式时间序列日志)。BRIN 提供'足够好'的过滤,淘汰大部分块;PostgreSQL 再检查剩余行。对大型追加式表,BRIN 是颠覆性的。Hash 索引只支持等值(不支持范围)但点查询很快。SP-GiST 适合 IP 路由表等不平衡数据。
# BRIN (Block Range Index) — tiny, for naturally ordered data
# best for huge tables where data is physically ordered (e.g., time-series)
CREATE INDEX idx_logs_timestamp ON logs USING BRIN (timestamp);
# index is kilobytes, not gigabytes!
# BRIN works by storing min/max for each block range
# effective when physical order matches logical order (e.g., append-only logs)
# compare sizes:
# B-tree on 1B rows: ~20GB
# BRIN on 1B rows: ~2MB (10000x smaller)
# SP-GiST (space-partitioned GiST) — for non-balanced data
CREATE INDEX idx_prefix ON routes USING SPGiST (prefix); -- e.g., IP routing
# Hash index (equality only, crash-safe since PG10)
CREATE INDEX idx_users_session ON users USING HASH (session_token);
# only supports = operator, not ranges or sorting
# check what index types are available
SELECT amname FROM pg_am WHERE amtype = 'i';索引维护与分析
用 pg_stat_user_indexes 监控索引使用——未使用的索引浪费空间且拖慢写入。删除重复索引(相同列、相同表)。REINDEX 重建膨胀的索引(大量更新/删除后常见);CONCURRENTLY (12+) 避免锁定。索引膨胀是正常的——autovacuum 维护索引但有时需手动 REINDEX。超大索引考虑 REINDEX CONCURRENTLY 或 pg_repack(无锁重建的第三方工具)。索引变更始终在预发环境测试。
# find unused indexes (candidates for removal)
SELECT
schemaname, relname, indexrelname,
idx_scan AS scans,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;
# find duplicate indexes
SELECT pg_size_pretty(sum(pg_relation_size(idx))::bigint) AS size,
(array_agg(idx::text))[1] AS indexes
FROM (
SELECT indexrelid::regclass AS idx, indrelid::regclass AS rel
FROM pg_index
GROUP BY indrelid, indkey
HAVING count(*) > 1
) sub;
# REINDEX (rebuild a bloated index)
REINDEX INDEX idx_users_email;
REINDEX TABLE CONCURRENTLY users; -- PG12+, no lock
# check index bloat
SELECT
relname, pg_size_pretty(pg_relation_size(relid)) AS size,
pg_size_pretty(pg_indexes_size(relid)) AS index_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_relation_size(relid) DESC;索引策略
从主键(自动索引)和外键(手动)开始。为热查询路径添加索引。复合索引遵循最左前缀规则——按选择性和查询模式排列列。INCLUDE(覆盖索引)让查询仅从索引满足(索引-only 扫描)无需取行——对读多写少负载意义重大。不要什么都索引:每个索引增加写入开销。用 pg_stat_user_indexes 定期审查和修剪。始终 EXPLAIN 验证索引使用。
# 1. index primary keys (auto-created)
# 2. index foreign keys (NOT auto-created!)
CREATE INDEX idx_posts_user_id ON posts(user_id);
# 3. index columns in WHERE, JOIN, ORDER BY, GROUP BY
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_orders_created ON orders(created_at);
# 4. use composite indexes for multi-column queries
# leftmost prefix rule: (a, b, c) helps WHERE a=?, WHERE a=? AND b=?
# but NOT WHERE b=? alone
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
# 5. partial indexes for common filters
CREATE INDEX idx_active_orders ON orders(created_at)
WHERE status = 'active';
# 6. covering indexes (INCLUDE for index-only scans)
CREATE INDEX idx_products_cat_price ON products(category, price)
INCLUDE (name); -- name available without heap fetch
# 7. don't over-index — every index slows writes
# drop indexes used < 10 times/month (check pg_stat_user_indexes)
# verify the index is used
EXPLAIN SELECT * FROM orders WHERE user_id = 1;事务与并发
事务基础
事务将语句组合为原子单元——全部成功或全部失败。BEGIN 开始,COMMIT 保存,ROLLBACK 撤销。保存点允许事务内部分回滚——适合复杂操作的错误恢复。默认 PostgreSQL 自动提交每条语句。多语句操作(转账、多行插入)始终用显式事务。保持事务简短以减少锁竞争和死锁。
# explicit transaction
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- or ROLLBACK to undo
# savepoints (partial rollback)
BEGIN;
INSERT INTO orders (user_id, amount) VALUES (1, 50);
SAVEPOINT order_inserted;
INSERT INTO order_items (order_id, product_id) VALUES (1, 100);
-- oops, wrong product
ROLLBACK TO order_inserted;
INSERT INTO order_items (order_id, product_id) VALUES (1, 200);
COMMIT;
# transaction control
BEGIN;
-- statements
SAVEPOINT my_savepoint;
-- more statements
ROLLBACK TO my_savepoint; -- undo back to savepoint
RELEASE my_savepoint; -- remove savepoint (commit its changes)
COMMIT;
-- or ROLLBACK to undo everything
# auto-commit (default — each statement is its own transaction)
SET autocommit = on; -- default隔离级别
PostgreSQL 默认 READ COMMITTED 适合大多数应用——无脏读,但同一事务内重读可能看到新数据。PostgreSQL 的 REPEATABLE READ 比 SQL 标准更强——也防止幻读(使用快照隔离)。SERIALIZABLE 最强但可能中止冲突的事务——应用必须在 SQLSTATE 40001 时重试。大多数 Web 应用用 READ COMMITTED 即可。关键正确性场景(财务账本)用 SERIALIZABLE 加重试逻辑。
# set isolation level for a transaction
BEGIN ISOLATION LEVEL READ COMMITTED; -- default
BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN ISOLATION LEVEL SERIALIZABLE;
# or set it for the session
SET default_transaction_isolation = 'serializable';
# READ COMMITTED (default):
# - each query sees committed data at query start
# - no dirty reads, but non-repeatable reads and phantoms possible
# REPEATABLE READ:
# - each query sees snapshot from transaction start
# - no non-repeatable reads, but serialization anomalies possible
# - PostgreSQL's RR is stronger than SQL standard (no phantoms!)
# SERIALIZABLE:
# - strongest isolation, behaves as if transactions ran one at a time
# - may abort with serialization failure (retry needed)
# - uses SSI (Serializable Snapshot Isolation)
# check current level
SHOW transaction_isolation;
# handle serialization failures (retry)
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- work
COMMIT; -- may fail with SQLSTATE 40001 -> retry the transaction锁与死锁
SELECT FOR UPDATE 锁定行防止并发修改——适合'读取-检查-更新'模式。咨询锁是使用整数键的应用级锁——适合协调分布式进程。死锁在两个事务互相等待时发生;PostgreSQL 检测到后中止其中一个(捕获并重试)。为避免死锁,事务间以一致顺序获取锁。保持事务简短。监控 pg_locks 查找卡住的会话。
# row-level locks (acquired by UPDATE, DELETE, SELECT FOR UPDATE)
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- lock row
SELECT * FROM accounts WHERE id = 1 FOR NO KEY UPDATE; -- weaker lock
SELECT * FROM accounts WHERE id = 1 FOR SHARE; -- allow reads, block writes
# advisory locks (application-level locks)
SELECT pg_advisory_lock(12345); -- session-level
SELECT pg_advisory_unlock(12345);
SELECT pg_try_advisory_lock(12345); -- non-blocking
SELECT pg_advisory_xact_lock(12345); -- transaction-level (auto-released)
# view active locks
SELECT pid, mode, granted, query
FROM pg_locks l JOIN pg_stat_activity a ON l.pid = a.pid
WHERE NOT l.granted;
# terminate a blocked session
SELECT pg_cancel_backend(pid); -- cancel query
SELECT pg_terminate_backend(pid); -- kill session
# deadlocks (PostgreSQL detects and resolves by killing one transaction)
-- Transaction A: UPDATE users WHERE id=1; UPDATE users WHERE id=2;
-- Transaction B: UPDATE users WHERE id=2; UPDATE users WHERE id=1;
-- -> ERROR: deadlock detectedMVCC 与快照隔离
PostgreSQL 的 MVCC 意味着读不阻塞写、写不阻塞读——每个事务看到一个快照。更新创建新行版本;旧版本在 VACUUM 清理前保留。长时间运行的事务阻止 VACUUM 清理旧版本,导致膨胀。保持事务简短!监控 'idle in transaction' 会话——它们持有快照并阻止清理。事务 ID 回卷是严重问题(autovacuum 会预防,但如果 autovacuum 跟不上需监控)。
# PostgreSQL uses MVCC (Multi-Version Concurrency Control)
# each transaction sees a consistent snapshot of data
# visible transactions
SELECT txid_current(); -- current transaction ID
SELECT txid_snapshot_xmin(txid_current_snapshot());
# see long-running transactions (can prevent vacuum)
SELECT
pid,
age(clock_timestamp(), xact_start) AS duration,
state,
query
FROM pg_stat_activity
WHERE state IN ('active', 'idle in transaction')
ORDER BY xact_start;
# long transactions block vacuum (old row versions can't be removed)
# check for this:
SELECT
pid,
age(txid_current(), backend_xmin) AS xmin_age
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY xmin_age DESC;
# vacuum and the transaction ID wraparound
# (autovacuum handles this, but monitor it!)
SELECT relname, last_autovacuum, last_autoanalyze, n_dead_tup
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000;锁定模式
FOR UPDATE 悲观锁行——用于关键一致性(转账)。乐观锁(版本列)在低竞争时更好:无锁,冲突时重试。SKIP LOCKED 非常适合任务队列——多个 worker 抓取不同任务而不阻塞。NOWAIT 被锁时立即失败(用于'尝试更新,不等待')。咨询锁协调应用级操作(迁移、定时任务)。高竞争用悲观锁,低竞争用乐观锁。
# pessimistic locking (lock before work)
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- now safe to modify
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
# optimistic locking (check version, retry on conflict)
-- uses a version column
UPDATE products SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5; -- 0 rows = conflict, retry
# SKIP LOCKED (process queue items without blocking)
SELECT * FROM job_queue
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 10;
-- grabs 10 jobs, skips locked ones (other workers don't wait)
# NOWAIT (fail immediately if locked)
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
-- ERROR: could not obtain lock if row is locked
# named lock for coordination
SELECT pg_advisory_lock(hashtext('migrate_users'));
-- only one session can hold this lock
SELECT pg_advisory_unlock(hashtext('migrate_users'));视图与物化视图
创建视图
视图是行为类似表的保存查询。简单视图(单表、无聚合)自动可更新——插入/更新透传到底层表。WITH CHECK OPTION 防止插入/更新后通过视图看不到的行。用视图简化复杂查询、强制安全(列级访问)和提供稳定 API。CASCADE 级联删除依赖对象。视图不存储数据——每次运行查询。
# simple view
CREATE VIEW active_users AS
SELECT id, username, email FROM users WHERE active = true;
# use it like a table
SELECT * FROM active_users WHERE username LIKE 'a%';
# view with computed columns
CREATE VIEW user_summary AS
SELECT
u.id,
u.username,
count(p.id) AS post_count,
max(p.created_at) AS last_post
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.username;
# updatable view (simple views can be inserted/updated through)
CREATE VIEW user_emails AS
SELECT id, email FROM users;
INSERT INTO user_emails (id, email) VALUES (1, '[email protected]'); -- works!
# WITH CHECK OPTION (prevent inserts that wouldn't be visible)
CREATE VIEW active_users AS
SELECT * FROM users WHERE active = true
WITH CHECK OPTION;
-- INSERT with active=false fails
# modify or drop a view
ALTER VIEW active_users RENAME TO current_users;
DROP VIEW IF EXISTS active_users CASCADE;物化视图
物化视图存储查询结果——查询快但刷新前是过时的。用于昂贵的聚合(仪表盘、报表)。CONCURRENTLY 无锁刷新(读者继续查询)但需要唯一索引。用 pg_cron 或外部工具调度刷新。准实时数据每几分钟刷新;历史分析每日刷新。权衡是新鲜度 vs 查询速度——物化视图是大型表快速分析的关键。
# create a materialized view (stores the result)
CREATE MATERIALIZED VIEW sales_summary AS
SELECT
date_trunc('day', created_at) AS day,
product_id,
count(*) AS order_count,
sum(amount) AS total_revenue
FROM orders
GROUP BY 1, 2
WITH DATA; -- or WITH NO DATA to populate later
# refresh (re-runs the query)
REFRESH MATERIALIZED VIEW sales_summary;
REFRESH MATERIALIZED VIEW CONCURRENTLY sales_summary; -- needs unique index
# create a unique index for concurrent refresh
CREATE UNIQUE INDEX idx_sales_summary_day_product
ON sales_summary(day, product_id);
# schedule refresh (use pg_cron extension or external scheduler)
-- pg_cron: SELECT cron.schedule('0 * * * *', 'REFRESH MATERIALIZED VIEW CONCURRENTLY sales_summary');
# drop
DROP MATERIALIZED VIEW IF EXISTS sales_summary;
# use it (like a regular table)
SELECT * FROM sales_summary WHERE day = '2025-01-01';安全视图
视图是强大的安全工具:只暴露用户应看到的列/行而无需授予表访问权限。在视图上授 SELECT,而非底层表。对于行级安全,考虑 PostgreSQL 内置的 RLS(行级安全)策略——更健壮且直接表访问也生效。带会话变量 (current_setting) 的视图实现按用户过滤。用视图创建稳定 API 层,向应用隐藏 schema 复杂性。
# column-level security (hide sensitive columns)
CREATE VIEW public_users AS
SELECT id, username, created_at FROM users;
-- password, email columns are hidden
# row-level security via view
CREATE VIEW user_posts AS
SELECT * FROM posts WHERE user_id = current_setting('app.current_user_id')::int;
-- set the user context per session
SET app.current_user_id = '42';
SELECT * FROM user_posts; -- only sees user 42's posts
# view with joins for simplified access
CREATE VIEW order_details AS
SELECT
o.id AS order_id,
o.created_at,
u.username,
p.name AS product_name,
oi.quantity,
oi.price
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;
# grant access to view but not underlying tables
GRANT SELECT ON public_users TO app_readonly;
REVOKE SELECT ON users FROM app_readonly;视图管理
CREATE OR REPLACE VIEW 有局限——只能在末尾添加列,不能删除或重排列。重大变更需删除重建(但先检查依赖)。视图可依赖其他视图;删除基础视图需 CASCADE。用 pg_views 检查定义。刷新所有物化视图的 DO 块适合维护脚本——但尽量用 CONCURRENTLY 避免锁定。
# list views
SELECT viewname, definition FROM pg_views WHERE schemaname = 'public';
# get view definition
\d+ view_name -- in psql
# create or replace (limited — can't change columns)
CREATE OR REPLACE VIEW user_summary AS
SELECT id, username, email FROM users WHERE active = true;
-- can add columns at the end, but can't remove/reorder
# to change columns significantly, drop and recreate
DROP VIEW IF EXISTS user_summary;
CREATE VIEW user_summary AS
SELECT id, username FROM users WHERE active = true;
# view dependencies (what depends on this view)
SELECT dependee.relname AS view, depender.relname AS depends_on
FROM pg_depend d
JOIN pg_class dependee ON d.objid = dependee.oid
JOIN pg_class depender ON d.refobjid = depender.oid
WHERE dependee.relname = 'user_summary';
# refresh all materialized views
DO $$
DECLARE r RECORD;
BEGIN
FOR r IN SELECT matviewname FROM pg_matviews WHERE schemaname = 'public' LOOP
EXECUTE 'REFRESH MATERIALIZED VIEW ' || r.matviewname;
END LOOP;
END $$;递归视图
递归视图 (CREATE RECURSIVE VIEW) 封装递归 CTE——适合树/图遍历。递归部分始终包含深度/距离限制以防无限循环(数据中的环)。path 列(文本拼接)是跟踪和显示遍历路径的简单方式。对于非常深的层次结构,考虑用闭包表或 ltree 扩展获得更好性能。递归视图方便但在大数据集上可能慢——在连接列上添加索引。
# recursive view for hierarchical data
CREATE RECURSIVE VIEW employee_tree(id, name, manager_id, level, path) AS
SELECT
id, name, manager_id, 0, name::text
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT
e.id, e.name, e.manager_id, et.level + 1,
et.path || ' > ' || e.name
FROM employees e
JOIN employee_tree et ON e.manager_id = et.id;
# query the tree
SELECT * FROM employee_tree ORDER BY path;
# find all subordinates (2 levels deep)
SELECT * FROM employee_tree
WHERE path LIKE 'CEO > Engineering > %' AND level <= 3;
# view for graph traversal (e.g., friend connections)
CREATE RECURSIVE VIEW friend_chain(person1, person2, distance, path) AS
SELECT person1, person2, 1, person1 || '->' || person2
FROM friendships
UNION
SELECT
fc.person1, f.person2, fc.distance + 1,
fc.path || '->' || f.person2
FROM friend_chain fc
JOIN friendships f ON fc.person2 = f.person1
WHERE fc.distance < 6; -- limit depth to prevent infinite loops函数与运算符
字符串函数
PostgreSQL 有全面的字符串函数。position() 和 strpos() 查找子串。substring() 配 FROM/FOR 是 SQL 标准;substr() 更简洁。split_part() 处理分隔数据很方便。initcap() 每个单词首字母大写。lpad/rpad 填充到固定宽度(格式化有用)。string_to_array/array_to_string 在字符串和数组间转换。模式匹配有 LIKE/ILIKE(通配符)、~(正则)和 SIMILAR TO(SQL 模式)。
# length and position
SELECT length('hello'), char_length('hello'), octet_length('hello');
SELECT position('lo' in 'hello'); -- 4
SELECT strpos('hello', 'lo'); -- 4
# substring and slicing
SELECT substring('hello' from 2 for 3); -- 'ell'
SELECT substring('hello' from 2); -- 'ello'
SELECT substr('hello', 2, 3); -- 'ell'
SELECT left('hello', 3); -- 'hel'
SELECT right('hello', 3); -- 'llo'
# case and trim
SELECT upper('hello'), lower('HELLO'), initcap('hello world');
SELECT trim(' hello '), ltrim('xxhello', 'x'), rtrim('helloxx', 'x');
SELECT btrim('xxhelloxx', 'x'); -- trim both sides
# split and join
SELECT split_part('a,b,c', ',', 2); -- 'b' (1-indexed)
SELECT string_to_array('a,b,c', ','); -- {a,b,c}
SELECT array_to_string(ARRAY['a','b','c'], ', '); -- 'a, b, c'
# replace and pad
SELECT replace('hello', 'l', 'L'); -- 'heLLo'
SELECT lpad('5', 3, '0'); -- '005'
SELECT rpad('5', 3, '0'); -- '500'
SELECT repeat('ab', 3); -- 'ababab'
SELECT reverse('hello'); -- 'olleh'日期/时间函数
now() 和 current_timestamp 等价(事务开始时间)。EXTRACT/date_part 取组件。date_trunc 向下取整到单位——时间分桶必备。age() 返回 interval(人类可读)。to_char/to_date/to_timestamp 处理格式化/解析。generate_series 配合日期对填补时间序列数据空缺极其有用(LEFT JOIN 它以显示零计数天)。始终用 timestamptz;日期算术遵守会话时区。
# current date/time
SELECT now(), current_timestamp, transaction_timestamp();
SELECT current_date, current_time;
# extract components
SELECT EXTRACT(YEAR FROM now()), EXTRACT(MONTH FROM now());
SELECT date_part('dow', now()); -- day of week (0=Sunday)
SELECT date_part('epoch', now()); -- Unix timestamp
# date arithmetic
SELECT now() + interval '1 day';
SELECT now() - interval '2 hours';
SELECT age('2025-01-01'); -- interval since date
SELECT age('2025-01-01', '2024-01-01'); -- interval between dates
SELECT date '2025-01-15' - date '2025-01-01'; -- 14 (integer days)
# truncation and rounding
SELECT date_trunc('month', now()); -- first day of month, 00:00
SELECT date_trunc('hour', now()); -- current hour, 00 minutes
# formatting
SELECT to_char(now(), 'YYYY-MM-DD HH:MI:SS');
SELECT to_char(now(), 'Day, DD Mon YYYY');
SELECT to_char(12345.678, 'FM999,999.00');
SELECT to_date('2025/01/15', 'YYYY/MM/DD');
SELECT to_timestamp(1700000000); -- Unix epoch
# generate series (date ranges)
SELECT generate_series(
'2025-01-01'::date,
'2025-01-31'::date,
'1 day'::interval
);数值与数学函数
round() 带两个参数作用于 numeric(不是 double——先转换)。三角函数用弧度(用 radians()/degrees() 转换)。random() 返回 0-1;配合数学运算得到范围。generate_series 是 PostgreSQL 的利器——动态生成行用于序列、日期范围和填补空缺。gcd/lcm (13+) 很方便。财务计算始终用 numeric(精确)——double 上的 round 等浮点函数可能有精度问题。
# rounding
SELECT round(3.14159, 2); -- 3.14
SELECT ceil(3.1), ceiling(3.1); -- 4
SELECT floor(3.9); -- 3
SELECT trunc(3.14159, 2); -- 3.14 (truncate, no rounding)
# power and roots
SELECT power(2, 10); -- 1024
SELECT sqrt(16); -- 4
SELECT cbrt(27); -- 3
SELECT exp(1); -- 2.718... (e^x)
SELECT ln(10), log(100); -- natural log, base-10 log
# trigonometry (radians!)
SELECT sin(radians(90)); -- 1 (convert degrees to radians)
SELECT degrees(1.5708); -- ~90
SELECT pi(); -- 3.14159...
# random
SELECT random(); -- 0 to 1
SELECT floor(random() * 100)::int; -- 0 to 99
SELECT setseed(0.5); -- seed for reproducibility
# sequences and ranges
SELECT generate_series(1, 10);
SELECT generate_series(1, 10, 2); -- 1, 3, 5, 7, 9
SELECT generate_series(0, 1, 0.1); -- 0.0, 0.1, ..., 1.0
# absolute, sign, gcd
SELECT abs(-5), sign(-5); -- 5, -1
SELECT gcd(12, 8); -- 4 (PG 13+)
SELECT lcm(12, 8); -- 24 (PG 13+)条件与 NULL 函数
COALESCE 返回第一个非 NULL 值——设置默认值必备。NULLIF 在值匹配时返回 NULL——比 CASE 更简洁地避免除零。GREATEST/LEAST 比较值。CASE 是通用条件表达式。ISNULL/NOTNULL 是 PG 快捷方式。行比较 (ROW(a,b) < ROW(c,d)) 按字典序比较——适合复合键分页。理解 NULL 行为(NULL = NULL 结果是 NULL,不是 true)在 PostgreSQL 中至关重要。
# COALESCE (return first non-NULL)
SELECT COALESCE(nickname, username, email, 'anonymous') FROM users;
# NULLIF (return NULL if two values are equal)
SELECT NULLIF(status, '') FROM users; -- '' becomes NULL
-- useful to avoid division by zero:
SELECT total / NULLIF(count, 0) FROM stats; -- NULL instead of error
# GREATEST / LEAST
SELECT GREATEST(1, 2, 3); -- 3
SELECT LEAST(NULL, 1, 2); -- NULL (NULLs propagate)
SELECT GREATEST(a, b, c) FROM numbers;
# CASE expressions
SELECT
CASE
WHEN age < 18 THEN 'minor'
WHEN age >= 65 THEN 'senior'
ELSE 'adult'
END AS age_group
FROM users;
# ISNULL / NOTNULL (PostgreSQL shortcuts)
SELECT * FROM users WHERE email ISNULL; -- IS NULL
SELECT * FROM users WHERE email NOTNULL; -- IS NOT NULL
# row comparison
SELECT ROW(1, 2) < ROW(1, 3); -- true (compares column by column)数组与 JSON 函数
数组函数实现强大的库内数据操作:unnest 将数组展开为行(适合连接),array_agg 是逆操作。JSONB 运算符:-> 和 #> 返回 jsonb,->> 和 #>> 返回文本。@>(包含)是最重要的 JSONB 运算符且使用 GIN 索引。? 检查键是否存在。jsonb_build_object 从列构造 JSON。jsonb_set/||/- 修改 JSON。这些函数使 JSONB 成为一等数据类型——灵活且可查询。
# array functions
SELECT array_length(ARRAY[1,2,3], 1); -- 3
SELECT array_append(ARRAY[1,2], 3); -- {1,2,3}
SELECT array_prepend(1, ARRAY[2,3]); -- {1,2,3}
SELECT array_concat(ARRAY[1,2], ARRAY[3,4]); -- {1,2,3,4}
SELECT array_remove(ARRAY[1,2,3], 2); -- {1,3}
SELECT array_replace(ARRAY[1,2,1], 1, 9); -- {9,2,9}
SELECT unnest(ARRAY[1,2,3]); -- 3 rows
SELECT array_agg(name) FROM users; -- aggregate to array
# JSONB functions
SELECT data->'name' -- jsonb (key access)
SELECT data->>'name' -- text (key access)
SELECT data#>'{address,city}' -- jsonb (path access)
SELECT data#>>'{address,city}'-- text (path access)
SELECT jsonb_build_object('name', username, 'age', age) FROM users;
SELECT jsonb_object_keys(data) FROM products; -- keys as rows
SELECT jsonb_array_elements(data->'tags') FROM products; -- expand array
# JSONB predicates
SELECT data @> '{"active": true}' -- contains
SELECT data ? 'name' -- key exists
SELECT data ?| ARRAY['name','email'] -- any key exists
SELECT data ?& ARRAY['name','email'] -- all keys exist
# modify JSONB
SELECT jsonb_set(data, '{price}', '29.99')
SELECT data || '{"new": true}' -- merge
SELECT data - 'old_key' -- remove key
SELECT data #- '{nested,old_key}' -- remove nested key触发器
触发器函数
触发器函数必须返回 trigger,用 PL/pgSQL(或其他语言)编写。TG_OP、TG_TABLE_NAME、OLD、NEW 是触发器内的特殊变量。BEFORE 触发器可修改 NEW(返回 NULL 跳过操作)。AFTER 触发器用于副作用(审计日志、通知)。FOR EACH ROW 逐行触发;FOR EACH STATEMENT 触发一次。触发器强大但可能造成隐藏副作用——用于横切关注点(审计、时间戳),不要用于业务逻辑。