想轻松搞定数据库查询?下面本文从 SQL 执行顺序开始,带着我们一步步掌握基础查询、表连接、分组聚合等基本技能。不管是筛选用户信息、关联多表数据,还是统计分析、处理复杂的业务场景,我们都给出了清晰示例和实用技巧,并搭配了电商、金融、日志分析等真实场景模板,还有 15 道练习题供我们练习巩固,即使我们是新手也能快速上手,让 SQL 查询不再是难题。(所有代码块或表格均可左右滚动)
一、SQL 执行顺序
-- SQL 的执行顺序(非书写顺序)如下:
-- 1、FROM → 加载数据源(表、视图)
-- 2、JOIN / ON → 连接其他表
-- 3、WHERE → 行级过滤(不能使用 SELECT 中的别名)
-- 4、GROUP BY → 按字段分组(每组生成一行)
-- 5、HAVING → 对分组后的结果过滤(可使用聚合函数)
-- 6、SELECT → 选择要返回的列或表达式
-- 7、DISTINCT → 去除完全重复的行
-- 8、ORDER BY → 对最终结果排序
-- 9、LIMIT / OFFSET → 分页,限制返回行数
说明:由于 WHERE 在 SELECT 之前执行,所以我们不能在 WHERE 中使用 SELECT 中定义的别名。示例:
-- ❌ 错误写法
SELECT age + 1 AS next_age FROM users WHERE next_age > 19;
-- ✅ 正确写法
SELECT age + 1 AS next_age FROM users WHERE age + 1 > 19;
二、基础查询:SELECT + FROM + WHERE
-- 目的:从表中查询满足条件的数据
-- 使用场景:查看用户信息、订单列表等
-- ⚠️ 注意:WHERE 条件应尽量使用索引列,避免全表扫描
-- 示例数据(users 表):
-- | user_id | name | age | city | status | email | created_at |
-- |---------|---------|-----|-----------|---------|---------------|-------------|
-- | 1 | Alice | 25 | Beijing | active | a@x.com | 2025-01-10 |
-- | 2 | Bob | 30 | Shanghai | active | b@x.com | 2025-02-15 |
-- | 3 | Charlie | 17 | Beijing | inactive| c@x.com | 2025-03-01 |
SELECT
-- 【SELECT 子句】定义要返回的字段或表达式
user_id, -- 返回原始列
name, -- 多列用逗号分隔
UPPER(name) AS upper_name, -- 函数处理:转大写,别名 upper_name
age + 1 AS next_year_age, -- 算术运算:明年年龄
'Active User' AS status_label, -- 添加常量字段
NOW() AS query_timestamp -- 当前查询时间(MySQL/PG)
FROM
-- 【FROM 子句】指定数据来源
users -- 表名
-- FROM schema.users -- PostgreSQL 中指定 schema
-- FROM users u -- 给表起别名 u,方便引用
WHERE
-- 【WHERE 子句】行级过滤,只保留符合条件的行
-- 注意:WHERE 不能使用 SELECT 中的别名(如:upper_name)
age >= 18 -- 年龄 >= 18
AND city IN ('Beijing', 'Shanghai') -- 匹配多个城市
AND status = 'active' -- 状态为 active
AND name LIKE 'A%' -- 名字以 A 开头
AND created_at >= '2025-01-01' -- 注册时间在 2025 年后
-- AND YEAR(created_at) = 2025 -- ❌ 错误!破坏索引,应避免
-- ✅ 正确:created_at BETWEEN '2025-01-01' AND '2025-12-31'
-- NULL 值处理
AND email IS NOT NULL -- 推荐:使用 IS NOT NULL
-- AND email != NULL -- ❌ 错误!NULL 不能用 = 或 != 比较
;
执行结果示例:
| user_id | name | upper_name | next_year_age | status_label | query_timestamp |
|---------|-------|------------|---------------|--------------|---------------------|
| 1 | Alice | ALICE | 26 | Active User | 2025-08-19 12:00:00 |
语法元素解析:
- AS:为列或表达式起别名,提高可读性;
- UPPER():字符串函数,转大写;
- IN:等价于多个 OR,性能优于 OR 链;
- LIKE:模糊匹配,% 表明任意字符,_ 表明单个字符。
三、表连接:JOIN(INNER / LEFT / RIGHT)
-- 目的:关联多个表,获取跨表信息
-- 使用场景:用户 + 订单信息联合查询
-- ⚠️ 注意:LEFT JOIN 中的 WHERE 条件可能改变 JOIN 行为
-- 示例数据(orders 表):
-- | order_id | user_id | product | amount | status | order_date |
-- |----------|---------|-------------|--------|------------|-------------|
-- | 1001 | 1 | iPhone | 999 | completed | 2025-01-15 |
-- | 1002 | 2 | Laptop | 1299 | completed | 2025-02-20 |
-- | 1003 | 3 | Headphones | 199 | pending | 2025-03-05 |
SELECT
u.user_id,
u.name,
o.order_id,
o.product,
o.order_date,
COALESCE(o.amount, 0) AS amount -- 处理 NULL,返回 0
FROM
users u -- 主表:用户(左表)
-- 【JOIN 子句】连接订单表
LEFT JOIN
orders o -- 被连接表(右表)
ON u.user_id = o.user_id -- 连接条件:通过 user_id 关联
-- 可选:在 ON 中加入过滤条件(如:按日期筛选)
-- ON u.user_id = o.user_id AND o.order_date >= '2025-02-01'
-- 区别:
-- - ON 中的条件:在连接时过滤右表
-- - WHERE 中的条件:在连接后过滤整个结果
WHERE
-- WHERE 过滤最终结果
u.city = 'Beijing' -- 只保留北京用户
-- AND o.order_id IS NOT NULL -- 如果加此条件,LEFT JOIN 会退化为 INNER JOIN!
-- 可选连接类型:
-- INNER JOIN:只保留两边都匹配的行
-- RIGHT JOIN:保留右表所有行(较少用)
-- FULL OUTER JOIN:两边都保留(MySQL 不支持)
;
执行结果示例:
| user_id | name | order_id | product | order_date | amount |
|---------|---------|----------|------------|-------------|--------|
| 1 | Alice | 1001 | iPhone | 2025-01-15 | 999 |
| 3 | Charlie | NULL | NULL | NULL | 0 | -- LEFT JOIN 保留左表
LEFT JOIN vs INNER JOIN:
- LEFT JOIN:保留左表所有行,右表无匹配则为 NULL;
- INNER JOIN:只显示有订单的用户。
四、分组与聚合:GROUP BY + HAVING
-- 目的:对数据进行分组统计
-- 使用场景:按城市统计用户数量、平均年龄等
-- ⚠️ 注意:SELECT 中非聚合字段必须出目前 GROUP BY 中
SELECT
city, -- 分组字段
COUNT(*) AS user_count, -- 统计每组行数
AVG(age) AS avg_age, -- 平均年龄
SUM(COALESCE(age, 0)) AS total_age,-- 总年龄
MAX(created_at) AS latest_signup, -- 最晚注册时间
STRING_AGG(name, ', ') AS names -- 拼接名字(PostgreSQL)
-- GROUP_CONCAT(name) AS names -- MySQL 等价函数
FROM
users
WHERE
status = 'active' -- 先过滤再分组,提升性能
GROUP BY
city -- 按城市分组
HAVING
-- 【HAVING 子句】对分组后的结果进行过滤
COUNT(*) >= 1 -- 至少有 1 个用户
AND AVG(age) > 20 -- 平均年龄 > 20
ORDER BY
user_count DESC, -- 按用户数降序
city ASC -- 城市升序
;
执行结果示例:
| city | user_count | avg_age | total_age | latest_signup | names |
|-----------|------------|---------|-----------|---------------|---------|
| Shanghai | 1 | 30.0 | 30 | 2025-02-15 | Bob |
| Beijing | 1 | 25.0 | 25 | 2025-01-10 | Alice |
WHERE vs HAVING:
- WHERE:在分组前过滤原始行;
- HAVING:在分组后过滤分组结果。
五、子查询与 CTE(公用表表达式)
-- 目的:将复杂查询拆解为多个逻辑步骤
-- 使用场景:先统计订单,再筛选高价值用户
-- ⚠️ 注意:CTE 只在当前查询中有效,不保存数据
-- 【CTE】使用 WITH 定义临时结果集(推荐)
WITH user_stats AS (
-- 第一步:统计每个用户的订单情况
SELECT
user_id,
COUNT(*) AS order_count,
AVG(amount) AS avg_amount
FROM
orders
GROUP BY
user_id
HAVING
COUNT(*) >= 1 -- 至少有一单
),
top_users AS (
-- 第二步:筛选高价值用户
SELECT
user_id
FROM
user_stats
WHERE
avg_amount > 500 -- 平均订单金额 > 500
)
-- 主查询:结合用户信息与统计结果
SELECT
u.name,
u.city,
us.order_count,
us.avg_amount
FROM
users u
INNER JOIN
user_stats us
ON u.user_id = us.user_id
WHERE
u.user_id IN (SELECT user_id FROM top_users) -- 子查询过滤
ORDER BY
us.avg_amount DESC
LIMIT 10;
执行结果示例:
| name | city | order_count | avg_amount |
|-------|----------|-------------|------------|
| Bob | Shanghai | 1 | 1299.0 |
| Alice | Beijing | 1 | 999.0 |
CTE 优势:
- 提高可读性;
- 支持递归查询(如:组织架构树);
- 可被多次引用。
六、窗口函数(Window Functions)
-- 目的:在不减少行数的前提下进行分组内计算
-- 使用场景:排名、移动平均、累计求和
-- ⚠️ 注意:窗口函数在 SELECT 阶段计算,不能在 WHERE 中使用
SELECT
user_id,
product,
amount,
order_date,
-- 【ROW_NUMBER】组内唯一排名
ROW_NUMBER() OVER (
PARTITION BY user_id -- 按用户分组
ORDER BY amount DESC -- 组内按金额降序
) AS rn, -- 排名:1,2,3...
-- 【RANK】并列排名(一样值同名次,跳过后续)
RANK() OVER (
ORDER BY amount DESC
) AS global_rank, -- 全局排名:1,2,2,4...
-- 【SUM OVER】累计求和
SUM(amount) OVER (
PARTITION BY user_id
) AS user_total_amount, -- 每行都显示该用户的总消费
-- 【移动平均】过去3天平均
AVG(amount) OVER (
ORDER BY order_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3day
FROM
orders
WHERE
order_date >= '2025-01-01' -- 基于订单日期过滤
ORDER BY
user_id, rn;
执行结果示例:
| user_id | product | amount | order_date | rn | global_rank | user_total_amount |
|---------|-----------|--------|-------------|----|-------------|-------------------|
| 1 | iPhone | 999 | 2025-01-15 | 1 | 2 | 999 |
| 2 | Laptop | 1299 | 2025-02-20 | 1 | 1 | 1299 |
窗口函数核心:
OVER (…) 定义“窗口”,即:计算的范围。
常用在:Top-N、趋势分析、同比环比。
七、条件逻辑:CASE WHEN
-- 目的:实现 SQL 中的 if-else 逻辑
-- 使用场景:分类、状态转换、条件统计
SELECT
name,
age,
city,
-- 【CASE WHEN】通用条件判断
CASE
WHEN age < 18 THEN '未成年'
WHEN age BETWEEN 18 AND 60 THEN '成年人'
ELSE '老年人'
END AS age_group,
-- 【简写 CASE】当判断单个字段时
CASE status
WHEN 'active' THEN 1
WHEN 'inactive' THEN 0
ELSE -1
END AS status_code,
-- 【条件聚合】统计北京用户数量
SUM(
CASE WHEN city = 'Beijing' THEN 1 ELSE 0 END
) AS beijing_count
FROM
users
GROUP BY
name, age, city, status; -- 注意:非聚合字段必须出目前 GROUP BY
执行结果示例:
| name | age | city | age_group | status_code | beijing_count |
|---------|-----|---------|-----------|-------------|---------------|
| Alice | 25 | Beijing | 成年人 | 1 | 2 |
| Bob | 30 | Shanghai| 成年人 | 1 | 2 |
| Charlie | 17 | Beijing | 未成年 | 0 | 2 |
八、去重与分页
-- 【DISTINCT】去除完全重复的行
SELECT DISTINCT
city, status
FROM
users;
-- 结果:('Beijing', 'active'), ('Shanghai', 'active'), ('Beijing', 'inactive')
-- 【分页】LIMIT + OFFSET
SELECT
user_id, name, city
FROM
users
ORDER BY
user_id
LIMIT 10 OFFSET 20; -- 跳过前 20 行,取 10 行
-- 等价写法:LIMIT 10, 20(MySQL)
-- ⚠️ 深分页问题:OFFSET 越大,性能越差(需跳过大量行)
-- ✅ 优化:使用“游标分页”(基于上一页最后 ID)
-- WHERE user_id > 100 ORDER BY user_id LIMIT 10
九、存在性检查:EXISTS vs IN
-- ✅ 推荐:EXISTS(一般性能更好,可短路)
SELECT
u.name
FROM
users u
WHERE
EXISTS (
SELECT 1 -- 习惯写 1,不关心具体值
FROM orders o
WHERE o.user_id = u.user_id
AND o.amount > 1000
);
-- ❌ 注意:IN 子查询若含 NULL,结果可能为空
-- WHERE col IN (1, 2, NULL) → 永远返回 FALSE 或 NULL
EXISTS vs IN:
- EXISTS:只要找到一条就返回 true,适合大表;
- IN:适合小集合,但子查询不能有 NULL。
十、数据操作:INSERT / UPDATE / DELETE
-- 插入数据
INSERT INTO users (name, age, city, status)
VALUES ('David', 28, 'Guangzhou', 'active');
-- 更新数据
UPDATE users
SET status = 'inactive', updated_at = NOW()
WHERE city = 'Shanghai';
-- 删除数据
DELETE FROM users
WHERE user_id = 999;
-- ⚠️ 生产环境慎用!提议先 SELECT 验证
-- SELECT * FROM users WHERE user_id = 999;
DML 注意事项:
- INSERT:字段与值数量、类型必须匹配;
- UPDATE:务必加 WHERE,否则全表更新;
- DELETE:务必加 WHERE,否则全表删除。
十一、性能优化提议
/*
优化小技巧:
1、避免 SELECT *,只取需要的列;
2、WHERE 条件优先使用索引列(如:user_id, city);
3、避免在索引列上使用函数(如:WHERE YEAR(date)=2025);
4、大表 JOIN 时,小表驱动大表;
5、使用 EXPLAIN 分析执行计划;
6、思考添加复合索引:
CREATE INDEX idx_users_city_status ON users(city, status);
CREATE INDEX idx_orders_user_amount ON orders(user_id, amount);
*/
十二、数据库方言差异
|
功能 |
MySQL |
PostgreSQL |
SQL Server |
Oracle |
|
分页 |
LIMIT 10 OFFSET 20 |
LIMIT 10 OFFSET 20 |
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY |
ROWNUM <= 10 (需嵌套) |
|
字符串拼接 |
CONCAT(a,b) |
a || b 或 CONCAT() |
a + b |
a || b |
|
当前时间 |
NOW() |
NOW() |
GETDATE() |
SYSDATE |
|
Top N |
LIMIT 10 |
LIMIT 10 |
TOP 10 |
ROWNUM <= 10 |
十三、电商场景 SQL 模板
示例表:users(用户)、orders(订单)、products(商品)、order_items(订单明细)
1、计算月度 GMV(总交易额)与订单数
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month, -- 基于订单日期分组
COUNT(*) AS order_count,
SUM(amount) AS gmv,
AVG(amount) AS avg_order_value
FROM orders
WHERE order_status IN ('paid', 'shipped', 'completed')
AND order_date >= '2025-01-01' -- 按订单日期筛选时间范围
GROUP BY month
ORDER BY month;
2、找出复购率(购买 ≥2 次的用户占比)
WITH user_orders AS (
SELECT
user_id,
COUNT(*) AS order_count
FROM orders
GROUP BY user_id
)
SELECT
COUNT(CASE WHEN order_count >= 2 THEN 1 END) * 100.0 / COUNT(*) AS repurchase_rate
FROM user_orders;
3、热销商品 Top 10(按销量)
SELECT
p.product_name,
SUM(oi.quantity) AS total_sold,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.product_id, p.product_name
ORDER BY total_sold DESC
LIMIT 10;
4、用户生命周期价值(LTV)计算(最近 12 个月)
SELECT
user_id,
SUM(amount) AS ltv_12m
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH) -- 基于订单日期计算时间范围
AND order_status = 'completed'
GROUP BY user_id;
十四、金融场景 SQL 模板
示例表:accounts(账户)、transactions(交易)、customers(客户)
1、每月净流入(收入 – 支出)
SELECT
DATE_FORMAT(trans_date, '%Y-%m') AS month,
SUM(CASE WHEN trans_type = 'deposit' THEN amount ELSE 0 END) AS income,
SUM(CASE WHEN trans_type = 'withdrawal' THEN amount ELSE 0 END) AS expense,
SUM(CASE WHEN trans_type = 'deposit' THEN amount ELSE -amount END) AS net_flow
FROM transactions
GROUP BY month
ORDER BY month;
2、大额交易监控(单笔 > 10万)
SELECT
c.name,
c.phone,
t.trans_date,
t.amount,
t.trans_type
FROM transactions t
JOIN accounts a ON t.account_id = a.account_id
JOIN customers c ON a.customer_id = c.customer_id
WHERE t.amount > 100000
AND t.trans_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
ORDER BY t.amount DESC;
3、客户资产分布(按资产区间统计人数)
SELECT
CASE
WHEN total_assets < 10000 THEN '0-1万'
WHEN total_assets BETWEEN 10000 AND 100000 THEN '1-10万'
WHEN total_assets BETWEEN 100000 AND 1000000 THEN '10-100万'
ELSE '100万以上'
END AS asset_level,
COUNT(*) AS customer_count,
AVG(total_assets) AS avg_assets
FROM (
SELECT
customer_id,
SUM(balance) AS total_assets
FROM accounts
GROUP BY customer_id
) t
GROUP BY asset_level
ORDER BY avg_assets;
十五、日志分析场景 SQL 模板
示例表:web_logs(访问日志),字段:ip, url, status, user_agent, timestamp
1、每日 PV、UV 统计
SELECT
DATE(timestamp) AS log_date,
COUNT(*) AS pv,
COUNT(DISTINCT ip) AS uv
FROM web_logs
WHERE timestamp >= '2025-08-01'
GROUP BY log_date
ORDER BY log_date;
2、接口错误率监控(status >= 500)
SELECT
DATE(timestamp) AS day,
COUNT(*) AS total_requests,
COUNT(CASE WHEN status >= 500 THEN 1 END) AS error_count,
ROUND(
COUNT(CASE WHEN status >= 500 THEN 1 END) * 100.0 / COUNT(*), 2
) AS error_rate_percent
FROM web_logs
GROUP BY day
HAVING error_rate_percent > 1.0 -- 只看错误率 >1% 的天
ORDER BY error_rate_percent DESC;
3、热门页面 Top 10
SELECT
url,
COUNT(*) AS visit_count,
COUNT(DISTINCT ip) AS unique_visitors
FROM web_logs
WHERE status = 200
AND url NOT LIKE '%favicon%' -- 过滤无关请求
GROUP BY url
ORDER BY visit_count DESC
LIMIT 10;
4、用户行为路径分析(会话级)
-- 假设会话定义为同一 IP 30 分钟内连续访问
-- 此为简化版,真实需用窗口函数定义会话
WITH session_logs AS (
SELECT
ip,
url,
timestamp,
-- 模拟会话 ID:每 30 分钟重置
FLOOR(UNIX_TIMESTAMP(timestamp) / 1800) AS session_id
FROM web_logs
WHERE DATE(timestamp) = '2025-08-18'
)
SELECT
ip,
session_id,
GROUP_CONCAT(url ORDER BY timestamp) AS page_path
FROM session_logs
GROUP BY ip, session_id
HAVING COUNT(*) >= 3; -- 至少访问 3 页
十六、练习题(共 15 题)
示例表:基于前文示例表结构(users, orders, products)
练习 1:查询“北京的活跃用户中,名字以 A 开头,且年龄在 20-30 岁之间”的用户姓名和邮箱。
参考答案:
SELECT name, email
FROM users
WHERE city = 'Beijing'
AND status = 'active'
AND name LIKE 'A%'
AND age BETWEEN 20 AND 30;
解析:基础 WHERE 多条件组合,使用 BETWEEN 比 >= AND <= 更清晰。
练习 2:列出每个用户的订单总数,只显示有订单的用户,按订单数降序。
参考答案:
SELECT
u.name,
COUNT(o.order_id) AS order_count
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.name
ORDER BY order_count DESC;
解析:INNER JOIN 自动过滤无订单用户;GROUP BY 必须包含非聚合字段。
练习 3:找出“从未下过订单”的用户姓名。
参考答案:
SELECT name
FROM users
WHERE user_id NOT IN (
SELECT DISTINCT user_id
FROM orders
WHERE user_id IS NOT NULL
);
-- 或使用 LEFT JOIN
SELECT u.name
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.user_id IS NULL;
解析:NOT IN 子查询需注意 NULL 值;LEFT JOIN + IS NULL 是经典写法。
练习 4:计算每个城市的用户平均年龄,只显示平均年龄 > 22 的城市。
参考答案:
SELECT
city,
AVG(age) AS avg_age
FROM users
GROUP BY city
HAVING AVG(age) > 22;
解析:分组后过滤用 HAVING,不能用 WHERE。
练习 5:给每个订单按金额从高到低排名,一样金额排名一样,但不跳过名次(DENSE_RANK)。
参考答案:
SELECT
order_id,
amount,
DENSE_RANK() OVER (ORDER BY amount DESC) AS rank
FROM orders;
解析:DENSE_RANK 连续排名(1,2,2,3);RANK 跳过(1,2,2,4)。
练习 6:查询“每个用户的第一笔订单”信息(订单ID、金额、时间)。
参考答案:
SELECT
user_id,
order_id,
amount,
order_date
FROM (
SELECT
user_id,
order_id,
amount,
order_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date) AS rn -- 按订单日期排序
FROM orders
) t
WHERE rn = 1;
解析:窗口函数 ROW_NUMBER 按用户分组,按订单日期排序,取每组第一行即为用户的第一笔订单。
练习 7:统计“本月新增用户数”(注册时间在本月)。
参考答案:
SELECT
COUNT(*) AS new_users
FROM users
WHERE created_at >= '2025-08-01'
AND created_at < '2025-09-01';
-- 或使用 DATE 函数
-- WHERE DATE_FORMAT(created_at, '%Y-%m') = '2025-08'
解析:日期范围查询推荐用 >= AND <,避免时间戳问题。
练习 8:将用户按年龄分组:“青年(18-35)”,“中年(36-55)”,“老年(>55)”,统计每组人数。
参考答案:
SELECT
CASE
WHEN age BETWEEN 18 AND 35 THEN '青年'
WHEN age BETWEEN 36 AND 55 THEN '中年'
WHEN age > 55 THEN '老年'
ELSE '未成年'
END AS age_group,
COUNT(*) AS count
FROM users
GROUP BY age_group
ORDER BY count DESC;
解析:CASE WHEN 分类 + GROUP BY 统计。
练习 9:查询“订单金额最高的用户”的姓名和总消费。
参考答案:
SELECT
u.name,
SUM(o.amount) AS total_spent
FROM users u
JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.name
ORDER BY total_spent DESC
LIMIT 1;
解析:先按用户聚合,再排序取 Top 1。
练习 10:使用 CTE 查询“订单数 ≥2 且平均金额 > 500”的用户姓名。
参考答案:
WITH user_stats AS (
SELECT
user_id,
COUNT(*) AS order_count,
AVG(amount) AS avg_amount
FROM orders
GROUP BY user_id
)
SELECT u.name
FROM users u
JOIN user_stats us ON u.user_id = us.user_id
WHERE us.order_count >= 2 AND us.avg_amount > 500;
解析:CTE 提高可读性,适合复杂逻辑拆解。
练习 11(电商):计算“购物车转化率”(加购数 → 下单数)
-- 假设有表:user_actions (user_id, action_type, product_id, timestamp)
-- action_type: 'add_to_cart', 'place_order'
参考答案:
SELECT
COUNT(DISTINCT CASE WHEN action_type = 'place_order' THEN user_id END) * 100.0 /
COUNT(DISTINCT CASE WHEN action_type = 'add_to_cart' THEN user_id END) AS conversion_rate
FROM user_actions
WHERE timestamp >= '2025-08-01';
解析:分子:下单人数;分母:加购人数;计算整体转化率。
练习 12(金融):找出“连续 3 天都有交易”的客户。
参考答案:
-- 简化版:使用窗口函数找连续登录
WITH daily_trans AS (
SELECT
customer_id,
DATE(trans_date) AS trans_day
FROM transactions
GROUP BY customer_id, trans_day
),
ranked AS (
SELECT
customer_id,
trans_day,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY trans_day) AS rn
FROM daily_trans
),
grouped AS (
SELECT
customer_id,
DATE_SUB(trans_day, INTERVAL rn DAY) AS grp -- 连续日期差为常数
FROM ranked
)
SELECT DISTINCT customer_id
FROM grouped
GROUP BY customer_id, grp
HAVING COUNT(*) >= 3;
解析:经典“连续 N 天”问题,利用 日期 – 排名 一样则连续。
练习 13(日志):统计“移动端访问占比”(根据 user_agent 判断)
参考答案:
SELECT
COUNT(CASE WHEN user_agent LIKE '%Mobile%' THEN 1 END) * 100.0 / COUNT(*) AS mobile_ratio
FROM web_logs
WHERE DATE(timestamp) = '2025-08-18';
解析:LIKE '%Mobile%' 是判断移动端的简单方式(实际可用正则)。
练习 14:修复错误 SQL
-- 以下 SQL 有语法错误,请修正:
SELECT name, age, AVG(age)
FROM users
WHERE AVG(age) > 20
GROUP BY name;
参考答案:
-- 修正后:
SELECT name, age, AVG(age)
FROM users
GROUP BY name, age
HAVING AVG(age) > 20;
-- 但逻辑不合理,应改为:
SELECT name, age
FROM users
WHERE age > 20;
-- 或分组统计:
SELECT name, AVG(age)
FROM users
GROUP BY name
HAVING AVG(age) > 20;
解析:WHERE 不能用聚合函数;SELECT 非聚合字段必须在 GROUP BY。
练习 15:写出“分页查询第 3 页,每页 10 条,按 user_id 排序”的 SQL。
参考答案:
-- MySQL/PostgreSQL:
SELECT * FROM users
ORDER BY user_id
LIMIT 10 OFFSET 20;
-- 第1页:OFFSET 0;第2页:OFFSET 10;第3页:OFFSET 20
-- 深分页优化(游标分页):
SELECT * FROM users
WHERE user_id > 20 -- 假设上一页最后 user_id 是 20
ORDER BY user_id
LIMIT 10;
解析:OFFSET 越大越慢,生产环境提议用游标(基于 ID)。
掌握了这些 SQL 知识,我们就能应对多数数据查询需求。从简单的条件筛选到复杂的分组统计、窗口函数应用,再到不同场景的实战模板,每一步都离不开对执行逻辑的理解。多练习、多分析执行计划,能让我们的查询更高效。