SELECT 与查询基础
SELECT、WHERE 与 ORDER BY
SELECT 从一个或多个表中检索行。始终显式指定列名而非使用 *,以提升性能和清晰度(模式变更不会破坏你的应用)。WHERE 在分组前过滤行。ORDER BY 对结果排序(ASC 默认升序,DESC 降序)。LIMIT/OFFSET 实现分页——对于大型数据集,优先使用键集分页(WHERE id > last_id)。
-- 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)可缩短查询语句,在表自连接时是必需的。列别名用于重命名输出列以提高可读性。
-- 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 且通常更快。
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 无法使用索引——考虑使用全文搜索以提升性能。
-- 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 以获得可预测的结果。
-- 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;JOIN
INNER JOIN
INNER JOIN 仅返回在两个表中都有匹配的行。JOIN 是 INNER JOIN 的简写。对于多表查询,逐步连接表。ON 指定连接条件;当两个表有相同列名时,USING(column) 是简写形式。内连接会排除两侧不匹配的行。
-- 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。
-- 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 来模拟。
-- 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 的每一行配对。用于生成组合(尺寸 × 颜色)。自连接(表与自身连接)常用于层次数据(员工-经理)、查找重复项或比较同一表内的行。在自连接中始终使用表别名以区分两个'副本'。
-- 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。
-- 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;GROUP BY 与聚合
GROUP BY 与 HAVING
GROUP BY 将行折叠为组,每组一行。聚合函数(COUNT、SUM、AVG、MIN、MAX)对每个组操作。WHERE 在分组前过滤单个行;HAVING 在聚合后过滤组。SELECT 中的非聚合列必须出现在 GROUP BY 中(标准 SQL)。MySQL 较宽松但不可预测——始终在 GROUP BY 中包含所有非聚合列。
-- 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。
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 查询至关重要。
-- 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。
-- 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),它保留时间戳类型。对大型表的日期列建立索引以提升性能。
-- 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;子查询与 CTE
标量与列子查询
标量子查询返回单个值,可在任何需要值的地方使用。列子查询返回一列,与 IN、ANY、ALL 一起使用。SELECT 中的子查询(相关子查询)对外查询的每一行执行一次——在大型数据集上可能很慢。考虑改写为带 GROUP BY 的 JOIN 以获得更好的性能。
-- 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 更快。数据库可能将相关子查询优化为连接。
-- 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。
-- 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 文档。
-- 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 常用,但可以更自然地表达某些查询。
-- 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);窗口函数
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)在分析中极其常见。
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 以获得确定性结果。
-- 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。
-- 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 rowsNTILE 与 PERCENT_RANK
NTILE(n) 将有序行分为 n 个大致相等的组(四分位数、十分位数、百分位数)。PERCENT_RANK 给出相对排名(0 到 1)。CUME_DIST 给出累积分布。FIRST_VALUE/LAST_VALUE 返回帧中第 一行/最后一行的值——注意 LAST_VALUE 需要显式帧(UNBOUNDED FOLLOWING),因为默认帧在当前行结束。
-- 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 的关键区别:你保留所有明细行,同时也能看到汇总。非常适合将单个值与组平均值比较、计算百分比,以及为明细报表添加上下文列。
-- 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 attachedDDL:表与模式
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。
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 强制自定义规则。在数据库中(而非仅应用代码中)定义约束可确保无论数据如何被访问都保持完整性。
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)进行版本控制的模式变更。
-- 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 索引支持全文搜索。部分索引仅索引匹配的行以节省空间。
-- 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)。
-- 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;DML:插入、更新、删除
INSERT
INSERT 添加行。一条语句中使用多个 VALUES 比单独插入更高效。INSERT...SELECT 在表之间复制数据。RETURNING(PostgreSQL/Oracle)在一次往返中检索自动生成的值(如 SERIAL id)——对应用代码至关重要。使用 DEFAULT VALUES 插入一行全部默认值。始终指定列名以使代码对模式变更具有弹性。
-- 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 以验证受影响的行。
-- 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!