SQL 入门:产品经理自己取数
会用 SELECT/WHERE/GROUP BY/JOIN 四件套自己捞数,能查新增留存、渠道转化、漏斗各层人数,不背八股会查会改就行
- 理解 SQL 是“跟数据库说话把数据捞出来”
- 掌握 SELECT / WHERE / GROUP BY / JOIN 四件套
- 会写 PM 最常用的三类查询:新增留存、渠道转化、漏斗人数
- 建立“筛选+分类汇总 = Excel 筛选/透视表”的直觉
SQL 到底是干嘛的
SQL 就是用一套固定句式跟数据库说话,把你要的数据从表里捞出来。对产品经理来说,它的直觉和 Excel 一模一样:WHERE 是筛选条件,GROUP BY 是按列分类汇总(就是透视表)。你会用 Excel 筛选和透视表,就已经会了一半 SQL,剩下的只是把它写成数据库听得懂的句子。
高频四件套
SELECT:你想让它把哪几列捞出来。
WHERE:只保留满足条件的行(筛选)。
GROUP BY:按某个维度分组后做汇总,如“每天”“每个渠道”各多少。
JOIN:把两张表按共同字段拼起来,如把行为表和用户表按 user_id 对上。
JOIN 的四种类型
拼表有四种,PM 最常用的是前两种。INNER JOIN:只保留两边都能对上的行(交集)。LEFT JOIN:保留左表全部行,右表对不上的补 NULL。RIGHT JOIN 和 FULL JOIN 用得少,记住「以谁为主表就把谁放左边」就行。举例:想统计所有用户、包括那些从没行为的,就用 LEFT JOIN(左表是用户表);只想看有行为的用户,用 INNER JOIN。
INNER JOIN:两张表都有的才算LEFT JOIN:左表全要,右表对不上补 NULLRIGHT JOIN:右表全要(少用,换成 LEFT 更顺)FULL JOIN:两边全要(更少用)
GROUP BY 与聚合函数
GROUP BY 总是配着聚合函数用:COUNT 数行数、SUM 加总、AVG 平均、MAX/MIN 最大最小。写完 SELECT channel, COUNT(*) FROM events GROUP BY channel,意思就是「按 channel 分组,每组数一下有几行」。记住一个铁律:GROUP BY 后面出现的列,才能出现在 SELECT 里;其他列必须包在聚合函数里,否则报错。
子查询 vs CTE
一个查询套着另一个查询,就是子查询。嵌两层就开始难读了,这时用 CTE(WITH 子句)把内层查询起个名字,主查询里直接引用,可读性立刻上来。功能一模一样,CTE 只是给复杂查询起变量名,让它分层好看。
# CTE 概念示意(WITH 把内层结果命名后再用)
print("""
WITH ch AS (
SELECT channel, COUNT(*) AS cnt
FROM events GROUP BY channel
)
SELECT channel FROM ch WHERE cnt > 1;
""")窗口函数一句话
窗口函数(如 ROW_NUMBER())一句话:分组内给每行编个号,但不合并行。比如「每个渠道下,按时间给用户排个名次」——GROUP BY 会把每组压成一行,ROW_NUMBER 保留每一行并多带一个排名列。PM 了解到这一层够用,真要写再查语法。
一个 JOIN + GROUP BY 的完整例子
把两张表拼起来再分组,是 PM 取数最常见的组合:events 表记行为,users 表记渠道,按渠道统计每个渠道带来了多少付费用户。先用 LEFT JOIN 把两表按 user_id 拼上,再 WHERE 筛出付费事件,最后 GROUP BY 渠道数人数。下面这段可以直接在 py3.12 的 sqlite3 里跑一遍看结果。
import sqlite3
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE users (user_id TEXT, channel TEXT)")
cur.execute("CREATE TABLE events (user_id TEXT, event TEXT)")
cur.executemany("INSERT INTO users VALUES (?,?)", [
("u1", "ads"), ("u2", "ads"), ("u3", "org"), ("u4", "org"),
])
cur.executemany("INSERT INTO events VALUES (?,?)", [
("u1", "pay"), ("u3", "pay"), ("u4", "view"),
])
conn.commit()
sql = """
SELECT u.channel,
COUNT(DISTINCT CASE WHEN e.event = 'pay' THEN u.user_id END) AS pay_users
FROM users u
LEFT JOIN events e ON u.user_id = e.user_id
GROUP BY u.channel
"""
for row in cur.execute(sql):
print(row)
# 预期输出:
# ('ads', 1) -- ads 来了2人,1人付费(u2没付费不算)
# ('org', 1) -- org 来了2人,1人付费(u4只浏览不算)
# 读法:LEFT JOIN 保留所有用户,CASE WHEN 只数付费的那个,
# 没付费的渠道也会出现(pay_users=0),换成 INNER JOIN 就看不到0了写完先看有没有 GROUP BY:分组了却在 SELECT 里放没聚合的列会报错再看 JOIN 类型:以哪张表为主就把它放左边用 LEFT JOIN最后 COUNT 前加 DISTINCT:一个用户多条行为,别数成多个人。
取数结果怎么读:别被一个数唬住
捞出数据之后,PM 最常犯的错是看一个总数就下结论。正确读法是三问:这个数和比(上期/对照组)比是高了还是低了?拆维度看是所有人都这样还是某一个渠道特别差?样本量够不够——一周只有 50 个用户,波动大到根本说明不了问题。一个孤立的数字没有意义,有对比、有拆分、有样本量的数才敢写进周报。
和谁比?没对比的数字等于没数拆开看?总数好可能是某个渠道特别好、其他都在跌多少样本?几十人的波动别当结论PM 取数不是把 SQL 跑通就完事,是读出能指导决策的那句话。
最后留个心态:PM 写 SQL 不要求写出最优雅的查询,能自己捞出要的数、不等数据同学排期,就已经赢过大部分同行。遇到报错别死磕,把报错信息和你想查的业务问题丢给有经验的人问一句,比自己坐那查一小时语法高效得多。
PM 最常用的三类查询
按天看新增/留存:按日期分组,数每天新注册、还回来的人。
按渠道看转化:按来源渠道分组,看哪个渠道进来的人转化最高。
漏斗各层人数:按事件节点分组计数,就是上一课那张漏斗表。
PM 不需要背复杂语法和优化技巧。遇到不会的,查一下、照着改一个能跑的例子,能回答自己那个业务问题就够了。
跑一个小例子(静态示例)
下面用 Python 内置的 sqlite3 建一张迷你用户行为表,再跑一条典型查询。重点看 SQL 长什么样、预期输出是什么。
import sqlite3
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
# 建一张迷你用户行为表(示例口径,教学数据)
cur.execute("""
CREATE TABLE events (
user_id TEXT, channel TEXT, event TEXT, dt TEXT
)""")
cur.executemany("INSERT INTO events VALUES (?,?,?,?)", [
("u1", "ads", "曝光", "2026-09-01"),
("u2", "ads", "曝光", "2026-09-01"),
("u3", "ads", "点击", "2026-09-01"),
("u4", "org", "曝光", "2026-09-01"),
("u5", "org", "点击", "2026-09-01"),
])
conn.commit()
# 典型 PM 查询:按渠道看每个事件各有多少人(筛选+分组汇总)
sql = """
SELECT channel, event, COUNT(*) AS cnt
FROM events
WHERE dt = '2026-09-01'
GROUP BY channel, event
"""
for row in cur.execute(sql):
print(row)
# 预期输出:
# ('ads', '曝光', 2)
# ('ads', '点击', 1)
# ('org', '曝光', 1)
# ('org', '点击', 1)
# 读法:ads 渠道曝光2、点击1;org 曝光1、点击1资深工程师加餐
底层原理 · 大厂视角 · 工程经验,点卡片展开
上线前要建一套 golden set:覆盖典型问法、边界情况和历史坏案例,从正确性、相关性、有害率、延迟、成本多个维度打分。之后每次改 prompt、换模型、调参数都要在同一套集子上回归对比。没有评测集的「迭代」本质是凭感觉,今天变好、明天悄悄变差都无从知晓。