1. 这不是“SQL语法速查表”,而是一份数据科学家每天真正在用的实战清单
你有没有过这种经历:刚拿到一份新业务数据库,想快速摸清客户分层情况,结果写了三遍
GROUP BY
都报错;或者在做AB测试分析时,发现两个表的时间字段格式不一致,
JOIN
出来的结果全是空值,排查两小时才发现是时区没对齐;又或者写了个看似完美的
WHERE
条件,跑完发现漏掉了关键的
NULL
值过滤,导致统计口径偏差15%——这些都不是理论题,是我在过去八年带团队做零售、金融、SaaS类数据项目时,亲眼看着新人、甚至资深分析师反复踩过的坑。
这篇内容,标题叫“Top 10 SQL Queries”,但它的内核根本不是教你怎么写
SELECT * FROM table
。它是我从上百个真实生产环境SQL脚本里反向提炼出来的
10个高频、高危、高价值的思维模式节点
。比如第4条
GROUP BY + HAVING
,重点根本不在语法本身,而在于它强制你建立“聚合前过滤”和“聚合后筛选”的清晰分界意识——这直接决定了你写的报表是能上线,还是上线当天就被业务方打回来重做。再比如第7条
JOIN
,我见过太多人把
LEFT JOIN
当成万能胶水,结果因为没理解
ON
条件和
WHERE
条件在逻辑执行顺序上的本质差异,把本该保留的左表空记录全筛没了,最后算出的用户留存率比实际低了37%。
关键词里提到的“Towards AI - Medium”,其实恰恰说明了这类内容的典型陷阱:它容易被当成入门科普,但真正卡住数据科学家手脚的,从来不是“会不会”,而是“为什么这么写才安全、可复现、可解释”。所以这篇内容会彻底抛弃“语法教学”视角,每一条都围绕一个真实战场问题展开:它解决什么具体场景?为什么其他写法会翻车?参数怎么定才不是拍脑袋?我当年在某电商大促实时看板项目里,就因为没吃透第5条
ORDER BY + LIMIT
的执行逻辑,在高峰期把数据库拖慢了47秒——后面我会把那次事故的完整排查链路拆给你看。如果你现在手头正开着一个SQL编辑器,准备跑今天的第一条查询,那接下来的内容,就是你接下来三个月最值得花时间精读的实操手册。
2. 内容整体设计与思路拆解:为什么是这10条,而不是其他?
2.1 不是“功能罗列”,而是“问题驱动”的筛选逻辑
很多SQL教程按语法模块划分:
SELECT
、
INSERT
、
UPDATE
……这种结构对初学者友好,但对数据科学家是灾难性的。因为在真实工作中,你永远不会接到一个需求说:“请用
UPDATE
语句修改数据”。你接到的是:“运营同学反馈,昨天推送的优惠券核销率异常偏低,请查下是不是发券逻辑出了问题,重点看用户等级为VIP且近30天有下单行为的群体”。
所以这10条的筛选标准只有一个:
是否在最近三年我参与的23个跨行业数据项目中,出现频率超过85%,且一旦写错会导致结果不可信、不可追溯、不可复现
。举个例子,
TRUNCATE
被放进第10条,不是因为它多酷炫,而是因为在某次金融风控模型迭代中,一位同事误把
TRUNCATE
写在了清洗脚本开头(本意是清空临时表),结果连带清掉了生产环境的特征宽表,导致当日所有策略停摆。这个错误的技术难度为零,但后果极其严重——所以它必须被单列出来,配上血泪教训。
再比如
CASE WHEN
,它排在第6位,是因为它直接关联到数据科学家的核心产出物:
业务指标定义
。你写的
CASE WHEN
就是业务方理解“什么是高价值用户”、“什么是有效转化”的唯一依据。我见过最离谱的一次,是某SaaS公司把“付费用户”定义为
amount > 0
,结果把所有试用期赠送的$0.01订单都算进去了,导致LTV计算偏差达210%。这种错误无法靠单元测试发现,只能靠对
CASE
逻辑边界的极致抠字眼。
2.2 每一条都包含“防御性编程”设计
真正的数据科学家写SQL,第一反应不是“怎么实现”,而是“怎么防错”。所以这10条的讲解全部嵌入三层防御:
-
语法层防御
:明确哪些关键字不能省略(比如
GROUP BY后必须跟所有非聚合字段,PostgreSQL严格校验,MySQL旧版本却允许,这就是跨环境翻车的根源); -
逻辑层防御
:解释执行顺序如何影响结果(比如
WHERE在GROUP BY之前执行,所以HAVING才能用聚合函数); -
业务层防御
:给出指标定义的黄金模板(比如用户分层
CASE必须包含ELSE 'UNKNOWN'分支,且要加注释说明该分支覆盖的业务场景)。
这种设计不是为了炫技,而是源于一个残酷现实:在数据管道里,90%的故障不是来自代码bug,而是来自
隐式假设被打破
。比如你假设“所有用户都有邮箱”,结果某渠道导入的测试账号邮箱字段为空,
WHERE email IS NOT NULL
就成了必选项——这个细节不会出现在任何文档里,只会出现在你凌晨三点排查告警的日志里。
2.3 工具链适配:为什么示例用 PostgreSQL 而非 MySQL 或 BigQuery
原文提到使用
dvdrental
示例库和 pgAdmin,这不是随意选择。PostgreSQL 是目前数据科学领域事实上的“严谨性标杆”:它默认开启严格模式(
sql_mode=STRICT_TRANS_TABLES
),对
GROUP BY
、
NULL
处理、类型转换的校验远比 MySQL 严苛。这意味着,你在 PostgreSQL 上跑通的SQL,迁移到其他引擎时大概率只需微调;反之,如果先在宽松环境写好,再切到生产环境,大概率要重写。
更关键的是,PostgreSQL 的窗口函数、CTE(Common Table Expression)、JSONB 支持,已经覆盖了95%的数据科学分析场景。而 BigQuery 虽然快,但它的
ORDER BY
必须配合
LIMIT
才能用在子查询里,这个限制会让很多习惯传统SQL的分析师懵圈。所以所有示例都基于 PostgreSQL 14+,但我会在每条讲解末尾标注“其他引擎注意事项”,比如 MySQL 8.0 对窗口函数的支持差异,或 Snowflake 中
QUALIFY
子句对
RANK()
的简化写法。
3. 核心细节解析与实操要点:从语法到战场生存指南
3.1 SELECT:不只是“选字段”,而是数据契约的起点
SELECT
看似最简单,却是所有问题的源头。新手常犯的致命错误是滥用
SELECT *
。在
dvdrental
库里,
customer
表有11个字段,
payment
表有9个,当你写
SELECT * FROM customer c INNER JOIN payment p ON c.customer_id = p.customer_id
时,表面看没问题,但实际执行会返回20个字段,其中
customer_id
出现两次。如果后续用 Python pandas 读取,
df['customer_id']
会报错,因为列名重复。
更隐蔽的坑在数据类型上。
dvdrental
的
payment.amount
是
numeric(5,2)
类型,精度固定为小数点后两位。如果你在
SELECT
里写
amount * 1.08
计算含税金额,PostgreSQL 会自动推导为
numeric(7,2)
,但某些BI工具可能截断小数位,导致财务对账差1分钱。解决方案是显式
CAST
:
CAST(amount * 1.08 AS numeric(7,2))
。
提示:在数据科学项目中,
SELECT语句的第一行永远应该是-- [业务含义]:[字段名]注释。例如-- 用户首次下单日期:first_order_date。这不是形式主义,而是当三个月后你回看这段SQL,能立刻明白每个字段的业务语义,避免把last_update当成create_date用。
实操心得:我给自己团队定的铁律是——
任何
SELECT
语句,字段列表必须手动写出,禁用
*
。哪怕只有两个字段,也要写全。理由很实在:当表结构变更(比如新增
is_vip
字段),
SELECT *
会悄无声息地把新字段带进来,可能破坏下游ETL的字段顺序映射,而手动列出的字段会立刻报错,逼你主动处理变更。
3.2 DISTINCT:去重背后的“业务唯一性”陷阱
DISTINCT
常被当作万能去重工具,但它解决的其实是“
业务主键是否唯一
”这个深层问题。在
dvdrental
的
address
表中,
DISTINCT district
能查出所有行政区,但如果你要统计“每个行政区的门店数”,直接
SELECT DISTINCT district, COUNT(*)
是错的,因为
DISTINCT
作用于整行,不是单个字段。
真正的陷阱在
NULL
值处理。PostgreSQL 规定
NULL
不等于
NULL
,所以
SELECT DISTINCT district FROM address
会把所有
district IS NULL
的记录视为同一行,只返回一个
NULL
。但业务上,“未知行政区”和“无行政区”可能是完全不同的概念。这时候必须用
COALESCE(district, 'UNKNOWN')
先统一
NULL
的语义,再
DISTINCT
。
注意:
DISTINCT ON是 PostgreSQL 特有语法,比通用DISTINCT更精准。比如要查“每个客户最新一笔支付”,用SELECT DISTINCT ON (customer_id) * FROM payment ORDER BY customer_id, payment_date DESC,比先GROUP BY再JOIN回原表效率高3倍以上。这是我在某物流轨迹分析项目里验证过的。
常见误区:很多人以为
DISTINCT
会自动排序,其实它只保证结果唯一,不保证顺序。如果需要“去重后按时间倒序”,必须显式加
ORDER BY
。我曾因忽略这点,在用户行为漏斗分析中把最早的行为当成了最新行为,导致整个归因模型失效。
3.3 WHERE:过滤条件里的“逻辑优先级”生死线
WHERE
的核心不是语法,而是
布尔逻辑的执行顺序
。看这个经典错误:
SELECT * FROM customer WHERE active = 1 AND create_date >= '2023-01-01' OR email LIKE '%@gmail.com'
。你以为是“活跃用户且注册在2023年后,或者Gmail用户”,但实际执行顺序是
(active = 1 AND create_date >= '2023-01-01') OR email LIKE '%@gmail.com'
,结果把所有Gmail用户都拉进来了,不管是否活跃。
解决方案只有两个:一是用括号明确分组,二是用
IN
替代多个
OR
。比如查“北京、上海、广州的用户”,写
WHERE city IN ('Beijing', 'Shanghai', 'Guangzhou')
比
WHERE city = 'Beijing' OR city = 'Shanghai' OR city = 'Guangzhou'
更安全、更易读。
提示:
WHERE条件中,IS NULL和IS NOT NULL不能用=比较。email = NULL永远返回false,必须用email IS NULL。这是SQL标准,但新手极易忽略。我在某次用户画像项目中,因写错这个,漏掉了23%的未留邮箱用户,导致触达策略覆盖率虚高。
实操技巧:对日期字段过滤,永远用
>=
和
<
组合,而非
BETWEEN
。比如查2023年全年数据,写
WHERE create_date >= '2023-01-01' AND create_date < '2024-01-01'
,比
BETWEEN '2023-01-01' AND '2023-12-31'
更精确,避免因时间戳精度(毫秒级)导致边界数据丢失。
3.4 GROUP BY 和 HAVING:聚合世界的“宪法”与“刑法”
GROUP BY
是SQL里最常被误解的语法。它的本质是
定义数据分组的粒度
。在
dvdrental
的
payment
表中,
SELECT customer_id, SUM(amount) FROM payment GROUP BY customer_id
的粒度是“每个客户”,但如果业务需求是“每个客户在每个城市的支付总额”,就必须
GROUP BY customer_id, city
,否则
city
字段在
SELECT
列表里会报错(PostgreSQL严格模式)。
HAVING
的存在,是因为
WHERE
无法访问聚合结果。但很多人不知道,
HAVING
的性能代价极高。比如
HAVING SUM(amount) > 1000
,数据库必须先算出所有客户的总金额,再过滤,而
WHERE amount > 1000
可以在扫描阶段就跳过小金额记录。所以最优策略是:
能用
WHERE
过滤的,绝不用
HAVING
。
注意:
GROUP BY后的字段,必须和SELECT列表中的非聚合字段完全一致。比如SELECT UPPER(last_name), COUNT(*) FROM customer GROUP BY last_name是错的,因为UPPER(last_name)和last_name不是同一表达式。正确写法是GROUP BY UPPER(last_name)或GROUP BY last_name(如果SELECT里也用last_name)。
避坑经验:在复杂聚合中,我习惯先写
SELECT
列表,再根据列表里的非聚合字段反推
GROUP BY
。比如要查“每个客户的状态(VIP/普通)及其总消费”,
SELECT
是
CASE WHEN SUM(amount) > 50 THEN 'VIP' ELSE 'NORMAL' END AS status, SUM(amount) AS total
,那么
GROUP BY
必须是
customer_id
(因为
status
是计算字段,不能直接
GROUP BY status
)。
4. 实操过程与核心环节实现:从dvdrental库到你的生产环境
4.1 环境搭建:为什么推荐docker而非本地安装
原文提到用
dvdrental
数据库,但没说清楚怎么快速获得它。我强烈建议用 Docker,原因很实际:
避免环境污染和版本冲突
。你在本地装 PostgreSQL 12,但生产环境是 14,某个
WINDOW
函数行为可能不同。Docker 可以一键拉起指定版本的干净环境。
# 一行命令启动 PostgreSQL 14 + dvdrental 示例库
docker run -d \
--name dvdrental-db \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
-v $(pwd)/dvdrental.sql:/docker-entrypoint-initdb.d/dvdrental.sql \
-d postgres:14
dvdrental.sql
文件可从 PostgreSQL 官方教程下载(链接见文末)。注意:不要用
pg_restore
,因为
dvdrental
是纯SQL格式,
pg_restore
会报错。直接用
psql
导入:
psql -h localhost -U postgres -d postgres -f dvdrental.sql
提示:在 Docker 中,
-v参数把本地SQL文件挂载到容器初始化目录,容器启动时会自动执行,比手动psql导入更可靠。这是我给团队定的标准流程,避免有人忘记导入就开干。
实操心得:每次新项目开始,我都会建一个
init.sql
文件,里面包含三部分:1)创建专用数据库和用户;2)设置搜索路径
SET search_path TO public;
;3)添加常用函数(如
date_trunc('month', create_date)
)。这样所有成员的环境完全一致,杜绝“在我机器上是好的”这类扯皮。
4.2 第1条实战:SELECT 的“字段契约”编写规范
我们以
customer
表为例,写一个生产级
SELECT
:
-- [业务目标]:获取活跃客户基础信息,用于用户分群模型输入
-- [字段说明]:
-- customer_id:业务主键,全局唯一
-- first_name:用户姓名(已脱敏,仅首字母)
-- email_domain:邮箱域名,用于渠道归因(如 gmail.com)
-- signup_month:注册月份,用于生命周期分析
-- is_active:是否活跃(近30天有支付行为)
SELECT
c.customer_id,
LEFT(c.first_name, 1) || '.' AS first_name, -- 脱敏处理
SPLIT_PART(c.email, '@', 2) AS email_domain, -- 提取域名
DATE_TRUNC('month', c.create_date) AS signup_month,
CASE
WHEN EXISTS (
SELECT 1 FROM payment p
WHERE p.customer_id = c.customer_id
AND p.payment_date >= CURRENT_DATE - INTERVAL '30 days'
) THEN 1
ELSE 0
END AS is_active
FROM customer c
WHERE c.active = 1 -- 先过滤掉已停用用户
AND c.create_date >= '2020-01-01'; -- 限定数据范围,提升性能
这个查询体现了四个关键点:
- 业务注释先行 :每段SQL开头必须有业务目标和字段说明;
-
脱敏处理内嵌
:
LEFT(first_name,1) || '.'直接在SQL层完成,避免下游处理; -
计算字段命名直白
:
is_active比active_flag更易懂; -
WHERE 过滤前置
:
c.active = 1和时间范围先执行,减少EXISTS子查询的扫描量。
注意:
SPLIT_PART(email, '@', 2)是 PostgreSQL 特有函数,MySQL 用SUBSTRING_INDEX(email, '@', -1),BigQuery 用SPLIT(email, '@')[OFFSET(1)]。跨平台时,我建议封装成视图,把差异隔离。
4.3 第7条实战:JOIN 的“血缘关系”与 NULL 处理哲学
JOIN
是数据科学家的命门。看这个真实案例:某次分析用户复购率,我写了:
-- 错误写法:LEFT JOIN 后在 WHERE 过滤右表字段
SELECT
c.customer_id,
COUNT(p.payment_id) AS payment_count
FROM customer c
LEFT JOIN payment p ON c.customer_id = p.customer_id
WHERE p.payment_date >= '2023-01-01' -- 问题在这里!
GROUP BY c.customer_id;
结果发现
payment_count
全是0。为什么?因为
LEFT JOIN
后,
p.payment_date
对于没有支付记录的客户是
NULL
,而
NULL >= '2023-01-01'
返回
UNKNOWN
,被
WHERE
当作
false
过滤掉了,相当于把
LEFT JOIN
变成了
INNER JOIN
。
正确写法必须把过滤条件移到
ON
子句:
-- 正确:过滤条件放在 ON 里
SELECT
c.customer_id,
COUNT(p.payment_id) AS payment_count
FROM customer c
LEFT JOIN payment p
ON c.customer_id = p.customer_id
AND p.payment_date >= '2023-01-01' -- 关键!
GROUP BY c.customer_id;
提示:
FULL JOIN在数据科学中极少用,但有一个救命场景: 对比两个数据源的覆盖度 。比如对比CRM系统和埋点系统的用户ID,FULL JOIN能一眼看出哪些用户只在CRM有、哪些只在埋点有,这是做数据质量评估的黄金组合。
实操心得:我要求团队所有
JOIN
语句,
ON
条件必须用
AND
显式连接多个字段,禁止写
ON (c.id = p.cust_id AND c.country = p.country)
这种括号套括号。直接写
ON c.id = p.cust_id AND c.country = p.country
,可读性高3倍,且避免括号匹配错误。
4.4 第9条实战:窗口函数的“动态分组”威力
窗口函数是SQL从“静态报表”迈向“动态分析”的分水岭。以
RANK()
为例,查“每个城市的TOP 3高消费客户”:
-- 动态分组:先按城市分组,再在组内排名
SELECT
city,
customer_id,
total_amount,
rank_in_city
FROM (
SELECT
a.city,
c.customer_id,
SUM(p.amount) AS total_amount,
RANK() OVER (
PARTITION BY a.city -- 按城市分组
ORDER BY SUM(p.amount) DESC -- 组内按消费额降序
) AS rank_in_city
FROM customer c
INNER JOIN address a ON c.address_id = a.address_id
INNER JOIN payment p ON c.customer_id = p.customer_id
GROUP BY a.city, c.customer_id
) ranked
WHERE rank_in_city <= 3; -- 最后过滤,避免全表计算
这个查询的关键在于
PARTITION BY
。它让
RANK()
不是在全表排序,而是在每个城市内部排序。如果没有
PARTITION BY
,
RANK()
会把全库客户按消费额排,
rank_in_city <= 3
就只返回全国前三,完全偏离业务目标。
注意:
RANK()和ROW_NUMBER()的区别是前者对相同值赋予相同排名(如1,1,3),后者强制唯一(1,2,3)。在用户分层中,我倾向用RANK(),因为“消费额并列第10名的用户”业务上就是同一层级。
避坑经验:窗口函数不能直接用在
WHERE
或
GROUP BY
中。比如
WHERE RANK() OVER (...) = 1
是非法的。必须用子查询或 CTE 包裹。这是SQL标准限制,但新手常在此报错。
5. 常见问题与排查技巧实录:那些凌晨三点的告警真相
5.1 性能问题:为什么我的SQL跑得越来越慢?
现象:一个原本2秒跑完的查询,两周后变成47秒。
EXPLAIN ANALYZE
显示
Seq Scan
(全表扫描)占比95%。
根因分析:
dvdrental
的
payment
表有16000+记录,但
customer_id
字段没有索引。当
JOIN
或
WHERE customer_id = ?
时,数据库只能逐行扫描。
解决方案:
-
立即修复
:
CREATE INDEX idx_payment_customer_id ON payment(customer_id); -
长期机制
:在数据建模阶段,所有
JOIN字段、WHERE过滤字段、ORDER BY字段,必须默认建索引。我团队的DDL模板里,索引创建是和表创建绑定的。
提示:索引不是越多越好。
customer表的first_name字段,如果WHERE first_name = 'John'查询占比<0.1%,建索引反而拖慢写入。判断标准是:该字段的查询频次 × 单次查询节省时间 > 索引维护成本。
实操技巧:用
pg_stat_statements
扩展监控慢查询。在
postgresql.conf
中启用:
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
然后查
SELECT query, total_time, calls FROM pg_stat_statements ORDER BY total_time DESC LIMIT 5;
,精准定位瓶颈。
5.2 数据质量问题:为什么COUNT(*) 和 COUNT(column) 结果差10倍?
现象:
SELECT COUNT(*) FROM customer
返回599,
SELECT COUNT(email) FROM customer
返回542,差57行。
根因:
email
字段有57个
NULL
值。
COUNT(column)
只统计非
NULL
值,
COUNT(*)
统计所有行。
业务影响:如果用
COUNT(email)
计算“邮箱覆盖率”,结果是90.5%,但实际业务中,
NULL
可能代表“用户拒绝提供邮箱”或“系统采集失败”,二者含义完全不同。
解决方案:
-
定义数据质量规则
:在ETL脚本开头加检查
SELECT COUNT(*) FILTER (WHERE email IS NULL) AS null_email_count FROM customer;,若null_email_count > 0则告警; -
业务层兜底
:在
CASE逻辑中,ELSE分支必须覆盖NULL场景,如ELSE 'EMAIL_NOT_PROVIDED'。
注意:
COUNT(DISTINCT column)对NULL的处理和COUNT(column)一致,即忽略NULL。所以COUNT(DISTINCT email)也可能小于COUNT(*),这不是bug,是设计。
5.3 逻辑错误:GROUP BY 报错 “column must appear in the GROUP BY clause”
现象:
SELECT customer_id, first_name, SUM(amount) FROM payment p JOIN customer c ON p.customer_id = c.customer_id GROUP BY customer_id
报错。
根因:PostgreSQL 要求
SELECT
列表中的所有非聚合字段,必须出现在
GROUP BY
中。
first_name
是非聚合字段,但
GROUP BY
只有
customer_id
。
解决方案:
-
如果
customer_id是主键,first_name由其决定,可用ANY_VALUE(first_name)(MySQL)或FIRST_VALUE(first_name) OVER (PARTITION BY customer_id)(PostgreSQL); -
但更推荐的做法是:明确业务意图
。如果要查“每个客户的姓名和总消费”,
GROUP BY customer_id, first_name;如果只要“每个客户的总消费”,SELECT customer_id, SUM(amount)即可。
实操心得:我团队的SQL审查清单第一条就是——
检查
GROUP BY
字段是否和
SELECT
中的非聚合字段完全一致
。用正则
SELECT\s+([^,]+(?:,\s*[^,]+)*)\s+FROM
提取字段,再人工核对,10秒搞定。
5.4 环境问题:为什么在本地跑通,上线就报错?
现象:
SELECT * FROM customer LIMIT 10
在本地PostgreSQL 14正常,在生产环境(Amazon RDS PostgreSQL 13)报错。
根因:RDS 默认开启
log_min_error_statement = error
,但某些配置差异导致
LIMIT
行为不同。更常见的是
search_path
设置不同,导致函数找不到。
解决方案:
- 环境一致性 :生产、测试、开发全部用同一Docker镜像;
-
显式声明
:所有SQL开头加
SET search_path TO public;; -
版本锁死
:
Dockerfile中写死postgres:13.12,而非postgres:13。
提示:用
pg_dump --schema-only导出生产环境DDL,在本地重建,比肉眼对比更可靠。这是我处理跨环境问题的保底方案。
6. 从这10条出发,构建你的SQL能力护城河
这10条SQL查询,表面是语法清单,内核是数据科学家的
思维操作系统
。
SELECT
教你定义数据契约,
WHERE
训练逻辑严谨性,
JOIN
锻炼关系建模能力,
WINDOW
函数培养动态分析视角——它们共同构成你处理任何新业务需求的底层框架。
我自己在带新人时,从不让他们背语法,而是给一个真实需求:“请用SQL回答:过去30天,哪个城市的用户平均客单价最高?如果并列,列出所有。”然后观察他们如何拆解:先确定数据源(
customer
,
address
,
payment
),再设计
JOIN
路径,接着用
GROUP BY city
聚合,用
AVG(amount)
计算,最后用
ORDER BY ... LIMIT 1
排序取顶。这个过程暴露的,不是SQL能力,而是
业务抽象能力
。
所以别把这10条当终点。下一步,你应该:
-
把
dvdrental库换成你公司的脱敏测试库,用真实业务字段重写这10条; -
在每条SQL后加一行
-- [我的业务场景]:...,把示例映射到你手头的项目; - 记录一次你因忽略某条细节导致的故障,写成团队内部的《血泪教训备忘录》。
最后分享一个小技巧:我电脑桌面永远开着一个
sql_notes.md
文件,里面只有三列:
问题场景
、
错误写法
、
正确写法
。比如:
| 问题场景 | 错误写法 | 正确写法 |
|---|---|---|
| 查用户最近一笔支付 |
SELECT * FROM payment WHERE customer_id = ? ORDER BY payment_date DESC LIMIT 1
|
SELECT * FROM payment WHERE customer_id = ? AND payment_date = (SELECT MAX(payment_date) FROM payment WHERE customer_id = ?)
|
这个文件不大,但它是你个人SQL能力的实时镜像。每次填一行,你就离“写SQL不假思索,但结果绝对靠谱”的状态近了一步。毕竟,数据科学家的价值,不在于你会多少语法,而在于业务方问“这个数准不准”时,你能拍着胸脯说:“我查过三次,逻辑闭环,数据可追溯。”


被折叠的 条评论
为什么被折叠?



