PostgreSQL逻辑结构图解

第一层:数据库 (Database)
类比: 大楼里的某一层楼。
特点: 数据库是物理上的最高隔离级别。通常一个项目会占用一个独立的数据库。
注意:不同数据库之间的数据默认是不通的,你不能在db_a里直接查询db_b 的表。
第二层:模式 (Schema) ------ 这是 PG 的灵魂
类比: 楼层里的各个"部门"或"房间"。
特点: 这是 PostgreSQL 区别于 MySQL 的重要特性。一个数据库下可以有多个 Schema。
默认值:默认所有的表都放在名为public 的模式下。
用途:你可以创建auth模式放用户表,storage 模式放文件表,从而实现逻辑上的分组和权限控制。
第三层:表 (Table)
类比: 房间里的"文件柜"。
特点: 这是真正存放数据的地方。
结构: 每一行(Row)代表一条记录,每一列(Column)代表一个字段。
核心:Schema
Schema是数据库的下一个层级也就是一个容器可以更好的容纳数据库和表吗
Schema 就是数据库内部的一个"逻辑容器"或"命名空间"。
如果把数据库比作一栋大楼,那么 Schema 就是大楼里的一个个独立房间(或部门),而表则是房间里的文件柜。
🏢 核心层级关系
为了让你更直观地理解,我们可以这样对应:
实例 = 整个物业公司(管理所有资源)
数据库 = 一栋独立的大楼(物理隔离,楼与楼之间不互通)
Schema = 大楼里的房间/部门(如:财务部、人事部;逻辑隔离,房间之间有墙)
表 = 房间里的文件柜(存放具体数据)
设计方式:
这种"数据库 -> 模式 -> 表"的设计方式,能让你在开发大型复杂系统时非常从容。比如,如果你要做多租户系统 (每个客户的数据逻辑隔离),你可以给每个客户分配一个独立的Schema,但它们共享同一个数据库连接池,这样既安全又省资源。
创建库、模式
数据库操作
sql
# 创建数据库
create database db_name;
// 查看数据库
\l 或者 SELECT datname FROM pg_database;
// 接入数据库
\c db_name;
// 删除数据库
drop database db_name; // 需要先切换到其他数据库才能删除,并且不能有会话连接
DROP DATABASE db WITH (FORCE); // 强制删除
模式操作
如果是小项目,直接用public模式就行
sql
// 如果不存在,就创建
CREATE SCHEMA IF NOT EXISTS storage;
// 查看模式
\dn
// 删除模式
-- 只有模式为空时才能删除
DROP SCHEMA IF EXISTS storage;
-- 【慎用】级联删除:删除模式以及模式里的所有表、视图、函数等
DROP SCHEMA IF EXISTS storage CASCADE;
pgsql表操作
1. 创建表 (CREATE TABLE)
在建表时,除了字段名和类型,约束(Constraints) 是保证数据质量的关键。
sql
// storage.files 这样写表示在storage下创建files表
// 如果要在public模式下创建files表,直接写files或者public.files
CREATE TABLE IF NOT EXISTS storage.files (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, -- 自增主键
file_id UUID DEFAULT gen_random_uuid(), -- uuid
file_name TEXT NOT NULL, -- 不能为空
file_size BIGINT CHECK (file_size >= 0), -- 检查约束:大小不能为负
is_public BOOLEAN DEFAULT false, -- 默认值
tags TEXT[], -- 数组类型(PG特色!)
created_at TIMESTAMPTZ DEFAULT NOW() -- 带时区的时间
);
查这个库下的表列表 \d
sql
SELECT
schemaname AS 模式名,
tablename AS 表名,
tableowner AS 所有人
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY schemaname, tablename;
查这个表的表结构 \d 表名 或者 \d+ 表名
SQL复制代码
sql
SELECT
column_name AS 字段名,
data_type AS 数据类型,
is_nullable AS 是否允许为空,
column_default AS 默认值,
character_maximum_length AS 最大长度
FROM information_schema.columns
WHERE table_name = 'cloud_files' -- 替换成你的表名
ORDER BY ordinal_position; -- 按照表中定义的顺序排列
2. 修改表 (ALTER TABLE)
项目需求经常变,你需要修改已经存在的表结构。
- 添加字段:
sql
ALTER TABLE storage.files ADD COLUMN download_count INT DEFAULT 0;
- 修改字段类型:
sql
ALTER TABLE storage.files ALTER COLUMN file_name TYPE VARCHAR(255);
- 重命名列名:
sql
ALTER TABLE storage.files RENAME COLUMN is_public TO is_shared;
- 删除字段:
sql
ALTER TABLE storage.files DROP COLUMN tags;
3. 删除表 (DROP TABLE)
删除表要非常谨慎,一旦删除,表内数据将全部丢失。
sql
-- 基本删除
DROP TABLE IF EXISTS storage.files;
-- 级联删除(如果这个表被其他表作为外键关联,用CASCADE会连带删除关联)
DROP TABLE storage.files CASCADE;
4. 清空表 (TRUNCATE)
如果你只想删掉表里所有的数据,但想保留这个表的结构(就像把房子里的家具搬空,但房子不拆),TRUNCATE比DELETE 快得多。
sql
TRUNCATE TABLE storage.files;
TRUNCATE TABLE storage.files i; // 清空表并且重置自增id
5. 查看表信息 (元指令与查询)
在操作表时,你经常需要确认当前的结构。
- 在 psql 命令行中:
\dt: 列出当前库所有表。\d storage.files: 查看这张表的详细结构(列、类型、索引、约束)。
- 使用 SQL 查询(pgAdmin常用):
sql
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'files';
6. 特色操作:表继承 (Inheritance)
这是 PostgreSQL 的黑科技之一。你可以创建一个"父表",然后让"子表"继承它。
sql
-- 父表:定义基础字段
CREATE TABLE content (
title TEXT,
author TEXT
);
-- 子表:继承基础字段,并增加自己的字段
CREATE TABLE video (
duration INT
) INHERITS (content);
查询content表时,你会发现它会自动把video 表里的数据也查出来。
数据操作
对数据的增删改查
插入
sql
-- 插入单条记录(注意数组用 ARRAY[...] 或 {'a','b'} 语法)
INSERT INTO files (file_name, file_size, is_public, tags)
VALUES ('课程大纲.pdf', 1024576, true, ARRAY['教育', 'PDF']);
-- 批量插入(Postgres 对批量插入的支持非常高效)
INSERT INTO files (file_name, file_size, tags)
VALUES
('demo.mp4', 50000000, '{"视频", "测试"}'),
('config.yaml', 1024, '{"配置"}');
如果约束失败,自增id也是会消耗的
查询
sql
-- 基础查询:查看所有公开的大文件
SELECT file_name, file_size, created_at
FROM files
WHERE is_public = true AND file_size > 1000000;
-- 模糊查询:查找所有 pdf
SELECT * FROM files WHERE file_name LIKE '%.pdf';
-- 数组查询(PG特色):查找标签中包含"视频"的文件
SELECT * FROM files WHERE '视频' = ANY(tags);
修改
sql
-- 模拟下载次数自增
UPDATE files
SET download_count = download_count + 1
WHERE file_name = 'demo.mp4';
-- 修改多个字段并切换布尔值
UPDATE files
SET is_public = false, tags = array_append(tags, '私有')
WHERE file_id = '你查出来的某个UUID';
删除
sql
-- 删除特定的文件
DELETE FROM files WHERE file_id = '某个UUID';
-- 删除所有下载次数为 0 的旧配置测试文件
DELETE FROM files
WHERE download_count = 0 AND tags @> ARRAY['测试'::text];
创建+回显
sql
-- 插入并立刻返回新生成的 UUID 和创建时间
INSERT INTO files (file_name, file_size)
VALUES ('枫枫老师的私房课.zip', 999999)
RETURNING file_id, created_at;
上面已经了解了一些基本的数据类型,pgsql里面支持很多数据类型
核心数据类型 (Data Types)
PostgreSQL 以其丰富的数据类型著称,以下是开发中最常用的几类:
数值类型
INTEGER/INT: 4 字节整数,范围约±21±21 亿。BIGINT: 8 字节整数,用于大 ID 或大数据量计数。NUMERIC(p, s)/DECIMAL: 精确的小数,用于金额。pp是总位数,ss 是小数点后的位数。SERIAL/BIGSERIAL: 自增整数(实际上是封装了SEQUENCE)。
字符类型
VARCHAR(n): 变长字符串,有长度限制。TEXT: 变长字符串,无长度限制(PostgreSQL 推荐直接用这个,除非有业务硬性长度约束)。CHAR(n): 定长字符串,不足部分补空格。
日期/时间类型
TIMESTAMP: 日期和时间。TIMESTAMPTZ: 带时区的日期和时间(生产环境推荐使用,避免时区混乱)。DATE: 仅日期。INTERVAL: 时间间隔(如1 day 2 hours)。tsrange: 对应timestamp without time zone的范围。tstzrange: 对应timestamp with time zone的范围(推荐用于跨时区业务)。daterange: 对应date的范围。
特色/高级类型
BOOLEAN:TRUE,FALSE, 或NULL。JSONB: 二进制存储的 JSON 数据,支持索引,性能极佳。UUID: 通用唯一识别码。ARRAY: 数组类型,例如TEXT[]可以存储标签列表。INET:ip类型
列约束 (Column Constraints)
约束用于确保数据的准确性和可靠性(实体完整性、参照完整性)。
| 约束类型 | 描述 | 示例 |
|---|---|---|
NOT NULL |
强制列不能包含NULL 值。 |
name TEXT NOT NULL |
UNIQUE |
确保列中的所有值互不相同。 | email TEXT UNIQUE |
PRIMARY KEY |
主键,唯一标识每一行。隐含NOT NULL和UNIQUE。 |
id SERIAL PRIMARY KEY |
FOREIGN KEY |
外键,建立表与表之间的链接,防止破坏关系。 | user_id INT REFERENCES users(id) |
CHECK |
检查值是否满足特定逻辑条件。 | age INT CHECK (age >= 18) |
DEFAULT |
如果插入时未指定值,则使用默认值。 | created_at TIMESTAMPTZ DEFAULT NOW() |
示例
sql
CREATE TABLE public.file_details (
-- 1. 自增长整数主键 (BIGSERIAL)
-- 知识点:BIGSERIAL 是 8 字节整数,支持极大数量级(达 922 亿亿次插入)
-- 适合作为数据库内部的关联外键,性能优于 UUID
id BIGSERIAL PRIMARY KEY,
-- 2. 现代唯一标识 (UUID)
-- 知识点:对外暴露的 ID,防止业务数据量泄露
detail_id UUID UNIQUE DEFAULT gen_random_uuid(),
-- 3. 时间范围类型 (TSTZRANGE)
-- 知识点:存储文件的"有效期"起止时间。
-- 例如:'[2023-01-01, 2024-01-01)' 表示从元旦开始,一年内有效。
valid_period TSTZRANGE NOT NULL DEFAULT tstzrange(now(), NULL, '[)'),
-- 4. 空间与网络 (INET)
uploader_ip INET NOT NULL,
-- 5. 集合容器 (ARRAY)
tags TEXT[] DEFAULT '{}',
-- 6. 半结构化神器 (JSONB)
metadata JSONB DEFAULT '{}',
-- 基础字段
file_name TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
插入数据
sql
INSERT INTO public.file_details (uploader_ip, tags, metadata, file_name, valid_period)
VALUES
-- 1. 文档:标准构造函数
('10.0.0.5', '{"PDF", "文档"}', '{"pages": 45, "author": "Fengfeng"}', 'PostgreSQL手册.pdf',
tstzrange('2026-01-01 00:00:00Z', '2027-01-01 00:00:00Z', '[)')),
-- 2. 图片:修正字符串格式 (去掉逗号后的空格)
-- 注意:'[start,]' 表示包含起点,无终点
('127.0.0.1', '{"素材", "封面"}', '{"dpi": 300, "color": "RGB"}', 'bilibili横屏封面.png',
'[2026-04-14 08:00:00+08,]'),
-- 3. 简单文件:另一种写法,使用 NULL 表示无穷大
('172.16.0.100', '{}', '{}', 'test_file.txt',
tstzrange('2025-01-01', NULL, '[)')),
-- 4. 视频文件:直接使用 DEFAULT
('192.168.50.20', '{"视频"}', '{"video": {"resolution": {"width": 1920}}}', 'test_file.mp4',
DEFAULT);
上面简单介绍了一些查询语句,下面来进阶学习一下pgsql里面的单表查询
先批量插入一些数据方便后续的查询
sql
INSERT INTO file_details (
file_name,
uploader_ip,
tags,
metadata,
created_at
) VALUES
-- 1. 匹配 ILIKE '%.PNG' 和 2026 年时间点
('Logo_Design.png', '192.168.1.5', ARRAY['素材', '封面', '设计'], '{"author": "Fengfeng", "dpi": 300, "color": "RGB"}', '2026-02-15 10:30:00'),
-- 2. 匹配数组重叠 (&&) 和包含 (@>)
('Tutorial_Video.mp4', '172.16.10.1', ARRAY['视频', '素材', '教程'], '{"author": "Zhang", "video": {"resolution": {"width": 1920, "height": 1080}}}', '2026-05-20 14:00:00'),
-- 3. 匹配特定的作者和文本转换
('Annual_Report.pdf', '172.16.20.5', ARRAY['文档', 'PDF'], '{"author": "Fengfeng", "pages": 45}', '2026-08-01 09:15:00'),
-- 4. 匹配 inet 范围 (172.16.0.0/16)
('Backup_Config.txt', '172.16.50.12', ARRAY['文档', '系统'], '{"priority": "high"}', '2025-12-30 23:59:59'),
-- 5. 匹配 JSONB 键值对存在性 (?)
('Thumbnail.PNG', '127.0.0.1', ARRAY['封面'], '{"author": "Li", "color": "CMYK", "dpi": 72}', '2026-01-10 11:00:00'),
-- 6. 聚合统计测试案例 (同一 IP 多次上传)
('Meeting_Notes.docx', '127.0.0.1', ARRAY['文档'], '{"author": "Fengfeng"}', '2026-03-05 16:20:00');
基础匹配和模糊查询
sql
-- 查找所有 png 图片,忽略大小写
SELECT file_name, uploader_ip
FROM file_details
WHERE file_name ILIKE '%.PNG';
-- 查找 2026 年上传的文件
SELECT * FROM file_details
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
SELECT * FROM file_details
WHERE EXTRACT(YEAR FROM created_at) = 2026;
数组类型的深度检索
sql
-- 匹配:查找标签中包含"文档"的所有文件
SELECT file_name, tags FROM file_details WHERE '文档' = ANY(tags);
-- 包含:查找同时包含"素材"和"封面"的文件 (@> 是包含操作符)
SELECT file_name, tags FROM file_details WHERE tags @> ARRAY['素材', '封面'];
-- 重叠:只要包含"PDF"或"视频"中的任意一个就行 (&& 是重叠操作符)
SELECT file_name, tags FROM file_details WHERE tags && ARRAY['PDF', '视频'];
jsonb的操作
注意:
->>提取出来的是text ,->提取出来的是json 对象。
sql
-- 提取:查找作者是 "Fengfeng" 的文件
SELECT file_name, metadata->>'author' AS author
FROM file_details
WHERE metadata->>'author' = 'Fengfeng';
-- 如果取出来的字段还是一个json,就得用->取了
SELECT file_name, metadata->'video' AS author
FROM file_details
WHERE (metadata->'video'->'resolution'->>'width')::int = 1920;
-- 嵌套判断:查找 DPI 等于 300 的图片 (将 text 转为 int 进行比较)
SELECT file_name, metadata FROM file_details
WHERE (metadata->>'dpi')::int = 300;
-- 键值对存在性判断:查询所有定义了 "color" 属性的文件
SELECT * FROM file_details WHERE metadata ? 'color';
-- json查询优化1
#>:返回 JSON 对象
#>>:返回文本
SELECT
file_name,
metadata #> '{video, author}' AS author -- 直接通过路径取值
FROM file_details
WHERE
-- 使用 #>> 直接获取文本并转换,无需中间层 ->
(metadata #>> '{video, resolution, width}')::int = 1920;
-- 使用 JSONPath (@@ 操作符)
如果你使用的是 PostgreSQL 12+,可以使用类似 JavaScript 的 JSONPath 语法。这种方式在处理 复杂逻辑(如模糊匹配、数组过滤)时非常强大。
SELECT file_name
FROM file_details
WHERE metadata @@ '$.video.resolution.width == 1920';
优势
语法极其简洁,接近自然语言。
网络地址inet的筛选
sql
-- 精确匹配 IP
SELECT * FROM file_details WHERE uploader_ip = '127.0.0.1';
-- 范围匹配:查找属于 172.16.0.0/16 网段的所有上传记录
SELECT file_name, uploader_ip FROM file_details
WHERE uploader_ip << '172.16.0.0/16';
排序和分页
sql
-- 按创建时间倒序排列,取前 2 条
SELECT file_name, created_at
FROM file_details
ORDER BY created_at DESC
LIMIT 2 OFFSET 0;
聚合统计
sql
-- 统计每个 IP 上传了多少次,并计算每个 IP 上传文件包含的总标签数
-- 使用 unnest 把数组展开才能计数
SELECT
uploader_ip,
COUNT(*) AS upload_count,
SUM(array_length(tags, 1)) AS total_tags
FROM file_details
GROUP BY uploader_ip;
数据转换与格式化
sql
-- 格式化输出:ID 前 8 位 + 文件名 + 简写日期
SELECT
left(detail_id::text, 8) AS short_id,
file_name,
to_char(created_at, 'YYYY-MM-DD') AS simple_date
FROM file_details;
常用函数
我们可以把常用的函数分为四大类:字符串处理 、数值计算 、日期时间 以及聚合处理。
字符串处理函数(清洗数据的利器)
当你的文件路径、用户名格式不统一时,这些函数能帮大忙。
CONCAT(a, b)/||:拼接字符串。UPPER()/LOWER():转换大小写。SUBSTR(string, start, len):截取字符串。REPLACE(string, from, to):替换内容。COALESCE(value, default):【超级常用】 如果字段是 NULL,就给它一个默认值。
数值计算函数
ROUND(numeric, 2):四舍五入。CEIL()/FLOOR():向上/向下取整。ABS():取绝对值。
日期时间函数(时间管理的灵魂)
NOW():获取当前完整时间。CURRENT_DATE:获取当前日期。AGE(timestamp):计算时间差。
sql
-- 看看这个文件上传了多久了
SET TIME ZONE 'Asia/Shanghai';
SELECT file_name, AGE(created_at) AS active_time FROM file_details;
EXTRACT(field FROM source):提取年、月、日、小时。
sql
-- 统计每个小时的上传量
SELECT EXTRACT(HOUR FROM created_at) AS hour, COUNT(*)
FROM file_details GROUP BY hour;
TO_CHAR():将时间格式化为字符串。
sql
-- 变成类似 2026-04-14 14:30:00 的格式
SELECT TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI:SS') FROM file_details;
流程控制与聚合函数
CASE WHEN:SQL 里的if-else。
sql
-- 给文件分等级
SELECT file_name,
CASE
WHEN file_size > 1024*1024*100 THEN '大文件'
WHEN file_size > 1024*1024*10 THEN '中文件'
ELSE '小文件'
END AS file_level
FROM files;
STRING_AGG(field, delimiter):【PG特色】 把多行结果合并成一行字符串。
sql
-- 把某个用户的所有标签拼成一个逗号分隔的字符串
SELECT uploader_ip, STRING_AGG(tags::text, ' | ') FROM file_details GROUP BY uploader_ip;
或者
sql
SELECT
uploader_ip,
-- 先把所有数组合并成一个大数组,再转为字符串
array_to_string(array_agg(unnest_tags), ', ')
FROM (
SELECT uploader_ip, unnest(tags) AS unnest_tags FROM file_details
) t
GROUP BY uploader_ip;
多表关系
一对一关系
表 A 的一行只能对应表 B 的一行。通常用于将"核心高频数据"与"扩展低频数据"分离,提高查询效率
示例:用户 (Users) 与 用户详细信息 (User_Profiles)
在用户系统中,用户的账号密码是核心,而个人介绍、头像 URL、收货地址属于扩展信息。
sql
-- 主表:核心账户
CREATE TABLE users (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
username TEXT NOT NULL UNIQUE
);
-- 扩展表:详细资料
CREATE TABLE user_profiles (
user_id BIGINT PRIMARY KEY, -- 既是主键,也是外键
avatar_url TEXT,
bio TEXT,
CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
实现要点 :在子表中,将外键字段设为PRIMARY KEY或UNIQUE,确保一个用户 ID 只能在 Profile 表里出现一次。
ON DELETE CASCADE(定义"连坐"逻辑) ------最核心部分
这是定义当**主表(users)**的数据被删除时,**从表(user_profiles)**该怎么办。
CASCADE** (级联)**:就像多米诺骨牌。如果 ID 为 1 的用户被删除了,数据库会自动、静默地把user_profiles里所有user_id = 1的行也删掉。- 为什么用它:在"一对一"或"一对多"关系中,如果主体(用户)都不存在了,相关的详细资料或头像信息也就没有存在的意义了。使用级联可以防止数据库堆积垃圾数据。
除了CASCADE,你有时也会看到这些写法:
| 选项 | 效果 | 适用场景 |
|---|---|---|
RESTRICT (默认) |
如果用户还有资料,就不准删这个用户。 | 保护重要数据,防止误删。 |
SET NULL |
用户删了,资料表里的user_id变成NULL。 |
用户注销了,但你想保留他的评论或痕迹。 |
NO ACTION |
与 RESTRICT 类似,但在事务结束时检查。 | 复杂的事务处理。 |
插入数据
sql
-- 第一步:插入核心用户
-- 我们利用 RETURNING 直接获取系统生成的自增 ID
INSERT INTO users (username)
VALUES ('fengfeng')
RETURNING id;
-- 假设上面返回的 ID 是 1
-- 第二步:插入对应的详细资料
INSERT INTO user_profiles (user_id, avatar_url, bio)
VALUES (1, 'https://example.com/avatar.png', '技术教育者,B站UP主');
-- 尝试再次为 ID 为 1 的用户插入资料(会报错!)
-- 报错信息:duplicate key value violates unique constraint "user_profiles_pkey"
-- 这正是我们设计成 PRIMARY KEY 的目的,保证了一对一的严格性。
查询数据
正向联查和反向
sql
select username, avatar_url from users join user_profiles up on users.id = up.user_id;
select avatar_url, username from user_profiles left join users u on u.id = user_profiles.user_id;
删除数据
设置CASCADE之后,删除用户之后,关联的用户详情也会删除
外键:一般业务里面都会去掉外键,使用逻辑外键
一对多关系
概念: 表 A 的一行可以对应表 B 的多行,但表 B 的一行只能属于表 A 的一个实体。
示例:女神与舔狗
一位"女神"可以拥有成千上万名"舔狗",但一名"舔狗"在特定的一段时间内(业务逻辑上)往往只能死心塌地给一位"女神"打钱。
sql
-- 1. 女神表 (一)
CREATE TABLE goddesses (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
star_sign TEXT -- 星座
);
-- 2. 舔狗表 (多)
CREATE TABLE simps (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
target_id BIGINT NOT NULL, -- 目标女神的ID (外键)
nickname TEXT NOT NULL, -- 舔狗昵称
contributed_amount DECIMAL(10, 2) DEFAULT 0, -- 累计贡献(打钱)金额
-- 外键约束:如果女神号注销了,舔狗记录也级联消失
CONSTRAINT fk_goddess
FOREIGN KEY (target_id) REFERENCES goddesses(id) ON DELETE CASCADE
);
- 实现要点:在"多"的一方(simps )添加一个字段存储"一"的一方(goddesses)的主键。
插入数据
sql
-- 插入两位女神
INSERT INTO goddesses (name, star_sign)
VALUES ('林青霞', '天蝎座'), ('王祖贤', '水瓶座')
RETURNING id, name;
-- 假设 林青霞 ID=1, 王祖贤 ID=2
-- 为 林青霞 (ID: 1) 插入忠实粉丝
INSERT INTO simps (target_id, nickname, contributed_amount) VALUES
(1, '阿强', 520.00),
(1, '小明', 1314.00),
(1, '旺财', 9.90);
-- 为 王祖贤 (ID: 2) 插入忠实粉丝
INSERT INTO simps (target_id, nickname, contributed_amount) VALUES
(2, '大壮', 8888.88),
(2, '铁柱', 0.01);
查询
查看每位舔狗正在追谁
sql
SELECT
s.nickname AS 舔狗,
g.name AS 女神,
s.contributed_amount AS 贡献值
FROM simps s
JOIN goddesses g ON s.target_id = g.id
ORDER BY s.contributed_amount DESC;
查看哪位女神收到的"贡献"总额最高
sql
SELECT
g.name AS 女神,
COUNT(s.id) AS 舔狗总数,
SUM(s.contributed_amount) AS 收到总金额
FROM goddesses g
LEFT JOIN simps s ON g.id = s.target_id
GROUP BY g.name
ORDER BY 收到总金额 DESC;
找出贡献金额低于 10 元的舔狗
sql
SELECT nickname, contributed_amount
FROM simps
WHERE contributed_amount < 10.00;
多对多关系
概念: 表 A 的一行对应表 B 的多行,反之亦然。
示例:学生 (Students) 与 社团 (Clubs)。
- 一个学生可以参加多个社团(比如既参加羽毛球社,又参加代码狂魔社)。
- 一个社团也可以拥有很多名学生。
- 这种关系无法在任何一张表里通过加一个字段来解决,必须有一个"中间人"来牵线搭桥。
sql
-- 1. 学生表 (核心实体 A)
CREATE TABLE students (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL
);
-- 2. 社团表 (核心实体 B)
CREATE TABLE clubs (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
club_name TEXT NOT NULL UNIQUE,
description TEXT
);
-- 3. 成员关系表 (中间桥梁)
-- 这张表记录了"谁"参加了"哪个"社团
CREATE TABLE memberships (
student_id BIGINT REFERENCES students(id) ON DELETE CASCADE,
club_id BIGINT REFERENCES clubs(id) ON DELETE CASCADE,
joined_at TIMESTAMPTZ DEFAULT NOW(), -- 甚至可以记录入社时间
PRIMARY KEY (student_id, club_id) -- 联合主键:保证一个学生不会在同一个社团里加两次
);
插入数据
sql
-- 插入学生
INSERT INTO students (name) VALUES ('枫枫'), ('小红'), ('老王') RETURNING id;
-- 假设 ID 分别是 1, 2, 3
-- 插入社团
INSERT INTO clubs (club_name, description) VALUES
('Go语言社', '高性能后端开发探讨'),
('篮球社', '只因你太美'),
('钓鱼社', '永不空军')
RETURNING id;
-- 假设 ID 分别是 1, 2, 3
-- 建立关系 (多对多)
INSERT INTO memberships (student_id, club_id) VALUES
(1, 1), -- 枫枫 参加了 Go语言社
(1, 2), -- 枫枫 参加了 篮球社
(2, 2), -- 小红 参加了 篮球社
(3, 1), -- 老王 参加了 Go语言社
(3, 3); -- 老王 参加了 钓鱼社
查询
查成员:想看"Go语言社"里都有哪些大神?
由于信息隔了两层,我们需要两次JOIN跳过去。
sql
SELECT
c.club_name AS 社团名,
s.name AS 成员名,
m.joined_at AS 入社时间
FROM clubs c
JOIN memberships m ON c.id = m.club_id
JOIN students s ON m.student_id = s.id
WHERE c.club_name = 'Go语言社';
查轨迹:想看"枫枫"同学一共参加了多少个社团?
sql
SELECT
s.name AS 学生名,
COUNT(m.club_id) AS 参加社团数,
string_agg(c.club_name, ', ') AS 社团清单 -- 把社团名拼成字符串
FROM students s
LEFT JOIN memberships m ON s.id = m.student_id
LEFT JOIN clubs c ON m.club_id = c.id
WHERE s.name = '枫枫'
GROUP BY s.name;
查热度:哪个社团人最多?
sql
SELECT
c.club_name,
COUNT(m.student_id) AS 总人数
FROM clubs c
LEFT JOIN memberships m ON c.id = m.club_id
GROUP BY c.club_name
ORDER BY 总人数 DESC;
索引
小时候我们查字典,如果不知道这个字大概读什么,可能就得一页一页翻字典
如果知道这个字的大概读音,翻字典的时候就直接到那一片区域去找
为什么需要索引
想象你有一张files表,里面存了 1000 万行数据。如果你执行:SELECT * FROM files WHERE file_name = '秘密文件.zip';如果没有索引,数据库得从第 1 行扫到第 1000 万行(全表扫描),你的硬盘灯会闪个不停,用户等到天荒地老。
场景:你的"枫枫网盘"用户量激增,查询文件名变得极慢。
sql
-- 1. 创建演示表
CREATE TABLE cloud_files (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
file_name TEXT NOT NULL,
file_type VARCHAR(20),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 2. 插入 100 万条模拟数据(演示时可以用这个快速生成)
INSERT INTO cloud_files (file_name, file_type)
SELECT
'file_' || i || '.pdf',
(ARRAY['video', 'image', 'doc'])[floor(random()*3)+1]
FROM generate_series(1, 1000000) s(i);
-- 3. 【未加索引前】查询耗时
EXPLAIN ANALYZE SELECT * FROM cloud_files WHERE file_name = 'file_999999.pdf';
-- 你会看到 "Seq Scan" (全表扫描),耗时可能在几十毫秒
创建索引
sql
-- 给文件名加上 B-Tree 索引(最常用的平衡树索引)
CREATE INDEX idx_file_name ON cloud_files(file_name);
-- 再次查询
EXPLAIN ANALYZE SELECT * FROM cloud_files WHERE file_name = 'file_999999.pdf';
-- 你会发现变成了 "Index Scan",速度提升几百倍!
其他类型的索引
sql
-- 显式指定或默认不写都是 B-Tree
CREATE INDEX idx_file_name ON cloud_files USING btree (file_name);
-- 仅适用于 = 查询
CREATE INDEX idx_file_hash ON cloud_files USING hash (file_name);
-- 注意字段顺序:(user_id, file_type) 遵循最左匹配原则
CREATE INDEX idx_user_file_type ON cloud_files (user_id, file_type);
-- 如果你的网盘支持"标签查询"或者存储了 JSON 格式的元数据,GIN 索引是神器。
-- 假设有一个 tags 字段(类型为 TEXT[])
CREATE INDEX idx_files_tags ON cloud_files USING gin (tags);
-- 假设有一个 meta_data 字段(类型为 JSONB)
CREATE INDEX idx_files_meta ON cloud_files USING gin (meta_data jsonb_path_ops);
-- 唯一索引
CREATE UNIQUE INDEX idx_file_md5 ON cloud_files (file_md5);
核心底层:为什么 B-Tree 这么快?
CREATE INDEX默认通常是B-Tree 索引 。它的核心逻辑是平衡多路查找树。
- **对数级增长:**在 1000 万行数据中,全表扫描需要 O(n) 的复杂度(看 1000 万次);而 B-Tree 索引只需要logm(n)logm(n) 的复杂度。
- 实际体感: 哪怕数据量翻了 10 倍达到 1 亿行,B-Tree 的树高度可能只增加了 1 层,查询耗时几乎没有明显变化。

索引的"代价":
索引虽好,但不能全表所有字段都加。每一条索引都会带来以下开销:
- 磁盘空间: 索引本身是一棵树,需要占用额外的存储空间。
- 写入性能:当你执行 **
INSERT、UPDATE或DELETE****时,数据库不仅要改数据,还要**同步更新索引树。索引越多,写操作越慢。 - **维护成本:**随着数据频繁变动,索引可能会产生碎片,偶尔需要
REINDEX(重建索引)来恢复性能。
这些情况下索引会"失效"
很多新手加了索引发现查询还是慢,通常是因为触发了索引失效:
| 场景 | 错误示例 (会导致 Seq Scan) | 正确姿势 |
|---|---|---|
| 模糊查询 | WHERE file_name LIKE '%file_999%'; |
只有"左前缀"匹配file_999% 才能用索引 |
| 函数操作 | WHERE UPPER(file_name) = 'FILE.PDF'; |
建立函数索引或在查询前处理数据 |
| 类型不匹配 | WHERE id = '123'; (id是数字,却传字符串) |
保持类型一致,避免隐式转换 |
| 最左匹配 | 复合索引(a, b)只查b |
复合索引必须从最左侧字段开始匹配 |
事务
用最通俗的话说,事务就是:"要么全做完,要么一点都不做,绝不允许停在半路。"
场景:用户花 100 元买 VIP。如果钱扣了,结果数据库断电了,会员没加上
sql
BEGIN; -- 开启事务,立下"生死状"
-- 1. 扣钱
UPDATE accounts SET balance = balance - 100 WHERE username = '小红';
-- 2. 增加会员时长 (假设这里 SQL 写错了或网络断了)
UPDATE members SET expire_date = expire_date + INTERVAL '1 year' WHERE username = '小红';
-- 检查一下:如果钱扣对了,会员也加了
COMMIT; -- 确认无误,正式写入硬盘
如果中途出错了
sql
ROLLBACK; -- 刚才的所有操作全部撤销,钱会回到小红账上,像没发生过一样
还可以在中途设置存档点
sql
BEGIN; -- 开启事务
UPDATE ...; -- 执行业务逻辑
SAVEPOINT my_sp; -- (可选) 设置保存点,类似于单机游戏的存档
UPDATE ...;
ROLLBACK TO my_sp; -- (可选) 发现错了,回滚到存档点
COMMIT; -- 提交,所有改动生效
ACID
不管是 PG 还是其他数据库,事务都必须遵守ACID 原则:
- A (Atomicity) 原子性:就是你说的"要么全做,要么不做"。
- C (Consistency) 一致性:数据必须从一个合法状态变到另一个合法状态(比如余额不能变成负数)。
- I (Isolation) 隔离性 :重点! 多个用户同时操作时,他们之间不能互相干扰。
- D (Durability) 持久性 :只要你点了
COMMIT,就算立马拔掉电源,数据也已经写死在硬盘里了。
锁
在 PostgreSQL 中,锁是确保数据库并发安全(Consistency & Isolation)的核心机制。简单来说,它防止了多个用户同时修改同一条数据而导致的混乱。
PostgreSQL 的锁体系非常庞大,主要可以分为以下三个层次:
1. 表级锁 (Table-Level Locks)
即使你只是对表进行简单的SELECT或UPDATE,PostgreSQL 也会自动加上表级锁。
- 常见的锁模式:
- Access Share (访问共享锁):
SELECT时自动获取,允许其他事务读写,但不允许修改表结构。 - Row Exclusive (行排他锁):
INSERT、UPDATE、DELETE时自动获取。 - Share (共享锁): 常用于创建索引,允许读,禁止改。
- **Access Exclusive (访问排他锁):**最强锁。
ALTER TABLE、DROP TABLE、TRUNCATE时触发。它会阻塞一切操作(包括 SELECT)。
- Access Share (访问共享锁):
2. 行级锁 (Row-Level Locks)
行级锁主要用于处理具体的数据记录。它们不在内存中维护列表,而是直接在数据页中标记。
- FOR SHARE: 保证你读取的行不会被别人修改或删除,直到你提交事务。
- **FOR UPDATE:**锁定行以进行更新。其他事务无法对这些行进行
UPDATE、DELETE或SELECT FOR UPDATE。 - FOR NO KEY UPDATE / FOR KEY SHARE: 更细粒度的锁,主要为了优化外键关联时的并发性能。
3. 建议锁 (Advisory Locks)
这是 PostgreSQL 的一大特色。它不是由数据库引擎自动加的,而是由程序员定义的一种逻辑锁。
- 用途: 比如你想锁定"发送邮件"这个动作,或者确保某个后台任务在同一时间只有一个实例在运行,就可以用建议锁。
- 特点: 它不锁定具体的表或行,只是在内存里占一个"标识位"。
如何使用
自动锁定
绝大多数情况下,你不需要手动加锁。
- 执行
UPDATE users SET name = 'Gemini' WHERE id = 1;时,数据库会自动给该行加排他锁。
手动锁定表
如果你需要进行大面积维护,不希望别人干扰:
sql
BEGIN;
-- 需要把这个锁放到事务里面
LOCK TABLE my_table IN ACCESS EXCLUSIVE MODE; -- 彻底锁死表
-- 执行操作
COMMIT;
手动锁定行(解决"超卖"等问题)
这是处理高并发业务(如扣减库存)时的标准写法:
sql
BEGIN;
-- 锁定 id 为 100 的行,别人必须等我处理完
SELECT * FROM products WHERE id = 100 FOR UPDATE;
UPDATE products SET stock = stock - 1 WHERE id = 100;
COMMIT;
别的连接只能进行普通的select查询,不能进行更新或者删除操作
使用建议锁
sql
-- 获取一个编号为 12345 的逻辑锁
SELECT pg_advisory_lock(12345);
-- 执行你的业务逻辑...
-- 释放锁
SELECT pg_advisory_unlock(12345);
如何查看当前的锁?
如果发现数据库"卡住了",可以用这张表查谁在锁谁:
sql
SELECT * FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid;
CTE
我们在写sql的时候,如果sql条件有那种嵌套的
sql
SELECT * FROM (
SELECT * FROM (
SELECT ... -- 这里的代码已经缩进到太平洋了
) AS inner_data
) AS outer_data;
这个sql看起来就很不直观了,我们可以里面的sql抽离成一个变量
比如枫枫网盘需要统计:"每个用户上传的文件总大小,并筛选出超过 1GB 的土豪用户"。
sql
-- 使用 CTE 让代码逻辑清晰
WITH user_file_stats AS (
-- 第一步:先把每个人的总和算出来,起个名字叫 user_file_stats
SELECT
u.username,
SUM(f.file_size) as total_size,
COUNT(f.id) as file_count
FROM users u
LEFT JOIN cloud_files f ON u.id = f.user_id
GROUP BY u.username
)
-- 第二步:直接像查表一样查这个"变量"
SELECT * FROM user_file_stats
WHERE total_size > 1024 * 1024 * 1024; -- 筛选超过 1GB 的人
视图
视图是一张"虚拟表"。它不存储实际数据,只存储一条查询语句。当你查询视图时,它会跑一遍背后的 SQL。
如果你经常需要通过JOIN关联用户、文件、存储节点这三张表来查看"文件完整路径",每次写 10 行 SQL 很累。
sql
-- 1. 创建视图:一次封装,永久受益
CREATE VIEW v_file_detail_full AS
SELECT
f.id,
f.file_name,
u.username AS owner_name,
n.node_name AS storage_location,
f.created_at
FROM cloud_files f
JOIN users u ON f.user_id = u.id
JOIN storage_nodes n ON f.node_id = n.id;
-- 2. 以后查起来就像查单表一样爽
SELECT * FROM v_file_detail_full WHERE owner_name = 'fengfeng';
视图 vs CTE:怎么选择
| 特性 | CTE (WITH) | 视图 (View) |
|---|---|---|
| 生命周期 | 只在当前这条 SQL 里有效 | 永久保存在数据库里 |
| 类比 | 局部变量 | 全局公共函数 |
| 适用场景 | 仅仅为了让这一行长查询变好看 | 经常要用的复杂查询、报表统计 |
| 权限控制 | 无 | 非常强大(可以给用户看视图,但不给他看原表) |
用户和角色
场景:你现在是 DBA,你要给前端开发人员创建一个账号,只能看数据,不能删库。
sql
-- 1. 创建一个组(不准登录)
CREATE ROLE managers;
-- 2. 创建一个用户(准许登录)
CREATE USER fengfeng WITH PASSWORD 'fengfeng';
-- 3. 让"用户"加入"组"
GRANT managers TO fengfeng;
查看我有哪些角色 \du
sql
SELECT
rolname AS 角色名,
rolcanlogin AS 能否登录,
rolsuper AS 是否超级管理员,
rolcreatedb AS 能否建库
FROM pg_roles
WHERE rolname NOT LIKE 'pg_%'; -- 过滤掉所有以 pg_ 开头的系统角色
现在这个用户,可以登录,可以看到有哪些库,有哪些表,但是不能创建库,创建表,查询表数据
示例1:给用户设置某一个库的只读权限
sql
-- 1. 创建用户
CREATE USER reader_user WITH PASSWORD 'fengfeng';
-- 2. 基础进门权
GRANT CONNECT ON DATABASE db TO reader_user; -- 可以进入db这个库
GRANT USAGE ON SCHEMA public TO reader_user; -- 可以进入public这个模式
-- 3. 核心只读权(现在的表 + 序列)
GRANT SELECT ON ALL TABLES IN SCHEMA public TO reader_user;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO reader_user;
-- 4. 自动化(未来的表)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO reader_user;
示例2:给用户设置一个库的增删改查权限
sql
CREATE USER feng_app WITH PASSWORD 'fengfeng';
GRANT CONNECT ON DATABASE fengfeng_db TO feng_app;
GRANT USAGE ON SCHEMA public TO feng_app;
-- 1. 针对表:赋予全部增删改查权限
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO feng_app;
-- 2. 针对序列:必须给权限!否则无法执行自增 ID 的插入
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO feng_app;
-- 后续新创建的表也拥有权限
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO feng_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO feng_app;
组管权限,用户管登录,用户进组
逻辑备份 (导出的 SQL 文件,易读、跨版本好用)和物理备份(直接拷贝底层文件,快但不易读)
pg_dump单库备份
pg_dump是一个命令行工具,它会把数据库里的表结构、数据、序列、权限等全部变成一段长长的 SQL 脚本。
打开你电脑的终端(CMD 或 PowerShell),不需要进入 psql。
sql
# 格式:pg_dump -U [用户名] -d [数据库名] > [备份文件名.sql]
pg_dump -U postgres -d db > db_0414.sql
-U: 指定用户名(通常用超级用户 postgres)。-t: 只备份某张表(例如:-t cloud_files)。-n: 只备份某个模式(例如:-n storage)。-F: 指定格式。-Fc会生成自定义二进制压缩格式,体积更小,且支持并行恢复(推荐!)。
sql
# 备份为压缩的二进制格式
pg_dump -U postgres -F c -d db > db_0414.bak
大小差距还是很大的

恢复
恢复的方式取决于你备份时的格式。
- 恢复纯文本 SQL 备份
如果你备份的是.sql文件,直接用psql 运行它即可
sql
# 格式:psql -U [用户名] -d [目标数据库] < [备份文件]
psql -U postgres -d db1 < db_0414.sql
# 这个库要先创建好
- 恢复二进制备份 (pg_restore)
如果你用了-Fc压缩格式,必须用专属的pg_restore 工具:
pg_restore -U postgres -d db2 -c db_0414.bak
- -c 代表先清理(删除)旧表再恢复,防止冲突
pg_dumpall全库备份
如果你想把整个 PostgreSQL 实例里的所有数据库 、所有的用户和权限(Role)一次性全带走,就要用这个:
set PGPASSWORD=root # 不然就会导一个数据库输一次密码
pg_dumpall -U postgres > all_databases_backup.sql
# 格式:psql -U [用户名] -f [备份文件路径] [数据库名]
psql -U postgres -f all_databases_backup.sql postgres
-f: 代表 file,即指定要运行的 SQL 脚本文件。postgres** (最后的参数)**:这是连接的初始数据库。因为pg_dumpall脚本内部包含了CREATE DATABASE和\c(切换库) 的命令,所以你只需要先连上一个默认存在的库(比如postgres),它会自动帮你创建并切换到其他的库。