前言
数据库是后端开发的基石。无论用 MySQL 还是 PostgreSQL,核心的 SQL 技能和数据库设计思路是相通的。这篇文章整理了日常开发中最常用的数据库知识和 SQL 写法。
关系模型基础
核心概念
| 术语 | 说明 |
|---|
| 表(Table) | 数据的集合,类似 Excel 表格 |
| 行 / 记录(Row) | 表中的一条数据 |
| 列 / 字段(Column) | 表中的一个属性 |
| 主键(Primary Key) | 唯一标识一条记录的字段或字段组合 |
| 外键(Foreign Key) | 指向另一张表的主键,建立表间关联 |
| 索引(Index) | 加速查询的数据结构 |
| 视图(View) | 虚拟表,基于 SQL 查询结果 |
| 事务(Transaction) | 一组原子性的数据库操作 |
常见数据类型
| 类型 | 说明 | 示例 |
|---|
INTEGER / INT | 整数 | 42 |
BIGINT | 大整数 | 9007199254740991 |
VARCHAR(n) | 可变长度字符串 | VARCHAR(255) |
TEXT | 长文本 | 无长度限制 |
BOOLEAN | 布尔值 | TRUE / FALSE |
DATE | 日期 | 2024-09-15 |
TIMESTAMP | 日期+时间 | 2024-09-15 10:30:00 |
NUMERIC(p,s) | 精确小数 | NUMERIC(10,2) |
UUID | 通用唯一标识符 | a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 |
JSON / JSONB | JSON 数据 | {"name": "Alice"} |
SQL 查询
SELECT 执行顺序
SQL 写的顺序和实际执行顺序不同:
写:SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT
执行:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
理解执行顺序很重要——例如 WHERE 中不能用 SELECT 中定义的别名,因为 WHERE 执行得更早。
基本查询
| 子句 | 作用 |
|---|
SELECT | 指定要返回的列 |
FROM | 指定数据来源表 |
WHERE | 行级过滤 |
GROUP BY | 分组聚合 |
HAVING | 组级过滤(GROUP BY 之后) |
ORDER BY | 排序 |
LIMIT / OFFSET | 分页 |
DISTINCT | 去重 |
JOIN 类型
SELECT * FROM a INNER JOIN b ON a.id = b.a_id;
-- LEFT JOIN:左表全部记录,右表没有的为 NULL
SELECT * FROM a LEFT JOIN b ON a.id = b.a_id;
SELECT * FROM a RIGHT JOIN b ON a.id = b.a_id;
SELECT * FROM a FULL JOIN b ON a.id = b.a_id;
SELECT * FROM a CROSS JOIN b;
| JOIN 类型 | 结果 |
|---|
INNER JOIN | 两表都匹配的行 |
LEFT JOIN | 左表全部 + 右表匹配的行(无匹配则 NULL) |
RIGHT JOIN | 右表全部 + 左表匹配的行 |
FULL JOIN | 两表的并集 |
CROSS JOIN | 每一行与另一表每一行组合 |
注意
RIGHT JOIN 基本上可以用 LEFT JOIN 交换表顺序实现。建议保持一致用 LEFT JOIN,可读性更好。
聚合函数
COUNT(DISTINCT category) AS categories,
SUM(quantity) AS total_quantity,
| 函数 | 作用 |
|---|
COUNT(*) | 统计行数 |
COUNT(DISTINCT col) | 统计不重复值的数量 |
SUM(col) | 求和 |
AVG(col) | 平均值 |
MIN(col) | 最小值 |
MAX(col) | 最大值 |
GROUP BY 与 HAVING
SELECT category, COUNT(*) AS product_count
SELECT category, COUNT(*) AS product_count
注意
WHERE 在 GROUP BY 之前过滤行,HAVING 在 GROUP BY 之后过滤组。两者作用不同,不矛盾。
子查询
WHERE price > (SELECT AVG(price) FROM products);
WHERE id IN (SELECT user_id FROM orders WHERE status = 'paid');
SELECT * FROM categories c
SELECT 1 FROM products p WHERE p.category_id = c.id
窗口函数(Window Functions)
窗口函数在不改变行数的情况下,对每一行计算一个聚合值或排名:
ROW_NUMBER() OVER (ORDER BY price DESC) AS rank
ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS rank_in_category
LAG(revenue) OVER (ORDER BY date) AS prev_day_revenue,
revenue - LAG(revenue) OVER (ORDER BY date) AS daily_change
| 函数 | 作用 |
|---|
ROW_NUMBER() | 行号 |
RANK() | 排名(并列会跳号) |
DENSE_RANK() | 排名(并列不跳号) |
LAG(col, n) | 取前 n 行的值 |
LEAD(col, n) | 取后 n 行的值 |
FIRST_VALUE(col) | 窗口内第一个值 |
SUM(col) OVER (...) | 累积求和 |
公共表表达式(CTE)
SELECT * FROM users WHERE last_login > NOW() - INTERVAL '30 days'
SELECT * FROM active_users WHERE plan = 'premium';
WITH RECURSIVE org_tree AS (
SELECT id, name, parent_id, 1 AS level
FROM employees WHERE parent_id IS NULL
SELECT e.id, e.name, e.parent_id, t.level + 1
JOIN org_tree t ON e.parent_id = t.id
SELECT * FROM org_tree ORDER BY level, name;
索引
索引是提高查询性能最直接的手段。但索引不是免费的——它会占用磁盘空间,并降低写入(INSERT/UPDATE/DELETE)速度。
索引类型
| 索引类型 | 适用场景 | 说明 |
|---|
| B-Tree | 大部分查询 | 默认类型,支持 =、>、<、BETWEEN、LIKE 'abc%' |
| Hash | 等值查询 | 仅支持 =,很少用 |
| GIN | 全文搜索、JSONB、数组 | 倒排索引 |
| GiST | 地理数据、全文搜索 | 支持范围查询和邻近搜索 |
| BRIN | 超大表且数据天然有序 | 按数据块统计,体积很小 |
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
CREATE INDEX idx_active_orders ON orders(status)
WHERE status = 'pending';
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
复合索引的最左前缀原则
对于复合索引 (a, b, c),以下查询能用到索引:
WHERE a = 1 AND b = 2 -- ✅
WHERE a = 1 AND b = 2 AND c = 3 -- ✅
WHERE a = 1 AND c = 3 -- ✅ 只用到了 a 列
注意
建复合索引时,把区分度最高的列放在最前面。(user_id, created_at) 通常比 (created_at, user_id) 更有效——因为先过滤用户 ID 能迅速缩小数据范围。
如何分析查询性能
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
执行计划中需要关注的关键信息:
| 指标 | 含义 |
|---|
Seq Scan | 全表扫描(通常意味着没走索引) |
Index Scan | 索引扫描 |
Index Only Scan | 覆盖索引扫描(只读索引,不回表) |
rows | 预计扫描行数 |
actual time | 实际耗时 |
cost | 成本估计 |
事务(Transaction)
ACID 特性
| 特性 | 含义 |
|---|
| Atomicity(原子性) | 事务中的所有操作要么全部成功,要么全部回滚 |
| Consistency(一致性) | 事务前后数据保持完整性约束 |
| Isolation(隔离性) | 并发事务之间互不干扰 |
| Durability(持久性) | 提交后数据永久保存 |
事务控制
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|
| Read Uncommitted | 可能 | 可能 | 可能 |
| Read Committed(默认) | — | 可能 | 可能 |
| Repeatable Read | — | — | 可能 |
| Serializable | — | — | — |
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
| 异常 | 说明 |
|---|
| 脏读 | 读到另一个事务未提交的数据(那个事务可能回滚) |
| 不可重复读 | 同一事务内两次读同一行,结果不同(被其他事务修改并提交) |
| 幻读 | 同一事务内两次范围查询,行数不同(其他事务插入/删除了行) |
表设计
范式 vs 反范式
| 优点 | 缺点 |
|---|
| 规范化 | 数据一致性好,无冗余,更新方便 | 查询时需要多表 JOIN,可能慢 |
| 反规范化 | 查询快,不用 JOIN | 冗余数据,更新时需同步多处 |
实际项目中通常混合使用:核心业务数据严格遵循范式,报表/读多写少的数据适度反范式。
常用约束
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
nickname VARCHAR(100) NOT NULL,
age INTEGER CHECK (age >= 0 AND age < 150),
plan VARCHAR(20) DEFAULT 'free',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
| 约束 | 作用 |
|---|
PRIMARY KEY | 主键,唯一且非空 |
FOREIGN KEY | 外键,保证引用完整性 |
NOT NULL | 不允许为空 |
UNIQUE | 唯一 |
CHECK | 值满足条件 |
DEFAULT | 默认值 |
外键
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
amount NUMERIC(10,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
CREATE INDEX idx_orders_user_id ON orders(user_id);
注意
外键在写入操作频繁且数据量很大的场景下可能成为性能瓶颈。有些团队选择在应用层维护引用关系,而不用数据库外键。但对数据一致性要求高的场景,外键仍是有力的保障。
常用 SQL 模式
分页
-- 传统分页(OFFSET 在数据量大时性能差)
SELECT * FROM articles ORDER BY created_at DESC LIMIT 20 OFFSET 0;
SELECT * FROM articles ORDER BY created_at DESC LIMIT 20 OFFSET 20;
SELECT * FROM articles ORDER BY created_at DESC LIMIT 20;
-- 取最后一条的 created_at 作为游标
WHERE created_at < '2024-09-15 10:30:00'
ORDER BY created_at DESC LIMIT 20;
插入或更新(UPSERT)
INSERT INTO users (id, email, nickname)
VALUES ('abc', 'test@example.com', 'Test')
ON CONFLICT (id) DO UPDATE
SET email = EXCLUDED.email,
nickname = EXCLUDED.nickname;
INSERT INTO users (id, email, nickname)
VALUES ('abc', 'test@example.com', 'Test')
nickname = VALUES(nickname);
批量插入
INSERT INTO users (email, nickname) VALUES
('alice@example.com', 'Alice'),
('bob@example.com', 'Bob'),
('carol@example.com', 'Carol');
学习路线图
总结
| 知识点 | 要点 |
|---|
| 查询 | SELECT 执行顺序、JOIN 类型、GROUP BY + HAVING |
| 高级查询 | 窗口函数、CTE、递归 CTE、子查询 |
| 索引 | B-Tree 为主、最左前缀、复合索引、EXPLAIN 分析 |
| 事务 | ACID、4 种隔离级别、脏读/不可重复读/幻读 |
| 表设计 | 主键/外键/约束、范式 vs 反范式 |
| 常用模式 | 游标分页、UPSERT、批量插入 |
SELECT * 在生产环境应避免,始终指定需要的列名。用完 EXPLAIN ANALYZE 后再也不会盲目建索引了。
参考来源
评论
GitHub 登录后可评论。
评论区会在滚动到这里时自动加载。