前言
数据库是后端开发的基石。无论用 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 column1, column2FROM table_nameWHERE conditionORDER BY column1 DESCLIMIT 10;| 子句 | 作用 |
|---|---|
SELECT | 指定要返回的列 |
FROM | 指定数据来源表 |
WHERE | 行级过滤 |
GROUP BY | 分组聚合 |
HAVING | 组级过滤(GROUP BY 之后) |
ORDER BY | 排序 |
LIMIT / OFFSET | 分页 |
DISTINCT | 去重 |
JOIN 类型
-- INNER JOIN:两表都有的记录SELECT * FROM a INNER JOIN b ON a.id = b.a_id;
-- LEFT JOIN:左表全部记录,右表没有的为 NULLSELECT * FROM a LEFT JOIN b ON a.id = b.a_id;
-- RIGHT JOIN:右表全部记录SELECT * FROM a RIGHT JOIN b ON a.id = b.a_id;
-- FULL JOIN:两表全部记录SELECT * FROM a FULL JOIN b ON a.id = b.a_id;
-- CROSS JOIN:笛卡尔积SELECT * FROM a CROSS JOIN b;| JOIN 类型 | 结果 |
|---|---|
INNER JOIN | 两表都匹配的行 |
LEFT JOIN | 左表全部 + 右表匹配的行(无匹配则 NULL) |
RIGHT JOIN | 右表全部 + 左表匹配的行 |
FULL JOIN | 两表的并集 |
CROSS JOIN | 每一行与另一表每一行组合 |
NOTE
RIGHT JOIN 基本上可以用 LEFT JOIN 交换表顺序实现。建议保持一致用 LEFT JOIN,可读性更好。
聚合函数
SELECT COUNT(*) AS total, COUNT(DISTINCT category) AS categories, AVG(price) AS avg_price, SUM(quantity) AS total_quantity, MIN(price) AS min_price, MAX(price) AS max_priceFROM products;| 函数 | 作用 |
|---|---|
COUNT(*) | 统计行数 |
COUNT(DISTINCT col) | 统计不重复值的数量 |
SUM(col) | 求和 |
AVG(col) | 平均值 |
MIN(col) | 最小值 |
MAX(col) | 最大值 |
GROUP BY 与 HAVING
-- 每个分类的商品数量SELECT category, COUNT(*) AS product_countFROM productsGROUP BY category;
-- 筛选出商品数大于 10 的分类SELECT category, COUNT(*) AS product_countFROM productsGROUP BY categoryHAVING COUNT(*) > 10;NOTE
WHERE 在 GROUP BY 之前过滤行,HAVING 在 GROUP BY 之后过滤组。两者作用不同,不矛盾。
子查询
-- 标量子查询(返回单个值)SELECT name, priceFROM productsWHERE price > (SELECT AVG(price) FROM products);
-- 行子查询(IN)SELECT * FROM usersWHERE id IN (SELECT user_id FROM orders WHERE status = 'paid');
-- EXISTS 子查询SELECT * FROM categories cWHERE EXISTS ( SELECT 1 FROM products p WHERE p.category_id = c.id);窗口函数(Window Functions)
窗口函数在不改变行数的情况下,对每一行计算一个聚合值或排名:
-- ROW_NUMBER:行号SELECT name, price, ROW_NUMBER() OVER (ORDER BY price DESC) AS rankFROM products;
-- 分组排名SELECT category, name, price, ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS rank_in_categoryFROM products;
-- LAG / LEAD:前后行访问SELECT date, revenue, LAG(revenue) OVER (ORDER BY date) AS prev_day_revenue, revenue - LAG(revenue) OVER (ORDER BY date) AS daily_changeFROM daily_revenue;| 函数 | 作用 |
|---|---|
ROW_NUMBER() | 行号 |
RANK() | 排名(并列会跳号) |
DENSE_RANK() | 排名(并列不跳号) |
LAG(col, n) | 取前 n 行的值 |
LEAD(col, n) | 取后 n 行的值 |
FIRST_VALUE(col) | 窗口内第一个值 |
SUM(col) OVER (...) | 累积求和 |
公共表表达式(CTE)
-- 普通 CTEWITH active_users AS ( SELECT * FROM users WHERE last_login > NOW() - INTERVAL '30 days')SELECT * FROM active_users WHERE plan = 'premium';
-- 递归 CTE(树形结构)WITH RECURSIVE org_tree AS ( -- 根节点 SELECT id, name, parent_id, 1 AS level FROM employees WHERE parent_id IS NULL
UNION ALL
-- 递归子节点 SELECT e.id, e.name, e.parent_id, t.level + 1 FROM employees e 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 | 超大表且数据天然有序 | 按数据块统计,体积很小 |
-- B-Tree 索引(最常用)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 -- ✅WHERE a = 1 AND b = 2 -- ✅WHERE a = 1 AND b = 2 AND c = 3 -- ✅WHERE a = 1 AND c = 3 -- ✅ 只用到了 a 列WHERE b = 2 -- ❌ 不走索引WHERE c = 3 -- ❌ 不走索引NOTE
建复合索引时,把区分度最高的列放在最前面。(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(持久性) | 提交后数据永久保存 |
事务控制
BEGIN; -- 开始事务 UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2;COMMIT; -- 提交
-- 回滚BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1;ROLLBACK; -- 撤销所有改动隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 |
| Read Committed(默认) | — | 可能 | 可能 |
| Repeatable Read | — | — | 可能 |
| Serializable | — | — | — |
-- 设置当前事务的隔离级别SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;| 异常 | 说明 |
|---|---|
| 脏读 | 读到另一个事务未提交的数据(那个事务可能回滚) |
| 不可重复读 | 同一事务内两次读同一行,结果不同(被其他事务修改并提交) |
| 幻读 | 同一事务内两次范围查询,行数不同(其他事务插入/删除了行) |
表设计
范式 vs 反范式
| 优点 | 缺点 | |
|---|---|---|
| 规范化 | 数据一致性好,无冗余,更新方便 | 查询时需要多表 JOIN,可能慢 |
| 反规范化 | 查询快,不用 JOIN | 冗余数据,更新时需同步多处 |
实际项目中通常混合使用:核心业务数据严格遵循范式,报表/读多写少的数据适度反范式。
常用约束
CREATE TABLE users ( 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 | 默认值 |
外键
CREATE TABLE orders ( 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);NOTE
外键在写入操作频繁且数据量很大的场景下可能成为性能瓶颈。有些团队选择在应用层维护引用关系,而不用数据库外键。但对数据一致性要求高的场景,外键仍是有力的保障。
常用 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 作为游标SELECT * FROM articlesWHERE created_at < '2024-09-15 10:30:00'ORDER BY created_at DESC LIMIT 20;插入或更新(UPSERT)
-- PostgreSQLINSERT INTO users (id, email, nickname)VALUES ('abc', 'test@example.com', 'Test')ON CONFLICT (id) DO UPDATESET email = EXCLUDED.email, nickname = EXCLUDED.nickname;
-- MySQLINSERT INTO users (id, email, nickname)VALUES ('abc', 'test@example.com', 'Test')ON DUPLICATE KEY UPDATEemail = VALUES(email),nickname = VALUES(nickname);批量插入
INSERT INTO users (email, nickname) VALUES('alice@example.com', 'Alice'),('bob@example.com', 'Bob'),('carol@example.com', 'Carol');学习路线图
- 理解关系模型和基本数据类型
- 掌握 SELECT / JOIN / WHERE 基本查询
- 学会 GROUP BY 和聚合函数
- 掌握子查询和 CTE 的用法
- 理解索引原理和 EXPLAIN 分析
- 掌握事务和隔离级别
- 能设计合理的数据库表结构
- 了解窗口函数的应用场景
总结
| 知识点 | 要点 |
|---|---|
| 查询 | SELECT 执行顺序、JOIN 类型、GROUP BY + HAVING |
| 高级查询 | 窗口函数、CTE、递归 CTE、子查询 |
| 索引 | B-Tree 为主、最左前缀、复合索引、EXPLAIN 分析 |
| 事务 | ACID、4 种隔离级别、脏读/不可重复读/幻读 |
| 表设计 | 主键/外键/约束、范式 vs 反范式 |
| 常用模式 | 游标分页、UPSERT、批量插入 |
SELECT * 在生产环境应避免,始终指定需要的列名。用完 EXPLAIN ANALYZE 后再也不会盲目建索引了。
评论
GitHub 登录后可评论。
评论区会在滚动到这里时自动加载。