SQLZOO这个名字,只要是认真学过SQL的人应该都不陌生。它是国外一个免费的SQL在线练习平台,按知识点拆成十几个模块,从最简单的SELECT语句一路做到JOIN、聚合、子查询、窗口函数,每道题都是直接在一个真实数据库上跑SQL验证结果。我当年就是靠这份中文版习题一点点把SQL底子打扎实的,后来带新人学SQL,我也都是直接甩这个链接,让他们先把这套题刷完再说话。
这篇东西不是光把答案贴一遍就完事,而是按模块拆解题目的设计意图、每个知识点的核心逻辑,以及做题时最容易踩的坑。无论你是刚看完SQL语法想找题练手的学生,还是工作中写SQL总是差点意思的开发、数据分析师,又或者是面试前想系统性过一遍SQL重点的人,这套题库和这份解析都足够你认真啃上一段时间。
1. SQLZOO到底练什么:整套习题的模块拆解
1.1 从SELECT basics到窗口函数:一条完整的SQL学习路线
SQLZOO中文版把SQL学习分成了十几个递进模块,每个模块聚焦一组核心语法,整体路线很清晰:
- SELECT basics:最基础的列筛选,认识SELECT和WHERE
- SELECT from WORLD:在真实国家数据表上练习条件过滤、BETWEEN、IN、LIKE
- SELECT from Nobel:用诺贝尔奖得主数据练习更复杂的WHERE组合
- SELECT within SELECT:子查询,即一个查询嵌套在另一个查询里
- SUM and COUNT:聚合函数和GROUP BY,配套HAVING过滤
- JOIN:两张表如何通过关联字段合并查询
- More JOIN:多表连接的高级场景,比如演员和电影表
- Using Null:COALESCE、IS NULL、LEFT JOIN等空值处理
- Self join:自连接,模拟同一张表内部的结构化关联
- 窗口函数(Window functions):ROW_NUMBER、RANK、LAG等分析函数
这条路线设计的精妙之处在于:它不是一股脑把所有语法堆给你,而是刻意制造了一个理解坡度。你刚刚觉得“SELECT好像也不过如此”,下一模块立刻用嵌套子查询打脸;你刚觉得JOIN已经摸透了,Using Null和Self join又告诉你“连接还能这么玩”。
1.2 为什么刷题比看教程有效
很多人学SQL有个误区,就是拿一本《SQL必知必会》从头翻到尾,看的时候觉得全懂了,一落到真实数据上就懵。SQLZOO逼你直接在数据库上写SQL、跑结果,错了就报错,对了才放行。这种“反馈闭环”恰恰是学习SQL最有效的方式——你写下的每一条语句,立刻能验证它对不对,而不是停留在理论上。
而且它用的不是那种精心构造的教学数据,而是真实世界的数据:国家人口、诺贝尔奖得主、电影演员表。这些数据有残缺、有NULL、有重复,恰好还原了实际工作中你面对的真实数据状态。
2. 核心模块习题答案精讲
2.1 SELECT basics:基础中的基础
这个模块是热身用的,只有十几道题,目标是让你熟悉最基本的SELECT语句结构。它用的是一张很小的世界表,包含name(国家名)、continent(大洲)、population(人口)几个字段。
几道代表性的题目和解法:
-- 展示法国的人口 SELECT population FROM world WHERE name = 'France'; -- 展示瑞典和挪威的人口 SELECT name, population FROM world WHERE name IN ('Sweden', 'Norway'); -- 找出人口在20万到25万之间的国家 SELECT name, population FROM world WHERE population BETWEEN 200000 AND 250000;这个模块虽然简单,但有一个细节值得注意:BETWEEN是闭区间,也就是包含边界的。很多人写BETWEEN的时候容易忽略这一点,导致边界数据被重复计算。实际工作中如果对边界值敏感,建议直接用>=和<=替代,避免歧义。
2.2 SELECT from WORLD:WHERE条件的组合艺术
这个模块的难度开始爬升,核心是组合WHERE条件。题目要求你从一个包含全世界国家数据的表里,按各种条件筛出目标国家。比较经典的几道:
-- 人口超过2亿的国家名称 SELECT name FROM world WHERE population > 200000000; -- 人口在10亿到20亿之间的国家名称和人口 SELECT name, population FROM world WHERE population BETWEEN 1000000000 AND 2000000000; -- 名称以AL开头的国家和人口 SELECT name, population FROM world WHERE name LIKE 'AL%';这个模块里最值得深挖的是LIKE。SQL里的LIKE支持%和_两种通配符:%匹配任意长度的字符串(包括0个字符),_匹配单个字符。实际工作中做模糊搜索时,LIKE的性能在大数据量下非常差,因为无法走索引,所以生产环境里一般用全文检索或者ES之类的方案。但在练习环境里,它是掌握字符串匹配逻辑的最好入口。
还有一道题我印象很深,是要求找出人口在25万以上且面积小于30万平方公里的国家:
SELECT name, population, area FROM world WHERE population > 250000 OR area < 300000;这道题考的是OR条件。OR看起来简单,但它和UNION有个性能差异:OR条件很难走索引合并,而UNION可以分别走索引再合并结果。数据量小的时候感觉不到差别,数据量大了以后,同样的语义用UNION写往往更快。具体对比后面讲慢SQL优化的时候细说。
2.3 SELECT from Nobel:复杂条件的逻辑组合
诺贝尔奖得主表有yr(年份)、subject(学科)、winner(获奖者)三个字段。这个模块的题目很多,它真正考验的是你对多个条件的组合能力。
典型题目和解法:
-- 1962年物理奖得主 SELECT winner FROM nobel WHERE yr = 1962 AND subject = 'physics'; -- 1980年之后每届和平奖得主 SELECT winner FROM nobel WHERE yr >= 1980 AND subject = 'peace'; -- 找出所有获奖者中包含"John"的人 SELECT winner FROM nobel WHERE winner LIKE 'John%';我特别想提一道题:找出物理学奖和化学奖得主,但要排除1964年的化学奖得主、排除1980年之前的物理奖得主:
SELECT * FROM nobel WHERE (subject = 'physics' AND yr >= 1980) OR (subject = 'chemistry' AND yr <= 1964);这道题的核心是理解AND和OR的优先级:AND比OR高。所以上面这个写法里,括号不是可选的,而是必须的。如果把括号去掉,SQL引擎会先执行两个AND,再执行OR,结果就完全变了。很多人写复杂条件的时候不习惯加括号,全凭缩进表达逻辑,这是迟早要踩坑的,因为对SQL解析器来说,缩进什么都不是。
2.4 SELECT within SELECT:子查询的进阶用法
这个模块是整套题目的第一个分水岭,很多人就是在这里开始觉得“SQL好像没那么简单”。子查询的本质就是:先查出一个结果集,再把这个结果集当作外层查询的输入。
经典题目:
-- 找出人口超过俄罗斯的国家 SELECT name FROM world WHERE population > (SELECT population FROM world WHERE name = 'Russia'); -- 找出GDP比欧洲任何一个国家都高的国家 SELECT name FROM world WHERE gdp > (SELECT MAX(gdp) FROM world WHERE continent = 'Europe'); -- 找出与阿根廷同属一个洲的国家名和所属洲 SELECT name, continent FROM world WHERE continent = (SELECT continent FROM world WHERE name = 'Argentina');第三道题特别有意思,它要求你先查阿根廷所属的洲,再拿这个结果去匹配外部表。子查询返回单个值的时候,就叫“标量子查询”,可以用=直接比较;如果子查询返回多个值,就要用IN或者ANY/ALL。这个区别是子查询最常见的错误来源:
-- 错误写法:子查询返回了多行,却用"="比较 SELECT name FROM world WHERE continent = (SELECT continent FROM world WHERE name IN ('Argentina', 'Brazil'));这时候应该用IN:
SELECT name FROM world WHERE continent IN (SELECT continent FROM world WHERE name IN ('Argentina', 'Brazil'));2.5 SUM and COUNT:聚合函数的入门门槛
这个模块引入了GROUP BY和HAVING,是SQL学习里一个真正的坎。很多人用聚合函数没问题,但一遇到GROUP BY和HAVING就晕。核心就一句话:GROUP BY把多行压缩成一组,SELECT只能出现分组字段或聚合函数。
经典题目:
-- 统计每个大洲的国家数量 SELECT continent, COUNT(name) FROM world GROUP BY continent; -- 统计人口不少于1000万的大洲 SELECT continent FROM world GROUP BY continent HAVING SUM(population) >= 10000000;第二道题里面,HAVING和WHERE的区别就体现出来了:WHERE在分组之前过滤行,HAVING在分组之后过滤组。举个例子,如果先按大洲分组再算总人口,但你想排除某个特定国家再分组,那就得用WHERE:
SELECT continent, SUM(population) FROM world WHERE name <> 'China' GROUP BY continent;这个模块里还经常用到COUNT(DISTINCT column),作用是去重计数。实际工作中这个用法也很常见,比如统计“活跃用户数”,一个用户可能有多条记录,用COUNT(DISTINCT user_id)才能得到真正的用户数。
2.6 JOIN与More JOIN:多表连接的思维转换
JOIN是SQLZOO里篇幅最多的模块之一,因为它确实是实际工作中用得最多的能力。SQLZOO有专门的演员表和电影表,More JOIN就是在这些表上练习INNER JOIN、LEFT JOIN等。
典型解法:
-- 找出每部电影的上映年份和导演 SELECT movie.title, movie.yr, movie.director FROM movie; -- 找出所有的演员和出演的电影名 SELECT actor.name, movie.title FROM actor JOIN casting ON actor.id = casting.actorid JOIN movie ON casting.movieid = movie.id;这个模块里最有价值的一道题是:列出每部电影的演员数量。它需要先用GROUP BY按电影分组,再统计每个电影的演员数:
SELECT movie.title, COUNT(actorid) FROM movie JOIN casting ON movie.id = casting.movieid GROUP BY movie.id;这里有个SQL细节:GROUP BY movie.id用的是主键,而不是movie.title或movie.name。为什么?因为不同电影可能有同名但不同年份的情况,按主键分组才是最可靠的分组依据。这是实际开发中被反复强调的原则:GROUP BY要用唯一标识,别用容易重复的字段。
2.7 Using Null:处理空值的那点事
这个模块专门教你如何处理NULL。NULL是SQL里最容易出幺蛾子的地方,因为NULL不是“空字符串”,也不是“0”,而是“未知值”。任何NULL参与的算术运算结果都是NULL,任何NULL参与的比较结果都是UNKNOWN,而WHERE只接受TRUE,不接受UNKNOWN。
典型题目:
-- 找出没有出演任何电影的演员 SELECT name FROM actor WHERE id NOT IN (SELECT actorid FROM casting); -- 危险版本:如果casting.actorid里有NULL,上面这条可能返回空结果这个坑特别隐蔽:如果子查询返回的结果集中包含NULL,NOT IN会返回空集,而不是你预期中的“不在里面”的记录。原因是NULL参与NOT IN比较时,结果既不是TRUE也不是FALSE,而是UNKNOWN,所以整条记录被过滤掉了。正确的做法是用NOT EXISTS:
SELECT name FROM actor WHERE NOT EXISTS (SELECT 1 FROM casting WHERE casting.actorid = actor.id);SQLZOO里还有配套的COALESCE函数练习,它可以把NULL替换成默认值:
SELECT name, COALESCE(party, 'No party') FROM teacher;实际工作中处理NULL,第一原则就是:别想当然地认为NULL会怎样参与运算,先想清楚NULL对结果的影响,再决定是用COALESCE、IS NULL还是NOT EXISTS。
2.8 Self join:一张表的自我关联
Self join,中文叫自连接,就是用一张表和自己做JOIN。这个模块最经典的例子是“找出伦敦巴士线路中,所有可以换乘的车站对”。
典型解法思路:
-- 找出所有“从A站到B站,再从B站到C站”的换乘关系 SELECT a.company, a.num FROM route a JOIN route b ON (a.company = b.company AND a.num = b.num) JOIN stops s ON a.stop = s.id WHERE s.name = '某站名';这里route表既是起点表,又是终点表。自连接的核心理解点在于:JOIN时不限定必须两个不同的表,同一个表只要起了不同的别名,就可以把它当作两个不同的集合来关联。我见过不少人在工作里碰到“同一张表要关联两次”的时候绕不过弯来,其实就是别怕起别名,把它想象成两张结构相同的表就行。
2.9 窗口函数:现代SQL的进阶必修课
窗口函数是SQLZOO后来加入的模块,也是现在面试的高频考点。窗口函数和GROUP BY的核心区别在于:GROUP BY会把多行合并成一行,窗口函数不会——它保留每一行的信息,同时额外返回一个聚合结果或排序序号。
经典题:
-- 按人口排名,ROW_NUMBER会生成无重复的排名 SELECT name, population, ROW_NUMBER() OVER (ORDER BY population DESC) AS rn FROM world; -- 按大洲分组,组内按人口排名 SELECT name, continent, population, RANK() OVER (PARTITION BY continent ORDER BY population DESC) AS rank FROM world;ROW_NUMBER、RANK、DENSE_RANK这三个函数的区别是高频考点:
| 函数 | 相同值排名 | 后续排名 | 举例 |
|---|---|---|---|
| ROW_NUMBER | 不同,按顺序给号 | 顺延 | 1, 2, 3, 4 |
| RANK | 相同,并列 | 跳过后续 | 1, 1, 3, 4 |
| DENSE_RANK | 相同,并列 | 不跳过 | 1, 1, 2, 3 |
实际工作中,计算“每个部门工资最高的员工”这类需求,窗口函数比自连接和子查询要简洁得多:
SELECT name, department, salary FROM ( SELECT name, department, salary, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rk FROM employees ) t WHERE rk = 1;窗口函数刚接触的时候觉得抽象,但一旦理解了PARTITION BY就是“分组但不压缩行”这个点,绝大部分场景都能很快上手。
3. 做题过程中最常见的几个坑
3.1 GROUP BY与SELECT的字段限制
SQLZOO的SUM and COUNT模块里,有大量题目明明看着很简单,写出来却报错。最常见的报错就是:SELECT里出现了既不在GROUP BY中,也不是聚合函数的字段。
-- 错误 SELECT name, continent, COUNT(*) FROM world GROUP BY continent; -- 正确 SELECT continent, COUNT(*) FROM world GROUP BY continent;第一条语句里,name既不在GROUP BY里面,也不是聚合函数,所以报错。现代数据库(比如MySQL的ONLY_FULL_GROUP_BY模式、PostgreSQL、SQL Server)都会直接拒绝这种写法。原因很简单:分组之后,每组里有多个name,数据库不知道该取哪一个。
我在实际工作中见过有人为了绕过这个问题,用ANY_VALUE(name)或者直接把name加进GROUP BY。前者要慎用,如果你不关心组里的具体值,那没问题;如果关心,就要想清楚你到底想取哪一条记录。后者则要小心:把name加入GROUP BY之后,分组的粒度会变得更细,聚合结果可能和你预期的完全不同。
3.2 WHERE和HAVING的边界
做题时经常有人问:“为什么这个过滤条件加WHERE不行,加HAVING才行?”答:看过滤的对象是行还是组。
- WHERE:在分组前逐行过滤,不能使用聚合函数
- HAVING:在分组后对组进行过滤,可以使用聚合函数
一个直观的例子:
-- 找出一个以上国家使用语言超过3种的大洲 SELECT continent FROM world GROUP BY continent HAVING COUNT(DISTINCT language) > 3;这里的COUNT(DISTINCT language) > 3是在分组结果上判断的,必须用HAVING,用WHERE会直接报语法错误,因为WHERE阶段还没聚合,不知道COUNT是多少。
3.3 JOIN时条件写ON还是WHERE
这个坑在More JOIN模块里体现得很明显。JOIN的条件写在哪里,会影响最终结果。
ON:决定两个表如何匹配WHERE:在匹配完成后的结果集上过滤
对于INNER JOIN,ON和WHERE的过滤逻辑是等价的,因为不符合ON条件的行根本不会被匹配进来。但对于LEFT JOIN,ON里的条件写在ON子句中,会保留左表的全部行,右表没有匹配到的以NULL填充;如果把条件放到WHERE里,那些右表为NULL的行就会被过滤掉。
-- LEFT JOIN + ON:返回所有教师,即使没有对应部门 SELECT teacher.name, dept.name FROM teacher LEFT JOIN dept ON teacher.dept_id = dept.id; -- LEFT JOIN + WHERE:会过滤掉没有部门的教师 SELECT teacher.name, dept.name FROM teacher LEFT JOIN dept ON teacher.dept_id = dept.id WHERE dept.name IS NOT NULL;想清楚你要的结果是“保留左表全部行”还是“只保留能匹配上的行”,再决定条件放ON还是WHERE。这个看起来很小的区别,在一次统计报表里写错,结果数据就全偏了。
3.4 子查询返回多行时的处理
SQLZOO子查询模块里,最容易犯的错误就是用=去比较一个返回多行的子查询。SQL规范里,=只接受标量(单个值),子查询返回多行时要用IN、ANY或ALL。
-- 找出比欧洲所有国家GDP都高的国家 SELECT name FROM world WHERE gdp > ALL (SELECT gdp FROM world WHERE continent = 'Europe' AND gdp IS NOT NULL); -- 找出比欧洲至少一个国家GDP高的国家 SELECT name FROM world WHERE gdp > ANY (SELECT gdp FROM world WHERE continent = 'Europe' AND gdp IS NOT NULL);ANY和ALL很多人在学习阶段很少用,但理解它们可以让你少写很多冗余的MAX/MIN子查询。注意子查询里通常要显式排除NULL,因为NULL参与比较会让结果变空或者不符合直觉。
3.5 NULL参与计算时的结果偏移
Using Null模块设计的题目,就是逼你意识到NULL的存在。比如统计某字段总和时,NULL会被直接忽略,而不是当作0。这在累加金额、汇总数量时尤其危险:
-- 如果amount字段有NULL,SUM的结果是不包含这些NULL行的 SELECT SUM(amount) FROM orders;要区分“NULL被忽略”和“NULL当作0”两种情况。如果业务上NULL表示“无数据”,SUM忽略它通常没问题;但如果NULL表示“欠款未录”,那SUM的结果就会偏小。这时候需要用COALESCE把NULL转成0再聚合:
SELECT SUM(COALESCE(amount, 0)) FROM orders;4. 做完SQLZOO之后,这些方向值得继续深挖
4.1 慢SQL优化:EXPLAIN主要看什么
SQLZOO帮你解决了“怎么写SQL”的问题,但实际工作中还有一关:SQL写对了还不够,还得跑得快。热搜词里的“慢SQL优化 explain主要看哪些信息”,就是这个方向的核心。
一条SQL执行慢,第一时间用EXPLAIN看执行计划。主要看四个信息:
| 指标 | 含义 | 关注点 |
|---|---|---|
| type | 访问类型 | 从好到差依次是system > const > eq_ref > ref > range > index > ALL,避免ALL全表扫描 |
| key | 实际使用的索引 | 是否为NULL |
| rows | 预估扫描行数 | 数字越小越好 |
| Extra | 附加信息 | 出现Using filesort、Using temporary要警惕 |
最常见的慢SQL原因就是:WHERE条件或JOIN条件没有走索引,导致全表扫描。解决办法也很直接:给索引列加索引,避免对索引列做函数运算(WHERE YEAR(create_time) = 2024会导致索引失效,应写成WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'),避免使用前置通配符的LIKE(LIKE '%abc'无法走索引)。
还有一类慢SQL是“大表JOIN小表”的顺序问题。SQL优化器一般会自动调整,但如果你用了复杂的子查询,优化器可能选错执行顺序。这种情况下,把子查询改成JOIN、或者强制指定驱动表,经常能立竿见影。
4.2 SQL去重:DISTINCT还是GROUP BY
热搜词里有大量的“sql语句去重查询”,说明这是日常开发的高频需求。去重的两种主流写法:
-- 方式一:DISTINCT SELECT DISTINCT customer_id FROM orders; -- 方式二:GROUP BY SELECT customer_id FROM orders GROUP BY customer_id;执行结果上,如果只是对单个字段或一组字段去重,两者语义相同。但GROUP BY可以做更复杂的事情:去重的同时统计每个分组的数量、保留其他聚合结果。DISTINCT则更“纯粹”,可读性更好。
还有一个高频需求是“去重后统计数量”:
-- 统计有多少个不同的客户下了单 SELECT COUNT(DISTINCT customer_id) FROM orders;注意COUNT(DISTINCT field)对NULL的处理:NULL不会计入COUNT,如果业务上需要把NULL也算作一个值,要用COUNT(DISTINCT COALESCE(field, 'unknown'))。
4.3 SQL注入:写SQL时必须有的一根弦
热搜词里“sql注入万能密码绕过”“ctfshow web入门 sql注入”这些都是安全方向的话题。虽然SQLZOO不教这个,但作为一个经常写SQL的人,必须知道SQL注入是怎么回事,否则你写的接口可能就是别人攻击的入口。
SQL注入最常见的原理是:外部输入直接拼进SQL语句,导致输入被当作SQL代码执行。比如:
-- 危险:直接把用户输入拼进SQL SELECT * FROM users WHERE username = 'admin' AND password = 'xxx'; -- 如果用户名输入 `admin' -- `,密码随便填,这就变成了 SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'xxx';--是SQL的注释符,后面的内容全被注释掉了,攻击者不需要知道密码就能登录。防御手段的核心是“参数化查询”,让数据库把输入当成数据而不是代码:
// 用PreparedStatement,不要字符串拼接 PreparedStatement ps = conn.prepareStatement( "SELECT * FROM users WHERE username = ? AND password = ?" ); ps.setString(1, username); ps.setString(2, password);平台上的SQLZOO是纯练习环境,不存在注入风险。但养成“任何外部输入都不直接拼SQL”的习惯,是每个SQL使用者走向专业必须迈过的一道坎。
4.4 SQL Server相关:版本选择与安装避坑
热搜词里大量出现“SQL Server 2019安装教程”“SQL Server 2022下载”“SQL Server 2008 R2”等词,说明不少人在学习SQLZOO之后,想在自己电脑上搭一套数据库环境练手。这里有个建议:如果是新学SQL,直接装最新稳定版SQL Server 2022 Developer版本,功能和企业版一样,免费,只是不能用在生产环境。2008 R2和2012虽然在一些旧公司还在用,但2008 R2早已停止主流支持,新环境不建议装。
安装过程中最常见的坑有三个:
- 安装时报“无法启动 Windows Management Instrumentation (WMI) 服务”,先检查Windows服务里WMI是否被禁用。
- 卸载报“警告26003:无法卸载安装程序支持文件”,多是因为残留了旧版本,先清理干净再装新的。
- 装完之后连接不上,大多数情况是SQL Server的TCP/IP协议没启用,在“SQL Server配置管理器”里把TCP/IP启用并重启服务就好了。
4.5 窗口函数的实战拓展
窗口函数是SQLZOO里比较新也比较难啃的模块,但它绝对是实际工作中投入产出比最高的SQL能力。举几个高频实战场景:
- 排名:每个部门工资排名TOP3
- 同比环比:计算每个月销售额与上个月的差额,用LAG函数
- 累计求和:计算每日累计销售额,用SUM(...) OVER (ORDER BY day)
- 分组TopN:每个分类下销量前5的商品
-- 每个月销售额与上个月的差额 SELECT month, sales, LAG(sales, 1) OVER (ORDER BY month) AS prev_sales, sales - LAG(sales, 1) OVER (ORDER BY month) AS diff FROM monthly_sales;窗口函数熟练之后,会发现它能替代掉大量原来要写自连接、写复杂子查询的场景,代码更短,性能也更好。这也是为什么现在数据分析师、后端开发的面试题里,窗口函数几乎成了必考点。
5. 给你的刷题顺序和学习建议
最后聊一点我的实际体会。
SQLZOO这套题,我见过不少人的刷法是:从第一题一路往后做,卡住了就搜答案,看完了就继续下一题。这样做完一遍,感觉都会了,但过一周再来看,又全忘了。原因很简单:做题的时候缺少一个“主动提取”的过程。
我的建议是:每做完一个模块,合上答案,自己重新写一遍这个模块里出错最多的几道题,然后自言自语讲一遍——这道题考的是什么知识点,为什么要这么写。讲得出来,才说明你真的掌握了,这比闷头刷完100道题有用得多。
另外,SQLZOO的英文原版里,每个模块前面有一段简短的知识点介绍,中文版的翻译虽然能看懂,但有些术语还是建议对照英文看一遍,比如“scalar subquery”“correlated subquery”“window frame”。因为在查文档、看面试题的时候,大部分资料都是英文的,提前把这些术语混个脸熟,后面会顺畅很多。
刷完这套题之后,你的SQL地基就算打牢了。接下来可以去找真实业务数据动手练,比如把公司某个报表需要的SQL写出来、去分析一条线上慢SQL的执行计划、或者尝试用窗口函数重写一段以前写得特别别扭的查询。SQL这门手艺,光看不练永远是纸上谈兵,写得多、错得多、再改得多,才是真正变成自己的东西。