当前位置: 首页 > news >正文

PostgreSQL 实战指南(面向 MySQL 开发者)

本文档面向有 MySQL 经验的开发者,采用对比的方式快速上手 PostgreSQL。
按照"快速上手 → 日常开发 → 进阶特性 → 生产实践"的路径组织。


目录

第一部分:快速上手

  1. 安装和工具
  2. 5 分钟快速开始
  3. 核心概念对比

第二部分:日常开发
4. 数据库和用户管理
5. 表和数据类型
6. SQL 语法差异
7. 索引和查询优化
8. 常用命令速查

第三部分:进阶特性
9. JSONB 和半结构化数据
10. UPSERT 和高级 DML
11. 高级 SQL 技巧
12. 触发器和存储过程
13. LISTEN/NOTIFY 通知机制
14. 其他增强特性

第四部分:生产实践
15. VACUUM 和表维护
16. 连接池和并发控制
17. 备份和恢复
18. 监控和诊断
19. 性能调优 Checklist

第五部分:迁移指南
20. 从 MySQL 迁移
21. CDC/逻辑复制(对应 binlog)
22. 常用扩展推荐


第一部分:快速上手

1. 安装和工具

1.1 macOS 推荐方式

方式一:Postgres.app(⭐ 最简单,推荐新手)

https://postgresapp.com/

  • 图形化应用,开箱即用
  • 包含多个 PostgreSQL 版本,可以一键切换
  • 自带常用扩展(PostGIS、plv8 等)
  • 菜单栏图标管理,启动/停止方便

安装后配置 PATH(可选,方便命令行使用):

# 添加到 ~/.zshrc 或 ~/.bash_profile
export PATH="/Applications/Postgres.app/Contents/Versions/latest/bin:$PATH"

方式二:Homebrew

brew install postgresql@16
brew services start postgresql@16

1.2 Linux 安装

Ubuntu/Debian:

sudo apt update
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql

CentOS/RHEL:

sudo yum install postgresql-server postgresql-contrib
sudo postgresql-setup initdb
sudo systemctl start postgresql

1.3 命令行工具

默认命令行工具是 psql

推荐安装 pgcli,有自动补全和语法高亮:

# macOS
brew install pgcli# Linux
pip install pgcli

1.4 图形化工具

  • pgAdmin:官方图形化工具(功能全面但较重)
  • DBeaver:跨平台,支持多种数据库(推荐)
  • DataGrip:JetBrains 出品(付费)
  • Postico:macOS 专用,界面美观(付费)

2. 5 分钟快速开始

2.1 连接数据库

使用 psqlpgcli

psql -h 127.0.0.1 -p 5432 -U postgres -d postgres

常用参数:

  • -h:主机(本机通常是 127.0.0.1 或留空)
  • -p:端口(默认 5432)
  • -U:用户名(默认安装后有超级用户 postgres
  • -d:数据库名

本地快速连接:

psql -U postgres

2.2 基本元命令(以 \ 开头)

命令说明MySQL 对比
\l列出所有数据库SHOW DATABASES;
\c dbname切换数据库USE dbname;
\dt列出当前 schema 的表SHOW TABLES;
\d table查看表结构DESC table;
\d+ table查看表结构(详细)SHOW CREATE TABLE;
\di列出索引SHOW INDEX;
\du列出用户/角色SELECT * FROM mysql.user;
\q退出exit\q

2.3 快速上手示例

-- 创建数据库和用户
CREATE DATABASE myapp;
CREATE USER app_user WITH PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE myapp TO app_user;-- 切换到新数据库
\c myapp-- 创建表
CREATE TABLE users (id BIGSERIAL PRIMARY KEY,name VARCHAR(100) NOT NULL,email VARCHAR(255) UNIQUE NOT NULL,created_at TIMESTAMPTZ DEFAULT now()
);-- 插入数据
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');-- 查询
SELECT * FROM users;-- 查看表结构
\d users

3. 核心概念对比(和 MySQL 有何不同)

3.1 数据库 + Schema(三层结构)

MySQL: database.table
PostgreSQL: database.schema.table

  • 每个数据库里可以有多个 schema,默认是 public
  • 多数简单项目可以直接用默认 public schema
  • 可以通过 schema 实现多租户隔离

类比示例:

  • MySQL:mydb.users
  • PostgreSQL:mydb.public.users(通常简写为 users,依赖 search_path

查看当前 schema:

SHOW search_path;  -- 默认:"$user", public

3.2 用户 / 角色(Role)

关键差异:

  • PostgreSQL 中是「角色」(role),带 LOGIN 权限的 role 就是用户
  • 一个 role 可以拥有数据库、表、序列等对象
  • 和 MySQL user@host 不同,PostgreSQL 通常只写用户名,host 控制在 pg_hba.conf
-- 创建角色(可以理解为用户组)
CREATE ROLE readonly;-- 创建用户(带登录权限的角色)
CREATE ROLE app_user WITH LOGIN PASSWORD 'password';-- 角色继承
GRANT readonly TO app_user;

3.3 标识符大小写(⚠️ 重要陷阱)

没加双引号的表名、列名会自动转小写:

CREATE TABLE MyTable (UserId INT);  -- 实际创建的是 mytable (userid)
SELECT * FROM MyTable;              -- 可以查询(会转成小写)
SELECT * FROM "MyTable";            -- 报错:表不存在

最佳实践:

  • 一律使用小写加下划线:users, created_at, user_id
  • 避免必须写 "MyTable" 这种带引号形式

3.4 自增主键

MySQL:

CREATE TABLE users (id BIGINT PRIMARY KEY AUTO_INCREMENT,...
);

PostgreSQL 推荐方式(两种):

-- 方式一:BIGSERIAL(传统写法)
CREATE TABLE users (id BIGSERIAL PRIMARY KEY,...
);-- 方式二:IDENTITY(SQL 标准,PG 10+)
CREATE TABLE users (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,...
);
  • BIGSERIAL 等价于 BIGINT + SEQUENCE
  • IDENTITY 更现代,推荐使用

3.5 MVCC 机制(多版本并发控制)

关键差异:

  • MySQL InnoDB:通过 undo log 实现 MVCC
  • PostgreSQL:通过在表中保留旧版本数据实现

影响:

  • PostgreSQL 的 UPDATE/DELETE 会产生"死元组"(dead tuple)
  • 需要定期 VACUUM 回收空间(见后文)
  • 性能特点:读不阻塞写,写不阻塞读

第二部分:日常开发

4. 数据库和用户管理

4.1 数据库操作

-- 创建数据库
CREATE DATABASE mydb OWNER myuser ENCODING 'UTF8';-- 删除数据库(需要没有活动连接)
DROP DATABASE mydb;-- 查看当前数据库
SELECT current_database();-- 查看所有数据库
\l
-- 或
SELECT datname FROM pg_database;

4.2 用户/角色管理

创建用户
-- 创建用户(带登录权限)
CREATE ROLE app_user WITH LOGIN PASSWORD 'secure_password';-- 创建用户并设置权限
CREATE ROLE app_user WITH LOGIN PASSWORD 'secure_password'CREATEDB           -- 可以创建数据库VALID UNTIL '2025-12-31';  -- 密码过期时间
查看用户
-- psql 命令
\du-- SQL 查询
SELECT * FROM pg_user;
SELECT * FROM pg_roles;
修改密码
ALTER ROLE app_user WITH PASSWORD 'new_password';
删除用户
DROP ROLE app_user;

4.3 权限管理

数据库级权限
-- 授予数据库所有权限
GRANT ALL PRIVILEGES ON DATABASE mydb TO app_user;-- 授予连接权限
GRANT CONNECT ON DATABASE mydb TO app_user;-- 撤销权限
REVOKE ALL PRIVILEGES ON DATABASE mydb FROM app_user;
表级权限
-- 授予 public schema 下所有表的权限
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_user;-- 授予特定表的权限
GRANT SELECT, INSERT, UPDATE ON users TO app_user;-- 授予序列权限(自增主键需要)
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO app_user;-- 设置默认权限(对未来创建的表生效)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON TABLES TO app_user;
查看权限
-- 查看某用户的表权限
SELECT *
FROM information_schema.role_table_grants
WHERE grantee = 'app_user';-- 查看某表的权限
\dp table_name

5. 表和数据类型

5.1 建表示例(MySQL 风格迁移)

PostgreSQL 推荐写法:

CREATE TABLE users (id          BIGSERIAL PRIMARY KEY,name        VARCHAR(50)      NOT NULL,email       VARCHAR(100)     UNIQUE,age         INTEGER          CHECK (age >= 0 AND age <= 150),is_active   BOOLEAN          NOT NULL DEFAULT TRUE,balance     NUMERIC(10, 2)   DEFAULT 0.00,created_at  TIMESTAMPTZ      NOT NULL DEFAULT now(),updated_at  TIMESTAMPTZ      NOT NULL DEFAULT now()
);-- 添加索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);-- 添加注释
COMMENT ON TABLE users IS '用户表';
COMMENT ON COLUMN users.email IS '用户邮箱';

对应的 MySQL 写法:

CREATE TABLE users (id          BIGINT PRIMARY KEY AUTO_INCREMENT,name        VARCHAR(50)      NOT NULL,email       VARCHAR(100)     UNIQUE,age         INT              CHECK (age >= 0 AND age <= 150),is_active   TINYINT(1)       NOT NULL DEFAULT 1,balance     DECIMAL(10, 2)   DEFAULT 0.00,created_at  DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP,updated_at  DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

5.2 常见类型对照

MySQLPostgreSQL说明
TINYINT, SMALLINT, INT, BIGINTSMALLINT, INTEGER, BIGINT整数类型
TINYINT(1)BOOLEAN布尔值(PG 有真正的布尔类型)
FLOAT, DOUBLEREAL, DOUBLE PRECISION浮点数
DECIMAL(p,s)NUMERIC(p,s)DECIMAL(p,s)精确数值
VARCHAR(n), TEXTVARCHAR(n), TEXT字符串
DATETIMETIMESTAMP时间戳(本地时间)
DATETIMETIMESTAMPTZ⭐ 推荐:带时区的时间戳(存 UTC)
DATEDATE日期
TIMETIME时间
BLOBBYTEA二进制数据
JSONJSONJSONB⭐ 推荐 JSONB(二进制,支持索引)
ENUM('a','b')自定义 ENUM 类型见下文

重要提示:

  • 时间类型推荐用 TIMESTAMPTZ(带时区),数据库内部统一存储 UTC,应用层自动转换时区
  • 布尔类型用 BOOLEAN,不要用 0/1
  • JSON 数据推荐用 JSONB,而不是 JSON

5.3 ENUM 类型

MySQL 写法:

CREATE TABLE orders (id BIGINT PRIMARY KEY AUTO_INCREMENT,status ENUM('pending', 'paid', 'shipped', 'cancelled') NOT NULL DEFAULT 'pending'
);

PostgreSQL 写法(自定义类型):

-- 1. 先创建枚举类型
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'cancelled');-- 2. 使用枚举类型
CREATE TABLE orders (id     BIGSERIAL PRIMARY KEY,status order_status NOT NULL DEFAULT 'pending'
);

修改枚举值:

-- 新增值
ALTER TYPE order_status ADD VALUE 'refunded';-- 重命名值(PG 10+)
ALTER TYPE order_status RENAME VALUE 'cancelled' TO 'canceled';

注意事项:

  • ⚠️ 不能删除枚举值
  • ⚠️ 不能直接修改顺序
  • 如果枚举值频繁变化,考虑用 TEXT + CHECK 约束,或单独建字典表

替代方案:

-- 方案一:CHECK 约束
CREATE TABLE orders (id     BIGSERIAL PRIMARY KEY,status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled'))
);-- 方案二:字典表 + 外键
CREATE TABLE order_status_dict (code VARCHAR(20) PRIMARY KEY,name VARCHAR(50) NOT NULL
);CREATE TABLE orders (id     BIGSERIAL PRIMARY KEY,status VARCHAR(20) REFERENCES order_status_dict(code)
);

5.4 数组类型(PostgreSQL 特有)

CREATE TABLE posts (id      BIGSERIAL PRIMARY KEY,title   TEXT NOT NULL,tags    TEXT[]  -- 文本数组
);-- 插入
INSERT INTO posts (title, tags) 
VALUES ('PostgreSQL Guide', ARRAY['database', 'postgresql', 'sql']);-- 或使用简写语法
INSERT INTO posts (title, tags) 
VALUES ('MySQL vs PG', '{"database", "mysql", "postgresql"}');-- 查询:包含某个标签
SELECT * FROM posts WHERE 'postgresql' = ANY(tags);-- 查询:包含所有指定标签
SELECT * FROM posts WHERE tags @> ARRAY['database', 'postgresql'];-- 数组长度
SELECT title, array_length(tags, 1) FROM posts;

适用场景:

  • 少量标签、关键词
  • 不需要频繁按元素查询
  • 否则建议用关联表

6. SQL 语法差异

6.1 LIMIT 和 OFFSET

MySQL:

SELECT * FROM users ORDER BY id LIMIT 10, 20;  -- 跳过 10 条,取 20 条

PostgreSQL:

SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 10;  -- 跳过 10 条,取 20 条

6.2 字符串拼接

MySQL:

SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;

PostgreSQL:

-- 方式一:|| 操作符(推荐)
SELECT first_name || ' ' || last_name AS full_name FROM users;-- 方式二:concat 函数
SELECT concat(first_name, ' ', last_name) AS full_name FROM users;

6.3 NULL 处理

MySQL:

SELECT IFNULL(nickname, name) FROM users;

PostgreSQL:

-- 方式一:COALESCE(SQL 标准)
SELECT COALESCE(nickname, name) FROM users;-- 方式二:NULLIF
SELECT NULLIF(value, '') FROM users;  -- 如果 value 是空字符串则返回 NULL

6.4 日期和时间函数

功能MySQLPostgreSQL
当前时间NOW(), CURRENT_TIMESTAMPnow(), CURRENT_TIMESTAMP
当前日期CURDATE(), CURRENT_DATECURRENT_DATE
时间运算DATE_ADD(date, INTERVAL 1 DAY)date + INTERVAL '1 day'
格式化DATE_FORMAT(date, '%Y-%m-%d')to_char(date, 'YYYY-MM-DD')
解析STR_TO_DATE('2024-01-01', '%Y-%m-%d')to_date('2024-01-01', 'YYYY-MM-DD')
时间戳UNIX_TIMESTAMP()extract(epoch from now())

示例:

-- 时间运算
SELECT now() + INTERVAL '1 day';           -- 明天
SELECT now() - INTERVAL '1 hour';          -- 1 小时前
SELECT now() + INTERVAL '1 month';         -- 下个月-- 日期截断
SELECT date_trunc('day', now());           -- 今天 00:00:00
SELECT date_trunc('month', now());         -- 本月 1 号 00:00:00
SELECT date_trunc('hour', timestamp_col);  -- 截断到小时-- 提取部分
SELECT extract(year from now());           -- 年份
SELECT extract(month from now());          -- 月份
SELECT extract(dow from now());            -- 星期几(0=周日)-- 格式化
SELECT to_char(now(), 'YYYY-MM-DD HH24:MI:SS');
SELECT to_char(now(), 'Day, DD Mon YYYY');

6.5 条件表达式

CASE WHEN(相同):

SELECT name,CASE WHEN age < 18 THEN 'minor'WHEN age < 60 THEN 'adult'ELSE 'senior'END AS age_group
FROM users;

IF 函数(MySQL)vs CASE(PostgreSQL):

-- MySQL
SELECT name, IF(is_active = 1, 'Active', 'Inactive') FROM users;-- PostgreSQL(推荐用 CASE)
SELECT name, CASE WHEN is_active THEN 'Active' ELSE 'Inactive' END FROM users;

6.6 字符串函数

功能MySQLPostgreSQL
长度LENGTH(str), CHAR_LENGTH(str)length(str), char_length(str)
截取SUBSTRING(str, pos, len)substring(str, pos, len)substr(str, pos, len)
位置LOCATE(substr, str)position(substr in str)strpos(str, substr)
替换REPLACE(str, from, to)replace(str, from, to)
大小写UPPER(str), LOWER(str)upper(str), lower(str)
去空格TRIM(str), LTRIM(str), RTRIM(str)trim(str), ltrim(str), rtrim(str)
正则匹配str REGEXP patternstr ~ pattern (区分大小写) / str ~* pattern (不区分)

正则表达式示例:

-- 匹配邮箱格式
SELECT * FROM users WHERE email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$';-- 不区分大小写
SELECT * FROM users WHERE name ~* 'alice';-- 替换(正则)
SELECT regexp_replace('abc123def', '\d+', 'NUM');  -- 结果:abcNUMdef

6.7 分组和聚合

基本相同,但注意:

-- GROUP_CONCAT(MySQL)vs string_agg(PostgreSQL)
-- MySQL
SELECT category, GROUP_CONCAT(name ORDER BY name SEPARATOR ', ')
FROM products
GROUP BY category;-- PostgreSQL
SELECT category, string_agg(name, ', ' ORDER BY name)
FROM products
GROUP BY category;

7. 索引和查询优化 ⭐

7.1 索引类型

PostgreSQL 的索引比 MySQL 更丰富:

索引类型适用场景对比 MySQL
B-Tree等值、范围查询(默认)和 MySQL 的 B+Tree 类似
Hash等值查询(PG 10+ 支持 WAL)MySQL 8.0 已废弃
GINJSONB、数组、全文检索MySQL 无直接对应
GiST地理数据、范围类型、最近邻MySQL 无直接对应
BRIN超大表、时间序列(物理顺序)MySQL 无直接对应
SP-GiST不规则数据结构MySQL 无直接对应

7.2 创建索引

-- 默认 B-Tree 索引
CREATE INDEX idx_users_email ON users(email);-- 唯一索引
CREATE UNIQUE INDEX idx_users_email ON users(email);-- 多列索引
CREATE INDEX idx_users_name_email ON users(name, email);-- 部分索引(条件索引)
CREATE INDEX idx_active_users ON users(created_at) WHERE is_active = true;-- 表达式索引
CREATE INDEX idx_users_lower_email ON users(LOWER(email));-- GIN 索引(JSONB)
CREATE INDEX idx_events_payload ON events USING GIN (payload);-- BRIN 索引(大表,物理顺序)
CREATE INDEX idx_logs_created_at ON logs USING BRIN (created_at);-- 并发创建索引(不锁表)
CREATE INDEX CONCURRENTLY idx_users_name ON users(name);

7.3 查看索引

-- psql 命令
\di
\di+ table_name  -- 查看某表的索引(带大小)-- SQL 查询
SELECT * FROM pg_indexes WHERE tablename = 'users';-- 查看未使用的索引
SELECT schemaname,tablename,indexname,idx_scan,pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0AND indexrelname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;

7.4 EXPLAIN 分析查询计划

基本语法:

-- 查看执行计划
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';-- 实际执行并显示统计(推荐)
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@example.com';-- 更详细的信息
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT * FROM users WHERE email = 'alice@example.com';

输出解读:

EXPLAIN ANALYZE SELECT * FROM users WHERE id > 100;-- 输出示例:
Seq Scan on users  (cost=0.00..25.50 rows=500 width=100) (actual time=0.020..0.250 rows=450 loops=1)Filter: (id > 100)Rows Removed by Filter: 50
Planning Time: 0.150 ms
Execution Time: 0.300 ms

关键指标:

  • cost=0.00..25.50:预估成本(启动成本…总成本)
  • rows=500:预估返回行数
  • actual time=0.020..0.250:实际耗时(毫秒)
  • rows=450:实际返回行数
  • Seq Scan:全表扫描(通常需要优化)
  • Index Scan:索引扫描(好)
  • Index Only Scan:索引覆盖扫描(最好)

常见节点类型:

  • Seq Scan:全表扫描(慢)
  • Index Scan:索引扫描
  • Index Only Scan:索引覆盖扫描(不需要回表)
  • Bitmap Heap Scan:位图扫描(多个索引合并)
  • Nested Loop:嵌套循环连接
  • Hash Join:哈希连接
  • Merge Join:归并连接

7.5 查询优化技巧

使用索引
-- ❌ 不走索引:函数包裹列
SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';-- ✅ 走索引:创建表达式索引
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';
避免 SELECT *
-- ❌ 查询不需要的列
SELECT * FROM users WHERE id = 123;-- ✅ 只查询需要的列(可能走 Index Only Scan)
SELECT id, name, email FROM users WHERE id = 123;
使用 LIMIT
-- ✅ 限制返回行数
SELECT * FROM users ORDER BY created_at DESC LIMIT 100;
JOIN 优化
-- ✅ 确保 JOIN 的列上有索引
SELECT u.name, o.amount
FROM users u
JOIN orders o ON o.user_id = u.id  -- user_id 需要索引
WHERE u.is_active = true;
使用部分索引
-- 如果经常查询 is_active = true 的数据
CREATE INDEX idx_active_users ON users(created_at) WHERE is_active = true;
定期更新统计信息
-- 手动更新统计信息
ANALYZE users;-- 或者全库
ANALYZE;

7.6 慢查询排查

-- 查看当前正在执行的慢查询
SELECT pid, now() - query_start AS duration, state, query
FROM pg_stat_activity
WHERE state != 'idle'AND now() - query_start > interval '5 seconds'
ORDER BY duration DESC;-- 开启 pg_stat_statements 扩展(需要重启)
-- postgresql.conf:
-- shared_preload_libraries = 'pg_stat_statements'-- 创建扩展
CREATE EXTENSION pg_stat_statements;-- 查看最慢的查询
SELECT calls,mean_exec_time,total_exec_time,query
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

8. 常用命令速查

8.1 psql 元命令

命令说明
\?显示所有元命令帮助
\h COMMAND显示 SQL 命令帮助(如 \h CREATE TABLE
\l\l+列出所有数据库(+ 显示大小)
\c dbname切换数据库
\dt\dt+列出表(+ 显示大小)
\d table查看表结构
\d+ table查看表结构(详细信息)
\di\di+列出索引
\dv列出视图
\df列出函数
\du\du+列出用户/角色
\dn列出 schema
\dp table查看表权限
\x切换扩展显示模式(类似 MySQL 的 \G
\timing开启/关闭查询计时
\e打开编辑器编辑上一条命令
\i file.sql执行 SQL 文件
\o file.txt输出重定向到文件
\q退出

8.2 MySQL 命令对照表

场景MySQLPostgreSQL
列出数据库SHOW DATABASES;\l
切换数据库USE db;\c db
查看当前数据库SELECT DATABASE();SELECT current_database();
列出表SHOW TABLES;\dt
查看表结构DESC table;\d table
查看建表语句SHOW CREATE TABLE table;pg_dump -s -t table dbname
查看索引SHOW INDEX FROM table;\di table\d table
查看连接SHOW PROCESSLIST;SELECT * FROM pg_stat_activity;
查看配置SHOW VARIABLES LIKE 'max%';SHOW max_connections;
查看表大小SHOW TABLE STATUS;\dt+SELECT pg_size_pretty(pg_total_relation_size('table'));
杀死连接KILL connection_id;SELECT pg_terminate_backend(pid);

8.3 信息查询 SQL

-- 查看当前数据库
SELECT current_database();-- 查看当前用户
SELECT current_user;-- 查看当前 schema
SHOW search_path;-- 查看 PostgreSQL 版本
SELECT version();-- 查看表大小
SELECT pg_size_pretty(pg_total_relation_size('users'));-- 查看索引大小
SELECT pg_size_pretty(pg_indexes_size('users'));-- 查看数据库大小
SELECT pg_size_pretty(pg_database_size(current_database()));-- 查看所有表及其大小
SELECT schemaname,tablename,pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;

第三部分:进阶特性

9. JSONB 和半结构化数据

9.1 为什么用 JSONB

PostgreSQL 有两种 JSON 类型:

  • JSON:存储原始文本,读写开销大(很少用)
  • JSONB:二进制格式,支持索引、高效查询(⭐ 推荐)

适用场景:

  • 结构频繁变化的属性(如用户自定义字段)
  • 嵌套层级较深的数据
  • 需要灵活查询的半结构化数据

9.2 基本操作

-- 创建表
CREATE TABLE events (id         BIGSERIAL PRIMARY KEY,event_type VARCHAR(50) NOT NULL,payload    JSONB NOT NULL,created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);-- 插入数据
INSERT INTO events (event_type, payload) VALUES ('user_login', '{"user_id": 123, "ip": "1.2.3.4", "device": "iPhone"}'),('order_created', '{"user_id": 123, "order_id": 456, "amount": 99.99}');-- 查询:提取字段
SELECT payload->>'user_id' AS user_id,        -- 返回文本payload->'user_id' AS user_id_json,    -- 返回 JSONpayload->'device'->>'model' AS model   -- 嵌套字段
FROM events;-- 查询:条件过滤
SELECT * FROM events WHERE payload->>'event_type' = 'login';-- 查询:包含某个键
SELECT * FROM events WHERE payload ? 'device';-- 查询:包含子对象(可走 GIN 索引)
SELECT * FROM events WHERE payload @> '{"user_id": 123}';-- 更新:修改某个字段
UPDATE events 
SET payload = jsonb_set(payload, '{ip}', '"2.3.4.5"')
WHERE id = 1;-- 删除:移除某个键
UPDATE events 
SET payload = payload - 'device'
WHERE id = 1;

9.3 JSONB 操作符

操作符说明示例
->提取 JSON 字段(返回 JSON)payload->'user_id'
->>提取 JSON 字段(返回文本)payload->>'user_id'
#>提取嵌套路径(返回 JSON)payload#>'{user,name}'
#>>提取嵌套路径(返回文本)payload#>>'{user,name}'
?是否包含键payload ? 'device'
?&是否包含所有键payload ?& array['user_id', 'ip']
`?`是否包含任意键
@>是否包含子对象payload @> '{"user_id": 123}'
<@是否被包含'{"user_id": 123}' <@ payload
-删除键payload - 'device'
``

9.4 JSONB 索引

-- GIN 索引(通用,推荐)
CREATE INDEX idx_events_payload ON events USING GIN (payload);-- 特定路径索引(更高效)
CREATE INDEX idx_events_user_id ON events ((payload->>'user_id'));-- 表达式索引
CREATE INDEX idx_events_user_id_int ON events ((payload->>'user_id')::bigint);

查询优化:

-- ✅ 走索引(使用 @> 操作符)
EXPLAIN SELECT * FROM events WHERE payload @> '{"user_id": 123}';-- ❌ 不走索引(使用 ->)
EXPLAIN SELECT * FROM events WHERE payload->>'user_id' = '123';-- ✅ 走表达式索引
CREATE INDEX idx_events_user_id ON events ((payload->>'user_id'));
EXPLAIN SELECT * FROM events WHERE payload->>'user_id' = '123';

9.5 JSONB 函数

-- jsonb_build_object:构建 JSON 对象
SELECT jsonb_build_object('name', 'Alice', 'age', 30);
-- 结果:{"name": "Alice", "age": 30}-- jsonb_build_array:构建 JSON 数组
SELECT jsonb_build_array(1, 2, 3);
-- 结果:[1, 2, 3]-- jsonb_agg:聚合为 JSON 数组
SELECT user_id, jsonb_agg(order_id) AS orders
FROM orders
GROUP BY user_id;-- jsonb_object_agg:聚合为 JSON 对象
SELECT jsonb_object_agg(name, value) FROM settings;-- jsonb_each:展开为行
SELECT * FROM jsonb_each('{"a": 1, "b": 2}'::jsonb);
-- 结果:
-- a | 1
-- b | 2-- jsonb_array_elements:展开数组
SELECT * FROM jsonb_array_elements('[1,2,3]'::jsonb);

10. UPSERT 和高级 DML

10.1 UPSERT(INSERT … ON CONFLICT)

MySQL 的 REPLACE INTO:

REPLACE INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com');

PostgreSQL 的 ON CONFLICT(更灵活):

-- 冲突时更新
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', 'alice@example.com')
ON CONFLICT (id) 
DO UPDATE SET name = EXCLUDED.name,email = EXCLUDED.email,updated_at = now();-- 冲突时忽略
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', 'alice@example.com')
ON CONFLICT (id) 
DO NOTHING;-- 条件更新(只在满足条件时更新)
INSERT INTO users (id, name, email, version)
VALUES (1, 'Alice', 'alice@example.com', 2)
ON CONFLICT (id) 
DO UPDATE SET name = EXCLUDED.name,version = EXCLUDED.version
WHERE users.version < EXCLUDED.version;  -- 乐观锁

EXCLUDED 表:

  • EXCLUDED 代表要插入的新值
  • users.column 代表表中的旧值

10.2 RETURNING 子句(MySQL 没有)

INSERT 后返回数据:

INSERT INTO users (name, email)
VALUES ('Bob', 'bob@example.com')
RETURNING id, created_at;
-- 直接返回插入的 id 和 created_at,无需再查询

UPDATE 后返回数据:

UPDATE users 
SET status = 'inactive'
WHERE last_login < now() - interval '1 year'
RETURNING id, name, email;
-- 返回所有被更新的行

DELETE 后返回数据:

DELETE FROM users
WHERE is_active = false
RETURNING *;
-- 返回所有被删除的行

在 CTE 中使用:

WITH deleted AS (DELETE FROM old_logsWHERE created_at < now() - interval '90 days'RETURNING *
)
SELECT count(*) FROM deleted;  -- 统计删除了多少行

10.3 批量 UPSERT

INSERT INTO users (id, name, email) VALUES(1, 'Alice', 'alice@example.com'),(2, 'Bob', 'bob@example.com'),(3, 'Charlie', 'charlie@example.com')
ON CONFLICT (id) 
DO UPDATE SET name = EXCLUDED.name,email = EXCLUDED.email;

10.4 UPDATE … FROM(类似 MySQL 的 UPDATE JOIN)

MySQL:

UPDATE orders o
JOIN users u ON o.user_id = u.id
SET o.user_name = u.name
WHERE u.is_active = true;

PostgreSQL:

UPDATE orders o
SET user_name = u.name
FROM users u
WHERE o.user_id = u.idAND u.is_active = true;

10.5 DELETE … USING(类似 MySQL 的 DELETE JOIN)

PostgreSQL:

DELETE FROM orders o
USING users u
WHERE o.user_id = u.idAND u.is_active = false;

11. 高级 SQL 技巧

11.1 CTE(WITH 语句)

基本 CTE:

WITH recent_orders AS (SELECT user_id, count(*) AS order_countFROM ordersWHERE created_at > now() - interval '30 days'GROUP BY user_id
)
SELECT u.name, ro.order_count
FROM users u
JOIN recent_orders ro ON u.id = ro.user_id
WHERE ro.order_count > 5;

多个 CTE:

WITH active_users AS (SELECT id, name FROM users WHERE is_active = true),recent_orders AS (SELECT user_id, count(*) AS cnt FROM orders WHERE created_at > now() - interval '30 days'GROUP BY user_id)
SELECT au.name, coalesce(ro.cnt, 0) AS order_count
FROM active_users au
LEFT JOIN recent_orders ro ON au.id = ro.user_id;

11.2 递归 CTE(查询树形结构)

查询组织架构树:

-- 假设有部门表
CREATE TABLE departments (id INT PRIMARY KEY,name VARCHAR(100),parent_id INT REFERENCES departments(id)
);-- 递归查询:从根节点开始遍历
WITH RECURSIVE org_tree AS (-- 基础查询:根节点SELECT id, name, parent_id, 1 AS level, name::text AS pathFROM departmentsWHERE parent_id IS NULLUNION ALL-- 递归查询:子节点SELECT d.id, d.name, d.parent_id, ot.level + 1, ot.path || ' > ' || d.nameFROM departments dJOIN org_tree ot ON d.parent_id = ot.id
)
SELECT * FROM org_tree ORDER BY path;

查询所有下级:

WITH RECURSIVE subordinates AS (SELECT id, name, manager_idFROM employeesWHERE id = 100  -- 起始员工 IDUNION ALLSELECT e.id, e.name, e.manager_idFROM employees eJOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates;

11.3 窗口函数(Window Functions)

排名函数:

-- 每个部门工资排名
SELECT name,department,salary,rank() OVER (PARTITION BY department ORDER BY salary DESC) AS rank,dense_rank() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_rank,row_number() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num
FROM employees;-- 取每个部门工资最高的 3 人
SELECT * FROM (SELECT name,department,salary,row_number() OVER (PARTITION BY department ORDER BY salary DESC) AS rnFROM employees
) t
WHERE rn <= 3;

聚合函数作为窗口函数:

-- 计算部门平均工资,但保留所有行
SELECT name,department,salary,avg(salary) OVER (PARTITION BY department) AS dept_avg,salary - avg(salary) OVER (PARTITION BY department) AS diff_from_avg
FROM employees;

移动平均:

-- 7 天移动平均
SELECT date,amount,avg(amount) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma_7
FROM daily_sales;

累计和:

SELECT date,amount,sum(amount) OVER (ORDER BY date) AS cumulative_total
FROM daily_sales;

LAG 和 LEAD(访问前后行):

-- 计算环比增长
SELECT date,amount,lag(amount) OVER (ORDER BY date) AS prev_amount,amount - lag(amount) OVER (ORDER BY date) AS growth
FROM daily_sales;-- 下一个值
SELECT date,amount,lead(amount) OVER (ORDER BY date) AS next_amount
FROM daily_sales;

11.4 LATERAL JOIN(横向连接)

为每个用户找最近 3 条订单:

SELECT u.name, o.*
FROM users u
LEFT JOIN LATERAL (SELECT * FROM ordersWHERE user_id = u.idORDER BY created_at DESCLIMIT 3
) o ON true;

等价于(但 LATERAL 更清晰):

SELECT u.name, o.*
FROM users u
LEFT JOIN (SELECT DISTINCT ON (user_id) *FROM (SELECT *, row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rnFROM orders) tWHERE rn <= 3
) o ON o.user_id = u.id;

11.5 DISTINCT ON(PostgreSQL 特有)

每个用户的最新订单:

SELECT DISTINCT ON (user_id) *
FROM orders
ORDER BY user_id, created_at DESC;

注意:

  • DISTINCT ON 的列必须是 ORDER BY 的前缀
  • 相当于 GROUP BY + MAX,但保留整行

11.6 数组聚合和展开

聚合为数组:

SELECT user_id, array_agg(order_id) AS orders
FROM orders
GROUP BY user_id;

展开数组:

SELECT unnest(ARRAY[1, 2, 3, 4, 5]);

结合使用:

-- 找出同时购买了多个产品的用户
WITH user_products AS (SELECT user_id, array_agg(product_id) AS productsFROM ordersGROUP BY user_id
)
SELECT user_id
FROM user_products
WHERE products @> ARRAY[1, 2, 3];  -- 包含产品 1, 2, 3

12. 触发器和存储过程

12.1 触发器(Trigger)

和 MySQL 的主要区别:

  • PostgreSQL 触发器需要先定义函数,再绑定到表
  • 使用 plpgsql 语言编写
  • 行级触发器必须 RETURN NEW/OLD/NULL

示例:自动更新 updated_at 字段

-- 1. 创建触发器函数
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGINNEW.updated_at = now();RETURN NEW;
END;
$$ LANGUAGE plpgsql;-- 2. 创建触发器
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

示例:审计日志

-- 创建审计表
CREATE TABLE audit_log (id BIGSERIAL PRIMARY KEY,table_name TEXT NOT NULL,operation TEXT NOT NULL,old_data JSONB,new_data JSONB,changed_by TEXT DEFAULT current_user,changed_at TIMESTAMPTZ DEFAULT now()
);-- 审计触发器函数
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGINIF TG_OP = 'DELETE' THENINSERT INTO audit_log (table_name, operation, old_data)VALUES (TG_TABLE_NAME, TG_OP, row_to_json(OLD));RETURN OLD;ELSIF TG_OP = 'UPDATE' THENINSERT INTO audit_log (table_name, operation, old_data, new_data)VALUES (TG_TABLE_NAME, TG_OP, row_to_json(OLD), row_to_json(NEW));RETURN NEW;ELSIF TG_OP = 'INSERT' THENINSERT INTO audit_log (table_name, operation, new_data)VALUES (TG_TABLE_NAME, TG_OP, row_to_json(NEW));RETURN NEW;END IF;
END;
$$ LANGUAGE plpgsql;-- 绑定到表
CREATE TRIGGER users_audit
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION audit_trigger();

触发器内可用变量:

  • NEW:新行数据(INSERT/UPDATE)
  • OLD:旧行数据(UPDATE/DELETE)
  • TG_OP:操作类型(‘INSERT’, ‘UPDATE’, ‘DELETE’)
  • TG_TABLE_NAME:表名
  • TG_WHEN:‘BEFORE’ 或 ‘AFTER’
  • TG_LEVEL:‘ROW’ 或 ‘STATEMENT’

12.2 存储过程和函数

创建函数:

CREATE OR REPLACE FUNCTION get_user_order_count(p_user_id BIGINT)
RETURNS INTEGER AS $$
DECLAREv_count INTEGER;
BEGINSELECT count(*) INTO v_countFROM ordersWHERE user_id = p_user_id;RETURN v_count;
END;
$$ LANGUAGE plpgsql;-- 调用
SELECT get_user_order_count(123);

存储过程(PG 11+):

CREATE OR REPLACE PROCEDURE update_user_status(p_user_id BIGINT,p_status TEXT
)
LANGUAGE plpgsql
AS $$
BEGINUPDATE users SET status = p_status WHERE id = p_user_id;-- 可以包含事务控制COMMIT;
END;
$$;-- 调用
CALL update_user_status(123, 'inactive');

返回表的函数:

CREATE OR REPLACE FUNCTION get_top_users(p_limit INT DEFAULT 10)
RETURNS TABLE (user_id BIGINT,name VARCHAR,order_count BIGINT
) AS $$
BEGINRETURN QUERYSELECT u.id, u.name, count(o.id) AS cntFROM users uLEFT JOIN orders o ON o.user_id = u.idGROUP BY u.id, u.nameORDER BY cnt DESCLIMIT p_limit;
END;
$$ LANGUAGE plpgsql;-- 调用
SELECT * FROM get_top_users(5);

13. LISTEN/NOTIFY 通知机制

这是 PostgreSQL 独有的特性,可以在数据库连接之间做轻量级通知。

13.1 基本用法

会话 A(监听):

LISTEN channel_name;

会话 B(发送通知):

NOTIFY channel_name, 'some payload';

会话 A 会收到异步通知。

13.2 配合触发器做变更通知

示例:用户表变更通知

-- 创建通知函数
CREATE OR REPLACE FUNCTION notify_user_changed()
RETURNS TRIGGER AS $$
DECLAREpayload TEXT;
BEGINpayload := json_build_object('operation', TG_OP,'id', COALESCE(NEW.id, OLD.id),'name', COALESCE(NEW.name, OLD.name))::text;PERFORM pg_notify('user_changed', payload);IF TG_OP = 'DELETE' THENRETURN OLD;ELSERETURN NEW;END IF;
END;
$$ LANGUAGE plpgsql;-- 创建触发器
CREATE TRIGGER users_notify
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION notify_user_changed();

应用程序监听:

# Python 示例(使用 psycopg2)
import psycopg2
import select
import jsonconn = psycopg2.connect("dbname=mydb user=postgres")
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)cursor = conn.cursor()
cursor.execute("LISTEN user_changed;")print("Waiting for notifications...")while True:if select.select([conn], [], [], 5) == ([], [], []):print("Timeout")else:conn.poll()while conn.notifies:notify = conn.notifies.pop(0)payload = json.loads(notify.payload)print(f"Received: {payload}")

适用场景:

  • 刷新应用缓存
  • 实时通知(如聊天应用)
  • 配置变更通知
  • 轻量级事件驱动

限制:

  • Payload 最大 8KB
  • 非持久化(内存队列)
  • 不能替代专业消息队列(Kafka、RabbitMQ)

14. 其他增强特性

14.1 全文检索(Full Text Search)

基本使用:

-- 创建 tsvector 列
ALTER TABLE articles ADD COLUMN tsv tsvector;-- 更新 tsvector
UPDATE articles
SET tsv = to_tsvector('english', coalesce(title, '') || ' ' || coalesce(content, ''));-- 创建 GIN 索引
CREATE INDEX idx_articles_tsv ON articles USING GIN (tsv);-- 查询
SELECT * FROM articles
WHERE tsv @@ plainto_tsquery('english', 'postgresql tutorial');-- 按相关度排序
SELECT *, ts_rank(tsv, plainto_tsquery('english', 'postgresql')) AS rank
FROM articles
WHERE tsv @@ plainto_tsquery('english', 'postgresql')
ORDER BY rank DESC;

中文分词:

PostgreSQL 核心不支持中文分词,需要安装扩展:

  • zhparser(常用)
  • pg_jieba

使用 zhparser:

-- 安装扩展
CREATE EXTENSION zhparser;-- 创建中文配置
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l WITH simple;-- 使用
SELECT * FROM articles
WHERE to_tsvector('chinese', content) @@ to_tsquery('chinese', '数据库');

14.2 行级安全(Row Level Security, RLS)

启用 RLS:

-- 启用行级安全
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;-- 创建策略:只能看到自己的订单
CREATE POLICY orders_isolation_policy
ON orders
FOR ALL
USING (user_id = current_setting('app.current_user_id')::bigint);-- 或使用 current_user
CREATE POLICY orders_owner_only
ON orders
FOR ALL
USING (owner = current_user);

不同操作的策略:

-- SELECT 策略
CREATE POLICY orders_select_policy
ON orders
FOR SELECT
USING (user_id = current_setting('app.current_user_id')::bigint);-- INSERT 策略
CREATE POLICY orders_insert_policy
ON orders
FOR INSERT
WITH CHECK (user_id = current_setting('app.current_user_id')::bigint);-- UPDATE 策略(可见 + 可修改)
CREATE POLICY orders_update_policy
ON orders
FOR UPDATE
USING (user_id = current_setting('app.current_user_id')::bigint)
WITH CHECK (user_id = current_setting('app.current_user_id')::bigint);-- DELETE 策略
CREATE POLICY orders_delete_policy
ON orders
FOR DELETE
USING (user_id = current_setting('app.current_user_id')::bigint);

应用层设置参数:

-- 在连接开始时设置
SET app.current_user_id = 123;-- 或在事务中设置
BEGIN;
SET LOCAL app.current_user_id = 123;
SELECT * FROM orders;  -- 只能看到 user_id = 123 的订单
COMMIT;

查看策略:

\d+ orders  -- psql 命令-- 或 SQL 查询
SELECT * FROM pg_policies WHERE tablename = 'orders';

禁用 RLS(对超级用户和表所有者):

-- 超级用户默认绕过 RLS
-- 如果需要强制执行:
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

适用场景:

  • SaaS 多租户隔离
  • 数据权限控制
  • 把部分权限逻辑下沉到数据库层

14.3 物化视图(Materialized View)

创建物化视图:

CREATE MATERIALIZED VIEW mv_user_stats AS
SELECT u.id,u.name,count(o.id) AS order_count,sum(o.amount) AS total_amount
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name;-- 创建索引
CREATE INDEX idx_mv_user_stats_id ON mv_user_stats(id);

刷新物化视图:

-- 普通刷新(会锁表)
REFRESH MATERIALIZED VIEW mv_user_stats;-- 并发刷新(不锁表,但需要有唯一索引)
CREATE UNIQUE INDEX idx_mv_user_stats_id_unique ON mv_user_stats(id);
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_user_stats;

查询物化视图:

SELECT * FROM mv_user_stats WHERE order_count > 10;

删除物化视图:

DROP MATERIALIZED VIEW mv_user_stats;

适用场景:

  • 复杂的报表查询
  • 聚合结果缓存
  • 跨表统计

和普通视图的区别:

  • 普通视图:每次查询都重新计算(类似 MySQL)
  • 物化视图:结果持久化,需要手动刷新

14.4 事务性 DDL

PostgreSQL 的 DDL 可以放在事务中回滚:

BEGIN;-- 创建表
CREATE TABLE test_table (id INT, name TEXT);-- 插入数据
INSERT INTO test_table VALUES (1, 'Alice');-- 修改表结构
ALTER TABLE test_table ADD COLUMN email TEXT;-- 回滚所有操作(包括 DDL)
ROLLBACK;-- test_table 不会被创建

MySQL 对比:

  • MySQL 的大多数 DDL 会隐式提交事务,无法回滚
  • PostgreSQL 支持事务性 DDL,对误操作更友好

限制:

  • 某些操作不支持事务:如 CREATE DATABASE, DROP DATABASE
  • VACUUM, CREATE INDEX CONCURRENTLY 也不能在事务中

14.5 范围类型(Range Types)

内置范围类型:

-- 整数范围
CREATE TABLE events (id BIGSERIAL PRIMARY KEY,event_name TEXT,participant_age_range int4range  -- 整数范围
);INSERT INTO events (event_name, participant_age_range) VALUES('少年赛', '[10,18)'),   -- [10, 18),包含10,不包含18('成年赛', '[18,60)');-- 查询:年龄 16 适合哪些比赛
SELECT event_name 
FROM events 
WHERE participant_age_range @> 16;  -- 结果:少年赛-- 时间范围
CREATE TABLE bookings (id BIGSERIAL PRIMARY KEY,room_id INT,booking_period tstzrange  -- 时间范围
);INSERT INTO bookings (room_id, booking_period) VALUES(101, '[2024-01-01 10:00, 2024-01-01 12:00)');-- 查询:是否有重叠
SELECT * FROM bookings 
WHERE room_id = 101 AND booking_period && '[2024-01-01 11:00, 2024-01-01 13:00)'::tstzrange;

范围操作符:

  • @>: 包含
  • <@: 被包含
  • &&: 重叠
  • <<: 严格左侧
  • >>: 严格右侧
  • -|-: 相邻

GiST 索引:

CREATE INDEX idx_bookings_period ON bookings USING GIST (booking_period);

14.6 生成列(Generated Columns,PG 12+)

CREATE TABLE products (id BIGSERIAL PRIMARY KEY,price NUMERIC(10, 2),quantity INT,total NUMERIC(10, 2) GENERATED ALWAYS AS (price * quantity) STORED
);INSERT INTO products (price, quantity) VALUES (10.50, 3);SELECT * FROM products;
-- id | price | quantity | total
-- 1  | 10.50 | 3        | 31.50

两种类型:

  • STORED: 物理存储(推荐)
  • VIRTUAL: 虚拟计算(PG 暂不支持)

第四部分:生产实践

15. VACUUM 和表维护 ⭐

15.1 为什么需要 VACUUM

PostgreSQL 的 MVCC 机制:

  • UPDATE/DELETE 不会立即删除旧数据
  • 而是标记为"死元组"(dead tuple)
  • 需要 VACUUM 回收空间

MySQL 对比:

  • MySQL InnoDB 通过 undo log 实现 MVCC
  • 自动清理机制不同

15.2 VACUUM 命令

-- 普通 VACUUM:清理死元组,不释放磁盘空间
VACUUM;
VACUUM users;-- VACUUM FULL:重建表,释放磁盘空间(⚠️ 锁表)
VACUUM FULL users;-- ANALYZE:更新统计信息(影响查询计划)
ANALYZE;
ANALYZE users;-- 一起做
VACUUM ANALYZE users;-- VERBOSE:显示详细信息
VACUUM VERBOSE users;

15.3 Autovacuum(自动清理)

检查是否启用:

SHOW autovacuum;  -- 应该是 on

配置参数(postgresql.conf):

autovacuum = on  # 启用自动清理
autovacuum_max_workers = 3  # 并行 worker 数量
autovacuum_naptime = 1min  # 检查间隔# 触发条件
autovacuum_vacuum_threshold = 50  # 最少死元组数
autovacuum_vacuum_scale_factor = 0.2  # 死元组比例(20%)

单表调整:

-- 更激进的清理策略
ALTER TABLE large_table SET (autovacuum_vacuum_scale_factor = 0.05,autovacuum_vacuum_threshold = 1000
);

15.4 监控表膨胀

查看死元组情况:

SELECT schemaname,tablename,n_live_tup AS live_rows,n_dead_tup AS dead_rows,round(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio,last_vacuum,last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;

查看表膨胀率:

SELECTschemaname || '.' || tablename AS table_name,pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size,pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) AS table_size,round(100 * pg_relation_size(schemaname||'.'||tablename)::numeric / NULLIF(pg_total_relation_size(schemaname||'.'||tablename), 0), 2) AS table_ratio
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;

15.5 最佳实践

  1. 确保 autovacuum 开启(生产环境必须)
  2. 高频更新的表考虑降低阈值
  3. 定期检查死元组比例
  4. VACUUM FULL 谨慎使用(会锁表,考虑使用 pg_repack)
  5. 大表考虑分区

16. 连接池和并发控制

16.1 为什么需要连接池

关键差异:

  • MySQL: 轻量级线程模型,支持较多连接
  • PostgreSQL: 每个连接是独立进程,开销较大

查看连接数:

SHOW max_connections;  -- 默认 100SELECT count(*) FROM pg_stat_activity;  -- 当前连接数

16.2 PgBouncer(推荐)

三种模式:

  1. session: 连接级复用(最安全,默认)

    • 客户端连接断开后才复用
    • 支持所有 PostgreSQL 特性
  2. transaction: 事务级复用(⭐ 推荐)

    • 事务结束后立即复用
    • 性能好,适合大多数场景
    • 不支持:LISTEN/NOTIFY, PREPARE, CURSOR
  3. statement: 语句级复用

    • 每条 SQL 后复用
    • 限制最多,不推荐

安装和配置:

# macOS
brew install pgbouncer# Ubuntu
sudo apt install pgbouncer

配置文件 pgbouncer.ini:

[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25

应用连接到 PgBouncer:

# 原来连接 PostgreSQL
conn = psycopg2.connect("host=localhost port=5432 dbname=mydb user=app")# 改为连接 PgBouncer
conn = psycopg2.connect("host=localhost port=6432 dbname=mydb user=app")

16.3 应用层连接池

HikariCP (Java):

spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000

asyncpg (Python):

import asyncpgpool = await asyncpg.create_pool(host='localhost',database='mydb',user='app',password='password',min_size=5,max_size=20
)

16.4 事务隔离级别

PostgreSQL 支持的级别:

-- 查看当前隔离级别
SHOW transaction_isolation;-- 设置隔离级别
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;

和 MySQL 的对比:

隔离级别MySQLPostgreSQL
Read Uncommitted❌(最低是 RC)
Read Committed✓(默认)
Repeatable Read✓(默认)
Serializable

PostgreSQL 默认是 Read Committed,MySQL 默认是 Repeatable Read。

16.5 锁机制

查看锁等待:

SELECT blocked_locks.pid AS blocked_pid,blocked_activity.usename AS blocked_user,blocking_locks.pid AS blocking_pid,blocking_activity.usename AS blocking_user,blocked_activity.query AS blocked_statement,blocking_activity.query AS blocking_statement
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktypeAND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.databaseAND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relationAND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;

手动锁表:

-- 显式锁表(少用)
BEGIN;
LOCK TABLE users IN EXCLUSIVE MODE;
-- 其他操作
COMMIT;

17. 备份和恢复

17.1 逻辑备份(pg_dump)

导出单个数据库:

# 自定义格式(推荐)
pg_dump -h localhost -U postgres -d mydb -F c -f mydb.dump# SQL 格式
pg_dump -h localhost -U postgres -d mydb -f mydb.sql# 压缩
pg_dump -h localhost -U postgres -d mydb | gzip > mydb.sql.gz

导出特定表:

pg_dump -h localhost -U postgres -d mydb -t users -t orders -f tables.dump

导出 schema only(不含数据):

pg_dump -h localhost -U postgres -d mydb -s -f schema.sql

导出所有数据库:

pg_dumpall -h localhost -U postgres -f all_databases.sql

17.2 恢复

从自定义格式恢复:

# 创建数据库
createdb -U postgres mydb_restore# 恢复
pg_restore -h localhost -U postgres -d mydb_restore mydb.dump# 并行恢复(加速)
pg_restore -h localhost -U postgres -d mydb_restore -j 4 mydb.dump

从 SQL 文件恢复:

psql -h localhost -U postgres -d mydb < mydb.sql

17.3 物理备份(pg_basebackup)

# 基础备份
pg_basebackup -h localhost -U postgres -D /backup/pgdata -Fp -Xs -P# 参数说明:
# -D: 备份目录
# -Fp: plain 格式
# -Xs: 包含 WAL 日志(stream 模式)
# -P: 显示进度

17.4 连续归档和 PITR(时间点恢复)

配置 WAL 归档(postgresql.conf):

wal_level = replica
archive_mode = on
archive_command = 'cp %p /archive/%f'

执行基础备份:

pg_basebackup -h localhost -U postgres -D /backup/base -Fp -Xs -P

恢复到特定时间点:

# 1. 恢复基础备份
cp -r /backup/base /var/lib/postgresql/data# 2. 创建 recovery.conf(PG 12+用 postgresql.auto.conf)
restore_command = 'cp /archive/%f %p'
recovery_target_time = '2024-01-01 12:00:00'# 3. 启动 PostgreSQL

17.5 推荐工具

  • pgBackRest: 企业级备份工具
  • Barman: 灾难恢复工具
  • wal-g: 云存储备份(S3, GCS)

18. 监控和诊断

18.1 查看活动连接

SELECT pid,usename,datname,client_addr,state,now() - query_start AS duration,query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;

18.2 查看长时间运行的查询

SELECT pid,now() - query_start AS duration,state,query
FROM pg_stat_activity
WHERE state != 'idle'AND now() - query_start > interval '5 minutes'
ORDER BY duration DESC;

18.3 杀死查询

-- 取消查询(温和)
SELECT pg_cancel_backend(pid);-- 强制终止连接
SELECT pg_terminate_backend(pid);-- 批量终止空闲连接
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'AND now() - state_change > interval '1 hour';

18.4 慢查询分析(pg_stat_statements)

安装扩展:

-- 需要修改 postgresql.conf
-- shared_preload_libraries = 'pg_stat_statements'
-- 重启数据库后CREATE EXTENSION pg_stat_statements;

查看最慢的查询:

SELECT calls,total_exec_time,mean_exec_time,max_exec_time,stddev_exec_time,rows,query
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

查看总耗时最多的查询:

SELECT calls,total_exec_time,mean_exec_time,query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

重置统计:

SELECT pg_stat_statements_reset();

18.5 查看表和索引大小

-- 最大的表
SELECTschemaname || '.' || tablename AS table_name,pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size,pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) AS table_size,pg_size_pretty(pg_indexes_size(schemaname||'.'||tablename)) AS indexes_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 20;-- 最大的索引
SELECTschemaname || '.' || tablename AS table_name,indexname,pg_size_pretty(pg_relation_size(schemaname||'.'||indexname)) AS index_size
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY pg_relation_size(schemaname||'.'||indexname) DESC
LIMIT 20;

18.6 缓存命中率

-- 表缓存命中率(应该 > 99%)
SELECT sum(heap_blks_read) AS heap_read,sum(heap_blks_hit) AS heap_hit,round(sum(heap_blks_hit) * 100.0 / NULLIF(sum(heap_blks_hit) + sum(heap_blks_read), 0), 2) AS hit_ratio
FROM pg_statio_user_tables;-- 索引缓存命中率
SELECT sum(idx_blks_read) AS idx_read,sum(idx_blks_hit) AS idx_hit,round(sum(idx_blks_hit) * 100.0 / NULLIF(sum(idx_blks_hit) + sum(idx_blks_read), 0), 2) AS hit_ratio
FROM pg_statio_user_indexes;

18.7 查看数据库统计

SELECT * FROM pg_stat_database WHERE datname = 'mydb';

19. 性能调优 Checklist

19.1 配置优化

postgresql.conf 关键参数:

# 内存设置(根据服务器内存调整)
shared_buffers = 4GB          # 建议:服务器内存的 25%
effective_cache_size = 12GB   # 建议:服务器内存的 50-75%
work_mem = 64MB               # 单个查询操作的内存
maintenance_work_mem = 512MB  # VACUUM, CREATE INDEX 的内存# 连接数
max_connections = 100         # 根据实际需求,配合连接池# WAL 设置
wal_buffers = 16MB
checkpoint_timeout = 10min
checkpoint_completion_target = 0.9# 查询规划
random_page_cost = 1.1        # SSD 建议 1.1,HDD 建议 4
effective_io_concurrency = 200 # SSD 建议 200# 日志
log_min_duration_statement = 1000  # 记录超过 1 秒的查询
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '

19.2 索引优化

检查缺失索引:

-- 查看全表扫描最多的表
SELECT schemaname,tablename,seq_scan,seq_tup_read,idx_scan,seq_tup_read / seq_scan AS avg_seq_read
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_scan DESC
LIMIT 20;

检查未使用的索引:

SELECT schemaname,tablename,indexname,idx_scan,pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0AND indexrelname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;

19.3 查询优化

常见问题和解决:

  1. N+1 查询 → 使用 JOIN 或 IN
  2. **SELECT *** → 只查询需要的列
  3. 没有 LIMIT → 添加合理的 LIMIT
  4. 函数包裹索引列 → 创建表达式索引
  5. OR 条件多 → 考虑 UNION ALL
  6. 子查询 → 考虑 JOIN 或 CTE

19.4 VACUUM 优化

-- 检查需要 VACUUM 的表
SELECT schemaname,tablename,n_dead_tup,n_live_tup,round(n_dead_tup * 100.0 / NULLIF(n_live_tup, 0), 2) AS dead_ratio,last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;

19.5 分区表

适用场景:

  • 单表超过 100GB
  • 按时间范围查询(如日志表)
  • 需要快速删除旧数据

示例:按月分区

CREATE TABLE logs (id BIGSERIAL,log_time TIMESTAMPTZ NOT NULL,message TEXT
) PARTITION BY RANGE (log_time);-- 创建分区
CREATE TABLE logs_2024_01 PARTITION OF logsFOR VALUES FROM ('2024-01-01') TO ('2024-02-01');CREATE TABLE logs_2024_02 PARTITION OF logsFOR VALUES FROM ('2024-02-01') TO ('2024-03-01');-- 自动创建分区扩展:pg_partman

第五部分:迁移指南

20. 从 MySQL 迁移

20.1 使用 pgloader(推荐)

安装:

# macOS
brew install pgloader# Ubuntu
sudo apt install pgloader

一键迁移:

pgloader mysql://root:password@localhost/mydb \postgresql://user:password@localhost/mydb

自定义迁移配置:

LOAD DATABASEFROM mysql://root:password@localhost/mydbINTO postgresql://user:password@localhost/mydbWITH include drop, create tables, create indexes, reset sequencesSET maintenance_work_mem to '512MB',work_mem to '128MB'CAST type datetime to timestamptzdrop default drop not null using zero-dates-to-null,type date drop not null drop default using zero-dates-to-null,type tinyint to boolean using tinyint-to-boolean;

20.2 手动迁移步骤

1. 导出 MySQL schema:

mysqldump -u root -p --no-data mydb > schema.sql

2. 转换 schema:

需要手动修改的地方:

-- AUTO_INCREMENT → SERIAL
-- MySQL
id BIGINT AUTO_INCREMENT PRIMARY KEY-- PostgreSQL
id BIGSERIAL PRIMARY KEY-- TINYINT(1) → BOOLEAN
is_active TINYINT(1)  →  is_active BOOLEAN-- DATETIME → TIMESTAMPTZ
created_at DATETIME  →  created_at TIMESTAMPTZ-- ENUM
status ENUM('active', 'inactive')CREATE TYPE user_status AS ENUM ('active', 'inactive');
status user_status-- 删除 ENGINE, CHARSET
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4  →  删除-- 索引语法
KEY idx_name (column)CREATE INDEX idx_name ON table(column);

3. 导出数据:

mysqldump -u root -p --no-create-info --complete-insert mydb > data.sql

4. 导入 PostgreSQL:

psql -U postgres -d mydb < schema.sql
psql -U postgres -d mydb < data.sql

20.3 常见问题和解决

问题 1:自增 ID 不连续

-- 重置序列
SELECT setval('users_id_seq', (SELECT MAX(id) FROM users));

问题 2:字符串大小写

-- MySQL 默认不区分大小写
-- PostgreSQL 区分大小写-- 解决:使用 ILIKE 或 LOWER
SELECT * FROM users WHERE name ILIKE 'alice';
SELECT * FROM users WHERE LOWER(name) = 'alice';

问题 3:排序规则(Collation)

-- MySQL 的 utf8mb4_unicode_ci → PostgreSQL 需要指定
CREATE COLLATION case_insensitive (provider = icu,locale = 'und-u-ks-level2',deterministic = false
);-- 或在查询时使用
SELECT * FROM users ORDER BY name COLLATE "en_US";

20.4 迁移 Checklist

  • 备份 MySQL 数据
  • 转换数据类型
  • 转换 SQL 语法(LIMIT, IFNULL, REPLACE INTO)
  • 测试应用兼容性
  • 性能测试
  • 配置连接池
  • 配置 autovacuum
  • 配置备份策略
  • 配置监控

21. CDC/逻辑复制(对应 binlog)

21.1 逻辑复制基础

PostgreSQL 的逻辑复制(PG 10+)类似 MySQL 的主从复制。

架构:

  • 发布者(Publisher):主库
  • 订阅者(Subscriber):从库

21.2 配置逻辑复制

主库配置(postgresql.conf):

wal_level = logical
max_replication_slots = 10
max_wal_senders = 10

创建发布:

-- 发布所有表
CREATE PUBLICATION my_publication FOR ALL TABLES;-- 发布特定表
CREATE PUBLICATION my_publication FOR TABLE users, orders;-- 查看发布
\dRp+

从库创建订阅:

-- 创建订阅(会自动开始同步)
CREATE SUBSCRIPTION my_subscription
CONNECTION 'host=master_host dbname=mydb user=replicator password=password'
PUBLICATION my_publication;-- 查看订阅
\dRs+-- 查看同步状态
SELECT * FROM pg_stat_subscription;

21.3 CDC 到消息队列(类似 Canal)

使用 Debezium:

Debezium 是一个开源 CDC 平台,支持 PostgreSQL → Kafka。

配置示例(Kafka Connect):

{"name": "postgres-connector","config": {"connector.class": "io.debezium.connector.postgresql.PostgresConnector","database.hostname": "localhost","database.port": "5432","database.user": "postgres","database.password": "password","database.dbname": "mydb","database.server.name": "dbserver1","table.include.list": "public.users,public.orders","plugin.name": "pgoutput"}
}

输出到 Kafka 的消息格式:

{"before": null,"after": {"id": 123,"name": "Alice","email": "alice@example.com"},"op": "c",  // c=create, u=update, d=delete"ts_ms": 1640000000000
}

21.4 WAL2JSON(输出 JSON 格式)

安装扩展:

# 需要安装 wal2json 插件
# 参考:https://github.com/eulerto/wal2json

配置:

# postgresql.conf
wal_level = logical
shared_preload_libraries = 'wal2json'

创建逻辑复制槽:

SELECT * FROM pg_create_logical_replication_slot('my_slot', 'wal2json');

读取变更:

SELECT * FROM pg_logical_slot_get_changes('my_slot', NULL, NULL);

21.5 对比 MySQL binlog/Canal

特性MySQL (binlog + Canal)PostgreSQL (逻辑复制)
内置支持✓ (PG 10+)
输出格式Row-based binlogWAL + logical decoding
第三方工具Canal, MaxwellDebezium, wal2json
表级过滤
DDL 变更✓ (部分)
延迟毫秒级毫秒级

22. 常用扩展推荐

22.1 查看已安装扩展

-- 查看可用扩展
SELECT * FROM pg_available_extensions ORDER BY name;-- 查看已安装扩展
\dx
-- 或
SELECT * FROM pg_extension;

22.2 必备扩展

pg_stat_statements(慢查询分析):

CREATE EXTENSION pg_stat_statements;

pgcrypto(加密函数):

CREATE EXTENSION pgcrypto;-- 生成 UUID
SELECT gen_random_uuid();-- 密码哈希
SELECT crypt('password', gen_salt('bf'));

uuid-ossp(UUID 生成):

CREATE EXTENSION "uuid-ossp";SELECT uuid_generate_v4();

22.3 高级扩展

PostGIS(地理信息):

CREATE EXTENSION postgis;-- 创建地理位置列
ALTER TABLE stores ADD COLUMN location GEOGRAPHY(POINT, 4326);-- 插入坐标
UPDATE stores SET location = ST_GeogFromText('POINT(-122.4194 37.7749)') WHERE id = 1;-- 查询附近的店铺(10km 内)
SELECT name, ST_Distance(location, ST_GeogFromText('POINT(-122.4 37.78)')) AS distance
FROM stores
WHERE ST_DWithin(location, ST_GeogFromText('POINT(-122.4 37.78)'), 10000)
ORDER BY distance;

TimescaleDB(时序数据):

CREATE EXTENSION timescaledb;-- 创建时序表
CREATE TABLE metrics (time TIMESTAMPTZ NOT NULL,device_id INT,temperature DOUBLE PRECISION
);SELECT create_hypertable('metrics', 'time');

pg_trgm(模糊搜索):

CREATE EXTENSION pg_trgm;-- 相似度搜索
SELECT name, similarity(name, 'Alice') AS sim
FROM users
WHERE name % 'Alice'  -- % 操作符表示相似
ORDER BY sim DESC;-- 创建 GIN 索引加速
CREATE INDEX idx_users_name_trgm ON users USING GIN (name gin_trgm_ops);

hstore(键值存储):

CREATE EXTENSION hstore;CREATE TABLE products (id BIGSERIAL PRIMARY KEY,name TEXT,attributes HSTORE  -- 键值对
);INSERT INTO products (name, attributes) VALUES('Laptop', 'brand=>Dell, ram=>16GB, cpu=>i7');-- 查询
SELECT * FROM products WHERE attributes->'brand' = 'Dell';
SELECT * FROM products WHERE attributes @> 'ram=>16GB';

pg_repack(在线 VACUUM FULL):

# 安装
brew install pg_repack# 使用(不锁表)
pg_repack -d mydb -t users

22.4 扩展生态

  • citus:分布式 PostgreSQL
  • pg_partman:自动分区管理
  • pg_cron:数据库内定时任务
  • zombodb:Elasticsearch 集成
  • pgroonga:全文检索(支持中文)

附录:参考资源

官方文档

  • 官方文档:https://www.postgresql.org/docs/
  • 中文文档:http://www.postgres.cn/docs/

工具

  • pgAdmin:https://www.pgadmin.org/
  • DBeaver:https://dbeaver.io/
  • Postgres.app:https://postgresapp.com/
  • pgcli:https://www.pgcli.com/

社区

  • Stack Overflow:[postgresql] 标签
  • Reddit:r/PostgreSQL
  • Slack:https://postgres-slack.herokuapp.com/

书籍推荐

  • 《PostgreSQL 即学即用》
  • 《PostgreSQL 修炼之道》
  • 《PostgreSQL 技术内幕:查询优化深度探索》

文档维护: 如有问题或建议,欢迎反馈!

http://www.dtcms.com/a/610313.html

相关文章:

  • 山东省建设执业师之家官方网站网站建设培训心得体会
  • 充电桩小程序开发实战:从零到一搭建完整系统【源码+解析+文档】
  • 配置安装mmsegmentation并同步至远程服务器
  • 了解一下Sentry(一个开源的实时错误监控平台)
  • 企业网站建设规划书网站建设制作要学什么软件
  • C#VB.NET中实现可靠的文件监控(新建、删除、改名、内容修改等事件的准确捕获)​
  • Python数据科学与图像处理利器组合:Prophet、Arch、Scikit-image、Pillow-heif用法全解析
  • wordpress 4.6.1海外广告优化师
  • 【运维】GNU/Linux 入门笔记
  • 长沙鞋网站建设煤矿建设工程质量监督总站网站
  • 学做川菜下什么网站爱网站黄
  • 前端自定义右键菜单与图片复制(兼容H5)
  • [Switch大气层]纯净版+特斯拉版 20.5.0大气层1.9.5心悦整合包 固件 工具 插件 前端等switch游戏资源下载合集
  • 同样算法的DFS求解数独C和Python程序用时比较
  • vue3+element-china-area-data 实现省市区三级联动
  • Next.js 项目常见报错排查与解决
  • Vue 校验输入时间与当前时间差大于等于3小时
  • html中网站最下面怎么做设计主题网站
  • 起重机智能选型:从血泪教训到科技护航的革新之路
  • java+maven配置yguard的一次实验
  • 汝南县网站建设Wordpress实现中英文
  • ASC学习笔记0006:游戏效果将如何复制到客户端
  • 延安市住建建设网站无锡网站营销推广
  • 我想做网站服务器选用什么电子商务网站建设总结与体会
  • Oracle分页sql
  • Airsim仿真、无人机、无人车、Lidar、深度强化学习
  • Airsim仿真、无人机、Lidar深度相机、DDPG深度强化学习
  • 做做网站下载2023常熟网站网站建设
  • app推广策略WordPress优化百度广告
  • pinctrl子系统介绍