Pandas 很强大,但现实世界中大部分数据都存放在关系型数据库里。无论你是从数据仓库取数、查询业务系统,还是做报表,SQL 都是数据分析师最核心的技能之一。

这篇文章不讲 DBA 那一套(建表、索引优化、事务管理),而是聚焦于数据分析师最常用的 SQL 操作,并用 SQLite 作为实践环境,配合 Python 打通数据链路。

环境准备

import sqlite3
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 创建内存数据库
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

我们创建三张模拟的业务表:

# 用户表
cursor.execute('''
CREATE TABLE users (
    user_id INTEGER PRIMARY KEY,
    name TEXT,
    age INTEGER,
    city TEXT,
    signup_date TEXT
)
''')

# 订单表
cursor.execute('''
CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    user_id INTEGER,
    product TEXT,
    amount REAL,
    quantity INTEGER,
    order_date TEXT,
    status TEXT
)
''')

# 产品表
cursor.execute('''
CREATE TABLE products (
    product_id INTEGER PRIMARY KEY,
    product_name TEXT,
    category TEXT,
    price REAL,
    stock INTEGER
)
''')

插入模拟数据:

import random
from datetime import datetime, timedelta

# 插入用户
users = []
for i in range(1, 101):
    users.append((
        i,
        f'用户{i}',
        random.randint(18, 65),
        random.choice(['北京', '上海', '广州', '深圳', '杭州', '成都']),
        f'2024-{random.randint(1,12):02d}-{random.randint(1,28):02d}'
    ))
cursor.executemany('INSERT INTO users VALUES (?,?,?,?,?)', users)

# 插入产品
products = [
    (1, '笔记本电脑', '电子产品', 5999, 100),
    (2, '手机', '电子产品', 3999, 200),
    (3, '耳机', '电子产品', 299, 500),
    (4, '运动鞋', '服饰', 599, 300),
    (5, 'T恤', '服饰', 99, 1000),
    (6, '咖啡机', '家居', 1299, 50),
    (7, '台灯', '家居', 199, 200),
]
cursor.executemany('INSERT INTO products VALUES (?,?,?,?,?)', products)

# 插入订单
orders = []
for i in range(1, 501):
    user_id = random.randint(1, 100)
    product_id = random.randint(1, 7)
    product_name = ['笔记本电脑', '手机', '耳机', '运动鞋', 'T恤', '咖啡机', '台灯'][product_id-1]
    price = [5999, 3999, 299, 599, 99, 1299, 199][product_id-1]
    qty = random.randint(1, 3)
    days_ago = random.randint(0, 365)
    order_date = (datetime(2024, 1, 1) + timedelta(days=days_ago)).strftime('%Y-%m-%d')
    
    orders.append((
        i, user_id, product_name, price * qty,
        qty, order_date,
        random.choice(['已完成', '已完成', '已完成', '已取消', '退款中'])
    ))
cursor.executemany('INSERT INTO orders VALUES (?,?,?,?,?,?,?)', orders)

conn.commit()

SQL 基础查询回顾

最常用的 SELECT 语句组合:

pd.read_sql('''
SELECT * FROM orders
WHERE status = '已完成'
  AND amount > 500
  AND order_date >= '2024-06-01'
ORDER BY amount DESC
LIMIT 10
''', conn)

聚合与分组

数据分析的核心是汇总和对比

# 基础聚合
pd.read_sql('''
SELECT 
    COUNT(*) AS 订单总数,
    SUM(amount) AS 总金额,
    AVG(amount) AS 平均金额,
    MIN(amount) AS 最小订单,
    MAX(amount) AS 最大订单,
    COUNT(DISTINCT user_id) AS 下单用户数
FROM orders
WHERE status = '已完成'
''', conn)

分组聚合是 SQL 分析的精髓:

# 按产品分组
pd.read_sql('''
SELECT 
    product,
    COUNT(*) AS 订单数,
    SUM(amount) AS 销售额,
    ROUND(AVG(amount), 2) AS 均价,
    SUM(quantity) AS 销量
FROM orders
WHERE status = '已完成'
GROUP BY product
ORDER BY 销售额 DESC
''', conn)
# 多维度分组
pd.read_sql('''
SELECT 
    strftime('%m', order_date) AS 月份,
    product,
    COUNT(*) AS 订单数,
    SUM(amount) AS 销售额
FROM orders
WHERE status = '已完成'
GROUP BY 月份, product
ORDER BY 月份, 销售额 DESC
''', conn)

HAVING 过滤聚合结果:

pd.read_sql('''
SELECT 
    user_id,
    COUNT(*) AS 订单数,
    SUM(amount) AS 总消费
FROM orders
WHERE status = '已完成'
GROUP BY user_id
HAVING 总消费 > 10000
ORDER BY 总消费 DESC
''', conn)

JOIN 连接表

真实分析几乎从不只查一张表。

# INNER JOIN
pd.read_sql('''
SELECT 
    o.order_id,
    u.name,
    u.city,
    o.product,
    o.amount,
    o.order_date
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
WHERE o.status = '已完成'
LIMIT 10
''', conn)

不同 JOIN 的区别:

# 用户与其订单(包括从未下单的用户)
pd.read_sql('''
SELECT 
    u.user_id,
    u.name,
    COUNT(o.order_id) AS 订单数,
    COALESCE(SUM(o.amount), 0) AS 总消费
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id AND o.status = '已完成'
GROUP BY u.user_id
ORDER BY 总消费 DESC
LIMIT 10
''', conn)
# 每个用户最近一次订单(用 LEFT JOIN 的技巧)
pd.read_sql('''
SELECT 
    u.user_id,
    u.name,
    o.product,
    o.amount,
    o.order_date
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
LEFT JOIN orders o2 ON u.user_id = o2.user_id AND o.order_date < o2.order_date
WHERE o2.order_id IS NULL
  AND o.status = '已完成'
ORDER BY u.user_id
LIMIT 15
''', conn)

子查询

# 查询消费高于平均水平的用户
pd.read_sql('''
SELECT 
    u.name,
    u.city,
    user_total.总消费
FROM (
    SELECT 
        user_id,
        SUM(amount) AS 总消费
    FROM orders
    WHERE status = '已完成'
    GROUP BY user_id
) user_total
JOIN users u ON user_total.user_id = u.user_id
WHERE user_total.总消费 > (
    SELECT AVG(总消费) FROM (
        SELECT SUM(amount) AS 总消费
        FROM orders
        WHERE status = '已完成'
        GROUP BY user_id
    )
)
ORDER BY user_total.总消费 DESC
''', conn)
# 每个城市消费最高的用户
pd.read_sql('''
SELECT 
    u.city,
    u.name,
    t.总消费
FROM (
    SELECT 
        user_id,
        SUM(amount) AS 总消费,
        RANK() OVER (PARTITION BY u2.city ORDER BY SUM(amount) DESC) AS rnk
    FROM orders o
    JOIN users u2 ON o.user_id = u2.user_id
    WHERE o.status = '已完成'
    GROUP BY o.user_id
) t
JOIN users u ON t.user_id = u.user_id
WHERE t.rnk = 1
ORDER BY t.总消费 DESC
''', conn)

WITH 子句(CTE)

CTE 让复杂查询变得像流水线一样清晰:

pd.read_sql('''
WITH 用户消费 AS (
    SELECT 
        user_id,
        COUNT(*) AS 订单数,
        SUM(amount) AS 总消费,
        AVG(amount) AS 客单价
    FROM orders
    WHERE status = '已完成'
    GROUP BY user_id
),
用户分层 AS (
    SELECT 
        user_id,
        CASE 
            WHEN 总消费 > 30000 THEN '高价值'
            WHEN 总消费 > 10000 THEN '中价值'
            ELSE '低价值'
        END AS 层级
    FROM 用户消费
)
SELECT 
    l.层级,
    COUNT(*) AS 用户数,
    ROUND(AVG(c.总消费), 2) AS 人均消费,
    ROUND(AVG(c.订单数), 2) AS 人均订单
FROM 用户分层 l
JOIN 用户消费 c ON l.user_id = c.user_id
GROUP BY l.层级
ORDER BY AVG(c.总消费) DESC
''', conn)

窗口函数

窗口函数是进阶 SQL 分析中最重要的技能。它能在一行上看到上下文中的其他行。

ROW_NUMBER / RANK / DENSE_RANK

# 每个产品类别的销量排名
pd.read_sql('''
SELECT 
    product,
    SUM(quantity) AS 销量,
    RANK() OVER (ORDER BY SUM(quantity) DESC) AS 总排名,
    ROW_NUMBER() OVER (PARTITION BY '整体' ORDER BY SUM(quantity) DESC) AS 序号
FROM orders
WHERE status = '已完成'
GROUP BY product
''', conn)

LAG / LEAD

# 月度销售额与环比增长
pd.read_sql('''
WITH 月度 AS (
    SELECT 
        strftime('%Y-%m', order_date) AS 月份,
        SUM(amount) AS 销售额
    FROM orders
    WHERE status = '已完成'
    GROUP BY 月份
)
SELECT 
    月份,
    销售额,
    LAG(销售额) OVER (ORDER BY 月份) AS 上月销售额,
    ROUND((销售额 - LAG(销售额) OVER (ORDER BY 月份)) / 
          LAG(销售额) OVER (ORDER BY 月份) * 100, 2) AS 环比增长率
FROM 月度
ORDER BY 月份
''', conn)

移动窗口函数

# 7 日移动平均销售额
pd.read_sql('''
WITH 每日 AS (
    SELECT 
        order_date,
        SUM(amount) AS 日销售额
    FROM orders
    WHERE status = '已完成'
    GROUP BY order_date
)
SELECT 
    order_date,
    日销售额,
    ROUND(AVG(日销售额) OVER (
        ORDER BY order_date 
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ), 2) AS 七日移动平均
FROM 每日
ORDER BY order_date
LIMIT 20
''', conn)

SUM OVER PARTITION

# 累计销售额
pd.read_sql('''
WITH 每日 AS (
    SELECT 
        order_date,
        SUM(amount) AS 日销售额
    FROM orders
    WHERE status = '已完成'
    GROUP BY order_date
)
SELECT 
    order_date,
    日销售额,
    ROUND(SUM(日销售额) OVER (ORDER BY order_date), 2) AS 累计销售额
FROM 每日
ORDER BY order_date
LIMIT 15
''', conn)

日期与字符串处理

# 日期函数
pd.read_sql('''
SELECT 
    order_date,
    strftime('%Y', order_date) AS 年,
    strftime('%m', order_date) AS 月,
    strftime('%w', order_date) AS 星期几,  -- 0=周日
    CASE CAST(strftime('%w', order_date) AS INTEGER)
        WHEN 0 THEN '周日'
        WHEN 6 THEN '周六'
        ELSE '工作日'
    END AS 日类型,
    strftime('%Y-%m', order_date) AS 年月,
    julianday('2024-12-31') - julianday(order_date) AS 距年底天数
FROM orders
LIMIT 10
''', conn)
# 字符串函数
pd.read_sql('''
SELECT 
    name,
    LENGTH(name) AS 姓名长度,
    SUBSTR(name, 1, 1) AS 首字符,
    UPPER(name) AS 大写,
    REPLACE(city, '北京', 'Beijing') AS 城市双语,
    '【' || city || '】' || name AS 完整名称
FROM users
LIMIT 10
''', conn)

性能优化基础

当数据量达到百万级时,SQL 查询性能就变得重要了。

使用 EXPLAIN 分析查询计划

cursor.execute('EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 42')
for row in cursor.fetchall():
    print(row)

输出会显示全表扫描还是索引查找。全表扫描 = 危险信号。

创建索引

# 为常用查询字段创建索引
cursor.execute('CREATE INDEX idx_orders_user_id ON orders(user_id)')
cursor.execute('CREATE INDEX idx_orders_date ON orders(order_date)')
cursor.execute('CREATE INDEX idx_orders_status ON orders(status)')
cursor.execute('CREATE INDEX idx_orders_user_status ON orders(user_id, status)')

# 再 EXPLAIN 一次,应该变成索引查找
cursor.execute('EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 42')
for row in cursor.fetchall():
    print(row)
# 索引效果对比
import time

# 无索引查询(先删除索引做对比)
cursor.execute('DROP INDEX IF EXISTS idx_orders_user_id')
start = time.time()
cursor.execute('SELECT * FROM orders WHERE user_id = 42')
no_index = time.time() - start

# 重建索引
cursor.execute('CREATE INDEX idx_orders_user_id ON orders(user_id)')
start = time.time()
cursor.execute('SELECT * FROM orders WHERE user_id = 42')
with_index = time.time() - start

print(f'无索引: {no_index*1000:.2f}ms')
print(f'有索引: {with_index*1000:.2f}ms')
print(f'加速比: {no_index/with_index:.1f}x')

性能优化原则

优化建议 = '''
1. SELECT 只查需要的列,不要 SELECT *
2. WHERE 条件用索引列
3. JOIN 的关联列要有索引
4. 避免在 WHERE 中对列做函数运算(WHERE YEAR(date)=2024 不如 WHERE date>='2024-01-01')
5. 大表分页用游标,不用 OFFSET
6. 聚合前先用 WHERE 过滤掉不需要的行
7. 小表驱动大表
'''
print(优化建议)

Python 与 SQL 的混合使用

真实项目中,SQL 和 Python 是互补的:

# SQL 负责取数和聚合,Python 负责分析和可视化
query = '''
SELECT 
    u.city,
    COUNT(DISTINCT o.user_id) AS 活跃用户数,
    SUM(o.amount) AS 总消费,
    AVG(o.amount) AS 客单价
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.status = '已完成'
GROUP BY u.city
ORDER BY 总消费 DESC
'''

df_city = pd.read_sql(query, conn)

# 用 Python 做后续分析
df_city['人均消费'] = df_city['总消费'] / df_city['活跃用户数']

plt.figure(figsize=(10, 5))
sns.barplot(data=df_city, x='city', y='总消费')
plt.title('各城市消费情况')
plt.show()

print(df_city.sort_values('总消费', ascending=False))

用 SQLAlchemy 管理更复杂的连接:

from sqlalchemy import create_engine

# 创建 SQLAlchemy 引擎(支持 PostgreSQL, MySQL, SQLite 等)
engine = create_engine('sqlite:///analysis.db')

# 直接用 pandas 读写
df = pd.read_sql('SELECT * FROM orders', engine)

# 把 DataFrame 写入数据库
df_agg = df.groupby('product').agg(
    订单数=('order_id', 'count'),
    销售额=('amount', 'sum')
).reset_index()

df_agg.to_sql('product_summary', engine, if_exists='replace', index=False)

pd.read_sql('SELECT * FROM product_summary ORDER BY 销售额 DESC', engine)

实战:RFM 用户分析

RFM(Recency, Frequency, Monetary)是经典的用户分层模型:

rfm_query = '''
WITH user_stats AS (
    SELECT 
        o.user_id,
        u.name,
        u.city,
        julianday('2024-12-31') - julianday(MAX(o.order_date)) AS recency,
        COUNT(DISTINCT o.order_id) AS frequency,
        SUM(o.amount) AS monetary
    FROM orders o
    JOIN users u ON o.user_id = u.user_id
    WHERE o.status = '已完成'
    GROUP BY o.user_id
),
rfm_scores AS (
    SELECT *,
        NTILE(5) OVER (ORDER BY recency DESC) AS R_score,  -- 越近分越高
        NTILE(5) OVER (ORDER BY frequency) AS F_score,
        NTILE(5) OVER (ORDER BY monetary) AS M_score
    FROM user_stats
)
SELECT *,
    R_score || F_score || M_score AS rfm_code,
    CASE 
        WHEN R_score >= 4 AND F_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 AND M_score <= 2 THEN '一般流失'
        ELSE '一般用户'
    END AS 用户分层
FROM rfm_scores
ORDER BY monetary DESC
LIMIT 20
'''

df_rfm = pd.read_sql(rfm_query, conn)

# 可视化
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
axes[0].hist(df_rfm['recency'], bins=20)
axes[0].set_title('Recency 分布')
axes[1].hist(df_rfm['frequency'], bins=20)
axes[1].set_title('Frequency 分布')
axes[2].hist(df_rfm['monetary'], bins=20)
axes[2].set_title('Monetary 分布')
plt.tight_layout()
plt.show()

# 用户分层统计
print(df_rfm['用户分层'].value_counts())

SQL 学习路线图

初级(日常取数)
├── SELECT / WHERE / ORDER BY / LIMIT
├── COUNT / SUM / AVG / GROUP BY / HAVING
├── JOIN(INNER / LEFT)
└── 子查询

中级(分析工作)
├── 窗口函数(RANK / LAG / SUM OVER)
├── WITH(CTE)重构复杂查询
├── CASE WHEN 条件聚合
├── 日期与字符串函数
└── UNION / UNION ALL

高级(性能与优化)
├── 索引设计与查询计划
├── 分区表处理百万级数据
├── 窗口框架(ROWS/RANGE/GROUPS)
├── 递归 CTE
└── 物化视图

小结

SQL 是数据分析师的"母语"。不管业务需求多复杂,最终都要落地为一条 SQL 查询。掌握本文的内容,你已经能胜任绝大多数分析场景下的 SQL 需求:

  • 用 JOIN 关联多张表,得到完整的业务视图
  • 用 GROUP BY 做多维度聚合分析
  • 用窗口函数计算排名、同环比、累计值
  • 用 CTE 组织复杂查询,让逻辑清晰可读
  • 用索引优化查询性能
  • 用 Python + SQL 形成完整的工作流

最后一篇文章,我们来处理一类特殊但常见的数据——时间序列。

Summary: SQL 聚合、JOIN、窗口函数与 Python 集成实战。