PostgreSQL 练习题(12 题)
学了那么多 SQL 语法,得动手写才算真的会。这一页给 12 道精选练习题,按难度分三级——基础 6 题(SELECT / INSERT / UPDATE / DELETE)、进阶 3 题(GROUP BY 聚合 / JOIN / JSONB)、高级 3 题(窗口函数 / CTE / UPSERT + 事务)。每题含题目描述、提示、可折叠的参考答案。
练习环境:本地 psql 或 DB Fiddle(在线 PostgreSQL 沙盒)。所有题目围绕三张表展开:employees(id, name, dept, salary)、customers(id, name, city) + orders(id, customer_id, amount, created_at)、users(id, name, profile JSONB)。先在本地 CREATE TABLE 灌点数据,再按题写 SQL。
使用建议:先别急着展开答案。读题后自己写,卡住再看提示,实在写不出再对照参考答案。SQL 写法灵活,你的答案可能比参考更好——能跑出正确结果、可读性强就是好 SQL。
基础(6 题):SELECT / INSERT / UPDATE / DELETE
这一组覆盖 SQL 的四大基础操作。目标是熟练"取数、写数、改数、删数"——日常 80% 的数据库操作就是它们。
第 1 题:基础 SELECT
题目:给定 employees(id, name, dept, salary) 表。(1) 查询全部列;(2) 只查姓名和工资;(3) 列出所有不重复的部门。
提示:SELECT * FROM ... 查全列(生产代码避免 *,影响性能和明确性);SELECT col1, col2 FROM ... 查指定列;SELECT DISTINCT col 去重。
参考答案
-- 假设有一张员工表:
-- employees(id, name, dept, salary)
-- 1) 查询全部列
SELECT * FROM employees;
-- 2) 只查姓名和工资(只取需要的列,不滥用 *)
SELECT name, salary FROM employees;
-- 3) 去重:列出所有部门
SELECT DISTINCT dept FROM employees;
-- 输出:
-- dept
-- ------
-- 技术部
-- 市场部
-- 人事部第 2 题:WHERE 过滤
题目:写出三条 SQL——(1) 工资大于 8000 的技术部员工;(2) 市场部或人事部、工资在 5000~9000 之间的员工;(3) 姓名以"小"开头的员工。
提示:AND / OR 组合条件;IN (...) 简化多值或;BETWEEN a AND b 含两端;LIKE '小%' 模糊匹配(% 任意长度、_ 单字符)。
参考答案
-- 查工资大于 8000 的技术部员工
SELECT name, salary
FROM employees
WHERE dept = '技术部' AND salary > 8000;
-- IN + BETWEEN:市场部或人事部,工资在 5000~9000 之间
SELECT name, dept, salary
FROM employees
WHERE dept IN ('市场部', '人事部')
AND salary BETWEEN 5000 AND 9000;
-- 模糊匹配:姓名以"小"开头
SELECT name FROM employees WHERE name LIKE '小%';
-- % 匹配任意长度任意字符;_ 匹配单个字符;ILIKE 不区分大小写第 3 题:ORDER BY + LIMIT(分页)
题目:(1) 查工资 Top 5 的员工;(2) 实现分页——每页 5 条,取第 2 页;(3) 工资未定(NULL)的排在最后。
提示:ORDER BY col DESC 降序;LIMIT n 取前 n 条;OFFSET n 跳过 n 条;NULLS LAST 控制 NULL 排序。OFFSET 必须配 ORDER BY。
参考答案
-- 工资 Top 5
SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 5;
-- 分页:每页 5 条,取第 2 页(跳过前 5 条)
SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 5 OFFSET 5;
-- NULL 处理:工资未定(NULL)的排到最后
SELECT name, salary FROM employees
ORDER BY salary DESC NULLS LAST;
-- OFFSET 必须配合 ORDER BY,否则顺序无意义第 4 题:INSERT 插入
题目:(1) 插入单条员工记录;(2) 一条语句批量插入 3 条;(3) 从 employees 复制技术部员工到 employees_backup。
提示:INSERT INTO t (col1, col2) VALUES (...), (...), (...) 一条语句插多行;INSERT INTO ... SELECT ... 从查询结果插入。
参考答案
-- 单条插入
INSERT INTO employees (name, dept, salary)
VALUES ('小明', '技术部', 9000);
-- 批量插入(一条语句插多行,比循环多次插入快得多)
INSERT INTO employees (name, dept, salary)
VALUES ('小红', '市场部', 8500),
('小刚', '技术部', 9500),
('小丽', '人事部', 7000);
-- INSERT ... SELECT:从另一张表复制
INSERT INTO employees_backup (name, dept, salary)
SELECT name, dept, salary FROM employees WHERE dept = '技术部';第 5 题:UPDATE 更新
题目:(1) 给技术部全员加薪 10%;(2) 把"小明"调到"研发部"并加薪 500。务必注意安全——漏写 WHERE 会更新全表。
提示:UPDATE t SET col = expr WHERE ...;多列用逗号分隔。安全习惯:先写等价的 SELECT 验证 WHERE 命中的行,再改 UPDATE。
参考答案
-- 给技术部全员加薪 10%
UPDATE employees
SET salary = salary * 1.1
WHERE dept = '技术部';
-- 多列同时更新
UPDATE employees
SET dept = '研发部', salary = salary + 500
WHERE name = '小明';
-- ⚠️ 务必带 WHERE!漏了 WHERE 会更新全表
-- UPDATE employees SET salary = 0; -- 灾难!所有人工资清零
-- 安全习惯:先用 SELECT 验证 WHERE 命中的行,再改成 UPDATE
SELECT name, dept, salary FROM employees WHERE dept = '技术部';第 6 题:DELETE 删除
题目:(1) 删除工资低于 5000 的员工;(2) 解释 DELETE vs TRUNCATE 的区别;(3) 在事务里删除,确保能回滚。
提示:DELETE FROM t WHERE ...;TRUNCATE 清空全表(更快、不可回滚、重置自增);危险操作务必包在事务里,验完再 COMMIT。
参考答案
-- 删除工资低于 5000 的员工
DELETE FROM employees WHERE salary < 5000;
-- ⚠️ 同样必须带 WHERE!DELETE FROM employees; 会清空全表
-- 想清空全表又想保留结构:TRUNCATE 更快(直接释放数据页,不写 WAL)
TRUNCATE employees;
-- 事务里删除,出错可回滚——这是 UPDATE/DELETE 的安全网
BEGIN;
DELETE FROM employees WHERE dept = '人事部';
-- 看一眼还剩几条,不对就 ROLLBACK,对了就 COMMIT
SELECT count(*) FROM employees;
ROLLBACK; -- 撤销删除,数据回来了进阶(3 题):聚合 / JOIN / JSONB
这一组是 SQL 真正强大的地方——多行合成一行、多表拼成一张、文档字段查询。
第 7 题:GROUP BY + HAVING
题目:按部门统计人数、平均工资、最高工资;只保留人数 ≥ 3 的部门,按平均工资降序。再解释 HAVING 和 WHERE 的区别、SQL 语句的执行顺序。
提示:GROUP BY 分组,COUNT/AVG/MAX/SUM 聚合;HAVING 过滤组(可用聚合),WHERE 过滤行(不可用聚合)。ROUND(x, 2) 保留两位小数。
参考答案
-- 按部门统计人数、平均工资、最高工资
-- 只保留人数 >= 3 的部门,按平均工资降序
SELECT
dept,
COUNT(*) AS headcount,
ROUND(AVG(salary), 2) AS avg_salary,
MAX(salary) AS max_salary
FROM employees
GROUP BY dept
HAVING COUNT(*) >= 3
ORDER BY avg_salary DESC;
-- HAVING vs WHERE:
-- WHERE 在分组前过滤行(不能用聚合);
-- HAVING 在分组后过滤组(可以用聚合)。
-- 执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT第 8 题:JOIN 连接
题目:(1) 查金额大于 100 的订单及其客户名(内连接);(2) 列出所有客户及其订单总数、总金额,没下过单的客户也要显示(左连接,金额合计为 0)。
提示:JOIN ... ON ... 内连接(只留能配上的);LEFT JOIN 左连接(保留左表所有行);COALESCE(x, 0) 把 NULL 兜成 0。
参考答案
-- 两张表:
-- customers(id, name, city)
-- orders(id, customer_id, amount, created_at)
-- 查每个订单的客户名、金额(内连接,只取能配上客户的订单)
SELECT c.name, c.city, o.amount, o.created_at
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.amount > 100
ORDER BY o.amount DESC;
-- LEFT JOIN:列出所有客户,即使没下过订单(没单的 amount 为 NULL)
SELECT c.name, COUNT(o.id) AS order_count, COALESCE(SUM(o.amount), 0) AS total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY total DESC;
-- COALESCE(x, 0):x 为 NULL 时取 0,常用于 LEFT JOIN 后的聚合兜底第 9 题:JSONB 文档查询(Postgres 杀手锏)
题目:给定 users(id, name, profile JSONB),profile 形如 {"role":"admin","age":30,"tags":["python","sql"]}。(1) 查 role 是 admin 的;(2) 查 tags 含 'python' 的;(3) 检查 age 字段是否存在;(4) 修改 JSONB;(5) 建索引加速。
提示:->> 取字段为文本;@> 包含查询(命中 GIN 索引);? 检查 key;jsonb_set 修改;CREATE INDEX ... USING GIN 建索引。
参考答案
-- users(id, name, profile JSONB)
-- profile 形如 {"role":"admin","age":30,"tags":["python","sql"]}
-- 1. ->> 取 JSON 字段为文本,能直接和字符串比较
SELECT name FROM users WHERE profile ->> 'role' = 'admin';
-- 2. @> 包含查询:tags 数组里含 'python'(命中 GIN 索引,极快)
SELECT name FROM users WHERE profile @> '{"tags":["python"]}';
-- 3. ? 检查 key 是否存在
SELECT name FROM users WHERE profile ? 'age';
-- 4. 修改 JSONB(生成新值,JSONB 不可变)
UPDATE users SET profile = jsonb_set(profile, '{age}', '31')
WHERE name = '小明';
-- 5. 加索引:GIN 索引让 @> / ? 查询飞起来(核心优化手段)
CREATE INDEX idx_users_profile ON users USING GIN (profile);
-- JSONB 兼具文档库的灵活和关系库的事务,这是 Postgres 比 MySQL 强的地方高级(3 题):窗口函数 / CTE / UPSERT + 事务
这一组是 PostgreSQL 区别于"会写 CRUD"的进阶能力——分析报表、复杂查询、并发安全。
第 10 题:窗口函数
题目:查出每位员工的部门内工资排名(dept_rank),以及他与部门平均工资的差值。再解释 ROW_NUMBER / RANK / DENSE_RANK 三者的区别。
提示:RANK() OVER (PARTITION BY dept ORDER BY salary DESC);PARTITION BY 分组、ORDER BY 排序;窗口函数不合并行(不像 GROUP BY),原行数不变。
参考答案
-- 按部门分组,工资降序排名(每位员工都能看到自己名次)
SELECT
name,
dept,
salary,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS dept_rank,
salary - AVG(salary) OVER (PARTITION BY dept) AS diff_from_avg
FROM employees;
-- 三种排名函数的区别:
-- ROW_NUMBER():1,2,3,4(永不重复,即使并列)
-- RANK(): 1,2,2,4(并列后跳号)
-- DENSE_RANK(): 1,2,2,3(并列不跳号)
--
-- OVER (...) 定义"窗口":PARTITION BY 分组,ORDER BY 排序
-- 窗口函数不合并行(不像 GROUP BY),原行数不变,只是多一列排名
-- 这是分析报表的核心工具:Top N per group、累计求和、移动平均都能做第 11 题:CTE(WITH 子句)
题目:用 CTE 找出每个部门工资最高的员工。再写一个多 CTE 查询:先算每个部门的平均工资,再筛出高于本部门平均的"高绩效员工",最后按部门统计高绩效人数。
提示:WITH name AS (...), name2 AS (...),后一个 CTE 可引用前一个;CTE 让复杂查询像"管道",可读性远高于嵌套子查询。
参考答案
-- 找每个部门工资最高的员工
-- 用 CTE + 窗口函数,逻辑分两步,可读性远高于嵌套子查询
WITH ranked AS (
SELECT
name, dept, salary,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS r
FROM employees
)
SELECT dept, name, salary
FROM ranked
WHERE r = 1;
-- 多个 CTE 用逗号分隔,后一个能引用前一个(像管道)
WITH dept_avg AS (
SELECT dept, AVG(salary) AS avg_sal FROM employees GROUP BY dept
),
above_avg AS (
SELECT e.name, e.dept, e.salary
FROM employees e
JOIN dept_avg d ON e.dept = d.dept
WHERE e.salary > d.avg_sal
)
SELECT dept, count(*) AS high_performers FROM above_avg
GROUP BY dept ORDER BY high_performers DESC;
-- CTE 让复杂查询像读"过程式脚本",推荐用它替代深层嵌套子查询第 12 题:UPSERT + 事务 + MVCC
题目:(1) 写一个 UPSERT——"小明"存在则更新工资,不存在则插入;(2) 用事务实现转账(两个账户加减余额,要么都成功要么都失败);(3) 解释 MVCC 如何让读写不互相阻塞。
提示:ON CONFLICT (col) DO UPDATE SET ...,EXCLUDED 指代新值;BEGIN/COMMIT/ROLLBACK;MVCC 给每个事务一个一致性快照,隔离级别 READ COMMITTED / SERIALIZABLE。
参考答案
-- 1. UPSERT:存在则更新,不存在则插入(用唯一约束判定冲突)
-- 要求 name 列有 UNIQUE 约束
INSERT INTO employees (name, dept, salary)
VALUES ('小明', '技术部', 10000)
ON CONFLICT (name) DO UPDATE
SET salary = EXCLUDED.salary; -- EXCLUDED 指代"本打算插入的新值"
-- 2. 事务:转账,两条 UPDATE 要么都成功要么都失败
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 检查无误后提交;任一步出错用 ROLLBACK 全部撤销
COMMIT;
-- 或:ROLLBACK;
-- 3. MVCC(多版本并发控制):每个事务看到一个一致的数据快照
-- 读不阻塞写、写不阻塞读——这是 Postgres 高并发的根基
-- 隔离级别默认 READ COMMITTED,可设 SERIALIZABLE 最严格
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- 在此隔离级别下,并发执行的"等价串行化"事务不会互相干扰
SELECT count(*) FROM employees;
COMMIT;练习建议
建表灌数据:把答案里的 SQL 复制到本地 psql 或 DB Fiddle,先 CREATE TABLE + INSERT 造数据,再练习查询。光看答案不动手,SQL 永远学不会。
试着扩展:做完第 7 题,能不能加"按工资区间分桶统计"(用 CASE WHEN 或 width_bucket)?做完第 8 题,能不能算出"每个客户的客单价"?做完第 10 题,能不能算"每个部门工资 Top 3"(用 RANK <= 3 过滤)?每个扩展都是一次实战。
性能意识:写完查询用 EXPLAIN ANALYZE 看执行计划——是全表扫描还是走索引?JOIN 用了什么算法?这是从"会用 SQL"到"精通 SQL"的分水岭。
← 上一篇 PostgreSQL 高级查询