查询数据

1. 简单查询 SELECT

  • SELECT是读取与组合数据的核心,按顺序写子句,按需省略未用部分。
SELECT [DISTINCT] expr_list
FROM table_or_subquery t
[JOIN other ON condition]
[WHERE row_predicate]
[GROUP BY grouping_exprs]
[HAVING group_predicate]
[UNION | INTERSECT | EXCEPT SELECT ...]
[ORDER BY sort_exprs]
[LIMIT n [OFFSET m] | FETCH FIRST n ROWS ONLY]
  • 子句作用速记

    • DISTINCT: 结果去重。
    • JOIN: 组合多表行。
    • WHERE: 分组前行级过滤。
    • GROUP BY: 按键聚合。未分组列必须聚合。
    • HAVING: 对聚合结果再过滤。
    • 集合运算: UNION/INTERSECT/EXCEPT整集合操作。
    • 排序 & 限制: ORDER BY + LIMIT/OFFSETFETCH 分页。
    • 聚合函数: COUNT/SUM/AVG/MAX/MIN 在分组或全表上聚合。
  • 常见顺序影响

    • 执行逻辑(简化): FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → UNION 等 → ORDER BY → LIMIT。
    • 早过滤(WHERE)减少后续分组与排序成本。
    • 只在需要展示顺序时才加 ORDER BY。分页必须与确定顺序绑定。

2. 列别名

列别名用于给输出列临时命名(不改表结构),提升可读性与复用。

  • 适用: 原始列、表达式、函数、聚合、CASE
  • 引用: 可ORDER BY、外层查询中使用别名,不能在同一层WHERE里引用它
  • 省略AS等价:col alias
-- 基本用法
SELECT column_name AS alias_name FROM table_name;
-- 表达式别名
SELECT
price * quantity AS total,
COUNT(*) AS cnt
FROM orders
ORDER BY total DESC; -- ORDER BY 可用别名

-- CASE 别名
SELECT
CASE
WHEN status = 'paid' THEN 'P'
ELSE 'U'
END AS pay_flag
FROM invoices;

-- WHERE 不能用别名(错误示例)
SELECT price * quantity AS total
FROM orders
WHERE total > 100; -- ❌ 错误

-- 正确做法:重复表达式或用子查询
WHERE price * quantity > 100; -- ✅

3. 排序 ORDER BY

  • ORDER BY按指定列或表达式排序。多列时先按第一列排,相同再按第二列排。默认升序ASC,降序用DESC。可排别名、表达式、函数。可控制空值位置:NULLS FIRST/NULLS LAST。不推荐用序号排序(如ORDER BY 1)。
-- 单列排序
SELECT id, created_at FROM posts
ORDER BY created_at DESC;

-- 控制NULL值位置
SELECT id, name FROM users
ORDER BY name ASC NULLS LAST;

-- 多列排序(先按total降序,相同则按id升序)
SELECT price * quantity AS total, id
FROM orders
ORDER BY total DESC, id ASC;

-- 表达式排序
SELECT name, age FROM users
ORDER BY age * 12 DESC;

-- 函数排序
SELECT email FROM users
ORDER BY LOWER(email);

4. 去重查询 DISTINCT

  • SELECT DISTINCTSQL中的一种用法,用于从查询结果中去除重复的记录,仅返回唯一的结果行。它适用于希望从数据库表中获取不重复的值或组合的场景。
-- 查询所有不重复的城市
SELECT DISTINCT city FROM customers;

-- 查询不重复的城市和州的组合
SELECT DISTINCT city, state FROM customers;

-- 统计有多少个不同的城市
SELECT COUNT(DISTINCT city) FROM customers;
  • 性能考虑: SELECT DISTINCT需要对结果进行去重处理,这在大数据集上可能会影响查询性能。使用时应注意查询效率,特别是在处理大表时。
  • 多列去重: 当使用多列时,DISTINCT是对所有列的组合进行去重,而不是单独对每一列去重。

5. 分页查询

  • 分页查询是数据库查询的一种技术,用于将查询结果分割成更小的部分(页),以方便数据的显示和管理,特别是在结果集非常大的情况下。分页能够提高应用的性能和用户体验,因为它可以减少一次性加载的数据量,并提供更快的响应时间。

OFFSET/LIMIT 分页

  • 最常见的分页方式,适合小数据集。
-- 基本语法
SELECT column1, column2, ...
FROM table_name
ORDER BY column_name
LIMIT page_size OFFSET (page_number - 1) * page_size;

-- 示例:每页10条记录 (page_size = 10)
-- 第一页 (page_number = 1)
SELECT * FROM employees ORDER BY id LIMIT 10 OFFSET 0;

-- 第二页 (page_number = 2)
SELECT * FROM employees ORDER BY id LIMIT 10 OFFSET 10;

-- 第三页 (page_number = 3)
SELECT * FROM employees ORDER BY id LIMIT 10 OFFSET 20;
  • 计算公式: OFFSET = (page_number - 1) * page_size
  • 优点: 实现简单,可以跳转到任意页
  • 缺点: OFFSET很大时性能差,数据库需要扫描所有跳过的记录 (一个简单的比喻就是你每次翻书都要从第一页翻到你之前看到的页数)

基于ID的分页

  • 基于上一页最后一条记录的ID继续查询,性能好。(得从 batch 中拿到下一次开始的 id)
-- 第一页
SELECT * FROM employees
ORDER BY id
LIMIT 10;
-- 应用层记录 last_id = 最后一条记录的id

-- 第二页(使用上一页的 last_id)
SELECT * FROM employees
WHERE id > last_id
ORDER BY id
LIMIT 10;
-- 应用层更新 last_id

-- 第三页(使用更新后的 last_id)
SELECT * FROM employees
WHERE id > last_id
ORDER BY id
LIMIT 10;
  • 优点: 性能稳定,不受数据量影响
  • 缺点: 不能跳转到指定页,只能顺序翻页

Cursor (游标) 分页

  • 数据库提供的游标机制,常用于存储过程中。
-- PostgreSQL 示例
BEGIN;
DECLARE my_cursor CURSOR FOR SELECT * FROM employees ORDER BY id;
FETCH 10 FROM my_cursor; -- 获取10条
FETCH 10 FROM my_cursor; -- 再获取10条
CLOSE my_cursor;
COMMIT;
  • 优点: 数据库原生支持,可以暂停和恢复
  • 缺点: 需要保持连接,不适合无状态的Web应用

注意事项

  • 性能考虑: OFFSET/LIMIT在大数据集上性能较差,推荐使用基于ID的分页。
  • ORDER BY的重要性: 必须使用ORDER BY确保结果顺序一致,否则分页结果可能重复或遗漏。
  • 选择合适的方案:
    • 需要跳页功能 → OFFSET/LIMIT
    • 无限滚动/顺序浏览 → 基于ID的分页
    • 存储过程/批处理 → Cursor游标

过滤数据

6. WHERE

  • WHERE子句是SQL中用于指定筛选条件的部分。它用于从数据库表中筛选出符合特定条件的行。WHERE子句可以用在SELECTUPDATEDELETESQL语句中,限制这些操作只应用于符合条件的行。
  • 常见运算符
    • 比较运算符: =, <> (不等于), !=, >, <, >=, <=
    • 逻辑运算符: AND, OR, NOT
    • 范围运算符: BETWEEN ... AND ...
    • 集合运算符: IN (value1, value2, ...)
    • 模糊匹配: LIKE
    • 空值判断: IS NULL, IS NOT NULL
-- 比较运算符
SELECT * FROM employees WHERE salary > 50000;
SELECT * FROM employees WHERE department = 'IT';
SELECT * FROM employees WHERE age <> 30;

-- 逻辑运算符
SELECT * FROM employees WHERE salary > 50000 AND department = 'IT';
SELECT * FROM employees WHERE department = 'IT' OR department = 'HR';
SELECT * FROM employees WHERE NOT department = 'IT';

-- BETWEEN
SELECT * FROM employees WHERE salary BETWEEN 40000 AND 60000;

-- IN
SELECT * FROM employees WHERE department IN ('IT', 'HR', 'Finance');

-- LIKE (模糊匹配)
SELECT * FROM employees WHERE name LIKE 'John%'; -- 以John开头
SELECT * FROM employees WHERE name LIKE '%son'; -- 以son结尾
SELECT * FROM employees WHERE name LIKE '%oh%'; -- 包含oh

-- IS NULL
SELECT * FROM employees WHERE phone IS NULL;
SELECT * FROM employees WHERE phone IS NOT NULL;
  • NULL值不能用=<>比较,必须使用IS NULLIS NOT NULL
  • LIKE%表示任意字符,_表示单个字符
  • 多个条件组合时注意使用括号明确优先级

7. LIMIT

  • LIMIT子句是SQL中的一部分,用于限制查询结果中返回的行数。它通常与SELECT语句一起使用,以控制从数据库中提取的行数。LIMIT子句对于分页、性能优化和控制数据输出量非常有用。
-- 基本语法
SELECT column1, column2, ...
FROM table_name
ORDER BY column_name
LIMIT number_of_rows;
-- 查询前5条记录
SELECT * FROM employees LIMIT 5;

-- 查询工资最高的前3名员工
SELECT * FROM employees
ORDER BY salary DESC
LIMIT 3;

-- 结合OFFSET实现分页
SELECT * FROM employees
ORDER BY id
LIMIT 10 OFFSET 20; -- 跳过前20条,返回第21-30条
  • LIMIT通常与ORDER BY一起使用,确保结果顺序一致
  • 不同数据库语法略有差异:
    • MySQL/PostgreSQL: LIMIT n OFFSET m
    • SQL Server: TOP nOFFSET m ROWS FETCH NEXT n ROWS ONLY
    • Oracle: ROWNUMFETCH FIRST n ROWS ONLY

8. OFFSET/FETCH

  • FETCH子句是SQL中的一部分,用于从查询结果中返回特定数量的行。它通常与OFFSET子句一起使用,以实现更加灵活和清晰的分页操作。在不同的数据库系统中,FETCH子句的支持和语法可能略有不同。
-- 基本语法
SELECT column1, column2, ...
FROM table_name
ORDER BY column_name
OFFSET number_of_rows ROWS
FETCH {FIRST | NEXT} number_of_rows {ROW | ROWS} ONLY;
  • FIRST: 表示从结果集的开头返回行数
  • NEXT: 表示从当前偏移量位置继续返回行数
  • ROW / ROWS: 用于指定返回行的单数或复数形式,两者在功能上没有差别
-- 获取前10条记录
SELECT * FROM employees
ORDER BY id
FETCH FIRST 10 ROWS ONLY;

-- 跳过前20条,获取接下来的10条
SELECT * FROM employees
ORDER BY id
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;

-- 获取工资最高的前5名员工
SELECT * FROM employees
ORDER BY salary DESC
FETCH FIRST 5 ROWS ONLY;

-- 分页示例:第3页,每页10条
SELECT * FROM employees
ORDER BY id
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;
  • 使用FETCH时必须有ORDER BY子句
  • OFFSETFETCH提供了更清晰的语义表达
  • 不同数据库支持情况:
    • PostgreSQL: 支持FETCHLIMIT
    • SQL Server: 仅支持FETCH
    • MySQL: 仅支持LIMIT

9. IN

  • IN运算符是SQL中用于指定多个可能值的一种条件操作符。它通常用于WHERE子句中,用来过滤那些列值在指定集合中的行。IN运算符允许你简洁地测试某个列的值是否匹配一组值中的任何一个,而无需使用多个OR条件。
-- 基本语法
SELECT column1, column2, ...
FROM table_name
WHERE column_name IN (value1, value2, ...);
-- 查询特定部门的员工
SELECT * FROM employees
WHERE department IN ('IT', 'HR', 'Finance');

-- 等价于使用多个OR
SELECT * FROM employees
WHERE department = 'IT' OR department = 'HR' OR department = 'Finance';

-- 查询特定ID的员工
SELECT * FROM employees
WHERE id IN (1, 5, 10, 15);

-- NOT IN: 排除特定部门
SELECT * FROM employees
WHERE department NOT IN ('HR', 'Engineering');

-- 子查询配合IN
SELECT * FROM employees
WHERE department_id IN (
SELECT id FROM departments WHERE location = 'New York'
);
  • IN比多个OR条件更简洁易读
  • NOT IN在遇到NULL值时要小心,可能得到意外结果
  • IN可以配合子查询使用
  • 列表值过多时可能影响性能,考虑使用JOIN或临时表

10. BETWEEN

  • BETWEEN运算符是SQL中用于筛选范围内的值的一种条件操作符。它通常用于WHERE子句中,以测试某个列的值是否在指定的上下限之间。BETWEEN运算符包括指定的边界值(即包含上下限),它可以用于数字、日期、时间等类型的数据。
-- 基本语法
SELECT column1, column2, ...
FROM table_name
WHERE column_name BETWEEN value1 AND value2;
-- 数值范围
SELECT * FROM employees
WHERE salary BETWEEN 30000 AND 60000;

-- 等价于
SELECT * FROM employees
WHERE salary >= 30000 AND salary <= 60000;

-- 日期范围
SELECT * FROM orders
WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31';

-- 字母范围
SELECT * FROM employees
WHERE name BETWEEN 'A' AND 'M';

-- NOT BETWEEN
SELECT * FROM employees
WHERE salary NOT BETWEEN 30000 AND 60000;

-- 等价于
SELECT * FROM employees
WHERE salary < 30000 OR salary > 60000;
  • BETWEEN包含边界值的(闭区间)
  • BETWEEN value1 AND value2等价于>= value1 AND <= value2
  • value1必须小于或等于value2,否则结果为空
  • 日期范围查询时注意时间部分,可能需要使用<而不是BETWEEN来避免边界问题
  • 可以使用NOT BETWEEN排除范围内的值

11. LIKE

  • LIKE运算符是SQL中用于进行模式匹配的运算符。它通常用于WHERE子句中,以根据指定的模式筛选出符合条件的行。LIKE运算符可以匹配字符串中的部分内容,因此特别适合用于查找类似的字符串数据。
-- 基本语法
SELECT column1, column2, ...
FROM table_name
WHERE column_name LIKE pattern;
  • 百分号 %: 匹配任意数量的字符,包括零个字符
  • 下划线 _: 匹配单个字符
-- 查询名字以"A"开头的员工
SELECT * FROM employees
WHERE name LIKE 'A%';

-- 查询名字以"son"结尾的员工
SELECT * FROM employees
WHERE name LIKE '%son';

-- 查询名字中包含"li"的员工
SELECT * FROM employees
WHERE name LIKE '%li%';

-- 查询名字第二个字母是"a"的员工
SELECT * FROM employees
WHERE name LIKE '_a%';

-- 查询名字正好是4个字符的员工
SELECT * FROM employees
WHERE name LIKE '____';

-- NOT LIKE: 排除匹配
SELECT * FROM employees
WHERE name NOT LIKE 'A%';

-- 转义特殊字符
SELECT * FROM products
WHERE name LIKE '%\_%' ESCAPE '\'; -- 查找包含下划线的产品名
  • LIKE区分大小写的(取决于数据库和字符集设置)
  • 使用%在开头会导致无法使用索引,影响性能
  • 如果需要匹配%_字符本身,需要使用转义符
  • 对于复杂的模式匹配,可以考虑使用正则表达式(如PostgreSQL的~运算符)
  • ILIKE(PostgreSQL)可以实现不区分大小写的匹配
-- ILIKE: 不区分大小写的匹配(PostgreSQL特有)
SELECT * FROM employees
WHERE name ILIKE 'john%'; -- 可以匹配 'John', 'JOHN', 'john', 'JoHn' 等

12. NULL 和 IS NULL

  • NULLSQL中用于表示数据的缺失或未知值的特殊标识符。它不同于零、空字符串或任何其他值,因为它表示没有数据或数据不可用。由于NULL代表未知值,因此它在逻辑运算和比较中有特殊的处理方式。

  • 没有特定类型: NULL可以出现在任何数据类型的列中

  • 不可比较: NULL不等于任何值,包括它自己。因此,NULL = NULL的表达式结果为FALSE。正确的做法是使用IS NULL来检查NULL

-- 检查NULL值
SELECT column1, column2, ...
FROM table_name
WHERE column_name IS NULL;

-- 检查非NULL值
SELECT column1, column2, ...
FROM table_name
WHERE column_name IS NOT NULL;
-- 查询没有电话号码的员工
SELECT * FROM employees
WHERE phone IS NULL;

-- 查询有电话号码的员工
SELECT * FROM employees
WHERE phone IS NOT NULL;

-- 错误写法(永远不会返回结果)
SELECT * FROM employees
WHERE phone = NULL; -- ❌ 错误

-- 正确写法
SELECT * FROM employees
WHERE phone IS NULL; -- ✅ 正确

-- NULL在计算中的表现
SELECT salary + bonus FROM employees; -- 如果bonus是NULL,结果也是NULL

-- 使用COALESCE处理NULL
SELECT name, COALESCE(phone, 'No Phone') FROM employees;

-- 使用IFNULL/ISNULL处理NULL(不同数据库语法不同)
SELECT name, IFNULL(phone, 'No Phone') FROM employees; -- MySQL
SELECT name, ISNULL(phone, 'No Phone') FROM employees; -- SQL Server
  • 不能使用 =!= 比较NULL, 必须使用IS NULLIS NOT NULL
  • NULL在逻辑运算中
    • NULL AND TRUE = NULL
    • NULL OR FALSE = NULL
    • NOT NULL = NULL
  • NULL在聚合函数中: 大多数聚合函数(如COUNT, SUM, AVG)会忽略NULL
  • NULL在排序中: ORDER BY时,NULL值通常排在最前面或最后面(取决于数据库)

连接多个表

13. 表别名

  • 表别名Table AliasSQL中用于为表指定临时名称的功能。表别名通常用于简化查询语句,特别是在多表联接JOIN或子查询中,使得查询更加简洁和易读。表别名仅在查询的上下文中有效,不会影响数据库中表的实际名称。
-- 基本语法
SELECT column1, column2, ...
FROM table_name AS alias_name
WHERE condition;
-- 使用AS关键字
SELECT e.name, e.salary
FROM employees AS e
WHERE e.salary > 50000;

-- 省略AS关键字
SELECT e.name, e.salary
FROM employees e
WHERE e.salary > 50000;

-- 多表联接中使用别名
SELECT e.name, d.department_name
FROM employees AS e
JOIN departments AS d ON e.department_id = d.id;

-- 自联接中使用别名(必须使用)
SELECT e1.name AS employee, e2.name AS manager
FROM employees e1
JOIN employees e2 ON e1.manager_id = e2.id;

-- 子查询中使用别名
SELECT *
FROM (
SELECT name, salary FROM employees WHERE salary > 50000
) AS high_earners;
  • 表别名在SELECTWHEREJOIN等子句中都可以使用
  • 自联接(同一张表联接自己)时必须使用别名
  • 使用别名可以提高查询可读性,特别是在复杂查询中
  • 别名只在当前查询中有效,不会改变数据库中的表名
  • 推荐使用有意义的别名,如e代表employeesd代表departments

14. INNER JOIN

  • INNER JOIN内连接是SQL中的一种联接操作,用于从多个表中查询相关联的数据。INNER JOIN只返回两个表中满足联接条件的行,也就是说,它只返回在两个表中都有匹配关系的记录。如果其中一个表没有匹配的记录,结果中不会包含该行。
-- 基本语法
SELECT column1, column2, ...
FROM table1
INNER JOIN table2
ON table1.common_column = table2.common_column;
-- employees
-- | id | name | department_id |
-- |----|---------|---------------|
-- | 1 | Alice | 1 |
-- | 2 | Bob | 2 |
-- | 3 | Charlie | NULL |

-- departments
-- | id | department_name |
-- |----|-----------------|
-- | 1 | HR |
-- | 2 | Engineering |
-- | 3 | Marketing |

SELECT e.name, d.department_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;

-- 查询结果
-- | name | department_name |
-- |-------|-----------------|
-- | Alice | HR |
-- | Bob | Engineering |

注意: Charlie 没有出现在结果中,因为他的 department_idNULL,在 departments 表中没有匹配的记录。同样,Marketing 部门也没有出现,因为没有员工属于该部门。

  • INNER JOIN 可以简写为 JOIN(效果相同)
  • 只返回两个表中都有匹配的记录
  • NULL 值不会被匹配
  • 使用表别名可以简化查询
  • 可以连接多个表,使用多个 INNER JOIN

15. LEFT JOIN

  • LEFT JOIN左连接是SQL中的一种联接操作,用于从两个表中查询相关的数据。与INNER JOIN不同,LEFT JOIN会返回左表中的所有记录,即使右表中没有匹配的记录。对于那些在右表中没有匹配的记录,结果集中相应右表的字段会显示为NULL
-- 基本语法
SELECT column1, column2, ...
FROM table1
LEFT JOIN table2
ON table1.common_column = table2.common_column;
  • 所有左表的行: 即使左表中的某一行在右表中没有匹配记录,该行也会包含在结果集中
  • 右表的匹配行: 如果有匹配的右表记录,这些记录会出现在结果集中
  • NULL值: 对于在右表中没有匹配的左表记录,结果集中右表的列会显示为NULL
-- employees
-- | id | name | department_id |
-- |----|---------|---------------|
-- | 1 | Alice | 1 |
-- | 2 | Bob | 2 |
-- | 3 | Charlie | NULL |
-- | 4 | David | 3 |

-- departments
-- | id | department_name |
-- |----|-----------------|
-- | 1 | HR |
-- | 2 | Engineering |

SELECT e.name, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id;

-- 查询结果
-- | name | department_name |
-- |---------|-----------------|
-- | Alice | HR |
-- | Bob | Engineering |
-- | Charlie | NULL |
-- | David | NULL |

注意: 所有员工都出现在结果中。Charlie 的 department_idNULL,David 的 department_id 是 3 但在 departments 表中不存在,所以他们的 department_name 都显示为 NULL

  • LEFT JOIN 也可以写作 LEFT OUTER JOIN(效果相同)
  • 返回左表的所有记录,右表没有匹配时显示 NULL
  • 常用于查找”孤立”记录(例如:没有部门的员工)
  • 可以通过 WHERE right_table.column IS NULL 来筛选只在左表存在的记录

16. SELF JOIN

  • 自连接Self JoinSQL中的一种联接操作,其中一个表与自身进行联接。自连接用于将表中的一行与同一表中的另一行进行比较或关联。这在处理表中行与行之间的关系时非常有用,例如层次结构(如员工与经理的关系)或相邻行之间的比较。

  • 同一张表: 自连接涉及的表实际上是同一张表,只是在查询中使用了不同的别名,使得它们在逻辑上看起来像是两个不同的表

  • 表别名: 为了区分表的不同实例,通常使用表别名。这样可以在同一个查询中引用表中的不同行

-- 基本语法
SELECT a.column1, b.column2, ...
FROM table_name a
JOIN table_name b
ON a.common_column = b.common_column;
-- employees
-- | id | name | manager_id |
-- |----|---------|------------|
-- | 1 | Alice | NULL |
-- | 2 | Bob | 1 |
-- | 3 | Charlie | 1 |
-- | 4 | David | 2 |

SELECT e.name AS employee_name, m.name AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

-- 查询结果
-- | employee_name | manager_name |
-- |---------------|--------------|
-- | Alice | NULL |
-- | Bob | Alice |
-- | Charlie | Alice |
-- | David | Bob |

注意: Alice 是最高层级的员工,没有经理,所以 manager_name 显示为 NULL。其他员工都能找到对应的经理。

-- 查找同一部门的员工对
SELECT e1.name AS employee1, e2.name AS employee2
FROM employees e1
JOIN employees e2 ON e1.department_id = e2.department_id
WHERE e1.id < e2.id; -- 避免重复和自己匹配自己

-- 查找没有下属的员工
SELECT e.name
FROM employees e
LEFT JOIN employees m ON e.id = m.manager_id
WHERE m.id IS NULL;

-- 查找工资比某个特定员工高的所有员工
SELECT e2.name, e2.salary
FROM employees e1
JOIN employees e2 ON e2.salary > e1.salary
WHERE e1.name = 'Alice';
  • 自连接必须使用表别名来区分同一表的不同实例
  • 通常使用 LEFT JOIN 来包含没有匹配的行(如没有经理的员工)
  • 注意避免笛卡尔积,确保有正确的 ON 条件
  • 自连接可能影响性能,大表需要注意优化

17. FULL OUTER JOIN

  • FULL OUTER JOIN全外连接是SQL中的一种联接操作,它返回两个表中所有符合联接条件的记录,以及不符合联接条件的记录。与INNER JOIN只返回两个表中有匹配记录的行不同
  • FULL OUTER JOIN会返回两个表中的所有记录,如果在某一表中没有匹配的记录,则该表的列会返回NULL
-- 基本语法
SELECT column1, column2, ...
FROM table1
FULL OUTER JOIN table2
ON table1.common_column = table2.common_column;
-- employees
-- | id | name | department_id |
-- |----|---------|---------------|
-- | 1 | Alice | 1 |
-- | 2 | Bob | 2 |
-- | 3 | Charlie | NULL |

-- departments
-- | id | department_name |
-- |----|-----------------|
-- | 1 | HR |
-- | 2 | Engineering |
-- | 3 | Marketing |

SELECT e.name, d.department_name
FROM employees e
FULL OUTER JOIN departments d ON e.department_id = d.id;

-- 查询结果
-- | name | department_name |
-- |---------|-----------------|
-- | Alice | HR |
-- | Bob | Engineering |
-- | Charlie | NULL |
-- | NULL | Marketing |

Charlie 出现在结果中,但 department_nameNULL,因为他没有分配部门
Marketing 部门也出现在结果中,但 nameNULL,因为该部门没有员工
FULL OUTER JOIN 结合了 LEFT JOINRIGHT JOIN 的效果

  • FULL OUTER JOIN 也可以简写为 FULL JOIN(效果相同)
  • 返回两个表中的所有记录,无匹配时显示 NULL
  • MySQL 不支持 FULL OUTER JOIN,需要用 UNION 组合 LEFT JOINRIGHT JOIN 实现
  • PostgreSQL、SQL Server、Oracle 支持 FULL OUTER JOIN
  • 常用于查找两个表中不匹配的记录

18. CROSS JOIN

  • CROSS JOIN交叉连接是SQL中的一种联接操作,它返回两个表中所有可能的行组合,生成笛卡尔积Cartesian ProductCROSS JOIN不使用任何联接条件,因此会将左表的每一行与右表的每一行组合在一起。
-- 基本语法
SELECT column1, column2, ...
FROM table1
CROSS JOIN table2;
-- products
-- | id | product_name |
-- |----|--------------|
-- | 1 | Shirt |
-- | 2 | Pants |

-- colors
-- | id | color_name |
-- |----|------------|
-- | 1 | Red |
-- | 2 | Blue |

SELECT p.product_name, c.color_name
FROM products p
CROSS JOIN colors c;

-- 查询结果
-- | product_name | color_name |
-- |--------------|------------|
-- | Shirt | Red |
-- | Shirt | Blue |
-- | Pants | Red |
-- | Pants | Blue |

注意: 结果集的行数 = 左表行数 × 右表行数。在这个例子中,2 × 2 = 4 行。

替代写法

-- 使用逗号(隐式CROSS JOIN)
SELECT p.product_name, c.color_name
FROM products p, colors c;

-- 等价于
SELECT p.product_name, c.color_name
FROM products p
CROSS JOIN colors c;

常见应用场景

-- 生成所有尺寸和颜色的组合
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;

-- 生成日期和员工的所有组合(用于考勤表)
SELECT e.name, d.date
FROM employees e
CROSS JOIN dates d;

-- 生成测试数据
SELECT n1.num * 10 + n2.num AS number
FROM (SELECT 0 AS num UNION SELECT 1 UNION SELECT 2) n1
CROSS JOIN (SELECT 0 AS num UNION SELECT 1 UNION SELECT 2) n2;
  • CROSS JOIN 生成笛卡尔积,结果集可能非常大
  • 没有 ON 条件,返回所有可能的组合
  • 通常用于生成组合数据或测试数据
  • 大表使用 CROSS JOIN 会严重影响性能
  • 如果意外忘记写 JOIN 条件,会变成 CROSS JOIN,导致性能问题

19. NATURAL JOIN

  • NATURAL JOIN自然连接是SQL中的一种联接操作,它根据两个表中同名的列自动进行联接。自然连接会自动比较两个表中所有同名的列,并返回这些列值相等的行。与INNER JOIN不同,NATURAL JOIN不需要显式指定联接条件,因为它会自动查找并使用同名列进行联接。
-- 基本语法
SELECT column1, column2, ...
FROM table1
NATURAL JOIN table2;
  • 自动联接: NATURAL JOIN自动查找并使用两个表中所有同名的列来进行联接
  • 同名列值相等: 只有当两个表中同名列的值相等时,才会在结果集中返回这些行
  • 不返回重复列: 结果集中不会包含重复的同名列,只会返回一次
-- employees
-- | id | name | department_id |
-- |----|---------|---------------|
-- | 1 | Alice | 1 |
-- | 2 | Bob | 2 |
-- | 3 | Charlie | 3 |

-- departments
-- | id | department_name |
-- |----|-----------------|
-- | 1 | HR |
-- | 2 | Engineering |
-- | 4 | Marketing |

SELECT *
FROM employees
NATURAL JOIN departments;

-- 查询结果(自动匹配 id 列)
-- | id | name | department_id | department_name |
-- |----|-------|---------------|-----------------|
-- | 1 | Alice | 1 | HR |
-- | 2 | Bob | 2 | Engineering |

NATURAL JOIN 自动使用两个表中的同名列 id 进行匹配
Charlie (id=3) 没有出现,因为 departments 表中没有 id=3 的记录
Marketing (id=4) 没有出现,因为 employees 表中没有 id=4 的记录
结果中 id 列只出现一次,不会重复

代替写法

-- NATURAL JOIN
SELECT *
FROM employees
NATURAL JOIN departments;

-- 等价于
SELECT e.id, e.name, e.department_id, d.department_name
FROM employees e
INNER JOIN departments d ON e.id = d.id;
  • 不推荐使用: NATURAL JOIN 在实际开发中很少使用,因为它依赖列名,容易出错
  • 隐式条件: 联接条件不明确,代码可读性差
  • 列名变化风险: 如果表结构发生变化(添加或重命名同名列),查询结果可能意外改变
  • 多个同名列: 如果有多个同名列,会同时用于联接条件
  • 推荐替代方案: 使用显式的 INNER JOINLEFT JOIN 并明确指定 ON 条件

为什么不推荐使用

-- 假设后来给 departments 表添加了 department_id 列
-- NATURAL JOIN 会同时匹配 id 和 department_id
-- 导致意外的查询结果
SELECT *
FROM employees
NATURAL JOIN departments; -- 结果可能为空或不符合预期

20. LATERAL JOIN

  • LATERAL JOIN横向连接是SQL中的一种高级联接操作,它允许联接的子查询可以引用来自连接左侧表的列。LATERAL子句使得每一行都可以独立地进行子查询,基于当前行的值进行计算或过滤。这使得LATERAL JOIN特别适用于那些需要处理依赖于当前行的复杂子查询的场景。
-- 基本语法
SELECT column1, column2, ...
FROM table1
JOIN LATERAL (
SELECT ...
FROM table2
WHERE table2.column = table1.column
) alias ON condition;
-- employees
-- | id | name |
-- |----|-------|
-- | 1 | Alice |
-- | 2 | Bob |

-- projects
-- | employee_id | project_name | start_date |
-- |-------------|--------------|------------|
-- | 1 | Project X | 2023-01-01 |
-- | 1 | Project Y | 2023-06-01 |
-- | 2 | Project Z | 2022-09-01 |

-- 查询每个员工最新的项目
SELECT e.name, p.project_name, p.start_date
FROM employees e
JOIN LATERAL (
SELECT project_name, start_date
FROM projects pr
WHERE pr.employee_id = e.id
ORDER BY start_date DESC
LIMIT 1
) p ON true;

-- 查询结果
-- | name | project_name | start_date |
-- |-------|--------------|------------|
-- | Alice | Project Y | 2023-06-01 |
-- | Bob | Project Z | 2022-09-01 |

子查询可以引用外部表 e.id
对每个员工独立执行子查询
ON true 表示无额外联接条件
Alice 的最新项目是 Project Y,Bob 的是 Project Z

常见应用场景

-- 获取每个部门工资最高的3名员工
SELECT d.department_name, e.name, e.salary
FROM departments d
JOIN LATERAL (
SELECT name, salary
FROM employees
WHERE department_id = d.id
ORDER BY salary DESC
LIMIT 3
) e ON true;

-- 计算每个客户的最近5次订单总额
SELECT c.name, orders.total
FROM customers c
JOIN LATERAL (
SELECT SUM(amount) as total
FROM orders
WHERE customer_id = c.id
ORDER BY order_date DESC
LIMIT 5
) orders ON true;

-- 使用 LEFT JOIN LATERAL 包含没有项目的员工
SELECT e.name, COALESCE(p.project_name, 'No Projects') as project
FROM employees e
LEFT JOIN LATERAL (
SELECT project_name
FROM projects pr
WHERE pr.employee_id = e.id
ORDER BY start_date DESC
LIMIT 1
) p ON true;
  • 数据库支持: PostgreSQL 9.3+, MySQL 8.0+ (使用 LATERAL 关键字)
  • 性能: 对左表的每一行都执行子查询,大表时需注意性能
  • LEFT JOIN LATERAL: 允许子查询返回空结果,类似于 LEFT JOIN
  • ON true: 当子查询已经包含所有过滤条件时,使用 ON true
  • 替代方案: 某些场景可以用窗口函数或普通 JOIN 替代

数据库兼容性

  • PostgreSQL: 完全支持 LATERAL
  • MySQL 8.0+: 支持 LATERAL(之前版本不支持)
  • SQL Server: 使用 CROSS APPLYOUTER APPLY 代替
  • Oracle: 使用 CROSS APPLYOUTER APPLY 代替

数据分组

21. GROUP BY 分组

  • GROUP BYSQL中的一个子句,用于将查询结果按一个或多个列进行分组。分组操作通常与聚合函数(如COUNTSUMAVGMAXMIN等)结合使用,以便对每个分组进行汇总计算。GROUP BY允许你将数据按特定的维度进行聚合,进而对这些聚合后的数据进行分析。
-- 基本语法
SELECT column1, column2, ..., aggregate_function(column)
FROM table_name
WHERE condition
GROUP BY column1, column2, ...;
-- sales
-- | id | product_id | quantity | price |
-- |----|------------|----------|-------|
-- | 1 | 101 | 5 | 10.00 |
-- | 2 | 102 | 3 | 15.00 |
-- | 3 | 101 | 2 | 10.00 |
-- | 4 | 101 | 4 | 10.00 |
-- | 5 | 102 | 1 | 15.00 |

-- 按产品ID汇总销售数量
SELECT product_id, SUM(quantity) AS total_quantity
FROM sales
GROUP BY product_id;

-- 查询结果
-- | product_id | total_quantity |
-- |------------|----------------|
-- | 101 | 11 |
-- | 102 | 4 |

-- 统计每个产品的销售次数和总金额
SELECT product_id,
COUNT(*) AS sale_count,
SUM(quantity * price) AS total_revenue
FROM sales
GROUP BY product_id;

-- 按多个列分组
SELECT product_id, price, SUM(quantity) AS total_quantity
FROM sales
GROUP BY product_id, price;

-- 结合WHERE过滤
SELECT product_id, SUM(quantity) AS total_quantity
FROM sales
WHERE price > 10
GROUP BY product_id;

常用聚合函数

-- COUNT: 计数
SELECT department_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id;

-- SUM: 求和
SELECT department_id, SUM(salary) AS total_salary
FROM employees
GROUP BY department_id;

-- AVG: 平均值
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;

-- MAX/MIN: 最大值/最小值
SELECT department_id, MAX(salary) AS max_salary, MIN(salary) AS min_salary
FROM employees
GROUP BY department_id;

-- 多个聚合函数组合
SELECT department_id,
COUNT(*) AS employee_count,
AVG(salary) AS avg_salary,
MAX(salary) AS max_salary
FROM employees
GROUP BY department_id;
  • SELECT中的列: SELECT子句中出现的非聚合列必须在GROUP BY中列出
  • 执行顺序: WHEREGROUP BY → 聚合函数 → HAVINGSELECT
  • NULL值处理: NULL值会被分为一组
  • 性能: 在分组列上建立索引可以提高性能

错误示例

-- ❌ 错误: name 不在 GROUP BY 中,也不是聚合函数
SELECT department_id, name, COUNT(*)
FROM employees
GROUP BY department_id;

-- ✅ 正确: 所有非聚合列都在 GROUP BY 中
SELECT department_id, name, COUNT(*)
FROM employees
GROUP BY department_id, name;

-- ✅ 正确: 只选择分组列和聚合结果
SELECT department_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id;

22. HAVING

  • HAVING子句是SQL中的一部分,主要用于在分组后的结果集上进行过滤。它通常与GROUP BY子句一起使用,以便对聚合后的数据进行进一步的筛选。与WHERE子句不同,HAVING子句用于过滤基于聚合函数的结果,而WHERE子句是在数据分组之前对行进行过滤。
-- 基本语法
SELECT column1, column2, aggregate_function(column3)
FROM table_name
WHERE condition
GROUP BY column1, column2
HAVING aggregate_condition;
-- sales
-- | id | product_id | quantity | price |
-- |----|------------|----------|-------|
-- | 1 | 101 | 50 | 10.00 |
-- | 2 | 102 | 30 | 15.00 |
-- | 3 | 101 | 60 | 10.00 |
-- | 4 | 103 | 20 | 20.00 |
-- | 5 | 102 | 10 | 15.00 |

-- 查询销售总量大于100的产品
SELECT product_id, SUM(quantity) AS total_quantity
FROM sales
GROUP BY product_id
HAVING SUM(quantity) > 100;

-- 查询结果
-- | product_id | total_quantity |
-- |------------|----------------|
-- | 101 | 110 |

-- 查询平均价格大于12且销售次数超过2次的产品
SELECT product_id,
COUNT(*) AS sale_count,
AVG(price) AS avg_price
FROM sales
GROUP BY product_id
HAVING COUNT(*) > 2 AND AVG(price) > 12;

-- 查询部门员工数超过5人的部门
SELECT department_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id
HAVING COUNT(*) > 5;

-- 结合WHERE和HAVING
SELECT product_id, SUM(quantity) AS total_quantity
FROM sales
WHERE price > 10 -- 分组前过滤:只统计价格大于10的销售
GROUP BY product_id
HAVING SUM(quantity) > 50; -- 分组后过滤:只显示总量大于50的产品

WHERE vs HAVING

-- WHERE: 在分组前过滤行
SELECT department_id, COUNT(*) AS employee_count
FROM employees
WHERE salary > 50000 -- 过滤掉工资≤50000的员工
GROUP BY department_id;

-- HAVING: 在分组后过滤组
SELECT department_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id
HAVING COUNT(*) > 10; -- 过滤掉员工数≤10的部门

-- 同时使用WHERE和HAVING
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
WHERE hire_date > '2020-01-01' -- 先过滤:只看2020年后入职的员工
GROUP BY department_id
HAVING AVG(salary) > 60000; -- 再过滤:只看平均工资>60000的部门

执行顺序

SELECT department_id, COUNT(*) AS employee_count
FROM employees
WHERE salary > 50000 -- 1. 先执行WHERE过滤行
GROUP BY department_id -- 2. 对过滤后的行分组
HAVING COUNT(*) > 5 -- 3. 对分组结果过滤
ORDER BY employee_count DESC -- 4. 最后排序
LIMIT 10; -- 5. 限制结果数量
  • 使用场景: HAVING用于过滤聚合结果,WHERE用于过滤原始行
  • 性能: 优先使用WHERE过滤,减少需要分组的数据量
  • 聚合函数: HAVING中可以使用聚合函数,WHERE中不能
  • 列引用: HAVING可以引用SELECT中的别名(部分数据库支持)
  • 执行顺序: WHEREGROUP BYHAVINGSELECTORDER BY

常见错误

-- ❌ 错误: WHERE中不能使用聚合函数
SELECT department_id, AVG(salary)
FROM employees
WHERE AVG(salary) > 60000 -- 错误!
GROUP BY department_id;

-- ✅ 正确: 在HAVING中使用聚合函数
SELECT department_id, AVG(salary)
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 60000;

23. PARTITION BY 分区

  • PARTITION BYSQL中的一个子句,通常与窗口函数一起使用,用于将查询结果集划分为多个分区,然后在每个分区内执行特定的计算。它类似于GROUP BY,但PARTITION BY不会将数据聚合到单一行,而是保留所有行,并在每个分区内执行计算。
-- 基本语法
SELECT column1,
column2,
window_function() OVER (
PARTITION BY column_name
ORDER BY column_name
) AS alias_name
FROM table_name;
-- sales
-- | id | employee_id | sale_amount | sale_date |
-- |----|-------------|-------------|------------|
-- | 1 | 101 | 500 | 2023-08-01 |
-- | 2 | 101 | 700 | 2023-08-02 |
-- | 3 | 102 | 300 | 2023-08-01 |
-- | 4 | 101 | 400 | 2023-08-03 |
-- | 5 | 102 | 800 | 2023-08-02 |

-- 按员工分区,对每个员工的销售额排名
SELECT employee_id,
sale_date,
sale_amount,
RANK() OVER (
PARTITION BY employee_id
ORDER BY sale_amount DESC
) AS sales_rank
FROM sales;

-- 查询结果
-- | employee_id | sale_date | sale_amount | sales_rank |
-- |-------------|------------|-------------|------------|
-- | 101 | 2023-08-02 | 700 | 1 |
-- | 101 | 2023-08-01 | 500 | 2 |
-- | 101 | 2023-08-03 | 400 | 3 |
-- | 102 | 2023-08-02 | 800 | 1 |
-- | 102 | 2023-08-01 | 300 | 2 |

每个员工的销售记录都保留了,同时在各自的分区内进行了排名。

常见窗口函数示例

-- ROW_NUMBER: 为每个分区内的行分配唯一序号
SELECT employee_id,
sale_date,
sale_amount,
ROW_NUMBER() OVER (
PARTITION BY employee_id
ORDER BY sale_date
) AS row_num
FROM sales;

-- SUM: 计算每个分区的累计总和
SELECT employee_id,
sale_date,
sale_amount,
SUM(sale_amount) OVER (
PARTITION BY employee_id
ORDER BY sale_date
) AS running_total
FROM sales;

-- AVG: 计算每个分区的平均值
SELECT employee_id,
sale_date,
sale_amount,
AVG(sale_amount) OVER (
PARTITION BY employee_id
) AS avg_sale
FROM sales;

-- DENSE_RANK: 排名,相同值排名相同,无间隔
SELECT employee_id,
sale_date,
sale_amount,
DENSE_RANK() OVER (
PARTITION BY employee_id
ORDER BY sale_amount DESC
) AS dense_rank
FROM sales;

-- LAG/LEAD: 访问前一行或后一行的数据
SELECT employee_id,
sale_date,
sale_amount,
LAG(sale_amount) OVER (
PARTITION BY employee_id
ORDER BY sale_date
) AS prev_sale,
LEAD(sale_amount) OVER (
PARTITION BY employee_id
ORDER BY sale_date
) AS next_sale
FROM sales;
  • 保留所有行: PARTITION BY不会减少行数,而是为每行添加计算结果
  • 窗口函数: 必须与窗口函数(如ROW_NUMBERRANKSUM等)一起使用
  • ORDER BY: 窗口函数内的ORDER BY定义分区内的排序,与查询的ORDER BY不同
  • 性能: 窗口函数可能比较耗费资源,大数据集需要注意性能
  • 数据库支持: 大多数现代数据库支持窗口函数(PostgreSQL、MySQL 8.0+、SQL Server、Oracle)

集合运算

24. UNION 组合

  • UNIONSQL中的一个运算符,用于将两个或多个SELECT语句的结果组合成一个结果集。UNION运算符将多个查询的结果合并为一个,且默认情况下会去除重复的行。如果你希望保留重复行,可以使用UNION ALL
-- 基本语法
SELECT column1, column2, ...
FROM table1
UNION
SELECT column1, column2, ...
FROM table2;
  • 列数和数据类型必须一致: 参与UNION的每个SELECT语句中,列数必须相同,且对应列的数据类型应该兼容或相同
  • 列的顺序和名称: 在合并的结果集中,列的顺序以第一个SELECT语句中的列为准,列名也是根据第一个SELECT语句的列名
-- employees_2023
-- | id | name | department |
-- |----|---------|------------|
-- | 1 | Alice | HR |
-- | 2 | Bob | IT |
-- | 3 | Charlie | Finance |

-- employees_2024
-- | id | name | department |
-- |----|-------|------------|
-- | 2 | Bob | IT |
-- | 4 | David | Marketing |
-- | 5 | Eve | HR |

-- UNION: 去除重复行
SELECT id, name, department
FROM employees_2023
UNION
SELECT id, name, department
FROM employees_2024;

-- 查询结果(Bob只出现一次)
-- | id | name | department |
-- |----|---------|------------|
-- | 1 | Alice | HR |
-- | 2 | Bob | IT |
-- | 3 | Charlie | Finance |
-- | 4 | David | Marketing |
-- | 5 | Eve | HR |

-- UNION ALL: 保留重复行
SELECT id, name, department
FROM employees_2023
UNION ALL
SELECT id, name, department
FROM employees_2024;

-- 查询结果(Bob出现两次)
-- | id | name | department |
-- |----|---------|------------|
-- | 1 | Alice | HR |
-- | 2 | Bob | IT |
-- | 3 | Charlie | Finance |
-- | 2 | Bob | IT |
-- | 4 | David | Marketing |
-- | 5 | Eve | HR |

UNION: 自动去重,性能较慢
UNION ALL: 保留所有行,性能更快

常见应用场景

-- 合并历史数据和当前数据
SELECT * FROM orders_archive
UNION ALL
SELECT * FROM orders_current;

-- 合并不同条件的查询结果
SELECT id, name FROM employees WHERE department = 'IT'
UNION
SELECT id, name FROM contractors WHERE status = 'active';

-- 生成报表数据
SELECT 'Total Sales' AS metric, SUM(amount) AS value FROM sales
UNION ALL
SELECT 'Average Sale', AVG(amount) FROM sales
UNION ALL
SELECT 'Max Sale', MAX(amount) FROM sales;
  • 列数必须相同: 所有SELECT语句必须返回相同数量的列
  • 数据类型兼容: 对应列的数据类型必须兼容
  • 列名: 结果集使用第一个SELECT的列名
  • 性能: UNION需要去重,比UNION ALL慢。如果确定没有重复或不需要去重,使用UNION ALL
  • ORDER BY: 只能在最后一个查询后使用,对整个结果集排序
  • NULL值: NULL被视为相等,会被UNION去重

25. INTERSECT 交集

  • INTERSECTSQL中的一个运算符,用于返回两个或多个SELECT语句结果集的交集。换句话说,它返回的是在所有参与查询的结果集中都存在的行。
-- 基本语法
SELECT column1, column2, ...
FROM table1
INTERSECT
SELECT column1, column2, ...
FROM table2;
  • 返回共有行: 只返回在所有SELECT语句结果集中都存在的行
  • 自动去重: 会自动去除结果中的重复行
-- employees_2023
-- | id | name | department |
-- |----|---------|------------|
-- | 1 | Alice | HR |
-- | 2 | Bob | IT |
-- | 3 | Charlie | Finance |

-- employees_2024
-- | id | name | department |
-- |----|-------|------------|
-- | 2 | Bob | IT |
-- | 3 | Charlie | Finance |
-- | 4 | David | Marketing |

-- 查询在2023年和2024年都存在的员工
SELECT id, name, department
FROM employees_2023
INTERSECT
SELECT id, name, department
FROM employees_2024;

-- 查询结果
-- | id | name | department |
-- |----|---------|------------|
-- | 2 | Bob | IT |
-- | 3 | Charlie | Finance |
  • 列数和数据类型必须一致
  • 自动去除重复行
  • MySQL 不支持 INTERSECT,需要用 INNER JOINWHERE EXISTS 实现
  • PostgreSQL、SQL Server、Oracle 支持 INTERSECT

26. EXCEPT 差集

  • EXCEPTSQL中的一个运算符,用于返回两个SELECT语句结果集的差集,即返回第一个查询的结果集中有但第二个查询的结果集中没有的行。
-- 基本语法
SELECT column1, column2, ...
FROM table1
EXCEPT
SELECT column1, column2, ...
FROM table2;
  • 返回差集: 返回第一个查询结果集中有,但第二个查询结果集中没有的行
  • 自动去重: 会自动去除结果中的重复行
-- employees_2023
-- | id | name | department |
-- |----|---------|------------|
-- | 1 | Alice | HR |
-- | 2 | Bob | IT |
-- | 3 | Charlie | Finance |

-- employees_2024
-- | id | name | department |
-- |----|---------|------------|
-- | 2 | Bob | IT |
-- | 4 | David | Marketing |

-- 查询在2023年存在但在2024年不存在的员工
SELECT id, name, department
FROM employees_2023
EXCEPT
SELECT id, name, department
FROM employees_2024;

-- 查询结果
-- | id | name | department |
-- |----|---------|------------|
-- | 1 | Alice | HR |
-- | 3 | Charlie | Finance |
  • 列数和数据类型必须一致
  • 自动去除重复行
  • 顺序很重要: A EXCEPT BB EXCEPT A
  • MySQL 使用 EXCEPTMINUS(MySQL 8.0.31+支持EXCEPT
  • Oracle 使用 MINUS 代替 EXCEPT
  • PostgreSQL、SQL Server 支持 EXCEPT

分组集

27. GROUPING SETS

  • GROUPING SETSSQL中的一种高级分组功能,用于在单个查询中实现多种分组方式。它允许你在一次查询中指定多个分组标准,并返回每个分组标准的聚合结果。
-- 基本语法
SELECT column1, column2, ..., aggregate_function(column)
FROM table_name
GROUP BY GROUPING SETS ( (column1), (column2), ...);
-- sales
-- | id | employee_id | product_id | sale_amount |
-- |----|-------------|------------|-------------|
-- | 1 | 101 | 1 | 100 |
-- | 2 | 102 | 1 | 200 |
-- | 3 | 101 | 2 | 150 |
-- | 4 | 103 | 2 | 250 |

-- 按员工、按产品、总计三种方式分组
SELECT employee_id,
product_id,
SUM(sale_amount) AS total_sales
FROM sales
GROUP BY GROUPING SETS (
(employee_id),
(product_id),
()
);

-- 查询结果
-- | employee_id | product_id | total_sales |
-- |-------------|------------|-------------|
-- | 101 | NULL | 250 |
-- | 102 | NULL | 200 |
-- | 103 | NULL | 250 |
-- | NULL | 1 | 300 |
-- | NULL | 2 | 400 |
-- | NULL | NULL | 700 |

NULL表示该维度未参与分组。例如第一组是按employee_id分组,所以product_id显示为NULL

  • NULL表示该列未参与分组
  • 比多个UNION ALL查询更高效
  • PostgreSQL、SQL Server、Oracle 支持
  • MySQL 不支持 GROUPING SETS,需要用UNION ALL实现
-- 对比普通 GROUP BY
SELECT employee_id, product_id, SUM(sale_amount) AS total_sales
FROM sales
GROUP BY employee_id, product_id;

-- 查询结果:只有细粒度的组合
-- | employee_id | product_id | total_sales |
-- |-------------|------------|-------------|
-- | 101 | 1 | 100 |
-- | 101 | 2 | 150 |
-- | 102 | 1 | 200 |
-- | 103 | 2 | 250 |

28. CUBE 分组

  • CUBESQL中的一个扩展分组功能,用于在单个查询中生成所有可能组合的分组和聚合结果。CUBEGROUP BY子句的一部分,提供了一种多维分析的方式,特别适合在需要对数据进行多维度汇总的场景中使用。
-- 基本语法
SELECT column1, column2, ..., aggregate_function(column)
FROM table_name
GROUP BY CUBE (column1, column2, ...);
-- sales
-- | region | product | sale_amount |
-- |--------|---------|-------------|
-- | North | A | 100 |
-- | North | B | 150 |
-- | South | A | 200 |
-- | South | B | 250 |

-- 生成所有可能的分组组合
SELECT region,
product,
SUM(sale_amount) AS total_sales
FROM sales
GROUP BY CUBE (region, product);

-- 查询结果
-- | region | product | total_sales |
-- |--------|---------|-------------|
-- | North | A | 100 | ← (region, product)
-- | North | B | 150 | ← (region, product)
-- | North | NULL | 250 | ← (region)
-- | South | A | 200 | ← (region, product)
-- | South | B | 250 | ← (region, product)
-- | South | NULL | 450 | ← (region)
-- | NULL | A | 300 | ← (product)
-- | NULL | B | 400 | ← (product)
-- | NULL | NULL | 700 | ← ()

生成的分组组合

对于CUBE(A, B),生成 2^2 = 4 种组合

  • (A, B) - 按两列分组
  • (A) - 只按A分组
  • (B) - 只按B分组
  • () - 不分组(总计)

对于CUBE(A, B, C),生成 2^3 = 8 种组合。

  • CUBE(n列)生成 2^n 种分组组合,列数多时结果集会很大
  • PostgreSQL、SQL Server、Oracle 支持
  • MySQL 不支持 CUBE,需要用GROUPING SETSUNION ALL实现
  • 适合多维分析和数据透视表场景

29. ROLLUP 分组

  • ROLLUPSQL中的一个扩展分组功能,用于生成基于层次结构的分组和聚合结果。它是GROUP BY子句的扩展,用于在一个查询中生成从详细到汇总的多个级别的聚合结果。ROLLUP常用于需要对数据进行层次分析的场景,例如销售数据按区域和时间进行汇总时。
-- 基本语法
SELECT column1, column2, ..., aggregate_function(column)
FROM table_name
GROUP BY ROLLUP (column1, column2, ...);
-- sales
-- | region | product | sale_amount |
-- |--------|---------|-------------|
-- | North | A | 100 |
-- | North | B | 150 |
-- | South | A | 200 |
-- | South | B | 250 |

-- 按区域和产品生成层次汇总
SELECT region,
product,
SUM(sale_amount) AS total_sales
FROM sales
GROUP BY ROLLUP (region, product);

-- 查询结果
-- | region | product | total_sales |
-- |--------|---------|-------------|
-- | North | A | 100 | ← (region, product)
-- | North | B | 150 | ← (region, product)
-- | North | NULL | 250 | ← (region) 小计
-- | South | A | 200 | ← (region, product)
-- | South | B | 250 | ← (region, product)
-- | South | NULL | 450 | ← (region) 小计
-- | NULL | NULL | 700 | ← () 总计

生成的分组组合

对于ROLLUP(A, B, C),生成层次化的组合

  • (A, B, C) - 最详细
  • (A, B) - 去掉最右边的C
  • (A) - 再去掉B
  • () - 总计

注意: ROLLUP有顺序的,从左到右逐层汇总。

ROLLUP vs CUBE

-- ROLLUP: 层次汇总,生成 n+1 种组合
SELECT region, product, SUM(sale_amount)
FROM sales
GROUP BY ROLLUP (region, product);
-- 生成: (region, product), (region), ()

-- CUBE: 所有组合,生成 2^n 种组合
SELECT region, product, SUM(sale_amount)
FROM sales
GROUP BY CUBE (region, product);
-- 生成: (region, product), (region), (product), ()

关键区别: ROLLUP不会生成(product)这种跳过region的组合。

  • ROLLUP(n列)生成 n+1 种分组(比CUBE少)
  • 顺序很重要:ROLLUP(A, B)ROLLUP(B, A)
  • 适合层次化数据(如:年→月→日,国家→省→市)
  • PostgreSQL、SQL Server、Oracle 支持
  • MySQL 8.0+ 支持

子查询

30. PostgreSQL 子查询

  • 子查询是嵌套在另一个查询中的SELECT语句。子查询可以出现在SELECTFROMWHEREHAVING等子句中,用于提供动态的过滤条件、计算值或临时表。
-- 基本语法
-- WHERE子句中的子查询
SELECT column1, column2
FROM table1
WHERE column1 IN (SELECT column1 FROM table2);

-- FROM子句中的子查询
SELECT *
FROM (SELECT column1, column2 FROM table1) AS subquery;

-- SELECT子句中的子查询
SELECT column1, (SELECT MAX(column2) FROM table2) AS max_value
FROM table1;
-- employees
-- | id | name | salary | department_id |
-- |----|---------|--------|---------------|
-- | 1 | Alice | 60000 | 1 |
-- | 2 | Bob | 55000 | 2 |
-- | 3 | Charlie | 70000 | 1 |
-- | 4 | David | 50000 | 2 |

-- departments
-- | id | name |
-- |----|------------|
-- | 1 | IT |
-- | 2 | HR |

-- WHERE子句:查询工资高于平均工资的员工
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

-- 查询结果
-- | name | salary |
-- |---------|--------|
-- | Alice | 60000 |
-- | Charlie | 70000 |

-- FROM子句:查询每个部门的平均工资
SELECT d.name, avg_salary
FROM departments d
JOIN (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
) dept_avg ON d.id = dept_avg.department_id;

-- SELECT子句:显示每个员工及其部门的平均工资
SELECT name,
salary,
(SELECT AVG(salary)
FROM employees e2
WHERE e2.department_id = e1.department_id) AS dept_avg
FROM employees e1;

-- IN子句:查询IT部门的员工
SELECT name, salary
FROM employees
WHERE department_id IN (
SELECT id FROM departments WHERE name = 'IT'
);

-- EXISTS子句:查询有员工的部门
SELECT name
FROM departments d
WHERE EXISTS (
SELECT 1 FROM employees e WHERE e.department_id = d.id
);

ANY/ALL 操作符

-- ANY:满足任意一个条件
SELECT name, salary
FROM employees
WHERE salary > ANY (SELECT salary FROM employees WHERE department_id = 2);

-- ALL:满足所有条件
SELECT name, salary
FROM employees
WHERE salary > ALL (SELECT salary FROM employees WHERE department_id = 2);
  • 子查询必须用括号包裹
  • SELECT子句中的子查询必须返回单个值
  • FROM子句中的子查询必须有别名
  • 相关子查询会对外部查询的每一行执行一次,性能较差
  • 能用JOIN的地方优先用JOIN,通常性能更好

31. ANY

  • ANY运算符用于将一个值与子查询返回的一组值进行比较。如果子查询的任何值满足条件,则返回true
-- 基本形式
SELECT title
FROM film
WHERE length > ANY(
SELECT length FROM film WHERE category_id = 1
);
-- 查找每个类别的最大电影时长
SELECT MAX(length)
FROM film
INNER JOIN film_category USING(film_id)
GROUP BY category_id;

-- 查找长度大于等于任何类别最大长度的电影
SELECT title
FROM film
WHERE length >= ANY(
SELECT MAX(length)
FROM film
INNER JOIN film_category USING(film_id)
GROUP BY category_id
);
  • 子查询必须返回单列
  • 如果子查询返回空结果集,整个查询返回空结果集
  • = ANY等价于IN
  • <> ANY不等价于NOT IN
    • x <> ANY(a, b, c)等价于x <> a OR x <> b OR x <> c

32. ALL

  • ALL运算符用于将一个值与子查询返回的所有值进行比较。只有当值满足与子查询返回的所有值的比较条件时,才返回true
-- 基本形式
SELECT title
FROM film
WHERE length > ALL(
SELECT length FROM film WHERE category_id = 1
);
  • ALL 运算符前面必须有比较运算符:=!=>>=<<=
  • ALL 运算符后面必须跟一个子查询,子查询也必须用括号括起来
-- 查询按电影评级分组的平均长度
SELECT ROUND(AVG(length), 2) AS avg_length
FROM film
GROUP BY rating
ORDER BY avg_length DESC;

-- 查找长度大于所有平均长度的电影
SELECT film_id,
title,
length
FROM film
WHERE length > ALL(
SELECT ROUND(AVG(length), 2)
FROM film
GROUP BY rating
)
ORDER BY length;
  • 子查询必须返回单列
  • 如果子查询返回空结果集,ALL 运算符始终返回 true
  • > ALL 等价于”大于最大值”
  • < ALL 等价于”小于最小值”
  • != ALL 等价于 NOT IN

33. EXISTS

  • EXISTS运算符是一个布尔运算符,用于测试子查询中是否存在行。
-- 基本形式
SELECT column1
FROM table_1
WHERE EXISTS(
SELECT 1
FROM table_2
WHERE column_2 = table_1.column_1
);
-- 查找至少有一笔付款金额大于11的客户
SELECT first_name, last_name
FROM customer c
WHERE EXISTS(
SELECT 1
FROM payment p
WHERE p.customer_id = c.customer_id
AND amount > 11
)
ORDER BY first_name, last_name;

-- NOT EXISTS: 查找没有支付过金额大于11的客户
SELECT first_name, last_name
FROM customer c
WHERE NOT EXISTS(
SELECT 1
FROM payment p
WHERE p.customer_id = c.customer_id
AND amount > 11
)
ORDER BY first_name, last_name;

-- EXISTS 和 NULL
SELECT first_name, last_name
FROM customer
WHERE EXISTS(SELECT NULL)
ORDER BY first_name, last_name;

子查询返回NULL时,EXISTS返回 true,因此查询返回customer表中的所有行。

  • EXISTS只关心是否有行返回,不关心行的内容
  • NOT EXISTSEXISTS相反
  • 如果子查询返回NULLEXISTS返回 true
  • EXISTS通常比IN性能更好,特别是子查询返回大量数据时

公共表表达式

34. WITH 查询 (CTE)

  • 公共表表达式(CTE,Common Table Expression)是一个临时结果集,您可以在另一个 SQL 语句中引用它。CTE 仅在查询执行期间存在。
-- 基本形式
WITH cte_film AS (
SELECT film_id,
title,
CASE
WHEN length < 30 THEN 'Short'
WHEN length < 90 THEN 'Medium'
ELSE 'Long'
END AS length
FROM film
)
SELECT film_id, title, length
FROM cte_film
WHERE length = 'Long'
ORDER BY title;
  • 首先,指定 CTE 的名称,后跟可选的列列表
  • 其次,在WITH子句主体内,指定返回结果集的查询
  • 第三,在SELECTINSERTUPDATEDELETE等语句中,将 CTE 用作表或视图
-- 将 CTE 与表连接
WITH cte_rental AS (
SELECT staff_id,
COUNT(rental_id) AS rental_count
FROM rental
GROUP BY staff_id
)
SELECT s.staff_id,
first_name,
last_name,
rental_count
FROM staff s
INNER JOIN cte_rental USING (staff_id);

-- 将 CTE 与窗口函数结合使用
WITH cte_film AS (
SELECT film_id,
title,
rating,
length,
RANK() OVER (
PARTITION BY rating
ORDER BY length DESC
) AS length_rank
FROM film
)
SELECT *
FROM cte_film
WHERE length_rank = 1;

-- 多个 CTE
WITH cte1 AS (
SELECT * FROM table1 WHERE condition1
),
cte2 AS (
SELECT * FROM table2 WHERE condition2
)
SELECT *
FROM cte1
JOIN cte2 ON cte1.id = cte2.id;
  • CTE 是临时的,仅在查询执行期间存在
  • CTE 可以引用自身(递归 CTE)
  • 可以在一个查询中定义多个 CTE
  • CTE 通常用于简化复杂连接和子查询

修改数据

35. INSERT

  • PostgreSQL 的INSERT语句允许您将新行插入表中
-- 基本形式
INSERT INTO table_name(column1, column2, …)
VALUES (value1, value2, …);

-- 带 RETURNING 子句
INSERT INTO table_name(column1, column2, …)
VALUES (value1, value2, …)
RETURNING *;
  • 首先,在INSERT INTO关键字后,指定要插入数据的表名和列列表
  • 其次,在VALUES关键字后提供逗号分隔的值列表
  • 列和值列表中的列和值必须具有相同的顺序
  • RETURNING子句用于返回插入行的信息
-- 创建示例表
CREATE TABLE links (
id SERIAL PRIMARY KEY,
url VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
description VARCHAR(255),
last_update DATE
);

-- 将单行插入表中
INSERT INTO links (url, name)
VALUES('https://www.rockdata.net/tutorial/', 'PostgreSQL Tutorial');

-- 插入包含单引号的字符串
INSERT INTO links (url, name)
VALUES('http://www.oreilly.com', 'O''Reilly Media');

-- 插入日期值
INSERT INTO links (url, name, last_update)
VALUES('https://www.google.com', 'Google', '2013-06-01');

-- 获取最后插入的ID
INSERT INTO links (url, name)
VALUES('http://www.postgresql.org', 'PostgreSQL')
RETURNING id;

-- 返回整个插入行
INSERT INTO links (url, name)
VALUES('http://www.example.com', 'Example')
RETURNING *;

-- 返回特定列
INSERT INTO links (url, name, description)
VALUES('http://www.test.com', 'Test', 'Test Description')
RETURNING id, name;

获取最后插入ID

  • PostgreSQL
    • RETURNING id - 直接返回插入的ID
    • currval(‘sequence_name’) - 获取序列当前值
    • lastval() - 获取最后使用的序列值
-- 使用 RETURNING (推荐)
INSERT INTO table_name (column1) VALUES (value1) RETURNING id;

-- 使用序列函数
SELECT currval('table_name_id_seq');
SELECT lastval();
  • SQL Server
    • SCOPE_IDENTITY() - 当前作用域最后插入的标识值(最常用)
    • @@IDENTITY - 当前会话最后插入的标识值
    • IDENT_CURRENT(‘table’) - 指定表的最后标识值
-- 使用 OUTPUT
INSERT INTO table_name (column1) OUTPUT INSERTED.id VALUES (value1);

-- 使用标识函数
SELECT SCOPE_IDENTITY(); -- 当前作用域 (推荐)
SELECT @@IDENTITY; -- 当前会话
SELECT IDENT_CURRENT('table_name'); -- 指定表

36. INSERT 插入多行

  • 要使用单个INSERT语句将多行插入表中,可以在VALUES子句中提供多个值列表
-- 基本形式
INSERT INTO table_name (column_list)
VALUES
(value_list_1),
(value_list_2),
...
(value_list_n);

-- 带 RETURNING 子句
INSERT INTO table_name (column_list)
VALUES
(value_list_1),
(value_list_2),
...
(value_list_n)
RETURNING * | output_expression;
  • 首先,在INSERT INTO关键字后指定要插入数据的表名
  • 其次,在表名称后面的括号中列出表中所需的列
  • 第三,在VALUES关键字后面提供以逗号分隔的行列表
-- employees
-- | employee_id | name | department | salary |
-- |-------------|---------|------------|--------|
-- | 1 | Alice | IT | 60000 |
-- | 2 | Bob | Sales | 55000 |

-- 插入多行
INSERT INTO employees (name, department, salary)
VALUES
('Charlie', 'IT', 65000),
('David', 'HR', 50000),
('Eve', 'Sales', 58000);

-- 插入多行并返回所有列
INSERT INTO employees (name, department, salary)
VALUES
('Frank', 'IT', 62000),
('Grace', 'Marketing', 56000)
RETURNING *;

-- 插入多行并只返回 ID
INSERT INTO employees (name, department, salary)
VALUES
('Henry', 'Finance', 70000),
('Ivy', 'HR', 52000)
RETURNING employee_id;

-- 插入多行并返回特定列
INSERT INTO employees (name, department, salary)
VALUES
('Jack', 'Sales', 59000),
('Kate', 'IT', 67000)
RETURNING employee_id, name, department;
  • 使用单个INSERT语句插入多行比多个单行INSERT语句性能更好
  • 每个值列表必须包含相同数量的值,且顺序与列列表匹配
  • 可以使用RETURNING子句返回插入的行或特定列
  • PostgreSQL返回格式为INSERT 0 n,其中n是插入的行数

37. UNNEST 批量插入

  • UNNEST是PostgreSQL中用于将数组展开成行的函数,常用于批量插入数据 (Bulk Upsert/Insert)
-- 基本形式
INSERT INTO table_name (column1, column2, ...)
SELECT * FROM UNNEST(
ARRAY[value1_1, value1_2, ...],
ARRAY[value2_1, value2_2, ...]
);

-- 带 RETURNING 子句
INSERT INTO table_name (column1, column2, ...)
SELECT * FROM UNNEST(
ARRAY[value1_1, value1_2, ...],
ARRAY[value2_1, value2_2, ...]
)
RETURNING *;
  • UNNEST将多个数组并行展开成行
  • 每个数组对应一列
  • 所有数组必须具有相同的长度
-- 创建示例表
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
category VARCHAR(100)
);

-- 使用 UNNEST 批量插入
INSERT INTO products (name, price, category)
SELECT * FROM UNNEST(
ARRAY['Laptop', 'Mouse', 'Keyboard'],
ARRAY[999.99, 29.99, 79.99],
ARRAY['Electronics', 'Electronics', 'Electronics']
);

-- 使用 UNNEST 并返回插入的行
INSERT INTO products (name, price, category)
SELECT * FROM UNNEST(
ARRAY['Monitor', 'Webcam'],
ARRAY[299.99, 89.99],
ARRAY['Electronics', 'Electronics']
)
RETURNING *;

-- 使用 UNNEST 只返回 ID
INSERT INTO products (name, price, category)
SELECT * FROM UNNEST(
ARRAY['Headphones', 'Speaker'],
ARRAY[149.99, 199.99],
ARRAY['Electronics', 'Electronics']
)
RETURNING id;

UNNEST vs VALUES

-- 方式1: 传统 VALUES
INSERT INTO products (name, price)
VALUES
('Laptop', 999.99),
('Mouse', 29.99),
('Keyboard', 79.99);

-- 方式2: UNNEST (等价)
INSERT INTO products (name, price)
SELECT * FROM UNNEST(
ARRAY['Laptop', 'Mouse', 'Keyboard'],
ARRAY[999.99, 29.99, 79.99]
);
  • 所有数组必须具有相同的长度,否则会报错
  • UNNEST适合从程序传入数组参数的场景
  • 对于大批量插入,UNNEST可能比多个VALUES性能更好
  • 数组元素类型需要与目标列类型匹配
  • 可以使用类型转换,如ARRAY[...]::type[]

38. UPDATE

  • PostgreSQL 的UPDATE语句允许修改表中的现有数据
-- 基本形式
UPDATE table_name
SET column1 = value1,
column2 = value2,
...
WHERE condition;

-- 带 RETURNING 子句
UPDATE table_name
SET column1 = value1,
column2 = value2,
...
WHERE condition
RETURNING * | output_expression;
-- employees
-- | employee_id | name | salary | department |
-- |-------------|---------|--------|------------|
-- | 1 | Alice | 60000 | IT |
-- | 2 | Bob | 55000 | Sales |
-- | 3 | Charlie | 50000 | Sales |
-- | 4 | David | 65000 | IT |

-- 更新单行
UPDATE employees
SET salary = 70000
WHERE employee_id = 3;

-- 更新多列
UPDATE employees
SET salary = 72000,
department = 'IT'
WHERE employee_id = 3;

-- 更新并返回更新后的行
UPDATE employees
SET salary = salary * 1.1
WHERE department = 'Sales'
RETURNING *;

-- 只返回特定列
UPDATE employees
SET department = 'Marketing'
WHERE employee_id = 2
RETURNING employee_id, name, department;

-- 使用表达式更新
UPDATE employees
SET salary = salary + 5000
WHERE salary < 60000;
  • WHERE子句是可选的,但省略时会更新所有行,需谨慎使用
  • UPDATE语句返回UPDATE count,其中count是更新的行数
  • SET子句中未出现的列保留其原始值
  • 可以使用RETURNING子句返回更新后的行
  • 可以在SET子句中使用表达式或其他列的值

39. UPDATE JOIN

  • 有时,需要根据另一个表中的值更新表中的数据。在这种情况下,可以使用 PostgreSQL 的 UPDATE 连接语法。
-- 基本形式
UPDATE t1
SET t1.c1 = new_value
FROM t2
WHERE t1.c2 = t2.c2;

-- 使用表别名
UPDATE t1
SET c1 = new_value
FROM t2
WHERE t1.c2 = t2.c2;
  • FROM子句中指定要连接的表
  • WHERE子句中提供连接条件
  • FROM子句必须紧接在SET子句之后出现
-- 创建 product_segment 表
CREATE TABLE product_segment (
id SERIAL PRIMARY KEY,
segment VARCHAR NOT NULL,
discount NUMERIC(4, 2)
);

INSERT INTO product_segment (segment, discount)
VALUES
('Grand Luxury', 0.05),
('Luxury', 0.06),
('Mass', 0.10);

-- 创建 product 表
CREATE TABLE product(
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
price NUMERIC(10, 2),
net_price NUMERIC(10, 2),
segment_id INT NOT NULL,
FOREIGN KEY(segment_id) REFERENCES product_segment(id)
);

INSERT INTO product (name, price, segment_id)
VALUES
('diam', 804.89, 1),
('vestibulum aliquet', 228.55, 3),
('lacinia erat', 366.45, 2),
('scelerisque quam turpis', 145.33, 3);

-- 根据产品细分的折扣计算净价
UPDATE product
SET net_price = price - price * discount
FROM product_segment
WHERE product.segment_id = product_segment.id;

-- 使用表别名简化查询
UPDATE product p
SET net_price = price - price * discount
FROM product_segment s
WHERE p.segment_id = s.id;

-- 更新并返回结果
UPDATE product p
SET net_price = price - price * discount
FROM product_segment s
WHERE p.segment_id = s.id
RETURNING p.id, p.name, p.price, p.net_price;
  • FROM子句允许在UPDATE中引用其他表
  • 可以使用任何类型的连接(INNER JOIN、LEFT JOIN等)
  • 使用表别名可以简化查询
  • 可以结合RETURNING子句返回更新后的行
  • 如果连接产生多个匹配行,可能会导致意外的更新结果

40. DELETE

  • PostgreSQL 的DELETE语句允许从表中删除一行或多行
-- 基本形式
DELETE FROM table_name
WHERE condition;

-- 带 RETURNING 子句
DELETE FROM table_name
WHERE condition
RETURNING * | select_list;
-- employees
-- | id | name | department | salary |
-- |----|---------|------------|--------|
-- | 1 | Alice | IT | 60000 |
-- | 2 | Bob | HR | 55000 |
-- | 3 | Charlie | IT | 70000 |
-- | 4 | David | Sales | 50000 |

-- 删除一行
DELETE FROM employees
WHERE id = 4;

-- 删除并返回删除的行
DELETE FROM employees
WHERE id = 3
RETURNING *;

-- 删除多行
DELETE FROM employees
WHERE department = 'IT';

-- 只返回特定列
DELETE FROM employees
WHERE id = 2
RETURNING id, name;

-- 删除所有行
DELETE FROM employees;
  • WHERE子句是可选的,但省略时会删除所有行,需谨慎使用
  • DELETE语句返回删除的行数,如果未删除任何行则返回 0
  • 可以使用RETURNING子句返回已删除的行
  • DELETE语句仅删除数据,不修改表结构
  • 如果要修改表结构(如删除列),应使用ALTER TABLE语句
  • 删除大量数据时,考虑使用TRUNCATE以获得更好的性能

41. TRUNCATE

  • TRUNCATE语句用于快速删除表中的所有数据。与DELETE相比,TRUNCATE性能更高,但功能更受限
-- 基本形式
TRUNCATE TABLE table_name;

-- 多个表
TRUNCATE TABLE table_name1, table_name2, ...;

-- 重置自增序列
TRUNCATE TABLE table_name RESTART IDENTITY;

-- 级联删除
TRUNCATE TABLE table_name CASCADE;
  • TRUNCATEDELETE快得多
  • TRUNCATE不能使用WHERE子句
  • TRUNCATE会重置自增序列(使用RESTART IDENTITY
  • TRUNCATE不触发DELETE触发器
  • TRUNCATE需要表的所有者权限或TRUNCATE权限
-- orders
-- | order_id | customer_id | amount | order_date |
-- |----------|-------------|--------|------------|
-- | 1 | 101 | 250.00 | 2024-01-15 |
-- | 2 | 102 | 180.00 | 2024-01-16 |
-- | 3 | 101 | 320.00 | 2024-01-17 |

-- 删除所有数据
TRUNCATE TABLE orders;

-- 删除数据并重置自增ID
TRUNCATE TABLE orders RESTART IDENTITY;

-- 同时清空多个表
TRUNCATE TABLE orders, order_items;

-- 级联删除(如果有外键引用)
TRUNCATE TABLE orders CASCADE;
  • TRUNCATE不能在有外键约束的表上使用,除非使用CASCADE
  • TRUNCATE操作不能回滚(在某些事务配置下)
  • TRUNCATE不记录单独的行删除日志
  • 使用RESTART IDENTITY重置序列从1开始
  • 使用CONTINUE IDENTITY保持序列当前值(默认)

42. UPSERT

  • 在关系数据库中,upsert(更新或插入的组合)指的是当您向表中插入新行时,如果该行已存在,PostgreSQL 将更新该行,否则,它将插入新行
INSERT INTO table_name(column_list) 
VALUES(value_list)
ON CONFLICT target action;

target 可以是

  • (column_name) - 列名称
  • ON CONSTRAINT constraint_name - UNIQUE 约束的名称
  • WHERE predicate - 带有谓词的 WHERE 子句

action 可以是

  • DO NOTHING - 如果该行已存在,则不执行任何操作
  • DO UPDATE SET column_1 = value_1, ... WHERE condition - 更新表中的字段
-- 创建示例表
CREATE TABLE customers (
customer_id SERIAL PRIMARY KEY,
name VARCHAR UNIQUE,
email VARCHAR NOT NULL,
active BOOL NOT NULL DEFAULT TRUE
);

INSERT INTO customers (name, email)
VALUES
('IBM', 'contact@ibm.com'),
('Microsoft', 'contact@microsoft.com'),
('Intel', 'contact@intel.com');

-- customers
-- | customer_id | name | email | active |
-- |-------------|-----------|------------------------|--------|
-- | 1 | IBM | contact@ibm.com | true |
-- | 2 | Microsoft | contact@microsoft.com | true |
-- | 3 | Intel | contact@intel.com | true |

-- DO NOTHING: 如果冲突则忽略
INSERT INTO customers (name, email)
VALUES('Microsoft', 'hotline@microsoft.com')
ON CONFLICT (name)
DO NOTHING;

-- 使用约束名称
INSERT INTO customers (name, email)
VALUES('Microsoft', 'hotline@microsoft.com')
ON CONFLICT ON CONSTRAINT customers_name_key
DO NOTHING;

-- DO UPDATE: 如果冲突则更新
INSERT INTO customers (name, email)
VALUES('Microsoft', 'hotline@microsoft.com')
ON CONFLICT (name)
DO UPDATE SET email = EXCLUDED.email;

-- 拼接新旧值
INSERT INTO customers (name, email)
VALUES('Microsoft', 'hotline@microsoft.com')
ON CONFLICT (name)
DO UPDATE SET email = EXCLUDED.email || ';' || customers.email;

-- 更新多个列
INSERT INTO customers (name, email, active)
VALUES('IBM', 'newemail@ibm.com', false)
ON CONFLICT (name)
DO UPDATE SET
email = EXCLUDED.email,
active = EXCLUDED.active;

EXCLUDED 关键字

  • EXCLUDED是一个特殊的表引用,代表要插入的行的值。
-- EXCLUDED.column_name 表示要插入的新值
-- table_name.column_name 表示表中已存在的旧值
INSERT INTO customers (name, email)
VALUES('Microsoft', 'new@microsoft.com')
ON CONFLICT (name)
DO UPDATE SET
email = EXCLUDED.email; -- 使用新值
  • ON CONFLICT子句仅在 PostgreSQL 9.5+ 可用
  • 必须有 UNIQUE 约束或 PRIMARY KEY 才能使用 ON CONFLICT
  • DO NOTHING不返回任何内容
  • DO UPDATE可以使用WHERE子句来限制更新条件
  • EXCLUDED引用要插入的行的值
  • 类似 MySQL 的INSERT ON DUPLICATE KEY UPDATE

43. MERGE

  • PostgreSQL 15 引入了MERGE语句,可将INSERTUPDATEDELETE操作合并到一个语句中,来简化数据操作。MERGE语句通常被称为 UPSERT 语句
MERGE INTO target_table
USING source_query
ON merge_condition
WHEN MATCHED [AND condition] THEN {merge_update | merge_delete | DO NOTHING}
WHEN NOT MATCHED [AND condition] THEN {merge_insert | DO NOTHING};
-- 创建示例表
CREATE TABLE leads(
lead_id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
active BOOL NOT NULL DEFAULT TRUE
);

CREATE TABLE customers(
customer_id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
active BOOL NOT NULL DEFAULT TRUE
);

-- 插入数据到 leads
INSERT INTO leads(name, email)
VALUES
('John Doe', 'john.doe@gmail.com'),
('Jane Doe', 'jane.doe@yahoo.com');

-- 示例1: 插入不匹配的行
MERGE INTO customers c
USING leads l ON c.email = l.email
WHEN NOT MATCHED THEN
INSERT (name, email)
VALUES(l.name, l.email);

-- 示例2: 插入和更新
MERGE INTO customers c
USING leads l ON c.email = l.email
WHEN NOT MATCHED THEN
INSERT (name, email)
VALUES(l.name, l.email)
WHEN MATCHED THEN
UPDATE SET
name = l.name,
email = l.email;

-- 示例3: 插入、更新和删除
MERGE INTO customers c
USING leads l ON c.email = l.email
WHEN NOT MATCHED THEN
INSERT (name, email)
VALUES(l.name, l.email)
WHEN MATCHED AND l.active = false THEN
DELETE
WHEN MATCHED AND l.active = true THEN
UPDATE SET
name = l.name,
email = l.email;
  • MERGE语句仅在 PostgreSQL 15+ 可用
  • 早期版本使用INSERT ... ON CONFLICT
  • 成功后返回MERGE total_count,其中total_count是插入、更新或删除的行的总数
  • MERGE可以在一个语句中执行 INSERT、UPDATE 和 DELETE
  • 适用于表之间的数据同步
  • 可以使用DO NOTHING忽略特定条件的行

事务

44. TRANSACTION

  • 数据库事务是由一个或多个操作组成的单个工作单元。PostgreSQL 事务是原子的、一致的、隔离的和持久的(ACID)
-- 开始事务
BEGIN;
-- 或
BEGIN TRANSACTION;
-- 或
BEGIN WORK;

-- 提交事务
COMMIT;
-- 或
COMMIT TRANSACTION;
-- 或
COMMIT WORK;

-- 回滚事务
ROLLBACK;
-- 或
ROLLBACK TRANSACTION;
-- 或
ROLLBACK WORK;
  • 原子性 (Atomicity): 事务以全有或全无的方式完成
  • 一致性 (Consistency): 写入数据库的数据更改必须有效并遵循预定义的规则
  • 隔离性 (Isolation): 事务完整性如何对其他事务可见
  • 持久性 (Durability): 已提交的事务将永久存储在数据库中
-- accounts
-- | id | name | balance |
-- |----|-------|----------|
-- | 1 | Bob | 10000.00 |
-- | 2 | Alice | 10000.00 |

-- 银行转账示例:从 Bob 转账 1000 到 Alice
BEGIN;

-- 从 Bob 账户扣款
UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;

-- 给 Alice 账户加款
UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;

-- 提交事务
COMMIT;

-- 查看结果
SELECT id, name, balance FROM accounts;
-- | id | name | balance |
-- |----|-------|----------|
-- | 1 | Bob | 9000.00 |
-- | 2 | Alice | 11000.00 |
  • 事务开始后,更改对其他会话不可见,直到提交
  • COMMIT使更改持久化,ROLLBACK撤销更改
  • 会话断开连接时,未提交的事务会自动回滚
  • 建议保持事务尽可能短

管理表

45. CREATE TABLE

  • 关系数据库由多个相关表组成。表格由行和列组成。要创建新表,请使用CREATE TABLE语句
CREATE TABLE [IF NOT EXISTS] table_name (
column1 datatype(length) column_constraint,
column2 datatype(length) column_constraint,
column3 datatype(length) column_constraint,
table_constraints
);

  • 首先,在CREATE TABLE关键字后指定表的名称
  • 其次,IF NOT EXISTS选项允许您仅当表不存在时才创建新表
  • 第三,指定以逗号分隔的表列列表,包括列名、数据类型、长度和约束
  • 最后,指定表约束,包括主键、外键和检查约束

约束类型

  • NOT NULL - 确保列中的值不能为NULL
  • UNIQUE - 确保列的值在表中是唯一的
  • PRIMARY KEY - 唯一标识表中的行,一个表只能有一个主键
  • CHECK - 确保数据必须满足布尔表达式
  • FOREIGN KEY - 确保列中的值存在于另一个表中
-- 创建 accounts 表
CREATE TABLE accounts (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(50) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_on TIMESTAMP NOT NULL,
last_login TIMESTAMP
);

-- 创建 roles 表
CREATE TABLE roles(
role_id SERIAL PRIMARY KEY,
role_name VARCHAR(255) UNIQUE NOT NULL
);

-- 创建 account_roles 表(复合主键和外键)
CREATE TABLE account_roles (
user_id INT NOT NULL,
role_id INT NOT NULL,
grant_date TIMESTAMP,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (role_id) REFERENCES roles (role_id),
FOREIGN KEY (user_id) REFERENCES accounts (user_id)
);

-- 使用 IF NOT EXISTS
CREATE TABLE IF NOT EXISTS products (
product_id SERIAL PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL CHECK (price > 0),
stock INT DEFAULT 0
);

-- 使用 CHECK 约束
CREATE TABLE employees (
employee_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT CHECK (age >= 18),
salary DECIMAL(10, 2) CHECK (salary > 0)
);
  • 创建已存在的表会导致错误,使用IF NOT EXISTS可以避免
  • SERIAL是自增整数,常用于主键
  • 表约束(如复合主键)必须在列定义之后单独定义
  • 外键引用的列必须是唯一的或主键
  • 约束可以在创建表时定义,也可以后续使用ALTER TABLE添加

46. SERIAL 自增列

  • 在 PostgreSQL 中,序列是一种特殊的数据库对象,它生成整数序列。序列通常用作表中的主键列。可以通过SERIAL伪类型创建自增列
CREATE TABLE table_name(
id SERIAL
);
  • SERIAL不是真正的数据类型,而是伪类型(语法糖)
  • 序列生成器操作不是事务安全的,回滚不会回滚序列号
  • 删除表或列时,关联的序列也会被删除

47. ALTER TABLE

  • 要更改现有表的结构,可以使用 PostgreSQL 的ALTER TABLE语句
ALTER TABLE table_name action;
-- 创建示例表
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2)
);

-- 添加列
ALTER TABLE products
ADD COLUMN stock INT;

-- 删除列
ALTER TABLE products
DROP COLUMN stock;

-- 重命名列
ALTER TABLE products
RENAME COLUMN name TO product_name;

-- 添加列并设置默认值
ALTER TABLE products
ADD COLUMN status VARCHAR(20);

ALTER TABLE products
ALTER COLUMN status SET DEFAULT 'active';

-- 添加 CHECK 约束
ALTER TABLE products
ADD CHECK (price > 0);

-- 添加 UNIQUE 约束
ALTER TABLE products
ADD CONSTRAINT unique_product_name UNIQUE (product_name);

-- 添加 NOT NULL 约束
ALTER TABLE products
ALTER COLUMN price SET NOT NULL;

-- 修改列类型
ALTER TABLE products
ALTER COLUMN product_name TYPE VARCHAR(200);

-- 重命名表
ALTER TABLE products
RENAME TO items;
  • 删除列会永久删除该列及其数据
  • 添加NOT NULL约束前,确保列中没有NULL
  • 修改列类型可能导致数据丢失或转换错误
  • 添加约束可能失败,如果现有数据不满足约束条件
  • 某些操作可能需要锁定整个表,影响并发访问
  • 可以在一个ALTER TABLE语句中执行多个操作

48. ADD COLUMN

  • 要将新列添加到现有表,请使用ALTER TABLE ADD COLUMN语句
-- 添加单列
ALTER TABLE table_name
ADD COLUMN column_name data_type constraint;

-- 添加多列
ALTER TABLE table_name
ADD COLUMN column_name1 data_type constraint,
ADD COLUMN column_name2 data_type constraint,
...
ADD COLUMN column_namen data_type constraint;
  • 首先,在ALTER TABLE关键字后指定要添加新列的表名
  • 其次,在ADD COLUMN关键字后指定新列的名称、数据类型和约束
  • 新列会被附加到表的末尾

49. DROP COLUMN

  • 要删除表中的列,请在ALTER TABLE语句中使用DROP COLUMN子句
-- 删除单列
ALTER TABLE table_name
DROP COLUMN column_name;

-- 删除列(如果存在)
ALTER TABLE table_name
DROP COLUMN IF EXISTS column_name;

-- 删除列及其依赖对象
ALTER TABLE table_name
DROP COLUMN column_name CASCADE;

-- 删除多列
ALTER TABLE table_name
DROP COLUMN column_name1,
DROP COLUMN column_name2,
...;
  • 删除列时,PostgreSQL 会自动删除涉及该列的所有索引和约束
  • 如果列被其他对象依赖,需要使用CASCADE选项
  • IF EXISTS选项可以避免删除不存在的列时报错

50. DROP TABLE

  • 要从数据库中删除表,请使用DROP TABLE语句
-- 删除单个表
DROP TABLE [IF EXISTS] table_name
[CASCADE | RESTRICT];

-- 删除多个表
DROP TABLE [IF EXISTS]
table_name_1,
table_name_2,
...
[CASCADE | RESTRICT];
  • 首先,在DROP TABLE关键字后指定要删除的表名
  • 其次,使用IF EXISTS选项仅在表存在时才删除
  • CASCADE选项允许删除表及其依赖对象
  • RESTRICT选项在有依赖对象时拒绝删除(默认)

51. TRUNCATE TABLE

  • 要快速删除表中的所有数据,请使用TRUNCATE TABLE语句。它比DELETE语句更快,因为不扫描表
-- 截断单个表
TRUNCATE TABLE table_name;

-- 重置自增序列
TRUNCATE TABLE table_name
RESTART IDENTITY;

-- 截断多个表
TRUNCATE TABLE
table_name1,
table_name2,
...;

-- 级联截断
TRUNCATE TABLE table_name
CASCADE;
  • TRUNCATE TABLE删除所有数据而不扫描表,比DELETE
  • 立即回收存储空间,不需要后续的VACUUM操作
  • RESTART IDENTITY重置自增列的序列值
  • CASCADE截断引用该表的其他表

52. 临时表

  • 临时表是在数据库会话期间存在的短期表。PostgreSQL 在会话或事务结束时自动删除临时表
-- 创建临时表
CREATE TEMPORARY TABLE temp_table_name(
column_list
);

-- 或使用 TEMP
CREATE TEMP TABLE temp_table_name(
column_list
);

-- 删除临时表
DROP TABLE temp_table_name;
  • TEMPTEMPORARY关键字是等效的,可以互换使用
  • 临时表仅对创建它的会话可见
  • 会话结束时自动删除
  • 不能在CREATE TEMP TABLE中指定模式

PostgreSQL 约束

53. 主键约束 (PRIMARY KEY)

  • 主键是用于唯一标识表中的行的一列或一组列。主键约束是非空约束和 UNIQUE 约束的组合。一张表只能有一个主键
-- 创建表时定义单列主键
CREATE TABLE table_name (
column_1 data_type PRIMARY KEY,
column_2 data_type,
...
);

-- 创建表时定义复合主键
CREATE TABLE table_name (
column_1 data_type,
column_2 data_type,
...
PRIMARY KEY (column_1, column_2)
);

-- 指定主键约束名称
CREATE TABLE table_name (
column_1 data_type,
column_2 data_type,
...
CONSTRAINT constraint_name PRIMARY KEY (column_1, column_2)
);
-- 单列主键
CREATE TABLE po_headers (
po_no INTEGER PRIMARY KEY,
vendor_no INTEGER,
description TEXT,
shipping_address TEXT
);

-- 复合主键
CREATE TABLE po_items (
po_no INTEGER,
item_no INTEGER,
product_no INTEGER,
qty INTEGER,
net_price NUMERIC,
PRIMARY KEY (po_no, item_no)
);

-- 使用 SERIAL 自增主键
CREATE TABLE customers (
customer_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255)
);

-- 指定主键约束名称
CREATE TABLE orders (
order_id INTEGER,
customer_id INTEGER,
order_date DATE,
CONSTRAINT orders_pk PRIMARY KEY (order_id)
);
  • 一张表只能有一个主键
  • 主键列不能包含NULL值,值必须唯一
  • PostgreSQL 自动为主键创建唯一的 B-tree 索引
  • 默认主键约束名称为table_name_pkey

54. 外键约束 (FOREIGN KEY)

  • 外键是表中引用另一个表的主键的一列或一组列。外键约束有助于维护子表和父表之间数据的引用完整性
[CONSTRAINT fk_name]
FOREIGN KEY(fk_columns)
REFERENCES parent_table(parent_key_columns)
[ON DELETE delete_action]
[ON UPDATE update_action]
-- 创建父表:部门
CREATE TABLE departments(
department_id INT GENERATED ALWAYS AS IDENTITY,
department_name VARCHAR(255) NOT NULL,
PRIMARY KEY(department_id)
);

-- 创建子表:员工(NO ACTION)
CREATE TABLE employees(
employee_id INT GENERATED ALWAYS AS IDENTITY,
employee_name VARCHAR(255) NOT NULL,
department_id INT,
salary DECIMAL(10, 2),
PRIMARY KEY(employee_id),
CONSTRAINT fk_department
FOREIGN KEY(department_id)
REFERENCES departments(department_id)
);

-- 插入数据
INSERT INTO departments(department_name)
VALUES('Engineering'), ('Sales'), ('HR');

INSERT INTO employees(employee_name, department_id, salary)
VALUES
('Alice', 1, 75000),
('Bob', 1, 68000),
('Charlie', 2, 62000),
('David', 3, 58000);

-- 尝试删除有员工的部门
DELETE FROM departments WHERE department_id = 1;
-- ERROR: update or delete on table "departments" violates foreign key constraint
-- 阻止删除,因为有员工属于该部门
  • 首先,在CONSTRAINT关键字后指定外键约束的名称(可选)
  • 其次,在FOREIGN KEY关键字后指定一个或多个外键列
  • 第三,在REFERENCES子句中指定父表和父键列
  • 最后,指定删除和更新操作

55. CHECK 约束

  • CHECK约束允许您指定列中的值是否必须满足特定要求。在插入或更新值之前,CHECK约束使用布尔表达式来验证值
-- 列级 CHECK 约束
column_name data_type CHECK (expression)

-- 列级 CHECK 约束(指定名称)
column_name data_type CONSTRAINT constraint_name CHECK (expression)

-- 表级 CHECK 约束
CONSTRAINT constraint_name CHECK (expression)
CREATE TABLE students (
student_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT CHECK (age >= 18 AND age <= 100),
grade CHAR(1) CHECK (grade IN ('A', 'B', 'C', 'D', 'F')),
gpa DECIMAL(3, 2) CHECK (gpa BETWEEN 0.0 AND 4.0)
);

-- 尝试插入无效数据
INSERT INTO students (name, age, grade, gpa)
VALUES ('Alice', 15, 'A', 3.8);
-- ERROR: new row for relation "students" violates check constraint
-- age 必须 >= 18

-- 插入有效数据
INSERT INTO students (name, age, grade, gpa)
VALUES ('Bob', 20, 'B', 3.5);

-- 指定约束名称
CREATE TABLE bank_accounts (
account_id SERIAL PRIMARY KEY,
account_number VARCHAR(20) NOT NULL,
balance DECIMAL(15, 2) CONSTRAINT non_negative_balance CHECK (balance >= 0),
account_type VARCHAR(20) CONSTRAINT valid_account_type
CHECK (account_type IN ('checking', 'savings', 'business'))
);

-- 表级 CHECK 约束(多列)
CREATE TABLE events (
event_id SERIAL PRIMARY KEY,
event_name VARCHAR(100) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
min_participants INT NOT NULL,
max_participants INT NOT NULL,
CONSTRAINT valid_date_range
CHECK (end_date >= start_date),
CONSTRAINT valid_participant_range
CHECK (max_participants >= min_participants AND min_participants > 0)
);
  • 可以引用同一行的多个列
  • NULL 值通过 CHECK 约束(除非同时有 NOT NULL 约束)
  • 默认约束名称格式:{table}_{column}_check

56. UNIQUE 约束

  • UNIQUE约束确保存储在一个列或一组列中的值在表中的所有行中是唯一的。当添加UNIQUE约束时,PostgreSQL 会自动在该列或列组上创建唯一索引
-- 列级 UNIQUE 约束
column_name data_type UNIQUE

-- 表级 UNIQUE 约束
UNIQUE (column_name)

-- 多列 UNIQUE 约束
UNIQUE (column1, column2, ...)

-- 指定约束名称
CONSTRAINT constraint_name UNIQUE (column_name)
-- 创建带 UNIQUE 约束的表
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE,
email VARCHAR(100) UNIQUE,
phone VARCHAR(20)
);

-- 插入数据
INSERT INTO users (username, email, phone)
VALUES ('alice', 'alice@example.com', '123-456-7890');

-- 尝试插入重复的 email
INSERT INTO users (username, email, phone)
VALUES ('bob', 'alice@example.com', '098-765-4321');
-- ERROR: duplicate key value violates unique constraint "users_email_key"

-- 表级 UNIQUE 约束
CREATE TABLE employees (
employee_id SERIAL PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
email VARCHAR(100),
UNIQUE (email)
);

-- 多列 UNIQUE 约束
CREATE TABLE course_enrollments (
enrollment_id SERIAL PRIMARY KEY,
student_id INT,
course_id INT,
semester VARCHAR(20),
UNIQUE (student_id, course_id, semester)
);

-- 指定约束名称
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
product_code VARCHAR(20),
barcode VARCHAR(50),
CONSTRAINT unique_product_code UNIQUE (product_code),
CONSTRAINT unique_barcode UNIQUE (barcode)
);
  • PostgreSQL 自动为UNIQUE约束创建唯一索引
  • UNIQUE约束允许多个NULL
  • 多列UNIQUE约束要求列组合唯一
  • 一个表可以有多个UNIQUE约束

57. NOT NULL 约束

  • NOT NULL约束确保列的值不为空。在数据库中,NULL 代表未知或信息缺失,与空字符串或数字零不同
-- 创建表时声明 NOT NULL
CREATE TABLE table_name(
column_name data_type NOT NULL,
...
);

-- 为现有列添加 NOT NULL
ALTER TABLE table_name
ALTER COLUMN column_name SET NOT NULL;

-- 删除 NOT NULL 约束
ALTER TABLE table_name
ALTER COLUMN column_name DROP NOT NULL;
  • 默认情况下,列可以保存 NULL
  • 使用IS NULLIS NOT NULL检查 NULL 值
  • 切勿使用=来比较 NULL,因为NULL = NULL返回 NULL
  • 一列可以有多个约束,顺序不重要
  • NOT NULL约束等同于CHECK(column IS NOT NULL)

数据类型

58. BOOLEAN 布尔类型

  • PostgreSQL 支持单一的布尔数据类型:BOOLEAN,它可以具有三个值:truefalseNULL。PostgreSQL 使用一个字节存储布尔值,BOOLEAN可以简写为BOOL
-- 定义布尔列
column_name BOOLEAN

-- 或使用简写
column_name BOOL

-- 带默认值
column_name BOOLEAN DEFAULT true
  • PostgreSQL 使用一个字节存储布尔值
  • BOOLEAN可以简写为BOOL
  • 布尔列可以有三个值:truefalseNULL
  • 前导或尾随空格不影响布尔值
  • truefalse外的常量值必须用单引号引起来
  • 可以直接使用布尔列名作为条件(隐式为 true)

59. 字符类型: CHAR、VARCHAR 和 TEXT

  • PostgreSQL 提供三种主要字符类型:CHARACTER(n)CHAR(n)CHARACTER VARYING(n)VARCHAR(n)、和TEXT
类型描述
CHAR(n)固定长度,空白填充
VARCHAR(n)可变长度,有长度限制
TEXTVARCHAR可变长度,无限制
-- 创建带字符类型的表
CREATE TABLE character_tests (
id SERIAL PRIMARY KEY,
x CHAR(1),
y VARCHAR(10),
z TEXT
);

-- 插入数据
INSERT INTO character_tests (x, y, z)
VALUES('Y', 'varchar(n)', 'This is a very long text for the PostgreSQL text column');

-- 尝试插入超长字符串
INSERT INTO character_tests (x, y, z)
VALUES('Yes', 'This is a test', 'text');
-- ERROR: value too long for type character(1)

INSERT INTO character_tests (x, y, z)
VALUES('Y', 'This is a test for varchar', 'text');
-- ERROR: value too long for type character varying(10)
  • 三种字符类型之间没有性能差异
  • CHAR(n)VARCHAR(n)最多存储n个字符
  • TEXT可以存储无限长度的字符串
  • VARCHAR不带长度限制时等同于TEXT
  • 大多数情况下应使用TEXTVARCHAR
  • 需要检查长度时使用VARCHAR(n)
  • CHAR不带长度限制时等同于CHAR(1)

60. NUMERIC 数值类型

  • NUMERIC类型可以存储具有很多位数的数字。通常用于需要精确性的数字,例如货币金额或数量
-- 指定精度和小数位数
NUMERIC(precision, scale)

-- 只指定精度(小数位数为0)
NUMERIC(precision)

-- 不指定精度和小数位数
NUMERIC
-- 创建带 NUMERIC 列的表
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price NUMERIC(5, 2) -- 最多5位数字,其中2位小数
);

-- 插入数据(自动四舍五入)
INSERT INTO products (name, price)
VALUES
('Phone', 500.215), -- 四舍五入为 500.22
('Tablet', 500.214); -- 四舍五入为 500.21

SELECT * FROM products;
-- | id | name | price |
-- |----|--------|--------|
-- | 1 | Phone | 500.22 |
-- | 2 | Tablet | 500.21 |

-- 插入超出精度的值会报错
INSERT INTO products (name, price)
VALUES('Phone', 123456.21);
-- ERROR: numeric field overflow
  • NUMERICDECIMAL是等效的,都是 SQL 标准
  • 最多可保存小数点前 131,072 位和小数点后 16,383 位
  • 小数位数大于声明时会四舍五入
  • 精度超出声明时会报错
  • NUMERIC计算通常比整数、浮点数慢
  • 不需要精确性时不应使用NUMERIC
  • NaN表示”非数字”,大于所有数字

61. INTEGER 整数类型

  • PostgreSQL 提供三种整数类型:SMALLINTINTEGERBIGINT,用于存储不同范围的整数
名称存储大小最小值最大值
SMALLINT2 字节-32,768+32,767
INTEGER4 字节-2,147,483,648+2,147,483,647
BIGINT8 字节-9,223,372,036,854,775,808+9,223,372,036,854,775,807
-- SMALLINT 示例(适用于小范围数值)
CREATE TABLE books (
book_id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
pages SMALLINT NOT NULL CHECK (pages > 0),
year SMALLINT
);

INSERT INTO books (title, pages, year)
VALUES ('PostgreSQL Guide', 350, 2024);

-- INTEGER 示例(最常用)
CREATE TABLE cities (
city_id SERIAL PRIMARY KEY,
city_name VARCHAR(255) NOT NULL,
population INT NOT NULL CHECK (population >= 0)
);

INSERT INTO cities (city_name, population)
VALUES
('New York', 8336817),
('Los Angeles', 3979576);

-- BIGINT 示例(大数值)
CREATE TABLE analytics (
id SERIAL PRIMARY KEY,
page_views BIGINT,
user_id INT
);

INSERT INTO analytics (page_views, user_id)
VALUES (9223372036854775000, 1001);
  • PostgreSQL 不提供无符号整数类型
  • INTEGER是最常用的整数类型,提供最佳平衡
  • INTINTEGER的同义词
  • 超出范围的值会导致错误
  • BIGINT消耗更多存储空间并降低性能,应谨慎使用
  • 选择合适的类型可以节省存储空间并提高性能

62. DATE 日期类型

  • PostgreSQL 使用DATE数据类型存储日期值。PostgreSQL 使用 4 个字节来存储日期值,范围为 4713 BC 至 5874897 AD
-- 定义 DATE 列
column_name DATE

-- 使用当前日期作为默认值
column_name DATE DEFAULT CURRENT_DATE

PostgreSQL 使用yyyy-mm-dd格式存储和插入日期值,例如 2000-12-31

日期函数

1) 获取当前日期

-- 方式1:使用 NOW() 并转换为日期
SELECT NOW()::DATE;

-- 方式2:使用 CURRENT_DATE
SELECT CURRENT_DATE;
-- 2024-11-22

2) 格式化日期输出

-- dd/mm/yyyy 格式
SELECT TO_CHAR(NOW()::DATE, 'dd/mm/yyyy');
-- 22/11/2024

-- Mon dd, yyyy 格式
SELECT TO_CHAR(NOW()::DATE, 'Mon dd, yyyy');
-- Nov 22, 2024

-- yyyy-mm-dd 格式
SELECT TO_CHAR(NOW()::DATE, 'yyyy-mm-dd');
-- 2024-11-22

3) 计算日期间隔

-- 计算员工工作天数
SELECT
first_name,
last_name,
NOW() - hire_date AS service_days
FROM employees;
-- | first_name | last_name | service_days |
-- |------------|-----------|-------------------------|
-- | Shannon | Freeman | 7266 days 08:25:30 |
-- | Sheila | Wells | 7997 days 08:25:30 |

4) 计算年龄

-- 计算员工当前年龄
SELECT
employee_id,
first_name,
last_name,
AGE(birth_date) AS age
FROM employees;
-- | employee_id | first_name | age |
-- |-------------|------------|-------------------------|
-- | 1 | Shannon | 44 years 10 mons 22 days|
-- | 2 | Sheila | 46 years 9 mons 17 days |

-- 计算特定日期时的年龄
SELECT
first_name,
last_name,
AGE('2015-01-01', birth_date) AS age_in_2015
FROM employees;

5) 提取日期部分

-- 提取年、月、日
SELECT
employee_id,
first_name,
EXTRACT(YEAR FROM birth_date) AS year,
EXTRACT(MONTH FROM birth_date) AS month,
EXTRACT(DAY FROM birth_date) AS day
FROM employees;
-- | employee_id | first_name | year | month | day |
-- |-------------|------------|------|-------|-----|
-- | 1 | Shannon | 1980 | 1 | 1 |
-- | 2 | Sheila | 1978 | 2 | 5 |

-- 提取星期几(0=周日,6=周六)
SELECT
EXTRACT(DOW FROM CURRENT_DATE) AS day_of_week;

-- 提取一年中的第几天
SELECT
EXTRACT(DOY FROM CURRENT_DATE) AS day_of_year;
  • PostgreSQL 使用yyyy-mm-dd格式存储日期
  • 使用CURRENT_DATE获取当前日期
  • 使用TO_CHAR()格式化日期输出
  • 使用AGE()计算年龄
  • 使用EXTRACT()提取日期部分
  • 日期减法返回间隔(interval)类型

63. TIMESTAMP 时间戳

  • PostgreSQL 提供两种时间戳数据类型:TIMESTAMP(不带时区)和TIMESTAMPTZ(带时区)
类型描述存储大小
TIMESTAMP不带时区的时间戳8 字节
TIMESTAMPTZ带时区的时间戳8 字节
  • TIMESTAMP - 存储日期和时间,不包含时区信息
  • TIMESTAMPTZ - 时区感知的日期和时间,以 UTC 存储
-- 创建带时间戳列的表
CREATE TABLE timestamp_demo (
ts TIMESTAMP,
tstz TIMESTAMPTZ
);

-- 设置时区
SET timezone = 'America/Los_Angeles';

-- 查看当前时区
SHOW TIMEZONE;
-- America/Los_Angeles

-- 插入数据
INSERT INTO timestamp_demo (ts, tstz)
VALUES('2016-06-22 19:10:25-07', '2016-06-22 19:10:25-07');

-- 查询数据
SELECT ts, tstz FROM timestamp_demo;
-- | ts | tstz |
-- |---------------------|------------------------|
-- | 2016-06-22 19:10:25 | 2016-06-22 19:10:25-07 |

-- 修改时区
SET timezone = 'America/New_York';

-- 再次查询
SELECT ts, tstz FROM timestamp_demo;
-- | ts | tstz |
-- |---------------------|------------------------|
-- | 2016-06-22 19:10:25 | 2016-06-22 22:10:25-04 |
-- TIMESTAMP 不变,TIMESTAMPTZ 自动调整

时间戳函数

获取当前时间

-- 获取当前时间戳(带时区)
SELECT NOW();
-- 2024-11-22 20:44:52.134125-07

-- 使用 CURRENT_TIMESTAMP
SELECT CURRENT_TIMESTAMP;
-- 2024-11-22 20:44:52.134125-07

-- 仅获取时间(带时区)
SELECT CURRENT_TIME;
-- 20:49:04.566025-07

-- 获取字符串格式的时间
SELECT TIMEOFDAY();
-- Wed Nov 22 20:51:12.632420 2024 PST

时区转换

-- 查看当前时区
SHOW TIMEZONE;
-- America/Los_Angeles

-- 转换到另一个时区
SELECT timezone('America/New_York', '2016-06-01 00:00');
-- 2016-06-01 03:00:00

-- 显式转换为 timestamptz
SELECT timezone('America/New_York', '2016-06-01 00:00'::TIMESTAMPTZ);
  • TIMESTAMPTZ以 UTC 存储,查询时转换为当前时区
  • 建议使用TIMESTAMPTZ存储时间戳数据
  • TIMESTAMP不包含时区信息,更改时区不影响存储值
  • 两种类型都使用 8 字节存储
  • PostgreSQL 不存储时区数据,只存储 UTC 时间

64. INTERVAL 间隔类型

  • 间隔数据类型允许您存储和操作一段时间(以年、月、日、小时、分钟、秒等为单位)
INTERVAL 'quantity unit [quantity unit...] [direction]'
-- 基本间隔值
SELECT INTERVAL '2 months';
SELECT INTERVAL '3 hours 20 minutes';
SELECT INTERVAL '1 year 2 months 3 days';
SELECT INTERVAL '2 weeks ago';

-- 使用间隔进行日期运算
SELECT
NOW(),
NOW() - INTERVAL '1 year 3 hours 20 minutes' AS past_time;

-- 加减间隔
SELECT NOW() + INTERVAL '1 day';
SELECT NOW() - INTERVAL '2 hours';
SELECT CURRENT_DATE + INTERVAL '7 days';

65. UUID 数据类型

  • UUID 代表通用唯一标识符(Universal Unique Identifier),是由算法生成的 128 位数值。UUID 值由连字符分隔的 32 位十六进制数字组成
-- UUID 格式示例
40e6215d-b5c6-4896-987c-f30f3678f608
6ecd8c99-4036-403d-bf84-cf8400f67836
3f333df6-90a4-4fda-8dd3-9485d27cee36
-- 安装 uuid-ossp 模块
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

生成 UUID 值

-- 基于 MAC 地址、时间戳和随机值生成
SELECT uuid_generate_v1();
-- 0e37df36-f698-11e6-8dd4-cb9ced3df976

-- 基于随机数生成(推荐)
SELECT uuid_generate_v4();
-- a81bc81b-dead-4e5d-abff-90865d1e13b1

- 创建带 UUID 主键的表
CREATE TABLE contacts (
contact_id UUID DEFAULT uuid_generate_v4(),
first_name VARCHAR NOT NULL,
last_name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
phone VARCHAR,
PRIMARY KEY (contact_id)
);

-- 插入数据(UUID 自动生成)
INSERT INTO contacts (first_name, last_name, email, phone)
VALUES
('John', 'Smith', 'john.smith@example.com', '408-237-2345'),
('Jane', 'Smith', 'jane.smith@example.com', '408-237-2344'),
('Alex', 'Smith', 'alex.smith@example.com', '408-237-2343');
  • UUID 保证在分布式系统中的唯一性
  • UUID 比 SERIAL 占用更多存储空间(16 字节 vs 4/8 字节)
  • uuid_generate_v4() 基于随机数,最常用
  • uuid_generate_v1() 基于 MAC 地址和时间戳
  • 适合分布式系统、微服务架构
  • 索引性能略低于整数类型

66. UUID v7

  • UUID v7 是最新的 UUID 标准(RFC 9562),基于时间戳生成,具有更好的排序性能和索引效率。

https://github.com/dverite/postgres-uuidv7-sql

-- 安装 pg_uuidv7 扩展(需要 PostgreSQL 13+)
CREATE EXTENSION IF NOT EXISTS pg_uuidv7;
-- 生成 UUID v7
SELECT uuid_generate_v7();
-- 018c4e62-45a2-7000-8000-000000000001

-- 批量生成
SELECT uuid_generate_v7() FROM generate_series(1, 5);
  • UUID v7 基于时间戳,提供更好的排序性能
  • 索引效率优于 UUID v4(减少索引碎片)
  • 适合需要时间排序的场景(日志、事件、交易)
  • 需要安装 pg_uuidv7 扩展
  • 仍然保证全局唯一性
  • 比 SERIAL 更适合分布式系统

67. JSON 数据类型

  • JSON(JavaScript Object Notation)是一种由键值对组成的开放标准格式。PostgreSQL 从 9.2 版本开始支持原生 JSON 数据类型
-- 定义 JSON 列
column_name JSON

-- 或使用 JSONB(二进制 JSON,推荐)
column_name JSONB
-- 创建带 JSON 列的表
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
info JSON NOT NULL
);

-- 插入 JSON 数据
INSERT INTO orders (info)
VALUES('{"customer": "John Doe", "items": {"product": "Beer", "qty": 6}}');

INSERT INTO orders (info)
VALUES
('{"customer": "Lily Bush", "items": {"product": "Diaper", "qty": 24}}'),
('{"customer": "Josh William", "items": {"product": "Toy Car", "qty": 1}}'),
('{"customer": "Mary Clark", "items": {"product": "Toy Train", "qty": 2}}');

-- 查询 JSON 数据
SELECT info FROM orders;

JSON 运算符

-> 运算符:返回 JSON 对象字段

-- 获取客户信息(JSON 格式)
SELECT info -> 'customer' AS customer
FROM orders;
-- | customer |
-- |------------|
-- | "John Doe" |

->> 运算符:返回 JSON 对象字段(文本格式)

-- 获取客户信息(文本格式)
SELECT info ->> 'customer' AS customer
FROM orders;
-- | customer |
-- |--------------|
-- | John Doe |
-- | Lily Bush |
-- | Josh William |

WHERE 子句中使用 JSON 运算符

-- 查找购买 Diaper 的客户
SELECT info ->> 'customer' AS customer
FROM orders
WHERE info -> 'items' ->> 'product' = 'Diaper';

-- 查找购买 2 个产品的客户
SELECT
info ->> 'customer' AS customer,
info -> 'items' ->> 'product' AS product
FROM orders
WHERE CAST(info -> 'items' ->> 'qty' AS INTEGER) = 2;

聚合函数

-- 计算最小、最大、总和、平均数量
SELECT
MIN(CAST(info -> 'items' ->> 'qty' AS INTEGER)) AS min_qty,
MAX(CAST(info -> 'items' ->> 'qty' AS INTEGER)) AS max_qty,
SUM(CAST(info -> 'items' ->> 'qty' AS INTEGER)) AS total_qty,
AVG(CAST(info -> 'items' ->> 'qty' AS INTEGER)) AS avg_qty
FROM orders;
-- | min_qty | max_qty | total_qty | avg_qty |
-- |---------|---------|-----------|---------|
-- | 1 | 24 | 33 | 8.25 |

JSON 函数

json_each() - 展开 JSON 对象为键值对

SELECT json_each(info)
FROM orders;

-- 获取文本格式的键值对
SELECT json_each_text(info)
FROM orders;

json_object_keys() - 获取 JSON 对象的键

-- 获取 items 对象的所有键
SELECT json_object_keys(info -> 'items')
FROM orders;
-- | json_object_keys |
-- |------------------|
-- | product |
-- | qty |

json_typeof() - 获取 JSON 值的类型

-- 获取 items 的数据类型
SELECT json_typeof(info -> 'items')
FROM orders;
-- | json_typeof |
-- |-------------|
-- | object |

-- 获取 qty 的数据类型
SELECT json_typeof(info -> 'items' -> 'qty')
FROM orders;
-- | json_typeof |
-- |-------------|
-- | number |

JSON vs JSONB

-- JSON:存储文本格式,保留空格和顺序
CREATE TABLE data_json (
id SERIAL PRIMARY KEY,
data JSON
);

-- JSONB:存储二进制格式,支持索引,性能更好(推荐)
CREATE TABLE data_jsonb (
id SERIAL PRIMARY KEY,
data JSONB
);

-- JSONB 支持更多运算符
SELECT '{"a": 1, "b": 2}'::JSONB @> '{"a": 1}'::JSONB; -- true
SELECT '{"a": 1}'::JSONB <@ '{"a": 1, "b": 2}'::JSONB; -- true
  • JSON 存储原始文本,JSONB 存储二进制格式(推荐使用 JSONB)
  • -> 返回 JSON 对象,->> 返回文本
  • JSONB 支持索引,查询性能更好
  • 使用 CAST 转换 JSON 值到其他数据类型
  • JSONB 支持更多运算符(@><@? 等)

条件表达式

68. CASE 表达式

  • PostgreSQL 的CASE表达式类似于其他编程语言中的 IF/ELSE 语句,允许您向查询添加条件逻辑
CASE 
WHEN condition_1 THEN result_1
WHEN condition_2 THEN result_2
[WHEN ...]
[ELSE else_result]
END
-- 员工薪资等级分类
SELECT
employee_id,
name,
salary,
CASE
WHEN salary < 40000 THEN 'Low'
WHEN salary >= 40000 AND salary < 70000 THEN 'Medium'
WHEN salary >= 70000 THEN 'High'
END AS salary_level
FROM employees;
-- | employee_id | name | salary | salary_level |
-- |-------------|---------|--------|--------------|
-- | 1 | Alice | 75000 | High |
-- | 2 | Bob | 55000 | Medium |
-- | 3 | Charlie | 35000 | Low |

-- 订单状态描述
SELECT
order_id,
status,
CASE
WHEN status = 'pending' THEN 'Order received'
WHEN status = 'processing' THEN 'Being prepared'
WHEN status = 'shipped' THEN 'On the way'
WHEN status = 'delivered' THEN 'Completed'
ELSE 'Unknown status'
END AS status_description
FROM orders;
  • CASE是一个表达式,可以在 SELECT、WHERE、GROUP BY、HAVING、ORDER BY 等子句中使用
  • 通用 CASE 使用条件表达式,简单 CASE 使用值比较
  • 如果省略ELSE子句,默认返回 NULL
  • CASE表达式从上到下顺序评估,找到第一个匹配后立即返回
  • 可以嵌套使用 CASE 表达式

69. COALESCE 函数

  • COALESCE函数返回参数列表中的第一个非空值。常用于处理和替换 NULL 值
COALESCE(argument_1, argument_2, ...)
-- 基本用法
SELECT COALESCE(1, 2);
-- 1

SELECT COALESCE(NULL, 2, 1);
-- 2

SELECT COALESCE(NULL, NULL, 'default');
-- 'default'

SELECT COALESCE(NULL, NULL, NULL);
-- NULL
  • COALESCE函数与 SQL 标准的NVL(Oracle)或IFNULL(MySQL)功能相同
  • 性能与CASE表达式相同,但更简洁
  • 第一个非 NULL 参数后的参数不会被评估
  • 所有参数应具有兼容的数据类型
  • CASE表达式更易读和维护

70. NULLIF 函数

  • NULLIF函数比较两个参数,如果相等则返回 NULL,否则返回第一个参数。常用于处理特殊值和防止错误
NULLIF(argument_1, argument_2)
-- 基本用法
SELECT NULLIF(1, 1);
-- NULL

SELECT NULLIF(1, 0);
-- 1

SELECT NULLIF('A', 'B');
-- 'A'

SELECT NULLIF('', '');
-- NULL
  • 常与 COALESCE 组合,防止除零和处理空字符串
  • CASE 简洁,但返回 NULL 需后续处理
  • 两参数需类型兼容

71. CAST 类型转换

  • PostgreSQL 的CAST运算符允许将一种数据类型的值转换为另一种数据类型
-- CAST 语法(SQL 标准)
CAST(expression AS target_type)

-- :: 运算符(PostgreSQL 特定)
expression::target_type
-- 字符串转整数
SELECT CAST('100' AS INTEGER);
-- 100

SELECT '100'::INTEGER;
-- 100

-- 字符串转日期
SELECT CAST('2015-01-01' AS DATE);
-- 2015-01-01

SELECT '01-OCT-2015'::DATE;
-- 2015-10-01

-- 字符串转浮点数
SELECT CAST('10.2' AS DOUBLE PRECISION);
-- 10.2

SELECT '10.2'::NUMERIC;
-- 10.2

-- 字符串转布尔值
SELECT
CAST('true' AS BOOLEAN),
CAST('false' AS BOOLEAN),
CAST('T' AS BOOLEAN),
CAST('F' AS BOOLEAN);
-- | true | false | true | false |

-- 字符串转时间戳
SELECT '2019-06-15 14:30:20'::TIMESTAMP;
-- 2019-06-15 14:30:20

-- 字符串转间隔
SELECT
'15 minute'::INTERVAL,
'2 hour'::INTERVAL,
'1 day'::INTERVAL,
'2 week'::INTERVAL,
'3 month'::INTERVAL;
  • CAST是 SQL 标准,::是 PostgreSQL 特定语法
  • 无效转换会导致错误
  • 转换可能导致精度损失(如 NUMERIC 转 INTEGER)
  • 在列上使用CAST可能影响索引性能
  • 使用CASE表达式处理可能失败的转换
  • 某些类型需要特定格式(如DOUBLE PRECISION而非DOUBLE

处理 JSON

72. 查询 JSON 数组

  • PostgreSQL 提供多种函数将 JSON 列中嵌套的数组转换为表行,便于查询和处理
  • json_array_elements() - 将 JSON 数组展开为行集
  • jsonb_array_elements() - 将 JSONB 数组展开为行集(推荐)
-- 展开简单数组
SELECT * FROM json_array_elements('[1, 2, 3]') AS elem;
-- | elem |
-- |------|
-- | 1 |
-- | 2 |
-- | 3 |

-- 展开字符串数组
SELECT * FROM json_array_elements('["apple", "banana", "orange"]') AS fruit;
-- | fruit |
-- |----------|
-- | "apple" |
-- | "banana" |
-- | "orange" |
-- 创建用户表
CREATE TABLE users(
user_id SERIAL PRIMARY KEY,
username VARCHAR(50),
hobbies JSONB
);

-- 插入数据
INSERT INTO users(username, hobbies)
VALUES
('alice', '["reading", "coding", "gaming"]'),
('bob', '["music", "sports"]');

-- 展开爱好数组
SELECT
user_id,
username,
jsonb_array_elements_text(hobbies) AS hobby
FROM users;
-- | user_id | username | hobby |
-- |---------|----------|---------|
-- | 1 | alice | reading |
-- | 1 | alice | coding |
-- | 1 | alice | gaming |
-- | 2 | bob | music |
-- | 2 | bob | sports |

-- 使用 WITH ORDINALITY 获取位置
SELECT
username,
position,
hobby
FROM users,
jsonb_array_elements_text(hobbies) WITH ORDINALITY AS t(hobby, position);
-- | username | position | hobby |
-- |----------|----------|---------|
-- | alice | 1 | reading |
-- | alice | 2 | coding |
-- | alice | 3 | gaming |
-- | bob | 1 | music |
-- | bob | 2 | sports |
  • json_to_recordset() - 将 JSON 对象数组转换为记录集
  • jsonb_to_recordset() - 将 JSONB 对象数组转换为记录集
-- 将 JSON 对象数组转换为记录集
SELECT * FROM
json_to_recordset('[
{"id": 1, "name": "Alice", "age": 28},
{"id": 2, "name": "Bob", "age": 35}
]') AS t(id INT, name TEXT, age INT);
-- | id | name | age |
-- |----|-------|-----|
-- | 1 | Alice | 28 |
-- | 2 | Bob | 35 |

-- 使用 JSONB
SELECT * FROM
jsonb_to_recordset('[
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]') AS t(id INT, name TEXT);
-- 订单表包含商品数组
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_name VARCHAR(100),
items JSONB
);

INSERT INTO orders (customer_name, items)
VALUES
('John', '[
{"product": "Laptop", "price": 999.99, "quantity": 1},
{"product": "Mouse", "price": 29.99, "quantity": 2}
]'),
('Jane', '[
{"product": "Monitor", "price": 299.99, "quantity": 1}
]');

-- 查询所有订单项
SELECT
o.order_id,
o.customer_name,
item->>'product' AS product,
(item->>'price')::NUMERIC AS price,
(item->>'quantity')::INTEGER AS quantity
FROM orders o,
jsonb_array_elements(o.items) AS item;
-- | order_id | customer_name | product | price | quantity |
-- |----------|---------------|---------|--------|----------|
-- | 1 | John | Laptop | 999.99 | 1 |
-- | 1 | John | Mouse | 29.99 | 2 |
-- | 2 | Jane | Monitor | 299.99 | 1 |

-- 计算订单总额
SELECT
o.order_id,
o.customer_name,
SUM((item->>'price')::NUMERIC * (item->>'quantity')::INTEGER) AS total
FROM orders o,
jsonb_array_elements(o.items) AS item
GROUP BY o.order_id, o.customer_name;
-- | order_id | customer_name | total |
-- |----------|---------------|---------|
-- | 1 | John | 1059.97 |
-- | 2 | Jane | 299.99 |
  • jsonb_array_elements()json_array_elements()性能更好(推荐)
  • 使用WITH ORDINALITY可以获取数组元素的位置(从 1 开始)
  • json_to_recordset()需要显式指定列名和类型
  • 展开数组会产生笛卡尔积,注意性能影响
  • 使用->获取 JSON 对象,->>获取文本值

73. 修改 JSON 数组

  • PostgreSQL 提供多种方法来修改 JSON 列中嵌套的数组,包括添加、删除和更新元素
-- 添加元素
UPDATE table_name
SET json_column = jsonb_set(
json_column,
'{array_key}',
json_column->'array_key' || '["new_value"]'::jsonb
)
WHERE condition;

-- 删除元素
UPDATE table_name
SET json_column = jsonb_set(
json_column,
'{array_key}',
(SELECT jsonb_agg(elem)
FROM jsonb_array_elements(json_column->'array_key') elem
WHERE elem != '"value_to_remove"'::jsonb)
)
WHERE condition;

添加元素

-- 创建用户表
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL,
meta_data JSONB NOT NULL DEFAULT '{}'::jsonb
);

-- 插入数据
INSERT INTO users (username, meta_data)
VALUES ('johndoe', '{"roles": ["hr", "marketing", "sales"]}'::jsonb);

SELECT * FROM users;
-- | id | username | meta_data |
-- |----|----------|-----------------------------------------|
-- | 1 | johndoe | {"roles": ["hr", "marketing", "sales"]} |

删除元素

-- 删除特定角色
UPDATE users
SET meta_data = jsonb_set(
meta_data,
'{roles}',
(SELECT jsonb_agg(elem)
FROM jsonb_array_elements(meta_data->'roles') elem
WHERE elem != '"marketing"'::jsonb)
)
WHERE id = 1;

SELECT * FROM users;
-- | id | username | meta_data |
-- |----|----------|-------------------------------------|
-- | 1 | johndoe | {"roles": ["hr", "sales", "development"]} |

-- 删除多个元素
UPDATE users
SET meta_data = jsonb_set(
meta_data,
'{roles}',
(SELECT jsonb_agg(elem)
FROM jsonb_array_elements(meta_data->'roles') elem
WHERE elem NOT IN ('"hr"'::jsonb, '"sales"'::jsonb))
)
WHERE id = 1;

替换特定位置的元素

-- 替换第一个元素(索引从0开始)
UPDATE users
SET meta_data = jsonb_set(
meta_data,
'{roles,0}',
'"admin"'::jsonb
)
WHERE id = 1;

-- 替换第二个元素
UPDATE users
SET meta_data = jsonb_set(
meta_data,
'{roles,1}',
'"manager"'::jsonb
)
WHERE id = 1;
  • || 连接数组,jsonb_agg() 聚合行为数组
  • ? 检查键/元素存在性,索引从 0 开始
  • 修改替换整个数组(非原地),用 COALESCE 处理空值

74. 提取 JSON 数据

  • PostgreSQL 提供->->>运算符从 JSON 数组中提取元素,或从 JSON 对象中提取键的值
-- 从数组中提取元素(返回 JSONB)
json_array -> n

-- 从数组中提取元素(返回文本)
json_array ->> n

-- 从对象中提取值(返回 JSONB)
json_object -> 'key'

-- 从对象中提取值(返回文本)
json_object ->> 'key'
  • n - 数组索引,从 0 开始,负数从末尾开始(-1 是最后一个)
  • -> 返回 JSONB 值
  • ->> 返回文本字符串

从数组中提取元素

-- 创建员工表
CREATE TABLE employees(
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
phones JSONB NOT NULL
);

-- 插入数据
INSERT INTO employees (name, phones)
VALUES
('John Doe', '["(408) 555-1111", "(408) 555-2222", "(408) 555-3333"]'),
('Jane Smith', '["(408) 666-1111", "(408) 666-2222", "(408) 666-3333"]');

-- 提取第一个电话(JSONB 格式)
SELECT
name,
phones -> 0 AS phone
FROM employees
WHERE name = 'John Doe';
-- | name | phone |
-- |----------|------------------|
-- | John Doe | "(408) 555-1111" |

-- 提取第一个电话(文本格式)
SELECT
name,
phones ->> 0 AS phone
FROM employees
WHERE name = 'John Doe';
-- | name | phone |
-- |----------|----------------|
-- | John Doe | (408) 555-1111 |

-- 提取最后一个电话
SELECT
name,
phones ->> -1 AS phone
FROM employees
WHERE name = 'Jane Smith';
-- | name | phone |
-- |------------|----------------|
-- | Jane Smith | (408) 666-3333 |

-- 提取不存在的元素(返回 NULL)
SELECT
name,
phones -> 5 AS phone
FROM employees
WHERE name = 'John Doe';
-- | name | phone |
-- |----------|-------|
-- | John Doe | null |

从对象中提取值

-- 创建请求表
CREATE TABLE requests(
id SERIAL PRIMARY KEY,
employee_id INT NOT NULL,
request_date DATE NOT NULL,
data JSONB NOT NULL
);

-- 插入数据
INSERT INTO requests (employee_id, request_date, data)
VALUES
(1, '2024-02-23', '{
"current_position": "Software Engineer",
"new_position": "Senior Software Engineer",
"effective_date": "2024-03-01"
}'),
(2, '2024-02-24', '{
"current_position": "Data Analyst",
"new_position": "Senior Data Analyst",
"effective_date": "2024-03-15"
}');

-- 提取当前职位(JSONB 格式)
SELECT
data -> 'current_position' AS current_position
FROM requests
WHERE employee_id = 1;
-- | current_position |
-- |---------------------|
-- | "Software Engineer" |

-- 提取当前职位(文本格式)
SELECT
data ->> 'current_position' AS current_position
FROM requests
WHERE employee_id = 1;
-- | current_position |
-- |-------------------|
-- | Software Engineer |

-- 提取多个键
SELECT
employee_id,
data ->> 'current_position' AS current_position,
data ->> 'new_position' AS new_position,
data ->> 'effective_date' AS effective_date
FROM requests;

-- 提取不存在的键(返回 NULL)
SELECT
data ->> 'salary' AS salary
FROM requests
WHERE employee_id = 1;
-- | salary |
-- |--------|
-- | null |

嵌套提取

-- 创建用户表
CREATE TABLE users(
user_id SERIAL PRIMARY KEY,
profile JSONB
);

INSERT INTO users (profile)
VALUES ('{
"name": "Alice",
"contact": {
"email": "alice@example.com",
"phone": "123-456-7890"
},
"skills": ["Python", "SQL", "Docker"]
}');

-- 提取嵌套对象的值
SELECT
profile -> 'contact' ->> 'email' AS email
FROM users;
-- | email |
-- |--------------------|
-- | alice@example.com |

-- 提取嵌套数组的元素
SELECT
profile -> 'skills' ->> 0 AS first_skill
FROM users;
-- | first_skill |
-- |-------------|
-- | Python |

-- 链式提取
SELECT
profile ->> 'name' AS name,
profile -> 'contact' ->> 'email' AS email,
profile -> 'contact' ->> 'phone' AS phone,
profile -> 'skills' ->> 0 AS primary_skill
FROM users;

在 WHERE 子句中使用

-- 查找特定职位的请求
SELECT *
FROM requests
WHERE data ->> 'current_position' = 'Software Engineer';

-- 查找有效日期在特定范围内的请求
SELECT *
FROM requests
WHERE (data ->> 'effective_date')::DATE >= '2024-03-01';

-- 查找特定电话号码的员工
SELECT *
FROM employees
WHERE phones ->> 0 = '(408) 555-1111';
  • -> 返回 JSONB,->> 返回文本
  • 数组索引从 0 开始,负数从末尾开始(-1 为最后)
  • 不存在的键/索引返回 NULL,支持链式提取嵌套值