SQL 数据分析实战¶
从查询到分析——数据分析师必备的 SQL 技能
🎯 SQL 在数据分析中的角色¶
| 环节 | 用 SQL 做什么 |
|---|---|
| 数据提取 | 从数据库拉取分析所需的数据 |
| 数据清洗 | WHERE 过滤、CASE WHEN 转换、JOIN 合并 |
| 聚合分析 | GROUP BY + 窗口函数做描述统计 |
| 特征工程 | 在数据库层构建特征,减少 Python 端计算 |
| 数据验证 | 检查数据质量、发现异常值 |
原则:尽量在 SQL 层完成数据聚合,把结果集控制在 Python 能轻松处理的规模(< 10 万行)。
📐 基础:SELECT + WHERE + JOIN¶
单表查询¶
-- 取最近 30 天的订单
SELECT
order_id,
user_id,
amount,
status,
created_at
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
AND status != 'cancelled'
ORDER BY created_at DESC
LIMIT 1000;
多表 JOIN¶
-- 用户 + 订单:每个用户的累计消费
SELECT
u.user_id,
u.name,
u.registration_date,
COUNT(o.order_id) AS order_count,
COALESCE(SUM(o.amount), 0) AS total_spent,
COALESCE(AVG(o.amount), 0) AS avg_order_value
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
AND o.status = 'completed'
WHERE u.registration_date >= '2024-01-01'
GROUP BY u.user_id, u.name, u.registration_date
ORDER BY total_spent DESC;
JOIN 类型速查:
| JOIN | 保留 | 典型场景 |
|---|---|---|
INNER JOIN |
两表都有的行 | 已完成订单 + 用户信息 |
LEFT JOIN |
左表全部 + 右表匹配 | 所有用户 + 他们的订单(含无订单用户) |
RIGHT JOIN |
右表全部 + 左表匹配 | 很少用(用 LEFT JOIN 换顺序即可) |
FULL OUTER JOIN |
两表全部 | 对账:两边差集一起看 |
CROSS JOIN |
笛卡尔积 | 生成所有组合(小心数据量!) |
🧮 聚合分析:GROUP BY + HAVING¶
-- 按渠道统计转化漏斗
SELECT
channel,
COUNT(DISTINCT user_id) AS visitors,
COUNT(DISTINCT CASE WHEN event = 'signup' THEN user_id END) AS signups,
COUNT(DISTINCT CASE WHEN event = 'purchase' THEN user_id END) AS buyers,
ROUND(
COUNT(DISTINCT CASE WHEN event = 'purchase' THEN user_id END) * 100.0
/ NULLIF(COUNT(DISTINCT user_id), 0), 2
) AS conversion_rate
FROM events
WHERE event_date >= '2024-06-01'
GROUP BY channel
HAVING COUNT(DISTINCT user_id) >= 100 -- 过滤小样本
ORDER BY conversion_rate DESC;
🪟 窗口函数:SQL 的灵魂¶
窗口函数 = 在分组内做排序/累计/移动计算,但不折叠行。
ROW_NUMBER / RANK / DENSE_RANK¶
-- 每个用户的第一笔订单
SELECT *
FROM (
SELECT
user_id,
order_id,
amount,
created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS rn
FROM orders
) sub
WHERE rn = 1;
| 函数 | 同值处理 | 示例(分数 90, 90, 85) |
|---|---|---|
ROW_NUMBER() |
随机排序 | 1, 2, 3 |
RANK() |
同值同排名,跳过 | 1, 1, 3 |
DENSE_RANK() |
同值同排名,不跳过 | 1, 1, 2 |
累计和 / 移动平均¶
SELECT
order_date,
daily_revenue,
-- 累计收入
SUM(daily_revenue) OVER (ORDER BY order_date) AS cumulative_revenue,
-- 7 日移动平均
AVG(daily_revenue) OVER (
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS ma_7d,
-- 环比增长率
ROUND(
(daily_revenue - LAG(daily_revenue) OVER (ORDER BY order_date))
* 100.0 / NULLIF(LAG(daily_revenue) OVER (ORDER BY order_date), 0), 2
) AS wow_change_pct
FROM daily_stats
ORDER BY order_date;
LAG / LEAD:拿前一行 / 后一行¶
-- 用户两次购买之间的间隔天数
SELECT
user_id,
order_id,
created_at,
LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_order_date,
created_at - LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at)
AS days_since_last_order
FROM orders
ORDER BY user_id, created_at;
NTILE:分桶¶
-- 将用户按消费金额分成 5 等份
SELECT
user_id,
total_spent,
NTILE(5) OVER (ORDER BY total_spent DESC) AS spending_quintile
FROM user_stats;
📊 实战分析案例¶
RFM 分析(在 SQL 中完成)¶
WITH rfm AS (
SELECT
user_id,
-- Recency: 距今天数
CURRENT_DATE - MAX(order_date) AS recency,
-- Frequency: 订单数
COUNT(DISTINCT order_id) AS frequency,
-- Monetary: 总消费
SUM(amount) AS monetary
FROM orders
WHERE status = 'completed'
GROUP BY user_id
),
rfm_scored AS (
SELECT
user_id,
recency,
frequency,
monetary,
-- R 分: 最近越好 → 分越高
NTILE(5) OVER (ORDER BY recency ASC) AS r_score,
-- F 分: 频率越高 → 分越高
NTILE(5) OVER (ORDER BY frequency DESC) AS f_score,
-- M 分: 金额越高 → 分越高
NTILE(5) OVER (ORDER BY monetary DESC) AS m_score
FROM rfm
)
SELECT
user_id,
r_score, f_score, m_score,
CONCAT(r_score, f_score, m_score) AS rfm_cell,
CASE
WHEN r_score >= 4 AND f_score >= 4 AND m_score >= 4 THEN '高价值'
WHEN r_score >= 4 AND f_score <= 2 THEN '新客户'
WHEN r_score <= 2 AND f_score >= 4 THEN '流失风险'
WHEN r_score <= 2 AND f_score <= 2 THEN '已流失'
ELSE '普通'
END AS segment
FROM rfm_scored;
留存分析¶
-- 周留存:注册后第 N 周还有活跃的用户比例
WITH user_cohorts AS (
SELECT
user_id,
DATE_TRUNC('week', registration_date) AS cohort_week
FROM users
),
active_weeks AS (
SELECT DISTINCT
user_id,
DATE_TRUNC('week', activity_date) AS activity_week
FROM user_activities
)
SELECT
c.cohort_week,
COUNT(DISTINCT c.user_id) AS cohort_size,
-- Week 0(注册当周)
ROUND(COUNT(DISTINCT CASE
WHEN a.activity_week = c.cohort_week THEN c.user_id
END) * 100.0 / COUNT(DISTINCT c.user_id), 1) AS week_0,
-- Week 1
ROUND(COUNT(DISTINCT CASE
WHEN a.activity_week = c.cohort_week + INTERVAL '7 days'
THEN c.user_id
END) * 100.0 / COUNT(DISTINCT c.user_id), 1) AS week_1,
-- Week 4
ROUND(COUNT(DISTINCT CASE
WHEN a.activity_week = c.cohort_week + INTERVAL '28 days'
THEN c.user_id
END) * 100.0 / COUNT(DISTINCT c.user_id), 1) AS week_4
FROM user_cohorts c
LEFT JOIN active_weeks a ON c.user_id = a.user_id
GROUP BY c.cohort_week
ORDER BY c.cohort_week;
⚡ 性能优化¶
1. 索引策略¶
-- 为常用查询条件创建索引
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
CREATE INDEX idx_orders_status ON orders(status) WHERE status = 'completed';
2. EXPLAIN 分析执行计划¶
3. 避免常见性能杀手¶
| 问题 | 影响 | 改进 |
|---|---|---|
SELECT * |
网络传输 + 内存 | 只选需要的列 |
WHERE YEAR(date) = 2024 |
无法用索引 | WHERE date >= '2024-01-01' AND date < '2025-01-01' |
WHERE col LIKE '%keyword%' |
全表扫描 | 用全文索引或 Elasticsearch |
| 大表 JOIN 小表 | 可以接受 | 小表放右边(或让优化器处理) |
| 子查询代替 JOIN | 可能更慢 | 用 CTE (WITH) + JOIN |
4. CTE 让复杂查询可读¶
-- ❌ 嵌套子查询(难读)
SELECT ... FROM (
SELECT ... FROM (
SELECT ... FROM orders WHERE ...
) t1 JOIN users ON ...
) t2 WHERE ...
-- ✅ CTE(逐层构建)
WITH
recent_orders AS (
SELECT * FROM orders WHERE created_at >= '2024-01-01'
),
user_order_stats AS (
SELECT user_id, COUNT(*) as n, SUM(amount) as total
FROM recent_orders
GROUP BY user_id
)
SELECT u.*, s.n, s.total
FROM users u
LEFT JOIN user_order_stats s ON u.user_id = s.user_id;
🔄 SQL ↔ Python 协作¶
import pandas as pd
from sqlalchemy import create_engine
# 连接数据库
engine = create_engine('postgresql://user:pass@localhost:5432/db')
# SQL → DataFrame
df = pd.read_sql("""
SELECT user_id, COUNT(*) as order_count, SUM(amount) as total
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY user_id
""", engine)
# DataFrame → SQL
df.to_sql('user_agg', engine, if_exists='replace', index=False)
# 大查询分批取
for chunk in pd.read_sql("SELECT * FROM large_table", engine, chunksize=50000):
process(chunk)