
前阵子有个做数据分析的朋友问我自己 Python、Excel 都玩得挺溜是不是没必要再学 SQL 了我当时直接回了句你要是哪天想从数据库里只取“最近 30 天、下单超过 3 次、且收货地址在华东地区”的用户用 Python 去连库、拉全量、再自己过滤当然能跑但你会被同事骂死的。SQL 之所以这几十年没被淘汰不是因为它新而是因为它把“怎么查数据”这件事抽象到了最舒服的程度。这篇文章我打算聊一部分 SQL 的“上半场”——也就是从基础语法到常用查询技巧、再到慢 SQL 和安全底线这一路适合刚入门的新手也适合写了好几年 SQL 但一直靠“复制粘贴”活着的人。内容不会太长但每段都是实际工作中用得到的。1. SQL 的定位一门“半编程语言”的底层逻辑1.1 数据到底是怎么存进关系型数据库的很多人学 SQL 上来就背语法SELECT、FROM、WHERE 一套组合拳打完能出结果就算会了。但一旦遇到“为什么这个查询这么慢”“为什么 LEFT JOIN 出来数据变多了”“为什么我删不掉这几行”立刻懵。根源在于你不理解数据在关系型数据库里是怎么组织的。关系型数据库的核心模型是“表”表由行和列组成。这个看起来太简单了但一切 SQL 语法都是围绕“表”这个二维结构展开的。SELECT 是在“列”上做投影WHERE 是在“行”上做筛选JOIN 是把两张表按某种关联条件横向拼接GROUP BY 是把行按某一列的值压缩成组窗口函数是“在组内保留每一行明细的同时做计算”。你一旦把 SQL 的每一个子句都映射到“表结构上的某种操作”就不会再觉得语法零碎了。我见过不少新手纠结“要不要背 SQL”我的观点是不要背。你去理解每一条子句对表做了什么比背一百条面试题有用得多。当你能用自己的话解释“GROUP BY 之后为什么 SELECT 里只能放分组列和聚合函数”时你的 SQL 水平已经过半了。1.2 主流数据库的差别在哪里很多人会在 MySQL 和 SQL Server 之间纠结还会有人问“我学的 MySQL 语法在 SQL Server 里能不能用”。这里我直接给一个结论核心语法 90% 通用坑主要出在“方言”上。同一套逻辑在不同数据库里写出来可能长这样取前 10 行MySQL 用 LIMIT 10SQL Server 用 SELECT TOP 10Oracle 老版本用 ROWNUM 10。字符串拼接MySQL 用 CONCAT()SQL Server 用 Oracle 用 ||。空值处理函数MySQL 的 IFNULL()SQL Server 的 ISNULL()PostgreSQL 和 SQL Server 新版本都支持标准的 COALESCE()。分页的完整写法MySQL 是 LIMIT offset, countSQL Server 是 OFFSET ... ROWS FETCH NEXT ... ROWS ONLY。所以我不建议你死记“某个数据库的某个函数”而是建议你建立一张“逻辑到函数”的映射表。遇到问题先去想我要实现什么逻辑再查当前数据库用哪个函数。你工作中可能一辈子就用一种数据库但能快速迁移的能力才是 SQL 真正的门槛。1.3 为什么文档数据库和向量数据库时代SQL 依然没死这几年 NoSQL 火过向量数据库也起来了很多人觉得 SQL 要过时。但你看现实几乎每个业务系统背后还是 MySQL、PostgreSQL、SQL Server、Oracle 这几个老家伙。为什么因为大多数业务的核心是“关系”是订单、用户、商品之间的关联而关系型数据库加上 SQL 的集合操作能力依然是这件事上最成熟的方案。更重要的一个点是SQL 是一种声明式语言。你只告诉数据库“我要什么”不用管它“怎么取”。数据库自己决定走索引还是全表扫、用哪种 JOIN 算法、是否并行执行。这跟写程序去遍历完全不一样。理解这个底层差异你才能真正理解为什么“SQL 优化”和“程序优化”的思路不同。2. 从第一行查询开始SELECT 的逻辑与常见误区2.1 书写顺序和执行顺序不是一回事这是一个老生常谈但真的值得放在最前面讲。一条典型的 SQL 是这么写的SELECT department_id, COUNT(*) AS emp_cnt FROM employees WHERE salary 8000 GROUP BY department_id HAVING COUNT(*) 5 ORDER BY emp_cnt DESC LIMIT 10;丢进数据库之后它的执行顺序其实是FROM employees -- 1. 确定数据来源 WHERE salary 8000 -- 2. 行级筛选 GROUP BY department_id -- 3. 分组 HAVING COUNT(*) 5 -- 4. 组级筛选 SELECT -- 5. 投影列、计算表达式 ORDER BY emp_cnt DESC -- 6. 排序 LIMIT 10; -- 7. 限制返回行数为什么理解这个顺序很重要因为它直接回答了一大堆报错和诡异行为。比如你在 WHERE 里引用 SELECT 里起的别名标准 SQL 是不允许的因为执行 WHERE 的时候 SELECT 还没执行但你在 ORDER BY 里引用别名又可以因为排序发生在 SELECT 之后。再比如 HAVING 可以筛聚合结果WHERE 不行因为 WHERE 执行时分组还没发生。我面试人的时候经常问一个经典问题想筛出“部门平均工资超过一万的部门”为什么条件是 HAVING AVG(salary) 10000 而不是 WHERE AVG(salary) 10000能答上来的人说明对执行顺序是真理解了而不是背下来的。2.2 WHERE 筛选的边界NULL、空字符串、函数包裹字段NULL 是 SQL 里最容易埋雷的地方。你要是没吃过亏说明你写的 SQL 还不够多。举一个实际场景用户表里有一个 nickname 字段你想查所有“昵称不等于管理员”的用户新手第一反应是SELECT * FROM users WHERE nickname ! 管理员;这条 SQL 会把 nickname 为 NULL 的用户全部丢掉。为什么因为 NULL 在 SQL 里表示“未知”拿“未知”和“管理员”比较结果既不是 TRUE 也不是 FALSE而是 NULL。既然条件不是 TRUE这行就不会返回。正确的姿势是明确你的业务预期是想把没填昵称的用户也查出来那要显式加上 IS NULLSELECT * FROM users WHERE nickname IS NULL OR nickname ! 管理员;另一个高频坑是“空字符串和 NULL 是不是一回事”。MySQL 里默认不是NULL 是“没有任何值” 是一个有值但长度为 0 的字符串。统计时如果不注意会把两者重复算尤其在 GROUP BY 时它们会各成一派。我做过一次数据核对因为一个遗留系统把“未填手机号”写成空字符串另一个系统写成 NULL最后两张表 join 出来就是对不上。排查半天问题就出在这个语义差异上。还有一类非常影响性能的写法在 WHERE 条件里对字段做函数运算。比如SELECT * FROM orders WHERE DATE(created_at) 2025-01-01;这条逻辑没错但 DATE(created_at) 包裹了字段导致数据库没法走 created_at 上的索引只能全表扫一遍再逐行算日期。改写成范围查询就好得多SELECT * FROM orders WHERE created_at 2025-01-01 00:00:00 AND created_at 2025-01-02 00:00:00;这个坑我在后面讲慢 SQL 的时候还会再提到因为它是索引失效的重灾区。2.3 去重查询的三种姿势和各自的坑“SQL 语句去重”是搜索热度很高的词说明大家都被重复数据折磨过。去重最简单的是 SELECT DISTINCTSELECT DISTINCT user_id, product_id FROM orders;它会把两列组合相同的结果合并成一个。注意是“组合相同”才算重复单看 user_id 相同但 product_id 不同不会被合并。第二种姿势是 GROUP BY 去重SELECT user_id, product_id FROM orders GROUP BY user_id, product_id;单看结果DISTINCT 和 GROUP BY 常常一样但语义有细微差别。GROUP BY 的核心是“分组聚合”只是我们没写聚合函数所以看起来像去重。它最大的优势在于是要顺便统计数量SELECT user_id, product_id, COUNT(*) AS buy_cnt FROM orders GROUP BY user_id, product_id;第三种姿势是窗口函数去重适合“每组只留一条”的场景比如每个用户只保留最近一次下单记录。这个我在第 4 节专门讲。去重最大的坑不是语法而是“你以为去重了其实没有”。比如 SELECT DISTINCT * 对全字段去重一条记录只要有一个字段不同就不算重复。很多业务上的“重复”其实是“某一两列重复”而不是整行重复。所以在动手之前先确认清楚你定义重复的维度是什么。3. 高频数据处理场景空值、默认值与常用函数3.1 空值处理COALESCE、IFNULL、NULLIF上一节说了 NULL 的查询坑这一节说处理。实际项目里最常见的需求是“某列是空值就显示一个默认值”。不同数据库写法不同但标准的 COALESCE 通用性最好SELECT user_name, COALESCE(phone, 未填写) AS phone_display FROM users;COALESCE 是“返回第一个非 NULL 值”可以传多个参数COALESCE(col1, col2, 默认值)。这比到处写 CASE WHEN 清爽得多。还有一个常被问到的 NULLIF(spaces)它的作用是“如果两个表达式相等就返回 NULL”。比如你想防止除零错误SELECT total_amount / NULLIF(item_count, 0) AS avg_price FROM orders;item_count 为 0 时NULLIF 返回 NULL除法结果也是 NULL至少不会直接报错。这属于一种“防御性写法”在很多报表 SQL 里很常见。3.2 默认值设计GUID、自增和时间戳的选择“SQL 默认值 GUID”是热搜词估计是有人看表结构里主键是 uniqueidentifier想搞明白这玩意怎么来的。我这么解释在设计表的时候主键除了自增还有一个常见选择是 GUID在 SQL Server 中叫 uniqueidentifierMySQL 里叫 CHAR(36) 或 UUID 字符串。自增主键的好处是短、有序、查询走索引效率高缺点是分布式环境下容易冲突而且会暴露业务量。GUID 的好处是全局唯一、可以在应用层生成再写入数据库适合分库分表场景缺点是长、无序作为主键在 InnoDB 里可能会有页分裂问题写入性能略差。你在建表时可以给 GUID 主键设置默认值。SQL Server 里是这样CREATE TABLE users ( id UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY, user_name NVARCHAR(50) NOT NULL );MySQL 8.0 里可以这样CREATE TABLE users ( id CHAR(36) DEFAULT (UUID()) PRIMARY KEY, user_name VARCHAR(50) NOT NULL );再到“时间戳默认值”几乎每张业务表都有 created_at。它有两个作用一是告诉你这行数据什么时候进来的二是排查问题时能快速定位数据范围。我建议所有业务表都建 created_at、updated_at 两个字段前者建表时给 DEFAULT CURRENT_TIMESTAMP后者在更新时由程序或触发器维护。很多老系统一开始没建后面做数据对账和增量同步时恨不得骂街。3.3 数字字符串判断这类“隐性数据类型”问题热搜里有一条“db2 判断数字字符串函数”这种问题本质是字段存的是字符串但你业务上想判断它是不是一个“看起来像数字”的值。为什么有这个需求因为很多系统在导入数据时把手机号、工号、金额全塞进了 VARCHAR 列。不同数据库的写法差异很大。以 DB2 为例判断某个字符串是否为数字可以用 TRANSLATE 配合 LENGTH 来做SELECT code, CASE WHEN TRANSLATE(code, , 0123456789) THEN 纯数字 ELSE 含非数字字符 END AS is_numeric FROM test_table;TRANSLATE 在这里的逻辑是把 code 中的所有数字字符替换成空字符串如果结果为空说明原串全是数字。MySQL 里更直观的写法是用正则SELECT code, CASE WHEN code REGEXP ^[0-9]$ THEN 纯数字 ELSE 含非数字字符 END AS is_numeric FROM test_table;这类问题真正的重点是数据入库时的类型设计比查询时补救更重要。你如果明知道这一列可能存手机号或工号却为了“什么都能塞”建成了 VARCHAR后面每个查询都要绕一圈判断早晚会被这种隐性类型问题坑一次。4. 窗口函数SQL 进阶的第一道分水岭4.1 为什么说窗口函数能替代大量自连接很多人写 SQL 用 JOIN 用得很多但一到“分组内排名”“分组内取前 N 条”“环比同比”这类需求就卡住了最后只能写子查询再来回 JOIN又长又难维护。窗口函数就是来解决这一类问题的。窗口函数和普通聚合函数的区别我用一句话说明白GROUP BY 聚合后每个分组只输出一行窗口函数在输出结果的每一行上都能保留自己的明细同时可以看到所在组的信息。用大白话说窗口函数让每一行都“带上它所在分组的小统计”。窗口函数的基本结构是聚合函数 / 排名函数 OVER (PARTITION BY 分组列 ORDER BY 排序列)比如你想看每个用户的所有订单里每笔订单金额占这个用户总金额的比例可以这样写SELECT user_id, order_id, amount, amount * 1.0 / SUM(amount) OVER (PARTITION BY user_id) AS pct FROM orders;这一条就把“用户级的总金额”算出来并放到了每一行旁边完全不打断明细。4.2 ROW_NUMBER、RANK、DENSE_RANK 的细微差别这三个函数都是排名很多新手会搞混面试也爱问。我用一张表说清楚差别函数排序值相同的人下一个名次典型场景ROW_NUMBER()随机/按其他条件定序连续递增分组去重、分页RANK()并列同一名次跳过后续名次竞赛排名DENSE_RANK()并列同一名次不跳名次排行榜展示举个例子成绩表里有 98、95、95、90 四个人ROW_NUMBER名次是 1、2、3、495 的两个人谁排 2 谁排 3 取决于 ORDER BY 有没有次级排序RANK名次是 1、2、2、490 直接到第 4 名DENSE_RANK名次是 1、2、2、390 是第 3 名我实际用得最多的是 ROW_NUMBER因为大部分业务要的是“唯一序号”而不是真正意义上的并列排名。特别是在做“分组去重”的时候ROW_NUMBER 是最可靠的选择。4.3 一个典型的分组 TopN 场景直接上一个我做过很多次的例子每个用户最近一笔订单。写法长这样WITH ranked AS ( SELECT user_id, order_id, order_date, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn FROM orders ) SELECT user_id, order_id, order_date FROM ranked WHERE rn 1;这里有两个容易错的地方。第一PARTITION BY user_id 表示“按用户分组”ORDER BY order_date DESC 表示组内按订单日期倒序排然后用 ROW_NUMBER 编号编号为 1 的就是每个用户最近的一笔。第二窗口函数不能直接在 WHERE 里用所以要先把它放进子查询或 CTE 里在外面再过滤 rn 1。如果你把 WHERE rn 1 直接写在窗口函数同一层数据库会报“Unknown column rn”。这个“窗口函数不能用于 WHERE”的坑几乎每个刚开始用窗口函数的人都踩过。记住一个原则窗口函数在 SELECT 阶段才计算而 WHERE 在它之前执行所以必须套一层。窗口函数能做的远不止 TopN还有 LAG、LEAD 做环同比SUM OVER 做移动累计AVG OVER 做滑动平均。对普通业务开发来说把这几个掌握好已经能覆盖 90% 的“高级查询”需求了。5. 慢 SQL 优化从执行计划到索引常识5.1 慢 SQL 的常见来源和优化顺序“慢 SQL 优化”这个话题很大但实际工作里有一套固定的排查顺序我分享我的做法。第一步先拿到慢 SQL。MySQL 里可以开慢查询日志slow_query_log并设置阈值比如超过 2 秒的就记录SQL Server 可以用 DMV 或扩展事件去看耗时靠前的查询。这一步的目标是“有靶子再开枪”不要凭感觉去优化一个还没确认慢的查询。第二步看是不是数据量大的问题。一张表才几千行再烂的 SQL 都慢不到哪去要是几百万行加没有索引慢是必然的。第三步分析执行计划。看这条语句是不是走了全表扫描是不是有临时文件排序有没有“回表”。第四步根据执行计划改写 SQL 或加索引。这个顺序很重要。我见过太多人一上来就加索引结果加了一堆浪费空间的查询速度一点没变。原因很简单SQL 写法本身就有问题比如对索引字段做了函数包裹或者 JOIN 时两张表的关联字段字符集不一致这类问题加索引根本救不了。5.2 索引失效的几个高频原因我从实战里提五个最容易踩的索引失效场景第一在索引列上使用函数或计算。前面提到的 DATE(created_at) 2025-01-01 就是典型。数据库为了计算值被迫放弃索引有序性。解决办法是把条件改写成范围查询或者把字段存成可直接比较的格式。第二隐式类型转换。比如 varchar 类型的 phone 列上有索引你写 WHERE phone 13800138000数字类型在比较时会把字符串转成数字再比较导致索引失效。常见的解法是让等号两边类型一致WHERE phone 13800138000。第三LIKE 以通配符开头。WHERE name LIKE %张% 无法走常规索引因为索引是按字符串前缀排列的你不知道“张”前面是什么。你只能改成 name LIKE 张%或者考虑全文本索引。第四联合索引没遵守最左前缀。联合索引 (a, b, c) 相当于建了 a、ab、abc 三套索引你查询只用 b 或只用 c索引用不上。第五OR 连接多个条件时如果其中一个字段没索引整个查询可能退化为全表扫描。把 OR 改写成 UNION 或改成 IN情况往往好很多。5.3 EXPLAIN 到底看什么MySQL 的 EXPLAIN 是排查慢 SQL 最强的工具没有之一。用法很简单在 SQL 前面加上 EXPLAIN 关键字数据库不真正执行查询只预估执行计划。我一般看四列列名重点看什么typeall 是最差的全表扫ref / range 是还可以const / eq_ref 是很好key实际用到的索引。如果是 NULL说明没走索引rows预估扫描的行数。这个数字越大查询往往越慢Extra出现 Using filesort、Using temporary 都是危险信号说明排序或分组没利用好索引比如你查一条订单列表EXPLAIN 出来 type 是 ALLrows 显示 500 万key 是 NULL那问题就非常明确了没有可用索引在全表扫。实际工作中我的习惯是先看 type再看 rows最后看 Extra。只要 type 从 ALL 变成 ref 或 range扫描行数降下来性能基本就有质变。这种“看执行计划”的能力比背一堆优化口诀管用得多。你要知道每条 SQL 在数据库眼里到底是怎么跑的而不是只看结果集对不对。6. SQL 注入不是历史问题一条安全底线6.1 为什么“拼接用户输入”非常危险网上经常能看到“万能密码绕过”这类词听着像什么黑客绝技其实背后原理特别朴素一段 SQL 如果只是把用户输入的原样拼到字符串里那只要输入的内容改变了 SQL 本身的语义整个查询条件就失效了。举个例子登录校验的逻辑你可能写成sql SELECT * FROM users WHERE user_name username AND password password 如果用户在用户名字段输入一个特殊构造的内容把后面的 AND password 处理成永远是 TRUE 的逻辑那这条 SQL 查出来就可能不是“匹配到的用户”而是“表里的第一个用户”。问题不在数据库而在你的代码把“数据”和“命令”混在了一起。SQL 注入这个事之所以值得反复讲不是因为它技术上多难而是因为它的影响面极大而且容易被忽略。只要有一处查询是字符串拼接的就等于给整条数据链路开了个口子。反过来只要你有参数化查询的习惯SQL 注入在应用层基本能防住。6.2 参数化查询和 ORM 调用原生 SQL 的姿势防御的核心思想只有一个用户输入只能作为“参数”传给数据库不能作为“SQL 文本”的一部分。Python 里直接操作 MySQL 时要写成这种姿势cursor.execute( SELECT * FROM users WHERE user_name %s AND password %s, (username, password) )Java 的 JDBC 里用 PreparedStatementMyBatis 里用 #{}Node.js 的 mysql 库用 ? 占位符Prisma 这类 ORM 里如果你需要写原生 SQL也有对应的参数绑定接口比如$queryRaw安全用法是把参数作为第二个参数传进去而不是直接拼进 SQL 字符串。const result await prisma.$queryRaw SELECT * FROM users WHERE user_name ${username} AND password ${password} ;ORM 不等于绝对安全关键还是看你怎么用。像动态排序这种需求ORDER BY 后面不能直接拼用户传来的字段名因为 ORDER BY 的参数化很难做。常规做法是做一个字段白名单映射用户传一个枚举值代码里再映射成真实的列名。这类细节在代码评审里我会重点盯。6.3 防御侧的代码自查清单我在项目里通常会带一个简单的 SQL 安全自查清单分享给读者检查点自查标准字符串拼接代码里搜 SQL 关键字附近有没有 号或字符串模板插值一律改成参数绑定LIKE 查询即使是模糊查询参数也要通过占位符传入不要拼接 %IN 列表传入多个查询值时要绑定参数数组不要手工拼成 a,b,cORDER BY / 列名动态排序字段必须走白名单映射不能透传用户输入报错信息不要把 SQL 异常和表结构信息直接返回给前端这套东西不是我凭空想出来的是这些年做开发和安全复盘时踩过坑、吃过亏之后总结的。一句话概括写 SQL 的时候默认所有外部输入都不可信能参数化的绝不拼接能白名单化的绝不放行原始字符串。7. 环境搭建与日常管理从安装到导出 SQL 文件7.1 SQL Server 安装和 SSMS 连接那几个经典坑热搜里 SQL Server 相关的一大堆看得我都能猜到大家卡在哪。装 SQL Server 本身不难难的是装完连不上。最常见的连接失败原因无非四个第一服务没启动。装完 SQL Server 之后SQL Server 服务不一定自动运行Windows 上可以打开“服务”管理面板确认一下。第二实例名搞错。默认实例是计算机名命名实例是 主机名\实例名端口默认 1433。很多人把默认实例的服务器名写成 IP 就万事大吉结果实例名忘了写连接自然失败。第三TCP/IP 协议被禁用。SQL Server 在安装后默认可能允许共享内存和命名管道但客户端通过 IP 访问要走 TCP/IP需要在 SQL Server 配置管理器里把 TCP/IP 协议启动。第四用户名密码或认证模式问题。sa 账号如果忘了密码或者实例是 Windows 身份验证模式你用 SQL 账号登录肯定失败。早期版本里 SA 密码还有过期策略也容易被骂坑。有些软件比如连 SQL Server 当数据库的行业软件会在安装时就报“无法连接到 SQL Server”排查思路也差不多先确认服务、再确认实例名、再确认认证信息。这类问题 90% 不是数据库本身坏了而是连接参数不对或网络没通。7.2 用命令行导出导入 .sql 文件不少人的 SQL Server 操作完全依赖图形界面导出数据库用“任务 - 生成脚本”导入就右键“导入数据”。这当然能用但脚本化一点更省事。SQL Server 里可以用 sqlcmd 命令行工具执行 .sql 文件sqlcmd -S 服务器名 -U 用户名 -P 密码 -d 数据库名 -i 导入文件.sqlMySQL 里对应的工具是 mysqldump 和 mysqlmysqldump -u root -p 数据库名 备份.sql mysql -u root -p 数据库名 备份.sql这类命令行操作的好处有两个一是可重复执行二是便于放进自动化脚本。很多从图形界面“导出”的 .sql 文件在另一个环境导入时会因为编码问题产生乱码尤其是中文数据。建议导出时明确指定字符集比如 MySQL 用 --default-character-setutf8mb4。导入时如果遇到“Unknown character set”或乱码优先检查字符集是否一致。另外提醒一句.sql 文件是文本文件里面包含大量 SQL 语句。从网上下载的 .sql 文件不要直接往库里导先用文本编辑器或命令行加载到内存里看一眼内容确认里面没有你理解不了的危险语句再执行。这是基本功也是安全习惯。7.3 一套适合练手的轻量环境组合如果你想练 SQL我不建议一上来就装企业版全家桶而是搭一套轻量的本地环境。有两个推荐方向方向一MySQL 8.x Docker。一条命令起一个容器docker run --name mysql-demo \ -e MYSQL_ROOT_PASSWORD你的密码 \ -p 3306:3306 \ -d mysql:8然后随便用一个客户端连上去。好处是干净、可销毁、多版本切换方便。坏处是如果你依赖 Docker对 Windows 老版本系统不太友好。方向二SQL Server Express 或 Developer 版 SSMS。Express 版免费但功能和性能有裁剪Developer 版功能完整但只能用于开发测试。连接本地实例时如果用户名密码搞不定先试 Windows 身份验证模式通常能直接连上。练手的数据集我建议不要用网上那张抄来抄去的“student/course/score”表而是拿你工作中真实存在的表结构做脱敏练习。哪怕只有三张表你去模拟查询“各区域销量排名”“环比变化”这类需求收获远大于照着教程敲一遍。结尾的一点个人体会SQL 这个东西入门容易但“会”和“熟”之间差着大量真实数据和真实业务场景的打磨。我自己的一个习惯是每到一个新项目先花半天时间把核心业务表的字段梳理一遍然后尝试用一条 SQL 回答“业务上最难回答的一个问题”。这个过程比刷一百道题都管用。如果你现在刚开始学 SQL我的建议很简单找一套真实数据逼自己只写一条 SQL 完成“分组排名”“空值转换”“去重统计”这三个场景跑通之后再回去看执行计划。你会发现SQL 的乐趣恰恰在于它用很短的话表达出非常精确的数据诉求。本文算是 SQL 的“上半场”后面有机会再聊 JOIN 的底层原理、事务与隔离级别、以及更复杂的调优案例。