Skip to content

SQL 速查表

用于管理和查询关系型数据库的标准语言。

01

SELECT 与查询基础

SELECT、WHERE 与 ORDER BY

SELECT 从一个或多个表中检索行。始终显式指定列名而非使用 *,以提升性能和清晰度(模式变更不会破坏你的应用)。WHERE 在分组前过滤行。ORDER BY 对结果排序(ASC 默认升序,DESC 降序)。LIMIT/OFFSET 实现分页——对于大型数据集,优先使用键集分页(WHERE id > last_id)。

sql
-- basic query: select specific columns
SELECT id, name, email
FROM users
WHERE age >= 18 AND status = 'active'
ORDER BY name ASC, created_at DESC
LIMIT 10 OFFSET 0;

-- select all columns (avoid in production)
SELECT * FROM products;

-- column aliases with AS
SELECT name AS product_name, price * 1.1 AS price_with_tax
FROM products;

DISTINCT 与别名

DISTINCT 从结果集中移除重复行。它作用于整行而非单个列——SELECT DISTINCT city, country 返回唯一的 city+country 组合。表别名(u、o)可缩短查询语句,在表自连接时是必需的。列别名用于重命名输出列以提高可读性。

sql
-- unique values only
SELECT DISTINCT country FROM users;
SELECT DISTINCT city, country FROM users;  -- unique combos

-- table aliases (essential for joins)
SELECT u.name, o.total
FROM users AS u
JOIN orders AS o ON u.id = o.user_id;

-- column alias (AS is optional)
SELECT name product_name, COUNT(*) count
FROM products
GROUP BY name;

过滤:BETWEEN、IN、IS NULL

BETWEEN 两端都是包含的。IN 匹配列表或子查询中的任意值。NULL 需要 IS NULL / IS NOT NULL(不能使用 = NULL)。使用 NOT IN 和子查询时要小心——如果子查询返回任何 NULL,NOT IN 将不返回任何行。改用 NOT EXISTS,它能正确处理 NULL 且通常更快。

sql
SELECT * FROM products
WHERE price BETWEEN 10 AND 100        -- inclusive range
  AND category IN ('tech', 'home')    -- match any value
  AND stock IS NOT NULL               -- exclude NULLs
  AND discount IS NULL;               -- only NULLs

-- NOT IN with NULL caveat: returns nothing if subquery has NULL!
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM orders);  -- risky if NULLs

-- safer: NOT EXISTS
SELECT * FROM users u
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id
);

LIKE 与模式匹配

LIKE 使用 %(零个或多个字符)和 _(恰好一个字符)作为通配符。在大多数数据库中 LIKE 区分大小写,但 MySQL 例外(默认不区分)。在 PostgreSQL 中使用 ILIKE 进行不区分大小写的匹配。对于复杂模式,使用正则表达式(PostgreSQL 中用 ~,MySQL 中用 REGEXP)。以 % 开头的 LIKE 无法使用索引——考虑使用全文搜索以提升性能。

sql
-- LIKE: basic pattern matching
SELECT * FROM users WHERE name LIKE 'A%';     -- starts with A
SELECT * FROM users WHERE name LIKE '%son';    -- ends with son
SELECT * FROM users WHERE name LIKE '%a%';     -- contains a
SELECT * FROM users WHERE name LIKE '_a%';     -- second char is a

-- ILIKE (PostgreSQL): case-insensitive
SELECT * FROM users WHERE name ILIKE 'a%';

-- SIMILAR TO (PostgreSQL): regex-like
SELECT * FROM users WHERE name SIMILAR TO '[AB]%';

-- full regex (PostgreSQL)
SELECT * FROM users WHERE name ~ '^A[a-z]+$';

CASE 表达式

CASE 是 SQL 的 if-then-else,按行求值。它可以出现在 SELECT、WHERE、ORDER BY 和 HAVING 中。'pivot' 模式(CASE 的 SUM)将行转换为列——适用于报表。如果没有 WHEN 匹配且没有 ELSE,CASE 返回 NULL。始终包含 ELSE 以获得可预测的结果。

sql
-- conditional logic in queries
SELECT
  name,
  price,
  CASE
    WHEN price < 10 THEN 'cheap'
    WHEN price < 50 THEN 'moderate'
    WHEN price < 100 THEN 'expensive'
    ELSE 'luxury'
  END AS price_category
FROM products;

-- CASE in aggregation (pivot table)
SELECT
  category,
  COUNT(*) AS total,
  SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active_count,
  SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) AS inactive_count
FROM products
GROUP BY category;
02

JOIN

INNER JOIN

INNER JOIN 仅返回在两个表中都有匹配的行。JOIN 是 INNER JOIN 的简写。对于多表查询,逐步连接表。ON 指定连接条件;当两个表有相同列名时,USING(column) 是简写形式。内连接会排除两侧不匹配的行。

sql
-- only matching rows from both tables
SELECT u.name, o.total, o.created_at
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.total > 100
ORDER BY o.total DESC;

-- multiple joins
SELECT u.name, o.total, p.product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;

-- USING (when column names match)
SELECT * FROM users
JOIN profiles ON users.id = profiles.user_id;

LEFT JOIN(LEFT OUTER JOIN)

LEFT JOIN 返回左表中的所有行,对于不匹配的右表行用 NULL 填充。这对于'包含所有内容'的查询至关重要。反连接模式(WHERE right.id IS NULL)查找左表中没有匹配右表的行——适用于查找'未下过订单的用户'。COUNT(right.id) 计数非 NULL 值,因此对于没有订单的用户返回 0。

sql
-- all users, with their orders (NULL if no orders)
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
ORDER BY u.name;

-- find users with NO orders (anti-join pattern)
SELECT u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

-- count orders per user (including zero)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;

RIGHT 与 FULL OUTER JOIN

RIGHT JOIN 返回所有右表行;它等价于交换表并使用 LEFT JOIN(后者更易读)。FULL OUTER JOIN 返回两个表中的所有行,在没有匹配处用 NULL 填充——适用于数据核对。MySQL 不直接支持 FULL OUTER JOIN;用 LEFT JOIN UNION RIGHT JOIN 来模拟。

sql
-- RIGHT JOIN: all rows from right table
SELECT u.name, o.total
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
-- returns all orders, even orphaned ones (user_id = NULL)

-- FULL OUTER JOIN: all rows from both tables
SELECT u.name, o.total
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;
-- returns all users AND all orders, matching where possible

-- Note: RIGHT JOIN is rarely used (just swap tables and use LEFT)
-- FULL OUTER JOIN is useful for finding mismatches between tables
SELECT u.name, o.total
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id
WHERE u.id IS NULL OR o.user_id IS NULL;

CROSS JOIN 与自连接

CROSS JOIN 产生笛卡尔积——A 的每一行与 B 的每一行配对。用于生成组合(尺寸 × 颜色)。自连接(表与自身连接)常用于层次数据(员工-经理)、查找重复项或比较同一表内的行。在自连接中始终使用表别名以区分两个'副本'。

sql
-- CROSS JOIN: Cartesian product (every combination)
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;
-- produces all size+color combinations

-- Self join: join a table to itself
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

-- self join for hierarchical data
SELECT c.name AS child, p.name AS parent
FROM categories c
JOIN categories p ON c.parent_id = p.id;

-- self join to find duplicates
SELECT a.id, a.email, b.id AS dup_id
FROM users a
JOIN users b ON a.email = b.email AND a.id < b.id;

NATURAL JOIN 与 JOIN 类型总结

NATURAL JOIN 自动按同名列连接——方便但危险,因为模式变更可能静默改变连接行为。生产环境中应避免使用。LATERAL 连接允许子查询引用外查询的列——对于'每组前 N 条'查询非常强大。逗号语法(FROM a, b)等价于 CROSS JOIN。

sql
-- NATURAL JOIN: joins on all matching column names
-- (rarely recommended — implicit, fragile)
SELECT * FROM users NATURAL JOIN profiles;
-- joins on any column that exists in BOTH tables

-- Summary of join types:
-- INNER JOIN : matching rows only
-- LEFT JOIN  : all left + matching right
-- RIGHT JOIN : all right + matching left
-- FULL JOIN  : all from both sides
-- CROSS JOIN : Cartesian product
-- SELF JOIN  : table joined to itself

-- LATERAL JOIN (PostgreSQL): subquery can reference outer query
SELECT u.name, recent.*
FROM users u,
LATERAL (
  SELECT * FROM orders o
  WHERE o.user_id = u.id
  ORDER BY o.created_at DESC
  LIMIT 3
) recent;
03

GROUP BY 与聚合

GROUP BY 与 HAVING

GROUP BY 将行折叠为组,每组一行。聚合函数(COUNT、SUM、AVG、MIN、MAX)对每个组操作。WHERE 在分组前过滤单个行;HAVING 在聚合后过滤组。SELECT 中的非聚合列必须出现在 GROUP BY 中(标准 SQL)。MySQL 较宽松但不可预测——始终在 GROUP BY 中包含所有非聚合列。

sql
-- aggregate per group
SELECT category, COUNT(*) AS cnt, AVG(price) AS avg_price
FROM products
GROUP BY category
HAVING COUNT(*) > 5
ORDER BY cnt DESC;

-- HAVING filters groups (after aggregation)
-- WHERE filters rows (before aggregation)
SELECT dept, AVG(salary) AS avg_sal
FROM employees
WHERE status = 'active'       -- filter rows first
GROUP BY dept
HAVING AVG(salary) > 50000;   -- then filter groups

聚合函数

COUNT(*) 计数所有行(包括 NULL);COUNT(column) 仅计数非 NULL 值。COUNT(DISTINCT col) 计数唯一值。SUM/AVG 忽略 NULL。AVG = SUM/COUNT(非 NULL),因此 NULL 会影响平均值。STRING_AGG(PostgreSQL)/ GROUP_CONCAT(MySQL)按组连接字符串。BOOL_OR/BOOL_and 在任一/所有值为真时返回 true。

sql
SELECT
  COUNT(*) AS total_rows,           -- counts all rows
  COUNT(email) AS emails_filled,    -- counts non-NULL emails
  COUNT(DISTINCT country) AS countries,
  SUM(amount) AS total_revenue,
  AVG(amount) AS avg_order,
  MIN(amount) AS smallest_order,
  MAX(amount) AS largest_order,
  -- string aggregation (PostgreSQL)
  STRING_AGG(name, ', ') AS all_names,
  -- boolean aggregation
  BOOL_OR(is_active) AS any_active,
  BOOL_AND(is_active) AS all_active
FROM orders;

多列 GROUP BY

按多列分组会创建组的层次结构。WITH ROLLUP 添加小计和总计行(在分组列中为 NULL)。GROUPING SETS 让你精确指定想要的分组组合——比 ROLLUP 更灵活。CUBE 生成所有可能的分组组合。这些对于报表和 OLAP 查询至关重要。

sql
-- multi-level grouping
SELECT
  EXTRACT(YEAR FROM created_at) AS yr,
  EXTRACT(MONTH FROM created_at) AS mon,
  category,
  COUNT(*) AS cnt,
  SUM(total) AS revenue
FROM orders
GROUP BY yr, mon, category
ORDER BY yr DESC, mon DESC, cnt DESC;

-- GROUP BY with ROLLUP (subtotals + grand total)
SELECT category, COUNT(*) AS cnt
FROM products
GROUP BY category WITH ROLLUP;
-- last row has NULL category = grand total

-- GROUPING SETS (PostgreSQL): specify multiple groupings
SELECT category, status, COUNT(*)
FROM products
GROUP BY GROUPING SETS ((category, status), (category), ());

HAVING 与 WHERE

关键区别:WHERE 在聚合前过滤单个行(不能使用 SUM、COUNT 等),而 HAVING 在聚合后过滤组(可以使用聚合函数)。先用 WHERE 减少数据(性能更好),再用 HAVING 过滤聚合结果。两者可出现在同一查询中——先 WHERE,再 GROUP BY,最后 HAVING。

sql
-- WHERE: filters rows BEFORE grouping
-- Cannot use aggregates
SELECT category, COUNT(*) AS cnt
FROM products
WHERE price > 10          -- OK: filter on raw column
GROUP BY category;

-- HAVING: filters groups AFTER grouping
-- Can use aggregates
SELECT category, COUNT(*) AS cnt
FROM products
GROUP BY category
HAVING COUNT(*) > 5       -- OK: filter on aggregate
   AND AVG(price) > 20;   -- OK: multiple aggregate filters

-- combining both
SELECT category, COUNT(*) AS cnt
FROM products
WHERE price > 10          -- filter rows
GROUP BY category         -- group
HAVING COUNT(*) > 5;      -- filter groups

日期/时间聚合

日期截断对时间序列报表至关重要。DATE(col) 仅提取日期;EXTRACT/TIME_PART 获取特定部分(年、月、小时)。TO_CHAR 格式化日期用于分组和显示。对于时间序列分析,考虑使用 DATE_TRUNC('month', col),它保留时间戳类型。对大型表的日期列建立索引以提升性能。

sql
-- group by date parts
SELECT
  DATE(created_at) AS order_date,
  COUNT(*) AS orders,
  SUM(total) AS revenue
FROM orders
GROUP BY DATE(created_at)
ORDER BY order_date DESC;

-- group by hour
SELECT
  EXTRACT(HOUR FROM created_at) AS hr,
  COUNT(*) AS cnt
FROM orders
WHERE created_at >= CURRENT_DATE
GROUP BY hr
ORDER BY hr;

-- monthly revenue trend
SELECT
  TO_CHAR(created_at, 'YYYY-MM') AS month,
  SUM(total) AS revenue,
  COUNT(*) AS orders,
  AVG(total) AS avg_order
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY month
ORDER BY month;
04

子查询与 CTE

标量与列子查询

标量子查询返回单个值,可在任何需要值的地方使用。列子查询返回一列,与 IN、ANY、ALL 一起使用。SELECT 中的子查询(相关子查询)对外查询的每一行执行一次——在大型数据集上可能很慢。考虑改写为带 GROUP BY 的 JOIN 以获得更好的性能。

sql
-- scalar subquery (returns single value)
SELECT name, age
FROM users
WHERE age > (SELECT AVG(age) FROM users);

-- column subquery (returns one column, multiple rows)
SELECT name
FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 100);

-- subquery in SELECT
SELECT
  u.name,
  (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;

相关子查询与 EXISTS

相关子查询引用外查询,并对外查询的每一行执行一次——可能较慢。EXISTS/NOT EXISTS 很高效,因为它们短路(在第一个匹配时停止)。NOT EXISTS 是查找'没有匹配行的行'的首选方式——它正确处理 NULL,且通常比 NOT IN 更快。数据库可能将相关子查询优化为连接。

sql
-- correlated: subquery references outer query
SELECT u.name
FROM users u
WHERE EXISTS (
  SELECT 1 FROM orders o
  WHERE o.user_id = u.id
    AND o.total > 1000
);

-- NOT EXISTS: users without any orders
SELECT u.name
FROM users u
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id
);

-- correlated subquery in SELECT (runs per row)
SELECT
  u.name,
  (SELECT MAX(o.total)
   FROM orders o
   WHERE o.user_id = u.id) AS max_order
FROM users u;

公用表表达式(CTE)

CTE(WITH 子句)创建命名的临时结果集,使复杂查询更易读。与子查询不同,CTE 可被多次引用并从上到下阅读。在大多数数据库中,CTE 会被内联(优化在查询级别发生)。PostgreSQL 12+ 支持 MATERIALIZED/NOT MATERIALIZED 提示。递归查询也需要 CTE。

sql
-- CTE: named temporary result set
WITH active_users AS (
  SELECT id, name FROM users WHERE status = 'active'
),
user_orders AS (
  SELECT user_id, COUNT(*) AS cnt, SUM(total) AS revenue
  FROM orders
  GROUP BY user_id
)
SELECT au.name, uo.cnt, uo.revenue
FROM active_users au
LEFT JOIN user_orders uo ON au.id = uo.user_id
ORDER BY uo.revenue DESC NULLS LAST;

-- CTEs improve readability for complex queries
-- They are NOT materialized (just syntactic sugar) in most DBs
-- (PostgreSQL 12+ can materialize with MATERIALIZED keyword)

递归 CTE

递归 CTE 引用自身,实现树/图遍历和序列生成。结构:基础情况 UNION ALL 递归情况。递归情况引用该 CTE 且必须终止(添加 WHERE 以防止无限循环)。常见用途:组织结构图、类别树、依赖图、日期序列。每个数据库的语法略有不同——请查阅你的 DBMS 文档。

sql
-- hierarchical data: org chart
WITH RECURSIVE org_tree AS (
  -- base case: top-level managers
  SELECT id, name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- recursive case: direct reports
  SELECT e.id, e.name, e.manager_id, ot.level + 1
  FROM employees e
  JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT level, name FROM org_tree ORDER BY level, name;

-- generate a series of dates
WITH RECURSIVE dates AS (
  SELECT DATE '2024-01-01' AS d
  UNION ALL
  SELECT d + 1 FROM dates WHERE d < '2024-01-31'
)
SELECT d FROM dates;

-- factorial
WITH RECURSIVE fact(n, result) AS (
  SELECT 1, 1
  UNION ALL
  SELECT n + 1, result * (n + 1) FROM fact WHERE n < 10
)
SELECT * FROM fact;

子查询运算符:ANY、ALL

ANY 和 ALL 将值与子查询结果集比较。> ANY 表示'大于至少一个'。> ALL 表示'大于每一个'。= ANY 等价于 IN。<> ALL 等价于 NOT IN 但更安全地处理 NULL。这些运算符不如 IN/EXISTS 常用,但可以更自然地表达某些查询。

sql
-- ANY: greater than ANY of the values (= at least one)
SELECT * FROM products
WHERE price > ANY (
  SELECT price FROM products WHERE category = 'tech'
);
-- true if price exceeds at least one tech product's price

-- ALL: greater than ALL values (= every one)
SELECT * FROM products
WHERE price > ALL (
  SELECT price FROM products WHERE category = 'tech'
);
-- true if price exceeds every tech product's price

-- = ANY is equivalent to IN
SELECT * FROM users
WHERE id = ANY (SELECT user_id FROM orders);

-- <> ALL is equivalent to NOT IN (but NULL-safe)
SELECT * FROM users
WHERE id <> ALL (SELECT user_id FROM orders WHERE total < 0);
05

窗口函数

ROW_NUMBER、RANK、DENSE_RANK

ROW_NUMBER 分配唯一的连续编号(1、2、3...)。RANK 对并列项给予相同排名但跳过后续编号(1、1、3)。DENSE_RANK 对并列项给予相同排名但不跳过(1、1、2)。PARTITION BY 将行分组;函数在每个分区重置。'每组前 N 条'模式(ROW_NUMBER + WHERE rn <= N)在分析中极其常见。

sql
SELECT
  name,
  salary,
  dept,
  ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
  RANK() OVER (ORDER BY salary DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;

-- top 3 earners per department
SELECT * FROM (
  SELECT
    name,
    dept,
    salary,
    ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
  FROM employees
) ranked
WHERE rn <= 3;

LAG 与 LEAD

LAG 访问前一行的值;LEAD 访问后一行的值。两者都接受可选的偏移量(默认 1)和默认值(默认 NULL)。对时间序列分析至关重要:日环比变化、移动比较、缺口检测。NULLIF 在百分比计算中防止除以零。始终在 OVER 子句中指定 ORDER BY 以获得确定性结果。

sql
-- compare each row to previous/next
SELECT
  date,
  revenue,
  LAG(revenue) OVER (ORDER BY date) AS prev_day,
  LEAD(revenue) OVER (ORDER BY date) AS next_day,
  revenue - LAG(revenue) OVER (ORDER BY date) AS daily_change,
  ROUND(
    (revenue - LAG(revenue) OVER (ORDER BY date)) * 100.0
    / NULLIF(LAG(revenue) OVER (ORDER BY date), 0),
    2
  ) AS pct_change
FROM daily_sales
ORDER BY date;

-- LAG with offset and default
SELECT
  date,
  revenue,
  LAG(revenue, 7) OVER (ORDER BY date) AS revenue_7_days_ago
FROM daily_sales;

累计总计与移动平均

窗口帧定义函数操作的行范围。ROWS BETWEEN 使用物理行偏移;RANGE 使用逻辑值范围(更适合日期缺口)。UNBOUNDED PRECEDING 表示'从开始'。累计总计(累计 SUM)和移动平均是最常见的分析模式。没有帧时,聚合窗口函数使用默认值:RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。

sql
-- cumulative sum (running total)
SELECT
  date,
  revenue,
  SUM(revenue) OVER (ORDER BY date) AS running_total,
  SUM(revenue) OVER (ORDER BY date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS last_7_days_sum,
  AVG(revenue) OVER (ORDER BY date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7d
FROM daily_sales
ORDER BY date;

-- window frame options:
-- ROWS BETWEEN n PRECEDING AND n FOLLOWING  -- physical rows
-- RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND CURRENT ROW -- logical range
-- ROWS UNBOUNDED PRECEDING = from start to current
-- ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING = all rows

NTILE 与 PERCENT_RANK

NTILE(n) 将有序行分为 n 个大致相等的组(四分位数、十分位数、百分位数)。PERCENT_RANK 给出相对排名(0 到 1)。CUME_DIST 给出累积分布。FIRST_VALUE/LAST_VALUE 返回帧中第一行/最后一行的值——注意 LAST_VALUE 需要显式帧(UNBOUNDED FOLLOWING),因为默认帧在当前行结束。

sql
-- divide into quartiles
SELECT
  name,
  salary,
  NTILE(4) OVER (ORDER BY salary DESC) AS quartile,
  PERCENT_RANK() OVER (ORDER BY salary) AS pct_rank,
  CUME_DIST() OVER (ORDER BY salary) AS cumulative_dist
FROM employees;

-- first/last value in a partition
SELECT
  dept,
  name,
  salary,
  FIRST_VALUE(name) OVER (PARTITION BY dept ORDER BY salary DESC) AS top_earner,
  LAST_VALUE(name) OVER (
    PARTITION BY dept ORDER BY salary DESC
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS lowest_earner
FROM employees;

窗口聚合函数

窗口聚合(带 OVER 的 SUM、AVG、COUNT 等)计算聚合值而不折叠行——每行都附加了聚合值。这是与 GROUP BY 的关键区别:你保留所有明细行,同时也能看到汇总。非常适合将单个值与组平均值比较、计算百分比,以及为明细报表添加上下文列。

sql
-- aggregates over windows (no row collapse!)
SELECT
  name,
  dept,
  salary,
  -- compare to department average
  AVG(salary) OVER (PARTITION BY dept) AS dept_avg,
  salary - AVG(salary) OVER (PARTITION BY dept) AS diff_from_avg,
  -- percentage of department total
  salary * 100.0 / SUM(salary) OVER (PARTITION BY dept) AS pct_of_dept,
  -- count per department
  COUNT(*) OVER (PARTITION BY dept) AS dept_size
FROM employees
ORDER BY dept, salary DESC;

-- key advantage: aggregates without GROUP BY
-- every row is preserved, with the aggregate value attached
06

DDL:表与模式

CREATE TABLE 与数据类型

CREATE TABLE 定义模式。SERIAL(PostgreSQL)/ AUTO_INCREMENT(MySQL)自动生成 ID。VARCHAR(n) 有长度限制;TEXT 无限制。DECIMAL(p,s) 是精确的(用于货币!),FLOAT 是近似的。CHECK 约束强制业务规则。DEFAULT 在未指定时提供值。JSONB(PostgreSQL)支持索引化的 JSON 查询。对于跨时区的时间戳,始终使用 TIMESTAMP WITH TIME ZONE。

sql
CREATE TABLE users (
  id          SERIAL PRIMARY KEY,          -- auto-increment (PostgreSQL)
  -- MySQL: id INT AUTO_INCREMENT PRIMARY KEY
  username    VARCHAR(50) UNIQUE NOT NULL,
  email       VARCHAR(255) UNIQUE NOT NULL,
  password    VARCHAR(255) NOT NULL,
  age         INT CHECK (age >= 0 AND age <= 150),
  salary      DECIMAL(10, 2) DEFAULT 0.00, -- precision, scale
  bio         TEXT,                        -- unlimited length
  avatar      BYTEA,                       -- binary (PostgreSQL)
  metadata    JSONB,                       -- JSON (PostgreSQL)
  status      VARCHAR(20) DEFAULT 'active',
  created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- common types:
-- INT, BIGINT, SMALLINT, DECIMAL(p,s), NUMERIC
-- VARCHAR(n), CHAR(n), TEXT
-- DATE, TIME, TIMESTAMP, INTERVAL
-- BOOLEAN, UUID, JSON/JSONB, BYTEA/BLOB

约束:PRIMARY、FOREIGN、UNIQUE、CHECK

约束在数据库级别强制数据完整性。PRIMARY KEY 唯一标识行(隐含 NOT NULL + UNIQUE)。FOREIGN KEY 维护引用完整性——ON DELETE CASCADE 在父行删除时移除子行。UNIQUE 防止重复。CHECK 强制自定义规则。在数据库中(而非仅应用代码中)定义约束可确保无论数据如何被访问都保持完整性。

sql
CREATE TABLE orders (
  id          SERIAL PRIMARY KEY,
  user_id     INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  total       DECIMAL(10,2) NOT NULL CHECK (total >= 0),
  status      VARCHAR(20) NOT NULL DEFAULT 'pending',
  created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  -- table-level constraints
  CONSTRAINT valid_status CHECK (status IN ('pending','paid','shipped','cancelled')),
  CONSTRAINT unique_user_order UNIQUE (user_id, created_at)
);

-- foreign key actions:
-- ON DELETE CASCADE  : delete child rows when parent deleted
-- ON DELETE SET NULL : set FK to NULL (column must be nullable)
-- ON DELETE RESTRICT : prevent parent deletion (default)
-- ON UPDATE CASCADE  : update FK when parent PK changes

-- add constraint to existing table
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);

ALTER TABLE

ALTER TABLE 修改现有表结构。添加带默认值的列通常很快(PostgreSQL 11+ 不会重写表)。删除列可能锁定表。更改列类型可能需要全表重写,且如果数据无法转换可能失败。始终先在副本上测试模式迁移。使用迁移工具(Flyway、Alembic、Rails migrations)进行版本控制的模式变更。

sql
-- add column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users ADD COLUMN verified BOOLEAN DEFAULT false;

-- drop column
ALTER TABLE users DROP COLUMN avatar;

-- rename column/table
ALTER TABLE users RENAME COLUMN username TO login;
ALTER TABLE users RENAME TO accounts;

-- change column type
ALTER TABLE users ALTER COLUMN age TYPE BIGINT;
-- MySQL: ALTER TABLE users MODIFY COLUMN age BIGINT;

-- add/drop constraints
ALTER TABLE users ADD CONSTRAINT email_unique UNIQUE (email);
ALTER TABLE users DROP CONSTRAINT email_unique;

-- set default
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'active';

DROP、TRUNCATE 与索引

DROP TABLE 完全移除表;TRUNCATE 清空表但保留结构(比 DELETE 快得多,重置标识)。索引加速查询但减慢写入——策略性地建立索引。复合索引从左到右工作:idx(a,b,c) 有助于 WHERE a=?、WHERE a=? AND b=?,但无助于 WHERE b=?。GIN 索引支持全文搜索。部分索引仅索引匹配的行以节省空间。

sql
-- DROP: permanently remove table (structure + data)
DROP TABLE IF EXISTS old_logs CASCADE;
-- CASCADE drops dependent objects (views, FKs)

-- TRUNCATE: remove all data, keep structure (faster than DELETE)
TRUNCATE TABLE logs;
TRUNCATE TABLE logs RESTART IDENTITY;  -- reset SERIAL counter
TRUNCATE TABLE orders, order_items CASCADE;  -- multiple tables

-- CREATE INDEX for query performance
CREATE INDEX idx_users_email ON users(email);
CREATE UNIQUE INDEX idx_users_username ON users(username);
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
CREATE INDEX idx_products_name ON products USING gin(to_tsvector('english', name));

-- partial index (PostgreSQL)
CREATE INDEX idx_active_users ON users(last_login)
WHERE status = 'active';

-- DROP INDEX
DROP INDEX IF EXISTS idx_users_email;

视图与物化视图

视图是保存的查询,作为虚拟表——它们每次运行底层查询。使用视图简化复杂查询、强制安全(列级访问)并提供稳定 API。物化视图存储实际结果——查询更快但必须刷新。对不需要实时数据的昂贵聚合使用物化视图。CONCURRENTLY 在不锁定的情况下刷新(PostgreSQL)。

sql
-- VIEW: stored query (virtual table, runs on access)
CREATE VIEW active_users AS
SELECT id, name, email FROM users WHERE status = 'active';

SELECT * FROM active_users WHERE name LIKE 'A%';

-- updatable view (simple views can be INSERTed/UPDATEd)
CREATE VIEW user_summary AS
SELECT id, name, email, age FROM users;

-- MATERIALIZED VIEW: stored result (must refresh)
CREATE MATERIALIZED VIEW monthly_stats AS
SELECT
  DATE_TRUNC('month', created_at) AS month,
  COUNT(*) AS orders,
  SUM(total) AS revenue
FROM orders
GROUP BY month;

REFRESH MATERIALIZED VIEW monthly_stats;
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_stats; -- no lock

-- drop
DROP VIEW IF EXISTS active_users;
DROP MATERIALIZED VIEW IF EXISTS monthly_stats;
07

DML:插入、更新、删除

INSERT

INSERT 添加行。一条语句中使用多个 VALUES 比单独插入更高效。INSERT...SELECT 在表之间复制数据。RETURNING(PostgreSQL/Oracle)在一次往返中检索自动生成的值(如 SERIAL id)——对应用代码至关重要。使用 DEFAULT VALUES 插入一行全部默认值。始终指定列名以使代码对模式变更具有弹性。

sql
-- single row
INSERT INTO users (name, email, age)
VALUES ('Alice', '[email protected]', 30);

-- multiple rows
INSERT INTO users (name, email) VALUES
  ('Bob', '[email protected]'),
  ('Carol', '[email protected]'),
  ('Dave', '[email protected]');

-- INSERT ... SELECT (copy data between tables)
INSERT INTO archive_users (name, email, deleted_at)
SELECT name, email, NOW()
FROM users
WHERE status = 'deleted';

-- INSERT with RETURNING (PostgreSQL)
INSERT INTO users (name, email)
VALUES ('Eve', '[email protected]')
RETURNING id, created_at;  -- returns the generated id

-- DEFAULT values
INSERT INTO users DEFAULT VALUES;

UPDATE

UPDATE 修改现有行。除非你打算更新每一行,否则始终包含 WHERE 子句。FROM 子句(PostgreSQL)允许在更新中进行连接。RETURNING 显示哪些行被修改。对多步更新使用事务,以便在出错时可以 ROLLBACK。一个常见错误是忘记 WHERE——考虑先用相同的 WHERE 运行 SELECT 以验证受影响的行。

sql
-- basic update
UPDATE users
SET age = 31, status = 'verified', updated_at = NOW()
WHERE id = 1;

-- update based on another table
UPDATE products p
SET price = p.price * 1.1
FROM categories c
WHERE p.category_id = c.id AND c.name = 'electronics';

-- update with subquery
UPDATE users
SET status = 'premium'
WHERE id IN (
  SELECT user_id FROM orders
  GROUP BY user_id HAVING SUM(total) > 1000
);

-- UPDATE with RETURNING (PostgreSQL)
UPDATE users SET status = 'inactive'
WHERE last_login < '2023-01-01'
RETURNING id, name;

-- WARNING: UPDATE without WHERE affects ALL rows!

DELETE 与 TRUNCATE

DELETE 逐行删除(有日志,可回滚,较慢)。TRUNCATE 一次删除所有行(最小日志记录,快得多,重置自增,在某些数据库中无法回滚)。对于审计跟踪,使用软删除(deleted_at 时间戳)而非硬删除。DELETE 始终使用 WHERE。考虑外键约束——ON DELETE CASCADE 自动处理子行。

sql
-- delete specific rows
DELETE FROM users WHERE status = 'inactive';

-- delete with subquery
DELETE FROM orders
WHERE user_id IN (
  SELECT id FROM users WHERE status = 'deleted'
);

-- delete with RETURNING (PostgreSQL)
DELETE FROM users
WHERE last_login < '2020-01-01'
RETURNING id, name;

-- delete all rows (slow, logged, can be rolled back)
DELETE FROM logs;

-- TRUNCATE (fast, minimal logging, resets identity)
TRUNCATE TABLE logs;
TRUNCATE TABLE logs RESTART IDENTITY CASCADE;

-- soft delete pattern (preferred for audit)
UPDATE users SET deleted_at = NOW() WHERE id = 1;
SELECT * FROM users WHERE deleted_at IS NULL; -- active users

UPSERT(INSERT ... ON CONFLICT)

UPSERT(更新或插入)以原子方式处理重复键冲突。PostgreSQL 使用 ON CONFLICT (column) DO UPDATE/DO NOTHING。MySQL 使用 ON DUPLICATE KEY UPDATE。EXCLUDED(PostgreSQL)/ VALUES()(MySQL)引用提议的插入值。这对幂等操作和避免竞态条件至关重要。没有 upsert,你需要先 SELECT 再 INSERT/UPDATE,这容易产生竞态条件。

sql
-- PostgreSQL: ON CONFLICT (upsert)
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', '[email protected]')
ON CONFLICT (id)
DO UPDATE SET email = EXCLUDED.email, updated_at = NOW()
RETURNING *;

-- DO NOTHING on conflict
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', '[email protected]')
ON CONFLICT (id) DO NOTHING;

-- MySQL: ON DUPLICATE KEY UPDATE
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', '[email protected]')
ON DUPLICATE KEY UPDATE email = VALUES(email);

-- SQLite: ON CONFLICT
INSERT INTO users (id, name)
VALUES (1, 'Alice')
ON CONFLICT(id) DO UPDATE SET name = excluded.name;

MERGE 语句

MERGE(又名增强版 UPSERT)根据行是否匹配,在单个原子语句中结合 INSERT、UPDATE 和 DELETE。它是同步源之间数据的最有效方式。WHEN MATCHED 为现有行触发 UPDATE/DELETE;WHEN NOT MATCHED 为新行触发 INSERT。SQL Server、Oracle、PostgreSQL 15+ 和 DB2 支持。MySQL 不支持 MERGE——使用 INSERT...ON DUPLICATE KEY。

sql
-- MERGE: conditional insert/update/delete in one statement
-- (SQL Server, Oracle, PostgreSQL 15+)
MERGE INTO products AS target
USING (VALUES
  (1, 'Widget', 9.99),
  (2, 'Gadget', 19.99),
  (3, 'Gizmo', 29.99)
) AS source (id, name, price)
ON target.id = source.id
WHEN MATCHED THEN
  UPDATE SET name = source.name, price = source.price
WHEN NOT MATCHED THEN
  INSERT (id, name, price) VALUES (source.id, source.name, source.price)
WHEN MATCHED AND source.price < 0 THEN
  DELETE;

-- useful for:
-- - syncing data from external sources
-- - bulk upsert with conditional logic
-- - ETL operations
08

事务与 ACID

BEGIN、COMMIT、ROLLBACK

事务将操作组合为原子单元——全部成功(COMMIT)或全部失败(ROLLBACK)。这是 ACID 中的'A'。BEGIN/START TRANSACTION 启动事务。SAVEPOINT 在事务内创建命名的回滚点——你可以回滚到它而不中止整个事务。始终提交或回滚——让事务保持打开状态会持有锁并可能导致死锁。

sql
-- basic transaction
BEGIN;
  UPDATE accounts SET balance = balance - 100 WHERE id = 1;
  UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- both updates succeed or both fail (atomicity)

-- rollback on error
BEGIN;
  INSERT INTO orders (user_id, total) VALUES (1, 50.00);
  -- oops, something went wrong
  ROLLBACK;
-- the insert is undone

-- transaction with savepoints
BEGIN;
  INSERT INTO logs (msg) VALUES ('step 1');
  SAVEPOINT my_savepoint;
  INSERT INTO logs (msg) VALUES ('step 2');
  ROLLBACK TO my_savepoint;  -- undo step 2, keep step 1
  INSERT INTO logs (msg) VALUES ('step 3');
COMMIT;  -- commits step 1 and step 3

隔离级别

隔离级别平衡一致性与并发性。READ COMMITTED(PostgreSQL/Oracle 默认)防止脏读但允许不可重复读。REPEATABLE READ 防止不可重复读但允许幻读。SERIALIZABLE 防止所有异常但降低并发性。更高的隔离级别 = 更多锁 = 更少并发。选择满足正确性要求的最低级别。

sql
-- set isolation level for transaction
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
  -- can see committed data from other transactions
  SELECT balance FROM accounts WHERE id = 1;
COMMIT;

-- isolation levels (from weakest to strongest):
-- READ UNCOMMITTED: can read uncommitted (dirty) data
-- READ COMMITTED: only committed data (PostgreSQL default)
-- REPEATABLE READ: same query returns same results within txn
-- SERIALIZABLE: transactions appear to run sequentially

-- PostgreSQL: set per transaction
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- MySQL: set per session
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- check current level
SHOW TRANSACTION ISOLATION LEVEL;

锁定与 SELECT FOR UPDATE

SELECT FOR UPDATE 锁定行,使其他事务在你提交前无法修改它们。这实现了悲观并发控制。SKIP LOCKED 对作业队列至关重要——多个工作进程可以获取作业而不会相互阻塞。NOWAIT 立即失败而非等待。谨慎使用锁定——它降低并发性并可能导致死锁。对大多数用例优先使用乐观并发(版本列)。

sql
-- pessimistic locking: lock rows for update
BEGIN;
  SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
  -- row is locked; other transactions must wait
  UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;  -- lock released

-- NOWAIT: don't wait if locked, error immediately
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;

-- SKIP LOCKED: skip locked rows (useful for job queues)
SELECT * FROM jobs WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 10;

-- SHARE LOCK: allow reads but prevent updates
SELECT * FROM products WHERE id = 1 FOR SHARE;

死锁与错误处理

当两个事务持有彼此需要的锁时发生死锁。数据库检测死锁并中止一个事务(受害者)。通过在所有事务中以一致顺序获取锁来防止死锁。始终准备好重试因死锁或序列化失败而失败的事务。保持事务简短以减少锁争用。应用代码应捕获 SQLSTATE 40P01(死锁)并重试。

sql
-- deadlock example:
-- Transaction A:
BEGIN;
  UPDATE accounts SET balance = balance - 50 WHERE id = 1; -- locks row 1
  UPDATE accounts SET balance = balance + 50 WHERE id = 2; -- waits for row 2

-- Transaction B (concurrent):
BEGIN;
  UPDATE accounts SET balance = balance - 30 WHERE id = 2; -- locks row 2
  UPDATE accounts SET balance = balance + 30 WHERE id = 1; -- waits for row 1
-- DEADLOCK! Database detects and kills one transaction

-- prevention: always lock in consistent order
-- Transaction A and B both lock id=1 first, then id=2

-- PostgreSQL: error codes for handling
-- 40P01: deadlock_detected
-- 40001: serialization_failure
-- 40P02: transaction_integrity_constraint_violation

-- retry pattern (pseudocode):
-- for attempt in range(3):
--     try:
--         BEGIN; ... COMMIT; break
--     except deadlock:
--         ROLLBACK; continue

ACID 属性

ACID 是可靠数据库事务的基础。原子性:事务中的所有操作一起成功或失败。一致性:事务将数据库从一个有效状态转移到另一个(约束被强制执行)。隔离性:并发事务不互相干扰(由隔离级别控制)。持久性:一旦提交,数据在崩溃后仍存活(通过预写日志实现)。NoSQL 数据库通常为可扩展性牺牲某些 ACID 属性。

sql
-- ACID guarantees for transactions:

-- A: Atomicity (all or nothing)
BEGIN;
  INSERT INTO orders (id, total) VALUES (1, 100);
  INSERT INTO order_items (order_id, product_id) VALUES (1, 5);
  -- if either fails, both are rolled back
COMMIT;

-- C: Consistency (valid state to valid state)
-- constraints are checked at commit
ALTER TABLE accounts ADD CONSTRAINT balance_non_negative
  CHECK (balance >= 0);
-- a transaction that would make balance negative fails

-- I: Isolation (concurrent transactions don't interfere)
-- controlled by isolation level (see previous section)

-- D: Durability (committed data survives crashes)
-- achieved via WAL (Write-Ahead Logging) + fsync
-- synchronous_commit = on (default) ensures durability
09

索引、视图与存储过程

索引类型与策略

B-tree 索引(默认)处理等值(=)和范围(<、>、BETWEEN)查询。复合索引遵循最左前缀规则——(a,b,c) 索引有助于 WHERE a=?、WHERE a=? AND b=?,但无助于 WHERE b=?。部分索引仅索引子集以节省空间。表达式索引支持对函数(LOWER、计算列)的索引化查询。使用 EXPLAIN ANALYZE 验证索引是否被使用——未使用的索引浪费空间并减慢写入。

sql
-- B-tree index (default): equality and range queries
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_date ON orders(created_at);

-- composite index (order matters!)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- helps: WHERE user_id = 1
-- helps: WHERE user_id = 1 AND status = 'paid'
-- does NOT help: WHERE status = 'paid' (leftmost prefix rule)

-- partial index: smaller, faster for common filters
CREATE INDEX idx_active_users ON users(email)
WHERE status = 'active';

-- expression index
CREATE INDEX idx_lower_email ON users(LOWER(email));
SELECT * FROM users WHERE LOWER(email) = '[email protected]';

-- unique index
CREATE UNIQUE INDEX idx_unique_email ON users(email);

-- EXPLAIN: see if index is used
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';

存储过程与函数

函数返回值并可在 SELECT 中使用;过程执行操作并通过 CALL 调用。存储过程在数据库中封装业务逻辑——减少网络往返并集中逻辑。然而,它们可能使扩展更困难(逻辑分散在应用和数据库之间)且与数据库相关。将它们用于受益于数据邻近性的数据密集型操作。PostgreSQL 使用 PL/pgSQL;MySQL 使用自己的过程化 SQL。

sql
-- PostgreSQL function
CREATE OR REPLACE FUNCTION get_user_orders(p_user_id INT)
RETURNS TABLE(order_id INT, total DECIMAL) AS $$
BEGIN
  RETURN QUERY
  SELECT id, total FROM orders WHERE user_id = p_user_id;
END;
$$ LANGUAGE plpgsql;

-- call function
SELECT * FROM get_user_orders(1);

-- PostgreSQL procedure (can manage transactions, PostgreSQL 11+)
CREATE PROCEDURE transfer_money(
  from_id INT, to_id INT, amount DECIMAL
) LANGUAGE plpgsql AS $$
BEGIN
  UPDATE accounts SET balance = balance - amount WHERE id = from_id;
  UPDATE accounts SET balance = balance + amount WHERE id = to_id;
  COMMIT;
END;
$$;

CALL transfer_money(1, 2, 100.00);

-- MySQL stored procedure
DELIMITER //
CREATE PROCEDURE GetActiveUsers()
BEGIN
  SELECT * FROM users WHERE status = 'active';
END //
DELIMITER ;
CALL GetActiveUsers();

触发器

触发器在数据变更时自动执行。BEFORE 触发器可以修改传入数据(例如,设置时间戳、验证)。AFTER 触发器执行副作用(例如,审计日志、反规范化)。谨慎使用触发器——它们对应用代码隐藏,使调试更困难。常见用例:审计跟踪、计算列、强制复杂约束、同步反规范化数据。始终清晰地记录触发器。

sql
-- PostgreSQL trigger: audit log on update
CREATE OR REPLACE FUNCTION audit_user_change()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO user_audit (user_id, old_name, new_name, changed_at)
  VALUES (OLD.id, OLD.name, NEW.name, NOW());
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_user_audit
AFTER UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION audit_user_change();

-- trigger timing: BEFORE / AFTER / INSTEAD OF
-- trigger events: INSERT / UPDATE / DELETE / TRUNCATE
-- granularity: FOR EACH ROW / FOR EACH STATEMENT

-- MySQL trigger
CREATE TRIGGER before_user_insert
BEFORE INSERT ON users
FOR EACH ROW
SET NEW.created_at = NOW();

-- drop trigger
DROP TRIGGER IF EXISTS trg_user_audit ON users;

JSON 操作(PostgreSQL)

JSONB(PostgreSQL)以二进制格式存储 JSON,支持索引化和高效查询。-> 返回 JSON,->> 返回文本。@> 检查包含(JSON 是否包含此项?)。GIN 索引使 JSON 查询快速。对灵活/半结构化数据(事件日志、API 响应、配置)使用 JSON 列,同时将关系数据保留在普通列中。JSONB 优于 JSON(更快、可索引、无重复键)。

sql
-- JSONB columns (PostgreSQL)
CREATE TABLE events (
  id SERIAL PRIMARY KEY,
  data JSONB NOT NULL
);

INSERT INTO events (data) VALUES
  ('{"type": "click", "user": {"id": 1, "name": "Alice"}, "tags": ["web", "mobile"]}');

-- extract fields (-> for JSON, ->> for text)
SELECT data->'type' AS type,           -- "click" (JSON)
       data->'user'->>'name' AS name,  -- Alice (text)
       data->'tags'->0 AS first_tag    -- "web"
FROM events;

-- filter by JSON field
SELECT * FROM events WHERE data->>'type' = 'click';
SELECT * FROM events WHERE data @> '{"type": "click"}';  -- containment

-- GIN index for JSON queries
CREATE INDEX idx_events_data ON events USING gin(data);
SELECT * FROM events WHERE data @> '{"user": {"id": 1}}';

-- modify JSON
UPDATE events SET data = jsonb_set(data, '{user,name}', '"Bob"');

全文搜索

全文搜索支持自然语言查询(词干提取、排名、停用词)。to_tsvector 将文本转换为可搜索的词元;to_tsquery 创建搜索查询;@@ 匹配。ts_rank 对结果评分;ts_headline 高亮匹配项。GIN 索引使其快速。对于大规模搜索,考虑专用引擎(Elasticsearch、Solr),但 PostgreSQL FTS 对中等规模数据集非常出色,并避免了基础设施复杂性。

sql
-- PostgreSQL full-text search
CREATE TABLE articles (
  id SERIAL PRIMARY KEY,
  title VARCHAR(200),
  body TEXT
);

-- create a full-text search index
CREATE INDEX idx_articles_search ON articles
USING gin(to_tsvector('english', title || ' ' || body));

-- search with ranking
SELECT
  title,
  ts_rank(to_tsvector('english', body), query) AS rank,
  ts_headline('english', body, query) AS snippet
FROM articles, to_tsquery('english', 'database & performance') query
WHERE to_tsvector('english', title || ' ' || body) @@ query
ORDER BY rank DESC
LIMIT 10;

-- simplified with generated column
ALTER TABLE articles ADD COLUMN search_vector tsvector
  GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED;
CREATE INDEX idx_articles_search_vec ON articles USING gin(search_vector);

SELECT * FROM articles WHERE search_vector @@ to_tsquery('database');
10

性能与查询优化

EXPLAIN 与查询计划

EXPLAIN 显示查询计划——数据库将如何执行你的查询。EXPLAIN ANALYZE 实际运行它并显示真实计时。注意大型表上的顺序扫描(添加索引)、昂贵的排序(添加索引)和行估计不匹配(运行 ANALYZE 更新统计信息)。成本数字是相对的,不是绝对的。理解查询计划是 SQL 性能调优的第一技能。

sql
-- EXPLAIN: show query plan without running
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';

-- EXPLAIN ANALYZE: run the query and show actual timing
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';

-- EXPLAIN ANALYZE with buffers (I/O stats)
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users
JOIN orders ON users.id = orders.user_id;

-- key things to look for in the plan:
-- Seq Scan: full table scan (bad for large tables — add index)
-- Index Scan: using an index (good)
-- Bitmap Index Scan: index + heap lookup (good for many rows)
-- Hash Join: builds hash table (good for large joins)
-- Nested Loop: good for small result sets
-- Sort: explicit sort (consider index to avoid)
-- cost: estimated cost (first row, all rows)
-- rows: estimated vs actual rows (big mismatch = stale stats)

常见性能陷阱

Sargability(Search Argument Able,可搜索参数化)意味着数据库可以使用索引。列上的函数(DATE(col)、UPPER(col))阻止索引使用——改写为范围查询或使用表达式索引。SELECT * 浪费 I/O 并阻止覆盖索引。OFFSET 分页是 O(n)——使用键集分页(WHERE id > last_id)实现 O(1)。大批量操作应分块以避免长锁和复制延迟。

sql
-- 1. Sargability: avoid functions on indexed columns
-- BAD: function prevents index usage
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
-- GOOD: range query uses index
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';

-- 2. Avoid SELECT * (more I/O, prevents covering indexes)
-- BAD
SELECT * FROM users WHERE status = 'active';
-- GOOD
SELECT id, name, email FROM users WHERE status = 'active';

-- 3. Use LIMIT with ORDER BY for pagination
-- BAD: loads all rows
SELECT * FROM products ORDER BY id;
-- GOOD: keyset pagination
SELECT * FROM products WHERE id > 1000 ORDER BY id LIMIT 20;

-- 4. Batch large updates
-- BAD: one giant transaction
DELETE FROM logs WHERE date < '2023-01-01';
-- GOOD: batch in chunks
DELETE FROM logs WHERE date < '2023-01-01' AND id <= 10000;
DELETE FROM logs WHERE date < '2023-01-01' AND id <= 20000;

UNION、INTERSECT 与 EXCEPT

集合操作合并结果集。UNION 移除重复项(昂贵的排序);UNION ALL 保留它们(更快——当你知道没有重复或想要重复时优先使用)。INTERSECT 返回两者中都有的行。EXCEPT 返回第一个中有但第二个中没有的行。所有都要求兼容的列类型。UNION ALL 可以替换复杂的 OR 条件,且通常性能更好,因为它可以为每个分支使用不同的索引。

sql
-- UNION: combine results, remove duplicates
SELECT name FROM customers
UNION
SELECT name FROM suppliers;
-- UNION ALL: faster, keeps duplicates
SELECT name FROM customers
UNION ALL
SELECT name FROM suppliers;

-- INTERSECT: rows in BOTH results
SELECT product_id FROM sales_2023
INTERSECT
SELECT product_id FROM sales_2024;

-- EXCEPT (MINUS in Oracle): rows in first but not second
SELECT product_id FROM all_products
EXCEPT
SELECT product_id FROM discontinued_products;

-- rules:
-- - same number of columns
-- - compatible types
-- - column names come from first query
-- - UNION is often faster than OR conditions

数据库特定提示

VACUUM(PostgreSQL)回收已删除行的空间(MVCC 留下'死元组')。ANALYZE 更新查询规划器的表统计信息——在批量加载后运行。OPTIMIZE TABLE(MySQL)对表进行碎片整理。显式索引外键(PostgreSQL 不会自动索引它们)。使用 pg_stat_user_indexes 监控索引使用情况并删除未使用的。连接池(PgBouncer、ProxySQL)对高流量应用至关重要——打开连接很昂贵。

sql
-- PostgreSQL: VACUUM to reclaim space
VACUUM ANALYZE users;  -- update stats, reclaim dead rows
VACUUM FULL users;     -- rewrites table (locks, but reclaims all space)

-- PostgreSQL: ANALYZE to update statistics
ANALYZE users;  -- helps query planner make better decisions

-- MySQL: OPTIMIZE TABLE
OPTIMIZE TABLE users;

-- Common indexing rules across databases:
-- 1. Index foreign keys (not automatic in all DBs)
-- 2. Index columns used in WHERE, JOIN, ORDER BY, GROUP BY
-- 3. Composite indexes: high selectivity column first
-- 4. Don't over-index (slows writes, uses disk)
-- 5. Drop unused indexes (check with pg_stat_user_indexes)

-- Connection pooling (application level):
-- - Use PgBouncer (PostgreSQL) or ProxySQL (MySQL)
-- - Reuse connections instead of reconnecting per request
-- - Set appropriate pool size (not too high!)

数据类型与 NULL 处理

NULL 表示未知/缺失的数据,不是零或空。NULL 比较始终产生 NULL(未知),在 WHERE 中为假。使用 IS NULL / IS NOT NULL 测试。COALESCE 提供回退值。NULLIF 将特定值转换为 NULL(对除以零有用)。聚合跳过 NULL——COUNT(col) 计数非 NULL 值,COUNT(*) 计数所有行。在 LEFT JOIN 中,使用 COUNT(right_table.col) 为不匹配的行获取 0。

sql
-- NULL is not zero or empty string — it's "unknown"
SELECT NULL = NULL;   -- NULL (not true!)
SELECT NULL IS NULL;  -- true
SELECT NULL <> 1;     -- NULL (unknown)

-- COALESCE: first non-NULL value
SELECT COALESCE(nickname, first_name, 'Anonymous') FROM users;

-- NULLIF: return NULL if two values are equal
SELECT NULLIF(score, 0) FROM tests;  -- NULL instead of 0
-- useful for avoiding division by zero:
SELECT total / NULLIF(count, 0) FROM stats;

-- aggregate functions ignore NULL
SELECT AVG(score) FROM tests;  -- avg of non-NULL scores only
SELECT COUNT(score) FROM tests;  -- count of non-NULL
SELECT COUNT(*) FROM tests;  -- count of all rows

-- use LEFT JOIN + COUNT carefully
SELECT u.name, COUNT(o.id) AS orders  -- COUNT(o.id) = 0 for no orders
FROM users u LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;
11

递归 CTE

基本递归 CTE 结构

递归 CTE 引用自身以生成层次或顺序数据。它由 UNION ALL 连接的两部分组成:锚点查询(基础情况/起点)和递归查询(引用 CTE 并添加到结果中)。递归持续到递归查询不返回行为止。将递归 CTE 用于树遍历(组织结构图、文件系统)、序列生成和图路径查找。始终在 WHERE 子句中包含终止条件以防止无限循环。

sql
-- Recursive CTE: a CTE that references itself
-- Three parts: anchor, UNION ALL, recursive member
WITH RECURSIVE countdown(n) AS (
    -- Anchor: starting point
    SELECT 1 AS n
    UNION ALL
    -- Recursive member: references the CTE itself
    SELECT n + 1 FROM countdown WHERE n < 10
)
SELECT n FROM countdown;
-- Result: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

-- PostgreSQL uses WITH RECURSIVE
-- SQL Server/MySQL 8+: WITH (RECURSIVE optional in MySQL)
-- SQLite: WITH RECURSIVE

层次数据(组织结构图)

递归 CTE 擅长遍历层次数据,如组织结构图、类别树或文件系统。锚点选择根节点;递归成员通过父子关系(manager_id = id)将表连接到 CTE。添加 depth 列跟踪每行有多少层深,path 列(字符串连接)显示完整的祖先链。这取代了多个自连接或应用端递归的需要。path 上的 CAST 防止递归期间的类型错误。

sql
-- Employee hierarchy with manager relationships
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    manager_id INT REFERENCES employees(id)
);

-- Find all direct and indirect reports of employee 1 (CEO)
WITH RECURSIVE org_chain AS (
    -- Anchor: the starting employee
    SELECT id, name, manager_id, 0 AS depth, CAST(name AS VARCHAR(500)) AS path
    FROM employees WHERE id = 1
    UNION ALL
    -- Recursive: find employees whose manager is in the chain
    SELECT e.id, e.name, e.manager_id, oc.depth + 1,
           CAST(oc.path || ' > ' || e.name AS VARCHAR(500))
    FROM employees e
    JOIN org_chain oc ON e.manager_id = oc.id
)
SELECT id, name, depth, path FROM org_chain
ORDER BY depth, name;

-- depth shows hierarchy level, path shows the management chain

生成序列与日期

递归 CTE 可以生成序列和日期范围——适用于填充时间序列报表中的缺口。通过生成范围内的所有日期并 LEFT JOIN 到你的数据,确保输出中出现每个日期,即使没有记录。这是仪表板和图表的常见模式。PostgreSQL 还有 generate_series() 作为更简单的替代方案。始终设置终止条件(WHERE n < 100)以防止无限递归。某些数据库限制递归深度(例如,MySQL 默认通过 cte_max_recursion_depth 为 100)。

sql
-- Generate a series of numbers
WITH RECURSIVE numbers(n) AS (
    SELECT 1
    UNION ALL
    SELECT n + 1 FROM numbers WHERE n < 100
)
SELECT n FROM numbers;

-- Generate a date range (all days in January 2024)
WITH RECURSIVE dates(d) AS (
    SELECT DATE '2024-01-01'
    UNION ALL
    SELECT d + INTERVAL '1 day' FROM dates
    WHERE d < DATE '2024-01-31'
)
SELECT d FROM dates;

-- Generate a time series for gap-filling in reports
-- (ensures every date appears even if no data exists)
SELECT d.date, COALESCE(SUM(o.amount), 0) AS daily_total
FROM dates d
LEFT JOIN orders o ON o.order_date = d.date
GROUP BY d.date ORDER BY d.date;

图路径查找(BFS)

递归 CTE 可以在图结构上执行广度优先搜索(BFS)。锚点查找从起始节点出发的边;递归成员通过将边连接到当前路径的端点来扩展路径。在循环图中防止循环至关重要——检查目标节点是否已在路径中(使用 LIKE 或字符串搜索)。跳数限制是防止无限递归的安全网。此方法适用于路线查找、依赖解析和网络分析。对于加权最短路径,考虑在应用代码中使用 Dijkstra 算法。

sql
-- Find all paths in a directed graph
CREATE TABLE edges (src VARCHAR(10), dst VARCHAR(10));

-- Find all reachable nodes from 'A' with the path taken
WITH RECURSIVE paths AS (
    -- Anchor: start from node A
    SELECT src, dst, CAST(src || '->' || dst AS VARCHAR(1000)) AS path,
           1 AS hops
    FROM edges WHERE src = 'A'
    UNION ALL
    -- Recursive: extend the path
    SELECT p.src, e.dst,
           CAST(p.path || '->' || e.dst AS VARCHAR(1000)),
           p.hops + 1
    FROM paths p
    JOIN edges e ON p.dst = e.src
    WHERE p.hops < 10  -- prevent infinite loops in cyclic graphs
      AND p.path NOT LIKE '%' || e.dst || '%'
)
SELECT DISTINCT path, hops FROM paths ORDER BY hops;

-- Cycle prevention: check the path doesn't already contain the node

阶乘与递归聚合

递归 CTE 可以通过在每次迭代中携带状态(n、fact)来执行阶乘等数学计算。锚点设置基础情况(0! = 1),递归成员从前一个值计算下一个值。运行总计也可以这样计算,尽管窗口函数(SUM(amount) OVER (ORDER BY id))对累计聚合更高效、更惯用。用于计算的递归 CTE 主要是教学性的——当窗口函数或过程化代码无法表达逻辑时使用它们。每次递归级别添加一行,因此结果集随深度增长。

sql
-- Compute factorial using recursive CTE
WITH RECURSIVE factorial(n, fact) AS (
    -- Anchor: 0! = 1
    SELECT 0, 1
    UNION ALL
    -- Recursive: n! = n * (n-1)!
    SELECT n + 1, fact * (n + 1) FROM factorial WHERE n < 10
)
SELECT n, fact FROM factorial;

-- Running accumulation: cumulative sum
WITH RECURSIVE running_total AS (
    SELECT id, amount, amount AS cumulative
    FROM transactions WHERE id = 1
    UNION ALL
    SELECT t.id, t.amount, rt.cumulative + t.amount
    FROM transactions t
    JOIN running_total rt ON t.id = rt.id + 1
)
SELECT * FROM running_total ORDER BY id;

-- Note: window functions (SUM OVER) are usually better for this
12

PIVOT 与 UNPIVOT

PIVOT(行转列)

PIVOT 将行转换为列——非常适合交叉报表,你希望将类别作为列标题。IN 列表指定哪些值成为列。SQL Server 和 Oracle 有原生 PIVOT 语法。内部查询提供源数据,PIVOT 为每个列组应用聚合(SUM、AVG、COUNT)。这等价于条件聚合,但对于宽透视更易读。当你有固定的、已知的值集要进行透视时使用 PIVOT。

sql
-- Convert rows to columns (cross-tabulation)
-- Source: sales data with rows per quarter
CREATE TABLE sales (quarter VARCHAR(10), region VARCHAR(50), amount DECIMAL(10,2));

-- SQL Server PIVOT syntax:
SELECT region, [Q1], [Q2], [Q3], [Q4]
FROM (
    SELECT quarter, region, amount FROM sales
) AS src
PIVOT (
    SUM(amount) FOR quarter IN ([Q1], [Q2], [Q3], [Q4])
) AS pvt;

-- Result:
-- region  | Q1    | Q2    | Q3    | Q4
-- North   | 1000  | 1500  | 1200  | 1800
-- South   | 800   | 1100  | 900   | 1300

条件聚合(通用 PIVOT)

条件聚合(SUM + CASE)是在每个 SQL 数据库中都可用的通用透视技术。每个 CASE 表达式过滤一个类别,SUM 聚合匹配的值。这通常比 PIVOT 更快且更灵活。ELSE 0 确保不匹配的行贡献零。PostgreSQL 的 crosstab() 函数(来自 tablefunc 扩展)更简洁但需要固定的输出列。当你需要跨数据库兼容性或 PIVOT 语法不可用时使用条件聚合。

sql
-- Works in ALL databases (MySQL, PostgreSQL, SQLite, etc.)
SELECT
    region,
    SUM(CASE WHEN quarter = 'Q1' THEN amount ELSE 0 END) AS Q1,
    SUM(CASE WHEN quarter = 'Q2' THEN amount ELSE 0 END) AS Q2,
    SUM(CASE WHEN quarter = 'Q3' THEN amount ELSE 0 END) AS Q3,
    SUM(CASE WHEN quarter = 'Q4' THEN amount ELSE 0 END) AS Q4,
    SUM(amount) AS total
FROM sales
GROUP BY region
ORDER BY region;

-- PostgreSQL-specific: crosstab() from tablefunc extension
-- SELECT * FROM crosstab('SELECT region, quarter, amount FROM sales ORDER BY 1,2')
-- AS ct(region VARCHAR, Q1 DECIMAL, Q2 DECIMAL, Q3 DECIMAL, Q4 DECIMAL);

动态 PIVOT(动态 SQL)

动态 SQL 在运行时构建查询字符串,当透视列事先未知时(例如,按月透视而月份变化)。过程:查询不同值、构建列列表、构造 PIVOT 语句,并用 sp_executesql(SQL Server)或 PREPARE/EXECUTE(MySQL)执行。始终用 QUOTENAME() 或 quote_ident() 清理以防止 SQL 注入。动态 SQL 功能强大但增加了复杂性和安全风险——谨慎使用,尽可能优先使用固定透视。应用端透视通常是更安全的替代方案。

sql
-- When pivot columns are unknown at write time, use dynamic SQL
-- SQL Server example:
DECLARE @cols AS NVARCHAR(MAX),
        @query AS NVARCHAR(MAX);

-- Build the column list dynamically
SELECT @cols = STUFF((
    SELECT DISTINCT ',' + QUOTENAME(quarter)
    FROM sales FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '');

-- Build and execute the pivot query
SET @query = 'SELECT region, ' + @cols + '
FROM (SELECT quarter, region, amount FROM sales) x
PIVOT (SUM(amount) FOR quarter IN (' + @cols + ')) p';

EXEC sp_executesql @query;

-- WARNING: dynamic SQL is vulnerable to SQL injection
-- — always use QUOTENAME() to sanitize column names

UNPIVOT(列转行)

UNPIVOT 反转 PIVOT——它将列转换为行。这适用于规范化反规范化数据、将宽导入文件转换为长格式,或为图表准备数据。SQL Server 有原生 UNPIVOT 语法。UNION ALL 方法到处可用:每个 SELECT 提取一列并用固定值标记。UNION ALL(不是 UNION)保留重复项且更快。UNPIVOT 在 ETL 管道中常见,当源数据以电子表格格式(宽)到达但需要规范化存储(长)时。

sql
-- Convert columns back to rows
-- Source: wide table with Q1-Q4 columns
-- Target: narrow table with quarter/amount rows

-- SQL Server UNPIVOT:
SELECT region, quarter, amount
FROM quarterly_sales
UNPIVOT (
    amount FOR quarter IN (Q1, Q2, Q3, Q4)
) AS unpvt;

-- Universal UNION ALL approach (all databases):
SELECT region, 'Q1' AS quarter, Q1 AS amount FROM quarterly_sales
UNION ALL
SELECT region, 'Q2' AS quarter, Q2 AS amount FROM quarterly_sales
UNION ALL
SELECT region, 'Q3' AS quarter, Q3 AS amount FROM quarterly_sales
UNION ALL
SELECT region, 'Q4' AS quarter, Q4 AS amount FROM quarterly_sales;

-- Result: one row per region/quarter combination

实用透视报表示例

这个真实世界的透视报表在单个查询中将月度明细与同比比较结合。条件聚合(SUM + CASE)创建月度列和年度总计。yoy_change 列内联计算差异。HAVING 过滤掉没有销售的产品。此模式在 BI 仪表板和财务报表中常见。EXTRACT 函数在大多数数据库中可用(在 SQL Server 中使用 DATEPART,SQLite 中使用 strftime)。对于真正的动态列,结合动态 SQL或在应用层处理透视。

sql
-- Monthly sales pivot with year-over-year comparison
SELECT
    product_name,
    SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 1  THEN amount ELSE 0 END) AS jan,
    SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 2  THEN amount ELSE 0 END) AS feb,
    SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 3  THEN amount ELSE 0 END) AS mar,
    SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 4  THEN amount ELSE 0 END) AS apr,
    SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 5  THEN amount ELSE 0 END) AS may,
    SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 6  THEN amount ELSE 0 END) AS jun,
    SUM(CASE WHEN EXTRACT(YEAR  FROM order_date) = 2024 THEN amount ELSE 0 END) AS total_2024,
    SUM(CASE WHEN EXTRACT(YEAR  FROM order_date) = 2023 THEN amount ELSE 0 END) AS total_2023,
    SUM(CASE WHEN EXTRACT(YEAR  FROM order_date) = 2024 THEN amount ELSE 0 END) -
    SUM(CASE WHEN EXTRACT(YEAR  FROM order_date) = 2023 THEN amount ELSE 0 END) AS yoy_change
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE order_date BETWEEN '2023-01-01' AND '2024-12-31'
GROUP BY product_name
HAVING SUM(amount) > 0
ORDER BY total_2024 DESC;
14

触发器

触发器基础(AFTER/BEFORE)

触发器是数据变更时自动运行的数据库级代码。AFTER 触发器记录或传播变更(无法修改 NEW)。BEFORE 触发器在数据写入前验证或转换(可修改 NEW)。FOR EACH ROW 对每个受影响行触发一次;FOR EACH STATEMENT 每条语句触发一次。将触发器用于审计日志、强制复杂约束和自动更新派生列。避免将触发器用于业务逻辑——它们隐藏、难以调试,且可能导致级联效应。每个数据库有不同的触发器语法;PostgreSQL 使用函数作为触发器体。

sql
-- A trigger fires automatically on INSERT/UPDATE/DELETE
-- MySQL syntax:
DELIMITER //
CREATE TRIGGER audit_log
AFTER INSERT ON employees
FOR EACH ROW
BEGIN
    INSERT INTO audit_table (table_name, action, row_id, changed_at)
    VALUES ('employees', 'INSERT', NEW.id, NOW());
END //
DELIMITER ;

-- BEFORE triggers can modify the NEW values:
CREATE TRIGGER validate_email
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
    IF NEW.email NOT LIKE '%@%.%' THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid email';
    END IF;
END //

-- PostgreSQL uses CREATE FUNCTION + CREATE TRIGGER:
-- CREATE TRIGGER audit AFTER INSERT ON employees
-- FOR EACH ROW EXECUTE FUNCTION audit_func();

审计日志触发器

审计触发器捕获每次数据变更以用于合规和调试。审计表存储操作类型、旧值和新值、谁进行了变更(CURRENT_USER)以及何时(CURRENT_TIMESTAMP)。你需要为 INSERT、UPDATE 和 DELETE 分别设置触发器。OLD 引用变更前的值(在 UPDATE/DELETE 中可用),NEW 引用变更后的值(在 INSERT/UPDATE 中可用)。审计表无限增长——按日期分区或归档旧数据。此模式满足 SOX、HIPAA 和 GDPR 对数据变更跟踪的要求。

sql
-- Track all changes to a critical table
CREATE TABLE employee_audit (
    audit_id INT AUTO_INCREMENT PRIMARY KEY,
    action VARCHAR(10),       -- INSERT, UPDATE, DELETE
    employee_id INT,
    old_name VARCHAR(100),
    new_name VARCHAR(100),
    old_salary DECIMAL(10,2),
    new_salary DECIMAL(10,2),
    changed_by VARCHAR(50) DEFAULT CURRENT_USER,
    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TRIGGER trg_audit_insert
AFTER INSERT ON employees
FOR EACH ROW
INSERT INTO employee_audit (action, employee_id, new_name, new_salary)
VALUES ('INSERT', NEW.id, NEW.name, NEW.salary);

CREATE TRIGGER trg_audit_update
AFTER UPDATE ON employees
FOR EACH ROW
INSERT INTO employee_audit (action, employee_id, old_name, new_name, old_salary, new_salary)
VALUES ('UPDATE', NEW.id, OLD.name, NEW.name, OLD.salary, NEW.salary);

CREATE TRIGGER trg_audit_delete
AFTER DELETE ON employees
FOR EACH ROW
INSERT INTO employee_audit (action, employee_id, old_name, old_salary)
VALUES ('DELETE', OLD.id, OLD.name, OLD.salary);

计算/派生列触发器

触发器可以自动计算派生列,无需应用代码即可确保一致性。BEFORE INSERT/UPDATE 触发器根据其他列设置 NEW.final_price。然而,现代数据库原生支持 GENERATED(计算)列——它们始终正确、无法手动覆盖,且可被索引。对于计算值,优先使用 GENERATED 列而非触发器。仅当计算涉及外部数据、条件逻辑或 GENERATED 列无法处理的跨表依赖时才使用触发器。请记住触发器为每次写操作增加开销。

sql
-- Auto-update a derived column when source data changes
CREATE TABLE products (
    id INT PRIMARY KEY,
    price DECIMAL(10,2),
    discount_percent DECIMAL(5,2),
    final_price DECIMAL(10,2)  -- computed: price * (1 - discount/100)
);

-- BEFORE INSERT: compute final_price
CREATE TRIGGER calc_final_price_insert
BEFORE INSERT ON products
FOR EACH ROW
SET NEW.final_price = NEW.price * (1 - NEW.discount_percent / 100);

-- BEFORE UPDATE: recompute if price or discount changes
CREATE TRIGGER calc_final_price_update
BEFORE UPDATE ON products
FOR EACH ROW
SET NEW.final_price = NEW.price * (1 - NEW.discount_percent / 100);

-- Alternative: use GENERATED columns (MySQL 5.7+, PostgreSQL 12+)
-- final_price DECIMAL(10,2) GENERATED ALWAYS AS
--     (price * (1 - discount_percent / 100)) STORED

用触发器防止删除

触发器可以强制执行 CHECK 约束无法表达的数据保护规则。BEFORE DELETE 触发器可以完全阻止删除(使用 SIGNAL/RAISE)或实现软删除(将记录标记为已删除而非移除)。SIGNAL SQLSTATE '45000' 是 MySQL 引发用户定义错误的方式。PostgreSQL 使用 RAISE EXCEPTION。这适用于保护参考数据、防止删除有子项的父记录或实现不可变审计跟踪。请谨慎:阻止操作的触发器可能让开发者意外——清晰地记录它们并考虑改用应用级检查。

sql
-- Prevent deletion of critical records
CREATE TRIGGER prevent_delete_admin
BEFORE DELETE ON users
FOR EACH ROW
BEGIN
    IF OLD.role = 'admin' THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Cannot delete admin users';
    END IF;
END //

-- Soft delete instead of hard delete
CREATE TRIGGER soft_delete
BEFORE DELETE ON articles
FOR EACH ROW
BEGIN
    -- Prevent actual deletion, mark as deleted instead
    INSERT INTO articles (id, title, body, deleted_at)
    VALUES (OLD.id, OLD.title, OLD.body, NOW())
    ON DUPLICATE KEY UPDATE deleted_at = NOW();
    -- Still need to prevent the DELETE — use SIGNAL or a flag
END //

-- PostgreSQL: raise exception in trigger function
-- IF OLD.role = 'admin' THEN
--     RAISE EXCEPTION 'Cannot delete admin users';
-- END IF;

触发器管理与调试

管理触发器对维护至关重要。SHOW TRIGGERS(MySQL)和 information_schema 视图列出所有触发器。用 DROP TRIGGER IF EXISTS 删除触发器。临时禁用触发器对批量数据加载有用(否则会为每行触发昂贵的审计/日志)。PostgreSQL 使用 ALTER TABLE ... DISABLE/ENABLE TRIGGER;SQL Server 使用 DISABLE/ENABLE TRIGGER。维护后始终重新启用触发器。调试触发器很困难——它们静默运行。向调试表添加日志,或先隔离测试触发器逻辑。过多的触发器会创建隐藏的复杂性和性能问题。

sql
-- View existing triggers
-- MySQL:
SHOW TRIGGERS;
SELECT * FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = 'mydb';

-- PostgreSQL:
SELECT tgname, tgrelid::regclass, tgtype FROM pg_trigger;

-- SQL Server:
SELECT name, type_desc FROM sys.triggers WHERE parent_id = OBJECT_ID('employees');

-- Drop a trigger
DROP TRIGGER IF EXISTS audit_log;  -- MySQL
DROP TRIGGER IF EXISTS audit_log ON employees;  -- PostgreSQL

-- Disable/enable triggers (PostgreSQL)
ALTER TABLE employees DISABLE TRIGGER ALL;
ALTER TABLE employees ENABLE TRIGGER ALL;

-- Disable for bulk operations (SQL Server)
DISABLE TRIGGER trg_audit ON employees;
-- ... bulk operations ...
ENABLE TRIGGER trg_audit ON employees;
15

用户定义函数(UDF)

标量函数(返回单个值)

标量 UDF 返回单个值,可在 SELECT、WHERE 和计算列中使用。DETERMINISTIC 表示输出仅取决于输入(启用缓存)。READS SQL DATA 声明函数从表中读取。UDF 封装可重用逻辑(折扣、格式化、计算),使其在查询间保持一致。然而,SQL Server 中的标量 UDF 可能导致性能问题(逐行执行)——尽可能使用内联表值函数或计算列。MySQL 8.0+ 更好地优化确定性函数。始终记录函数的用途和参数。

sql
-- MySQL: a function that returns one value
CREATE FUNCTION calculate_discount(
    price DECIMAL(10,2),
    customer_tier VARCHAR(20)
) RETURNS DECIMAL(10,2)
DETERMINISTIC
READS SQL DATA
BEGIN
    DECLARE discount_rate DECIMAL(5,2);
    SET discount_rate = CASE customer_tier
        WHEN 'gold'   THEN 0.20
        WHEN 'silver' THEN 0.10
        WHEN 'bronze' THEN 0.05
        ELSE 0.00
    END;
    RETURN price * (1 - discount_rate);
END //

-- Usage in queries:
SELECT name, price, calculate_discount(price, tier) AS final_price
FROM orders;

-- DETERMINISTIC: same inputs always give same output (cacheable)
-- READS SQL DATA: function reads but doesn't modify tables

表值函数(返回行)

表值函数(TVF)返回结果集(行),你可以像表一样查询。内联 TVF(SQL Server)与视图一样快——查询优化器将它们内联。多语句 TVF 先将结果物化到临时表,可能较慢。PostgreSQL 返回 TABLE 或 SETOF 的函数是等价的。TVF 是参数化视图——当你需要带参数的视图时使用它们。它们非常适合封装复杂的 JOIN 和过滤器。为性能优先使用内联 TVF 而非多语句 TVF。在 PostgreSQL 中,也考虑使用带 WHERE 子句的参数化视图。

sql
-- SQL Server: Inline table-valued function (fast, like a view)
CREATE FUNCTION fn_OrdersByCustomer(@cust_id INT)
RETURNS TABLE
AS
RETURN (
    SELECT o.id, o.order_date, o.total
    FROM orders o
    WHERE o.customer_id = @cust_id
);
-- Usage: SELECT * FROM fn_OrdersByCustomer(42);

-- PostgreSQL: function returning a table
CREATE OR REPLACE FUNCTION get_orders_by_customer(cust_id INT)
RETURNS TABLE(order_id INT, order_date DATE, total DECIMAL) AS $$
    SELECT id, order_date, total
    FROM orders
    WHERE customer_id = cust_id;
$$ LANGUAGE SQL;

-- Usage:
SELECT * FROM get_orders_by_customer(42);

-- Multi-statement TVF (SQL Server) — slower, materializes result
-- CREATE FUNCTION fn_ComplexReport(@date DATE)
-- RETURNS @result TABLE (...)
-- AS BEGIN ... INSERT INTO @result ... RETURN END

字符串操作函数

自定义字符串函数封装内置函数未覆盖的文本处理逻辑。get_first_name 函数使用 LOCATE 和 SUBSTRING 提取第一个单词。make_slug 函数链式使用 LOWER、REPLACE 和 REGEXP_REPLACE 创建 URL 友好的 slug。将这些标记为 DETERMINISTIC,因为相同输入始终产生相同输出。SQL 中的字符串函数与数据库相关——PostgreSQL 有 split_part(),MySQL 有 SUBSTRING_INDEX()。创建 UDF 可标准化应用中的行为。请注意,SQL 中的复杂字符串操作在应用代码中通常更清晰。

sql
-- Create a function to split full name into parts
CREATE FUNCTION get_first_name(full_name VARCHAR(200))
RETURNS VARCHAR(100)
DETERMINISTIC
BEGIN
    DECLARE space_pos INT;
    SET space_pos = LOCATE(' ', full_name);
    IF space_pos > 0 THEN
        RETURN SUBSTRING(full_name, 1, space_pos - 1);
    ELSE
        RETURN full_name;
    END IF;
END //

-- Function to generate slug from a title
CREATE FUNCTION make_slug(title VARCHAR(500))
RETURNS VARCHAR(500)
DETERMINISTIC
BEGIN
    DECLARE slug VARCHAR(500);
    SET slug = LOWER(title);
    SET slug = REPLACE(slug, ' ', '-');
    SET slug = REGEXP_REPLACE(slug, '[^a-z0-9-]', '');
    RETURN slug;
END //

-- Usage:
SELECT get_first_name('John Doe Smith') AS first_name;  -- John
SELECT make_slug('Hello World! 2024') AS slug;           -- hello-world-2024

聚合函数(自定义)

自定义聚合函数让你定义超越 SUM、AVG、COUNT 的新聚合逻辑。PostgreSQL 的 CREATE AGGREGATE 需要状态转换函数(SFUNC,每行调用)和最终函数(FINALFUNC,最后调用一次)。此示例计算几何平均数(乘积的 n 次方根)。自定义聚合对于统计、金融或领域特定计算非常强大。状态跨行累积;最终函数计算结果。MySQL 和 SQL Server 不直接支持自定义聚合——改用存储过程或应用端计算。

sql
-- PostgreSQL: custom aggregate function
-- Step 1: Define a state transition function
CREATE OR REPLACE FUNCTION geom_mean_state(state numeric[], val numeric)
RETURNS numeric[] AS $$
BEGIN
    IF val IS NULL THEN RETURN state; END IF;
    IF state IS NULL THEN
        RETURN ARRAY[val];
    ELSE
        RETURN state || val;
    END IF;
END;
$$ LANGUAGE plpgsql;

-- Step 2: Define the final function
CREATE OR REPLACE FUNCTION geom_mean_final(vals numeric[])
RETURNS numeric AS $$
BEGIN
    IF vals IS NULL OR array_length(vals, 1) IS NULL THEN
        RETURN NULL;
    END IF;
    RETURN exp(avg(ln(v)) FROM unnest(vals) AS v);
END;
$$ LANGUAGE plpgsql;

-- Step 3: Create the aggregate
CREATE AGGREGATE geometric_mean(numeric) (
    SFUNC = geom_mean_state,
    STYPE = numeric[],
    FINALFUNC = geom_mean_final,
    INITCOND = '{}'
);

-- Usage:
SELECT category, geometric_mean(price) FROM products GROUP BY category;

函数与存储过程

函数和存储过程用途不同。函数返回值并可嵌入 SELECT/WHERE——它们必须是确定性的(在大多数数据库中无副作用)。存储过程可以修改数据、管理事务并返回多个结果集——但无法在查询内使用(用 CALL/EXEC 调用)。将函数用于计算和数据检索;将过程用于多步操作(转账、批处理、ETL)。函数是可组合的;过程是命令式的。在 PostgreSQL 中,函数几乎可以做过程能做的一切(包括数据修改),模糊了区别。

sql
-- FUNCTIONS: return values, usable in queries, no side effects
CREATE FUNCTION get_full_name(fname VARCHAR, lname VARCHAR)
RETURNS VARCHAR(200)
DETERMINISTIC
RETURN CONCAT(fname, ' ', lname);

-- Can be used in SELECT:
SELECT get_full_name(first, last) FROM users;  -- OK
SELECT * FROM users WHERE get_full_name(first, last) LIKE 'J%';  -- OK

-- STORED PROCEDURES: can modify data, use transactions, return result sets
CREATE PROCEDURE transfer_funds(
    IN from_acct INT, IN to_acct INT, IN amount DECIMAL(10,2)
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;
    START TRANSACTION;
    UPDATE accounts SET balance = balance - amount WHERE id = from_acct;
    UPDATE accounts SET balance = balance + amount WHERE id = to_acct;
    INSERT INTO transfers (from_id, to_id, amount) VALUES (from_acct, to_acct, amount);
    COMMIT;
END //

-- Call procedure (can't use in SELECT):
CALL transfer_funds(1, 2, 100.00);
16

数据库设计与规范化

第一范式(1NF)

第一范式要求原子值——每个单元格持有一项数据,而非列表或数组。列中的逗号分隔值违反 1NF,因为你无法查询、索引或更新单个项。修复:每项创建一行(带复合主键)或拆分到单独的明细表。1NF 还要求主键唯一标识每一行。违反 1NF 使'查找包含鼠标的所有订单'等查询需要字符串解析——缓慢且易出错。始终从 1NF 合规开始。

sql
-- 1NF: each column contains atomic (indivisible) values
--       no repeating groups, each row is unique

-- VIOLATION: comma-separated values in one column
CREATE TABLE bad_orders (
    id INT,
    customer_name VARCHAR(100),
    products VARCHAR(500)  -- "laptop, mouse, keyboard" — BAD!
);

-- 1NF COMPLIANT: separate row per product
CREATE TABLE orders (
    order_id INT,
    customer_name VARCHAR(100),
    product_name VARCHAR(100),  -- one product per row
    PRIMARY KEY (order_id, product_name)  -- composite key for uniqueness
);

-- Better: normalize further with separate tables
CREATE TABLE orders (order_id INT PRIMARY KEY, customer_name VARCHAR(100));
CREATE TABLE order_items (
    order_id INT REFERENCES orders(order_id),
    product_name VARCHAR(100),
    quantity INT,
    PRIMARY KEY (order_id, product_name)
);

第二与第三范式(2NF、3NF)

2NF 消除部分依赖——每个非键列必须依赖于整个主键,而非仅部分。这仅在复合键时有意义。3NF 消除传递依赖——非键列必须仅依赖于主键,而非其他非键列。例如,customer_name 依赖于 customer_id,而 customer_id 依赖于 order_id(传递)。规范化减少数据冗余(每个事实存储一次)和异常(在一处更新客户名称,而非每个订单中)。大多数实际数据库目标为 3NF 或 BCNF。

sql
-- 2NF: 1NF + no partial dependencies (non-key attrs depend on FULL key)
-- Problem: order_items has composite key (order_id, product_id)
-- but product_name depends only on product_id (partial dependency)

-- VIOLATION of 2NF:
CREATE TABLE bad_order_items (
    order_id INT,
    product_id INT,
    product_name VARCHAR(100),  -- depends on product_id only!
    quantity INT,
    PRIMARY KEY (order_id, product_id)
);

-- 2NF COMPLIANT: move product_name to products table
CREATE TABLE products (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(100)
);
CREATE TABLE order_items (
    order_id INT,
    product_id INT REFERENCES products(product_id),
    quantity INT,
    PRIMARY KEY (order_id, product_id)
);

-- 3NF: 2NF + no transitive dependencies
-- (non-key attrs don't depend on other non-key attrs)
-- Problem: orders table has customer_name that depends on customer_id
-- Solution: separate customers table (see above)

反规范化(何时打破规则)

反规范化有意违反范式以改善读取性能,代价是写入复杂性和存储。在规范化数据库中,检索完整订单需要 4 个 JOIN——对高流量仪表板来说很昂贵。反规范化表预连接和预计算数据以实现快速读取。权衡:写入必须更新多处(不一致风险)且存储增加。对读密集型系统(分析、报表、数据仓库)使用反规范化。物化视图提供托管反规范化——数据库处理刷新。OLTP 系统应保持规范化;OLAP 系统通常反规范化(星型/雪花模式)。

sql
-- Denormalization: intentionally adding redundancy for performance
-- Normalized (3NF): requires JOINs to get full order info
SELECT o.order_id, c.name, p.product_name, oi.quantity, p.price
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.id;

-- Denormalized: store redundant data for read speed
CREATE TABLE order_summary (
    order_id INT PRIMARY KEY,
    customer_id INT,
    customer_name VARCHAR(100),    -- redundant (also in customers)
    customer_email VARCHAR(200),   -- redundant
    total_amount DECIMAL(10,2),    -- pre-calculated
    item_count INT,                -- pre-calculated
    order_date TIMESTAMP
);

-- Trade-off: faster reads, slower writes, risk of inconsistency
-- Use for: reporting tables, read-heavy dashboards, data warehouses

-- Materialized views are a managed form of denormalization:
CREATE MATERIALIZED VIEW sales_summary AS
SELECT date, region, SUM(amount) AS total FROM sales GROUP BY date, region;
REFRESH MATERIALIZED VIEW sales_summary;

主键、外键与约束

约束在数据库级别强制数据完整性。PRIMARY KEY 唯一标识行并创建聚集索引。UNIQUE 防止重复(在大多数数据库中允许多个 NULL)。CHECK 强制自定义规则(salary > 0)。FOREIGN KEY 维护引用完整性——ON DELETE SET NULL/CASCADE/RESTRICT 控制父行删除时发生什么。ON UPDATE CASCADE 将 PK 变更传播到 FK。约束是抵御坏数据的最后防线——即使应用代码有 bug,数据库也会拒绝无效数据。始终定义约束;它们既是文档又是强制执行。

sql
CREATE TABLE departments (
    dept_id INT PRIMARY KEY AUTO_INCREMENT,
    dept_name VARCHAR(100) NOT NULL UNIQUE,
    budget DECIMAL(12,2) CHECK (budget >= 0),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE employees (
    emp_id INT PRIMARY KEY AUTO_INCREMENT,
    emp_name VARCHAR(100) NOT NULL,
    email VARCHAR(200) UNIQUE,
    dept_id INT,
    salary DECIMAL(10,2) CHECK (salary > 0 AND salary < 1000000),
    hire_date DATE NOT NULL,
    manager_id INT REFERENCES employees(emp_id),  -- self-reference
    FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
        ON DELETE SET NULL      -- don't delete dept if employees exist
        ON UPDATE CASCADE,      -- update FK if dept_id changes
    INDEX idx_dept (dept_id),
    INDEX idx_email (email)
);

-- Constraint types:
-- PRIMARY KEY: unique + not null (clustered index)
-- UNIQUE: no duplicates (allows NULLs)
-- NOT NULL: required field
-- CHECK: custom condition
-- FOREIGN KEY: referential integrity
-- DEFAULT: value when not specified

索引策略

索引极大地加速读取但减慢写入(每个索引必须在 INSERT/UPDATE/DELETE 时更新)。B-tree 索引支持等值、范围和排序。复合索引遵循最左前缀规则——你可以对 a 或 a+b 的查询使用 (a, b),但不能单独使用 b。覆盖索引(INCLUDE 子句)存储额外列,使查询无需触及表——极快。部分索引仅索引行子集,节省空间。监控索引使用情况(PostgreSQL 中的 pg_stat_user_indexes)并删除未使用的。一个好规则:索引外键和 WHERE/JOIN 子句中的列。过度索引损害写入性能并浪费存储。

sql
-- B-tree index (default): good for =, <, >, BETWEEN, ORDER BY
CREATE INDEX idx_last_name ON employees(last_name);

-- Composite index: order matters! (leftmost prefix rule)
CREATE INDEX idx_dept_salary ON employees(dept_id, salary);
-- Usable for: WHERE dept_id = 5
-- Usable for: WHERE dept_id = 5 AND salary > 50000
-- NOT usable for: WHERE salary > 50000 (skips dept_id)

-- Covering index: includes all columns a query needs
CREATE INDEX idx_covering ON orders(customer_id, order_date)
    INCLUDE (total_amount, status);
-- Query can be satisfied from index alone (no table lookup)

-- Partial/partial index: index only matching rows
CREATE INDEX idx_active_users ON users(last_login)
    WHERE active = true;  -- PostgreSQL

-- Don't over-index: every index slows writes
-- Index columns used in: WHERE, JOIN, ORDER BY, GROUP BY
-- Drop unused indexes: SELECT * FROM pg_stat_user_indexes;
17

EXPLAIN 与查询优化

阅读 EXPLAIN 输出

EXPLAIN 揭示数据库如何执行查询——使用哪些索引、表如何连接以及检查多少行。EXPLAIN ANALYZE(PostgreSQL)或带执行的 EXPLAIN(MySQL 8.0+)实际运行查询并显示真实计时。注意:Seq Scan / ALL(全表扫描——对大型表不利)、Index Scan(好)、行估计(高 = 昂贵)。MySQL 中的 'Using filesort' 或 'Using temporary' 表示额外工作。如果 EXPLAIN 显示大型表上的全表扫描,你需要索引。优化前始终 EXPLAIN——不要猜测。

sql
-- EXPLAIN shows the query execution plan
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;

-- EXPLAIN ANALYZE actually runs the query (PostgreSQL)
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;

-- PostgreSQL output:
-- Index Scan using idx_customer on orders  (cost=0.29..8.31 rows=1 width=74)
--   Index Cond: (customer_id = 42)
--   Execution Time: 0.042 ms

-- Key columns to check:
-- - type: scan method (const > eq_ref > ref > range > index > ALL)
-- - rows: estimated rows examined (lower is better)
-- - key: which index is used (NULL = no index, bad!)
-- - Extra: "Using filesort" or "Using temporary" = warning signs

-- MySQL: EXPLAIN FORMAT=JSON for detailed output
-- SQL Server: SET SHOWPLAN_TEXT ON; or Actual Execution Plan in SSMS

常见性能问题

几种常见模式阻止索引使用并导致全表扫描。索引列上的函数(YEAR(date)、UPPER(name))阻止索引使用——改写为范围条件。LIKE 中的前导通配符('%pattern')无法使用 B-tree 索引——改用全文搜索。SELECT * 浪费带宽并阻止覆盖索引优化。隐式类型转换(将字符串列与整数比较)可能禁用索引。OR 条件有时比 IN 效率低。始终用 EXPLAIN 验证你的索引是否实际被使用——未使用的索引是浪费的存储和写入开销。

sql
-- 1. Missing index → full table scan
-- Bad: SELECT * FROM orders WHERE customer_id = 42; (no index)
-- Fix: CREATE INDEX idx_customer ON orders(customer_id);

-- 2. Index not used due to function on column
-- Bad: WHERE YEAR(order_date) = 2024  (function prevents index use)
-- Good: WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01'

-- 3. SELECT * instead of specific columns
-- Bad: SELECT * FROM large_table WHERE id = 1;
-- Good: SELECT id, name, email FROM large_table WHERE id = 1;

-- 4. OR conditions preventing index use
-- Bad: WHERE dept = 'A' OR dept = 'B' OR dept = 'C'
-- Good: WHERE dept IN ('A', 'B', 'C')

-- 5. LIKE with leading wildcard
-- Bad: WHERE name LIKE '%son'  (can't use index)
-- OK:  WHERE name LIKE 'John%'  (can use index)

-- 6. Implicit type conversion
-- Bad: WHERE string_column = 123  (converts to string, skips index)
-- Good: WHERE string_column = '123'

JOIN 优化

JOIN 优化对多表查询至关重要。确保连接列(通常是外键)被索引——未索引的连接导致嵌套循环扫描(O(n*m))。查询优化器通常选择最佳连接顺序,但你可以通过提前过滤(概念上 WHERE 在 JOIN 之前)来帮助。当你不需要不匹配的行时,INNER JOIN 比 OUTER JOIN 快。对于相关子查询,EXISTS 通常比 IN 更高效,因为它在第一个匹配时短路。避免连接你不需要的表——每个连接使工作成倍增加。对于复杂报表,考虑物化视图或预聚合汇总表。

sql
-- Join order matters: smallest table first (optimizer usually handles this)
-- Ensure join columns are indexed (usually foreign keys)
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_order_items_order ON order_items(order_id);

-- Use INNER JOIN when you don't need unmatched rows (faster than OUTER)
SELECT c.name, COUNT(o.order_id) AS order_count
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id
GROUP BY c.name;

-- Avoid joining unnecessary tables — fetch details lazily if needed
-- Bad: join 5 tables when you only need 2 columns
-- Good: split into simpler queries or use a covering index

-- EXISTS vs IN for subqueries
-- EXISTS is often faster for large subquery results:
SELECT name FROM customers c
WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
-- IN is better for small lists:
SELECT * FROM orders WHERE customer_id IN (1, 2, 3);

分页优化

基于 OFFSET 的分页(LIMIT 10 OFFSET 10000)是 O(n)——数据库必须扫描并丢弃所有跳过的行,使深层页面极慢。键集(游标)分页使用 WHERE last_value < cursor 直接定位——无论页面深度都是 O(1)。这需要排序列上的索引。对于并列(相同时间戳),使用复合游标(created_at、id)。避免在大型表上用 COUNT(*) 获取总计数——它会扫描整个表。使用近似计数(PostgreSQL 中的 pg_class.reltuples)或根本不显示总计数(无限滚动)。键集分页是高性能 API 的标准。

sql
-- Bad: OFFSET pagination (slow for large offsets)
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10 OFFSET 10000;
-- Must scan and discard 10000 rows — gets slower as you page deeper

-- Good: Keyset (cursor) pagination using WHERE
SELECT * FROM orders
WHERE created_at < '2024-01-15 10:30:00'  -- last seen value
ORDER BY created_at DESC
LIMIT 10;
-- Uses index efficiently — constant time regardless of page depth

-- For composite keyset pagination (handles ties):
SELECT * FROM orders
WHERE (created_at, id) < ('2024-01-15 10:30:00', 12345)
ORDER BY created_at DESC, id DESC
LIMIT 10;

-- Count total (expensive on large tables — avoid if possible)
-- Instead of COUNT(*), use an approximate count:
SELECT reltuples::bigint FROM pg_class WHERE relname = 'orders';

查询重写与优化清单

查询优化是迭代过程:EXPLAIN、识别瓶颈、重写、重复。关键技术:用 JOIN 替换 IN 子查询(通常更快)、用 UNION ALL 替换 UNION(跳过去重排序)、批量 INSERT(1 个查询 vs 1000 个)、使用预处理语句(缓存查询计划)。CTE 提高可读性,但在较旧的 PostgreSQL 版本中它们被物化(无法优化)——PostgreSQL 12+ 将它们内联。保持表统计信息更新(ANALYZE)以便规划器做出良好决策。黄金法则:用 EXPLAIN ANALYZE 测量,不要猜测。在一个数据库/版本上快的可能在另一个上慢。

sql
-- 1. Replace subqueries with JOINs when possible
-- Slow: SELECT * FROM orders WHERE customer_id IN
--       (SELECT id FROM customers WHERE active = true);
-- Fast: SELECT o.* FROM orders o
--       JOIN customers c ON o.customer_id = c.id WHERE c.active = true;

-- 2. Use UNION ALL instead of UNION (avoids dedup sort)
SELECT 'A' UNION ALL SELECT 'B';  -- fast
SELECT 'A' UNION SELECT 'B';       -- sorts to remove duplicates

-- 3. Batch operations instead of row-by-row
-- Slow: 1000 individual INSERTs
-- Fast: INSERT INTO t VALUES (1,'a'), (2,'b'), (3,'c'), ...;

-- 4. Use CTEs for readability, but know they may not optimize well
-- (PostgreSQL 12+ inlines CTEs; older versions materialize them)

-- 5. Avoid SELECT DISTINCT when you can use GROUP BY or EXISTS
-- 6. Use prepared statements for repeated queries (plan caching)
PREPARE get_user AS SELECT * FROM users WHERE id = $1;
EXECUTE get_user(42);

-- 7. Analyze tables for up-to-date statistics
ANALYZE orders;  -- PostgreSQL: updates planner statistics
18

NoSQL 与 SQL 比较

SQL 与 NoSQL:何时使用什么

SQL 与 NoSQL 的选择取决于你的数据模型、一致性要求和规模。SQL 数据库强制模式、支持 ACID 事务,并擅长带 JOIN 的复杂查询——适用于金融系统和任何数据完整性至关重要的应用。NoSQL 数据库以一致性换取可扩展性和灵活性:文档存储(MongoDB)用于演变的模式,键值存储(Redis)用于缓存,列族(Cassandra)用于海量写入吞吐量,图数据库(Neo4j)用于关系密集型数据。现代 SQL 数据库现在支持 JSON、全文搜索和扩展,减少了许多情况下对 NoSQL 的需求。

sql
-- SQL (Relational): structured, consistent, queryable
-- Best for: financial systems, e-commerce, CRM, any ACID-requiring app
-- Examples: PostgreSQL, MySQL, SQL Server, Oracle

-- NoSQL: flexible schema, horizontal scaling, specific data models
-- Document: MongoDB, CouchDB — JSON-like documents, flexible schema
-- Key-Value: Redis, DynamoDB — fast lookups, caching, sessions
-- Column-Family: Cassandra, HBase — wide-column, time-series, write-heavy
-- Graph: Neo4j, ArangoDB — relationships, social networks, recommendations

-- Decision factors:
-- 1. Data structure: fixed → SQL, evolving/varied → NoSQL
-- 2. Consistency: strict ACID → SQL, eventual consistency OK → NoSQL
-- 3. Scale: vertical (bigger server) → SQL, horizontal (more servers) → NoSQL
-- 4. Queries: complex JOINs → SQL, simple lookups → NoSQL
-- 5. Team expertise: SQL is universal, NoSQL varies

-- Many modern databases blur the line:
-- PostgreSQL: JSON columns, full-text search, pub/sub
-- MongoDB: transactions (multi-document ACID since 4.0)

文档存储模式(MongoDB 风格)

文档存储将相关数据嵌入单个文档中,而非跨表规范化。这消除了读取密集型访问模式的 JOIN,但会重复数据(每个订单中的客户信息)。当数据一起访问且大小有界时,嵌入有效。对于无界关系(有数千个订单的客户),使用引用(存储 customer_id,单独获取)。PostgreSQL 的 JSONB 列在关系数据库中提供文档存储灵活性——你获得 ACID 事务、索引(GIN)和对 JSON 的 SQL 查询。这种混合方法越来越流行,减少了对单独 NoSQL 数据库的需求。

sql
-- In SQL, you'd normalize this into 3 tables:
-- customers, orders, order_items

-- In a document store (MongoDB), you might embed everything:
-- (pseudo-code, not SQL)
// db.orders.insertOne({
//   _id: 1,
//   customer: { name: "Alice", email: "[email protected]" },
//   items: [
//     { product: "Laptop", price: 999, qty: 1 },
//     { product: "Mouse", price: 25, qty: 2 }
//   ],
//   total: 1049,
//   status: "shipped",
//   created_at: ISODate("2024-01-15")
// })

-- SQL equivalent with JSON column (PostgreSQL):
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    data JSONB NOT NULL  -- stores the entire document
);
INSERT INTO orders (data) VALUES ('{
    "customer": {"name": "Alice", "email": "[email protected]"},
    "items": [{"product": "Laptop", "price": 999, "qty": 1}],
    "total": 999
}');

-- Query JSON data:
SELECT data->'customer'->>'name' AS name FROM orders
WHERE data->'total' > 500;

键值存储模式(Redis 风格)

像 Redis 这样的键值存储擅长超快查找(亚毫秒级),因为数据驻留在内存中。常见用例:缓存昂贵的查询结果、会话存储(带 TTL 过期)、实时计数器(原子 INCR)和排行榜(有序集合)。Redis 数据结构(列表、集合、有序集合、哈希)超越简单的键值。权衡:数据在内存中(受 RAM 限制)且持久化是可选的。将 Redis 用作 SQL 前面的缓存层——写穿或缓存旁路模式。对于会话数据,Redis 的自动过期(TTL)是理想的。SQL 数据库可以用缓存表模拟缓存,但无法匹配 Redis 对热数据的速度。

sql
-- Redis is an in-memory key-value store — not SQL
-- Common patterns (Redis commands, not SQL):

-- Caching: store expensive query results
// SET user:42:profile '{"name":"Alice","age":30}' EX 3600
// GET user:42:profile  -- returns cached data, expires in 1 hour

-- Session storage: fast, ephemeral
// SET session:abc123 '{"user_id":42}' EX 1800  -- 30 min TTL

-- Counters & leaderboards:
// INCR page:home:views           -- atomic counter
// ZADD leaderboard 1500 "alice"  -- sorted set for rankings
// ZREVRANGE leaderboard 0 9      -- top 10 players

-- SQL equivalent for caching (materialized/precomputed):
CREATE TABLE cache (
    cache_key VARCHAR(200) PRIMARY KEY,
    cache_value TEXT,
    expires_at TIMESTAMP,
    INDEX idx_expires (expires_at)
);
-- Periodically: DELETE FROM cache WHERE expires_at < NOW();

-- When to use Redis vs SQL cache:
-- Redis: sub-millisecond reads, data structures (sets, sorted sets)
-- SQL: when you need ACID or already have a database connection

多语言持久化(混合数据库)

多语言持久化在一个应用中为不同数据需求使用不同的数据库。PostgreSQL 处理事务,Redis 处理缓存,Elasticsearch 处理搜索,S3 处理文件。挑战是保持跨存储的数据一致——解决方案是事件驱动架构:写入主数据库(真相来源),然后通过变更数据捕获(CDC)或消息队列(Kafka、RabbitMQ)异步传播变更到其他存储。这提供最终一致性——从辅助存储读取可能略有延迟。好处:每个存储针对其工作负载优化。成本:运营复杂性。从单个 SQL 数据库开始;仅当你达到明确的性能限制时才添加专用存储。

sql
-- Modern applications often use MULTIPLE database types:
-- Each data store handles what it does best

-- Typical architecture:
-- 1. PostgreSQL: core transactional data (users, orders, payments)
--    ACID guarantees, complex queries, foreign keys
CREATE TABLE users (id SERIAL PRIMARY KEY, email VARCHAR UNIQUE);
CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id));

-- 2. Redis: session storage, caching, rate limiting
--    Fast in-memory access, auto-expiring keys

-- 3. Elasticsearch: full-text search, log analytics
--    Inverted index, faceted search, aggregations

-- 4. S3/Object storage: files, images, backups
--    Cheap, unlimited, HTTP-accessible

-- 5. TimescaleDB/InfluxDB: time-series metrics
--    Optimized for timestamped data, downsampling

-- Challenge: data consistency across stores
-- Solution: event-driven architecture (CDC, message queues)
--   1. Write to PostgreSQL (source of truth)
--   2. Publish event to Kafka
--   3. Consumers update Redis cache, Elasticsearch index, etc.
--   4. eventual consistency — reads may be slightly stale

ACID 与 BASE 一致性模型

ACID(原子性、一致性、隔离性、持久性)保证严格的一致性——事务是全有或全无,数据始终满足约束。这对部分更新会导致错误的金融系统至关重要。BASE(基本可用、软状态、最终一致)以即时一致性换取可用性和分区容错性——数据可能暂时不一致但随时间收敛。CAP 定理指出在网络分区期间你无法同时拥有三者(一致性、可用性、分区容错性)。SQL 数据库优先考虑 C+A(单节点)或 C+P(分布式)。许多 NoSQL 数据库优先考虑 A+P(Cassandra、DynamoDB)。当正确性至关重要时选择 ACID;当可用性和规模更重要时选择 BASE。

sql
-- ACID (SQL databases):
-- Atomicity: all operations in a transaction succeed or fail together
-- Consistency: data always satisfies constraints (FK, CHECK, etc.)
-- Isolation: concurrent transactions don't interfere
-- Durability: committed data survives crashes

-- SQL transaction example:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- If either fails, ROLLBACK undoes both
COMMIT;  -- both updates are permanent

-- BASE (NoSQL databases):
-- Basically Available: system responds (may be stale)
-- Soft state: state changes without input (eventual consistency)
-- Eventually consistent: data converges over time

-- NoSQL trade-off: higher availability & partition tolerance
-- at the cost of immediate consistency

-- CAP Theorem: in a network partition, choose:
-- - CP (consistency): refuse writes (some NoSQL: HBase, MongoDB)
-- - AP (availability): accept writes, reconcile later (Cassandra, DynamoDB)

-- SQL databases are typically CA (consistent + available, no partition tolerance
-- in single-node setups; distributed SQL like CockroachDB adds P)
19

CTE 与递归 CTE

基本 CTE

CTE(公用表表达式)是临时命名结果集。通过分解复杂查询提高可读性。多个 CTE 可用逗号链接。CTE 仅对单条语句有效。

sql
WITH high_earners AS (
  SELECT * FROM employees WHERE salary > 80000
), by_dept AS (
  SELECT department, COUNT(*) AS cnt FROM high_earners GROUP BY department
)
SELECT * FROM by_dept ORDER BY cnt DESC;

递归 CTE

递归 CTE 引用自身。锚点是基础情况。UNION ALL 连接到递归部分。用于层次数据:组织结构图、文件系统、图遍历。必须有终止条件。

sql
WITH RECURSIVE org_chart AS (
  -- Anchor: top-level managers
  SELECT id, name, manager_id, 1 AS level
  FROM employees WHERE manager_id IS NULL
  UNION ALL
  -- Recursive: subordinates
  SELECT e.id, e.name, e.manager_id, oc.level + 1
  FROM employees e
  JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart;

用 CTE 生成斐波那契数列

递归 CTE 可以生成序列。锚点提供第一个值。每次迭代计算下一个。WHERE 子句防止无限递归。适用于数学序列。

sql
WITH RECURSIVE fib(n, a, b) AS (
  SELECT 1, 0, 1
  UNION ALL
  SELECT n + 1, b, a + b FROM fib WHERE n < 10
)
SELECT n, a FROM fib;
-- Result: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

树遍历

通过在每次递归中连接名称来构建路径。CAST 确保 path 列足够宽。适用于面包屑、文件路径和类别层次结构。ORDER BY path 按层次排序。

sql
WITH RECURSIVE category_tree AS (
  SELECT id, name, parent_id, CAST(name AS VARCHAR(1000)) AS path
  FROM categories WHERE parent_id IS NULL
  UNION ALL
  SELECT c.id, c.name, c.parent_id, ct.path || ' > ' || c.name
  FROM categories c JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT id, path FROM category_tree ORDER BY path;

CTE 与子查询

CTE 提高可读性且可被多次引用。子查询是内联的且无法重用。CTE 不总是被物化;优化器可能将它们内联。使用 CTE 以提高清晰度。

sql
-- CTE: more readable, can be referenced multiple times
WITH active_users AS (SELECT * FROM users WHERE active = 1)
SELECT * FROM active_users WHERE age > 18
UNION ALL
SELECT * FROM active_users WHERE age <= 18;
-- Subquery: inline, cannot be reused
SELECT * FROM (SELECT * FROM users WHERE active = 1) au WHERE au.age > 18;
20

索引深入

B-Tree 索引

B-Tree 是默认索引类型。复合索引遵循最左前缀规则:如果查询过滤前导列,则可使用索引。按选择性和查询模式排序列。

sql
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_name_age ON users(last_name, first_name, age);
-- Composite index: useful for
-- WHERE last_name = 'Smith' AND first_name = 'John'
-- WHERE last_name = 'Smith' (leftmost prefix)

部分索引

部分索引仅包含匹配 WHERE 子句的行。比完整索引更小更快。适用于始终按条件过滤的查询。减少写入开销。

sql
CREATE INDEX idx_active_users ON users(last_login) WHERE active = 1;
-- Only indexes active users, saving space
-- Useful when queries always filter on active = 1

覆盖索引

覆盖索引包含查询所需的所有列,启用仅索引扫描。PostgreSQL 使用 INCLUDE 添加非键列。通过避免表查找大幅加速 SELECT 查询。

sql
-- PostgreSQL: INCLUDE clause
CREATE INDEX idx_users_covering ON users(last_name) INCLUDE (first_name, email);
-- The query is "covered" if all columns are in the index:
SELECT first_name, email FROM users WHERE last_name = 'Smith';
-- No table lookup needed (index-only scan)

索引类型

不同索引类型满足不同需求。B-Tree 用于通用。Hash 仅用于等值。GIN 用于全文和 JSON。GiST 用于几何数据。根据查询模式选择。

sql
-- B-Tree: default, good for equality and range
CREATE INDEX idx_btree ON users(email);
-- Hash: equality only (PostgreSQL)
CREATE INDEX idx_hash ON users(email) USING HASH;
-- GIN: full-text search, arrays, JSON
CREATE INDEX idx_gin ON docs USING GIN (tsv);
-- GiST: geometric, range types
CREATE INDEX idx_gist ON places USING GIST (location);

索引维护

监控索引使用情况以移除减慢写入的未使用索引。REINDEX 重建碎片化的索引。ANALYZE 更新查询规划器的统计信息。定期维护保持性能最佳。

sql
-- Check index usage (PostgreSQL)
SELECT * FROM pg_stat_user_indexes;
-- Find unused indexes
SELECT relname, indexrelname FROM pg_stat_user_indexes WHERE idx_scan = 0;
-- Rebuild fragmented index
REINDEX INDEX idx_users_email;
-- Analyze for query planner
ANALYZE users;
21

事务

ACID 属性

ACID:原子性(全有或全无)、一致性(有效状态)、隔离性(并发事务不互相干扰)、持久性(提交的数据持久)。BEGIN 开始,COMMIT 保存,ROLLBACK 撤销。

sql
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- Or: ROLLBACK to undo

保存点

保存点在事务内创建部分回滚点。ROLLBACK TO 撤销到保存点而不结束事务。适用于在多步操作中处理错误而无需重启。

sql
BEGIN;
INSERT INTO orders VALUES (1);
SAVEPOINT sp1;
INSERT INTO orders VALUES (2);
-- Oops, rollback to savepoint
ROLLBACK TO sp1;
-- Only order 1 is inserted
INSERT INTO orders VALUES (3);
COMMIT;

隔离级别

隔离级别平衡一致性与性能。READ COMMITTED(默认)防止脏读。REPEATABLE READ 防止不可重复读。SERIALIZABLE 防止幻读但最慢。

sql
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Levels (increasing isolation):
-- READ UNCOMMITTED: dirty reads
-- READ COMMITTED: no dirty reads (default)
-- REPEATABLE READ: no non-repeatable reads
-- SERIALIZABLE: full isolation

死锁

当事务持有彼此需要的锁时发生死锁。数据库检测死锁并中止一个事务。通过以一致顺序访问表来防止。保持事务简短。

sql
-- Transaction 1
BEGIN;
UPDATE accounts SET balance = 0 WHERE id = 1;
UPDATE accounts SET balance = 0 WHERE id = 2;  -- Waits
-- Transaction 2
BEGIN;
UPDATE accounts SET balance = 0 WHERE id = 2;
UPDATE accounts SET balance = 0 WHERE id = 1;  -- Waits
-- Deadlock! Database aborts one transaction

乐观锁定

乐观锁定假设冲突很少发生。version 列跟踪变更。如果 UPDATE 影响 0 行,则数据已被另一事务修改。重试或通知用户。避免长时间持有锁。

sql
-- Add version column
ALTER TABLE products ADD COLUMN version INT DEFAULT 0;
-- Update with version check
UPDATE products SET price = 100, version = version + 1
WHERE id = 1 AND version = 5;
-- If 0 rows affected, someone else updated first
22

SQL 中的 JSON

PostgreSQL JSONB

JSONB 以二进制格式存储 JSON,支持索引化和快速查询。->> 提取为文本,-> 提取为 JSON。JSONB 优于 JSON 用于查询。对 JSONB 列使用 GIN 索引。

sql
CREATE TABLE events (id SERIAL, data JSONB);
INSERT INTO events (data) VALUES ('{"user": "alice", "action": "login"}');
SELECT data->>'user' AS user_name FROM events;
SELECT * FROM events WHERE data->>'action' = 'login';

JSON 查询

-> 导航 JSON,->> 返回文本。@> 检查包含。jsonb_set 更新嵌套值。jsonb_object_keys 返回顶级键。这些运算符支持强大的 JSON 查询。

sql
SELECT data->'address'->'city' AS city FROM users;
SELECT * FROM users WHERE data @> '{"role": "admin"}';
SELECT jsonb_object_keys(data) FROM users;
-- Update JSON
UPDATE users SET data = jsonb_set(data, '{last_login}', '"2024-01-01"');

JSON 聚合

json_agg 将行聚合为 JSON 数组。json_build_object 从列构造 JSON 对象。适用于直接从 SQL 生成 API 响应。结合关系数据和文档数据。

sql
SELECT department,
  json_agg(json_build_object('name', name, 'salary', salary)) AS employees
FROM employees
GROUP BY department;
-- Result: {"department": "Eng", "employees": [{"name": "Alice", "salary": 90000}, ...]}

MySQL JSON

MySQL 使用 $.path 语法处理 JSON。JSON_EXTRACT 获取值,JSON_SET 更新。->> 是 JSON_EXTRACT 返回文本的简写。MySQL JSON 在插入时验证。

sql
CREATE TABLE config (id INT, settings JSON);
INSERT INTO config VALUES (1, '{"theme": "dark", "lang": "en"}');
SELECT settings->>'$.theme' FROM config;
SELECT * FROM config WHERE JSON_EXTRACT(settings, '$.lang') = 'en';
-- Update
UPDATE config SET settings = JSON_SET(settings, '$.theme', 'light');

JSON 索引

JSONB 上的 GIN 索引支持对任意键的快速查询。特定路径上的表达式索引更小且对定向查询更快。为经常查询的 JSON 路径建立索引以提升性能。

sql
-- PostgreSQL GIN index on JSONB
CREATE INDEX idx_events_data ON events USING GIN (data);
-- Index specific path
CREATE INDEX idx_events_user ON events ((data->>'user'));
-- MySQL functional index
CREATE INDEX idx_theme ON config ((CAST(settings->>'$.theme' AS CHAR(50))));
23

性能调优

EXPLAIN ANALYZE

EXPLAIN 显示查询计划;ANALYZE 带计时执行它。Seq Scan 表示缺少索引。Index Scan 是理想的。注意高成本数字和慢操作。优化前始终 EXPLAIN。

sql
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = '[email protected]';
-- Shows: scan type, cost, rows, actual time
-- Seq Scan: full table scan (slow)
-- Index Scan: uses index (fast)
-- Bitmap Heap Scan: index + table lookup

查询优化

仅选择所需列以减少 I/O。避免在索引列上使用函数(非可搜索参数化)。Sargable(可搜索参数化)查询可使用索引。使用范围条件而非函数。

sql
-- BAD: SELECT * fetches all columns
SELECT * FROM users;
-- GOOD: select only needed columns
SELECT id, name FROM users;
-- BAD: function on indexed column
WHERE YEAR(created_at) = 2024
-- GOOD: sargable
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'

JOIN 优化

索引所有连接列。优化器根据统计信息选择连接顺序。INNER JOIN 通常最快。避免在表达式上连接。对于大型数据集,考虑反规范化或物化视图。

sql
-- Use INNER JOIN for required relationships
SELECT u.name, o.total FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- Index join columns
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- For large joins, ensure both columns are indexed

分页

OFFSET 分页是 O(n)——它扫描所有跳过的行。键集(游标)分页是 O(1)——它使用索引。使用元组比较以实现稳定排序。对深度分页快得多。

sql
-- BAD: OFFSET scans all skipped rows
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 10000;
-- GOOD: keyset pagination
SELECT * FROM users WHERE id > 10000 ORDER BY id LIMIT 10;
-- Stable pagination with cursor
SELECT * FROM users WHERE (created_at, id) > ('2024-01-01', 100) ORDER BY created_at, id LIMIT 10;

物化视图

物化视图物理存储查询结果。对于昂贵聚合比视图更快。REFRESH 更新数据(CONCURRENTLY 选项可并发刷新)。为快速查询建立索引。

sql
CREATE MATERIALIZED VIEW sales_summary AS
SELECT product_id, SUM(quantity) AS total, AVG(price) AS avg_price
FROM sales GROUP BY product_id;
-- Refresh periodically
REFRESH MATERIALIZED VIEW sales_summary;
-- Create index on materialized view
CREATE INDEX ON sales_summary (total);
24

高级 JOIN

自连接

自连接是表与自身的查询。使用别名区分。常用于层次数据(员工-经理)和查找配对。a.id < b.id 技巧避免重复配对。

sql
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
-- Find pairs in same department
SELECT a.name, b.name FROM employees a, employees b
WHERE a.department = b.department AND a.id < b.id;

交叉连接

CROSS JOIN 产生笛卡尔积:A 中的每一行与 B 中的每一行组合。适用于生成组合。请注意:它可能产生巨大的结果集。通常用逗号语法隐式使用。

sql
-- Cartesian product: every combination
SELECT s.size, c.color FROM sizes s CROSS JOIN colors c;
-- Generate all size/color combinations
-- Useful for generating test data or matrices

FULL OUTER JOIN

FULL OUTER JOIN 返回两个表中的所有行。NULL 填充不匹配的侧。适用于双向查找不匹配记录。MySQL 不支持(用 LEFT 和 RIGHT JOIN 的 UNION 模拟)。

sql
SELECT u.name, o.order_id
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;
-- Returns all users and all orders
-- NULLs where there is no match

反连接

反连接查找 A 中不匹配 B 的行。NOT EXISTS 通常最清晰且通常最快。带 IS NULL 的 LEFT JOIN 是替代方案。用于查找缺失的关系。

sql
-- Users who have never ordered
SELECT u.* FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.user_id = u.id
);
-- Alternative: LEFT JOIN ... WHERE IS NULL
SELECT u.* FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

半连接

半连接返回 A 中匹配 B 中至少一行的行。EXISTS 很高效,因为它在第一个匹配时停止。IN 是等价的但性能可能不同。对相关子查询使用 EXISTS。

sql
-- Users who have at least one order
SELECT u.* FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.user_id = u.id
);
-- Alternative: IN
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
25

常见陷阱

NULL 比较

NULL 是未知,不是值。= NULL 始终返回 NULL(被视为假)。使用 IS NULL 和 IS NOT NULL。NULL 通过算术传播。使用 COALESCE 提供默认值。

sql
-- NULL = NULL is NULL (not true!)
SELECT * FROM users WHERE phone = NULL;  -- Returns nothing
SELECT * FROM users WHERE phone IS NULL;  -- Correct
SELECT * FROM users WHERE phone IS NOT NULL;
-- NULL in arithmetic yields NULL
SELECT 5 + NULL;  -- NULL

SQL 注入

SQL 注入允许攻击者执行任意 SQL。永远不要将用户输入连接到查询中。始终使用参数化查询/预处理语句。验证和清理所有输入。使用 ORM 参数绑定。

sql
-- BAD: string concatenation
query = "SELECT * FROM users WHERE name = '" + input + "'"
-- GOOD: parameterized queries
SELECT * FROM users WHERE name = ?;
-- PostgreSQL: $1
-- MySQL: ?
-- Always use parameters, never concatenate

GROUP BY 陷阱

使用 GROUP BY 时,SELECT 中的所有非聚合列必须在 GROUP BY 中。否则结果不明确。MySQL 允许此操作(返回任意值)但不正确。始终遵循标准。

sql
-- BAD: non-aggregated column not in GROUP BY
SELECT department, name, COUNT(*) FROM employees GROUP BY department;
-- Error: which name to show?
-- GOOD: aggregate or include in GROUP BY
SELECT department, COUNT(*) FROM employees GROUP BY department;
SELECT department, MAX(name) FROM employees GROUP BY department;

浮点数

FLOAT 和 DOUBLE 是近似类型。对精确精度(货币、测量)使用 DECIMAL/NUMERIC。DECIMAL(10,2) 允许 10 位数字,小数点后 2 位。永远不要对财务数据使用 FLOAT。

sql
-- Floating point precision issues
SELECT 0.1 + 0.2;  -- 0.30000000000000004
-- Use DECIMAL for money
CREATE TABLE accounts (balance DECIMAL(10, 2));
SELECT 0.10 + 0.20;  -- 0.30 (exact)

隐式类型转换

隐式类型转换可能禁用索引并导致全表扫描。始终比较匹配的类型。如有必要,显式转换。检查列类型并确保查询参数匹配。

sql
-- BAD: comparing string to number
SELECT * FROM users WHERE phone = 1234567890;
-- May cause full table scan due to type conversion
-- GOOD: compare same types
SELECT * FROM users WHERE phone = '1234567890';
-- Or cast explicitly
SELECT * FROM users WHERE CAST(phone AS BIGINT) = 1234567890;

这篇内容对您有帮助吗?