技术备忘 SQLPostgreSQL数据库

前言

数据库是后端开发的基石。无论用 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 / JSONBJSON 数据{"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 执行得更早。

基本查询

sql
SELECT column1, column2
FROM table_name
WHERE condition
ORDER BY column1 DESC
LIMIT 10;
子句作用
SELECT指定要返回的列
FROM指定数据来源表
WHERE行级过滤
GROUP BY分组聚合
HAVING组级过滤(GROUP BY 之后)
ORDER BY排序
LIMIT / OFFSET分页
DISTINCT去重

JOIN 类型

sql
-- INNER 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;
-- 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,可读性更好。

聚合函数

sql
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_price
FROM products;
函数作用
COUNT(*)统计行数
COUNT(DISTINCT col)统计不重复值的数量
SUM(col)求和
AVG(col)平均值
MIN(col)最小值
MAX(col)最大值

GROUP BY 与 HAVING

sql
-- 每个分类的商品数量
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category;
-- 筛选出商品数大于 10 的分类
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category
HAVING COUNT(*) > 10;
NOTE

WHERE 在 GROUP BY 之前过滤行,HAVING 在 GROUP BY 之后过滤组。两者作用不同,不矛盾。

子查询

sql
-- 标量子查询(返回单个值)
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);
-- 行子查询(IN)
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE status = 'paid');
-- EXISTS 子查询
SELECT * FROM categories c
WHERE EXISTS (
SELECT 1 FROM products p WHERE p.category_id = c.id
);

窗口函数(Window Functions)

窗口函数在不改变行数的情况下,对每一行计算一个聚合值或排名:

sql
-- ROW_NUMBER:行号
SELECT
name,
price,
ROW_NUMBER() OVER (ORDER BY price DESC) AS rank
FROM products;
-- 分组排名
SELECT
category,
name,
price,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS rank_in_category
FROM 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_change
FROM daily_revenue;
函数作用
ROW_NUMBER()行号
RANK()排名(并列会跳号)
DENSE_RANK()排名(并列不跳号)
LAG(col, n)取前 n 行的值
LEAD(col, n)取后 n 行的值
FIRST_VALUE(col)窗口内第一个值
SUM(col) OVER (...)累积求和

公共表表达式(CTE)

sql
-- 普通 CTE
WITH 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大部分查询默认类型,支持 =><BETWEENLIKE 'abc%'
Hash等值查询仅支持 =,很少用
GIN全文搜索、JSONB、数组倒排索引
GiST地理数据、全文搜索支持范围查询和邻近搜索
BRIN超大表且数据天然有序按数据块统计,体积很小
sql
-- 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),以下查询能用到索引:

sql
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 能迅速缩小数据范围。

如何分析查询性能

sql
-- 查看查询计划
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(持久性)提交后数据永久保存

事务控制

sql
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
sql
-- 设置当前事务的隔离级别
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
异常说明
脏读读到另一个事务未提交的数据(那个事务可能回滚)
不可重复读同一事务内两次读同一行,结果不同(被其他事务修改并提交)
幻读同一事务内两次范围查询,行数不同(其他事务插入/删除了行)

表设计

范式 vs 反范式

优点缺点
规范化数据一致性好,无冗余,更新方便查询时需要多表 JOIN,可能慢
反规范化查询快,不用 JOIN冗余数据,更新时需同步多处

实际项目中通常混合使用:核心业务数据严格遵循范式,报表/读多写少的数据适度反范式。

常用约束

sql
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默认值

外键

sql
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 模式

分页

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 articles
WHERE created_at < '2024-09-15 10:30:00'
ORDER BY created_at DESC LIMIT 20;

插入或更新(UPSERT)

sql
-- PostgreSQL
INSERT INTO users (id, email, nickname)
VALUES ('abc', 'test@example.com', 'Test')
ON CONFLICT (id) DO UPDATE
SET email = EXCLUDED.email,
nickname = EXCLUDED.nickname;
-- MySQL
INSERT INTO users (id, email, nickname)
VALUES ('abc', 'test@example.com', 'Test')
ON DUPLICATE KEY UPDATE
email = VALUES(email),
nickname = VALUES(nickname);

批量插入

sql
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 后再也不会盲目建索引了。


参考来源