1. DQL的核心价值与执行顺序
1.1 为什么数据库入门必须死磕DQL
如果你手头刚好有一本数据库教材,翻到目录页,多半会看到“数据查询语言DQL”这个章节排在靠前的位置。很多初学者容易把它当成SQL众多语法分类里普通的一支,觉得背一背SELECT语法、记几个关键字就算过关了。但以我带过项目的经验来看,这种认知低估了DQL的重量级。
DQL是SQL四大分类里唯一一个专门负责“读数据”的部分。整个数据库系统花费大量精力去做的索引设计、缓存优化、存储引擎选择,最终服务的核心目标就是让DQL跑得更快、查得更准。你可以不写INSERT、不碰UPDATE、暂时用不到事务控制,但只要有数据要展示、有报表要统计、有接口要返回结果,就绝对绕不开SELECT。说得更直白一点:你日常在业务系统里写的所有查询逻辑,95%以上都属于DQL的范畴。
这篇笔记我想围绕DQL做一次系统性的拆解。我不会按教科书那样先从语法定义讲起,而是从执行顺序、过滤逻辑、聚合分组、关联查询、子查询这几个核心大块入手,把每个部分背后的设计思路和实际开发里的坑位都摆出来。适合刚学完SQL基础想巩固一遍的初学者,也适合写过一些查询但总觉得功力不够扎实的开发新人。
1.2 SQL书写顺序与执行顺序的差异
刚开始接触DQL的同学最容易懵的一个点,是SQL语句的书写顺序和实际执行顺序并不一致。你写的SELECT可能列在最前面,但数据库执行的时候,它反而是比较靠后的一步。
一条完整的查询语句,典型的书写顺序是这样的:
SELECT DISTINCT 列名或表达式 FROM 表名 JOIN 另一张表 ON 连接条件 WHERE 过滤条件 GROUP BY 分组字段 HAVING 对分组结果过滤 ORDER BY 排序字段 LIMIT 返回行数但数据库引擎内部的执行逻辑,一个经典的心智模型是这样排列的:
- 先确定数据来源:从哪些表取数据(FROM和JOIN)
- 再按条件逐行过滤:把不需要的行扔掉(WHERE)
- 然后对剩余行做分组聚合(GROUP BY)
- 分组之后用HAVING再筛掉不合格的组
- 接着才计算SELECT后面写的表达式和列
- 然后做去重(DISTINCT)
- 再排序(ORDER BY)
- 最后才做分页截断(LIMIT)
如果你把这个执行顺序记熟了,后面很多语法限制就不用死背了。举个例子,为什么WHERE里不能直接用聚合函数做条件?因为WHERE执行的时候,GROUP BY还没跑,数据还是逐行的状态,聚合结果压根不存在。为什么HAVING可以筛选分组字段?因为它排在GROUP BY之后执行,分组结果已经出来了。很多人分不清WHERE和HAVING的区别,其实核心就是执行位置不同。
这个顺序的心智模型,向后延伸到SQL调优也用得上。比如你想优化一条慢查询,脑子里第一个问题就应该是:这语句在执行顺序的哪一部损耗最大?是FROM阶段没走索引,还是WHERE阶段扫描量过大,或者排序阶段内存不够用。定位到具体阶段再动手,比瞎试快得多。
2. 单表查询的必备细节与实操技巧
2.1 SELECT的字段选择与别名使用
单表查询是DQL最基本的形态。语句本身不复杂,但细节决定了代码质量和后续维护成本。
先说说字段选择。我见过不少新手习惯写SELECT *,理由是“简单省事”。在自己练手的Demo里这么搞问题不大,但在真实的业务代码里,这是一个值得警惕的坏习惯。核心原因有两个。第一,SELECT *会把表里所有的列都查出来,如果这张表有十几二十个字段,大部分可能根本用不上。多查出来的数据会白白占用网络传输和内存开销,在单表数据量几百万行时尤其明显。第二,代码的健壮性差。表结构一变,比如某天DBA给表加了一个大字段,SELECT *的结果集就会跟着变,接口返回的JSON结构可能就悄悄变了,排查起来费老大劲。所以除非是临时在命令行里看一眼数据,正式代码里我都建议把需要的字段一个一个列出来。
再说别名。给表名和列名取别名,不只是为了少打几个字符。如果你的SQL涉及子查询或者多表关联,结构清晰与否全靠别名撑场面。实际开发中,我会给每个表都设置简短有意义的别名,例如users u、orders o,然后在所有字段引用的地方都带上这个别名前缀。这样做最大的好处是SQL的可读性和可维护性显著提升——哪怕一个月后回来看这条语句,也能一眼看出每个字段来自哪张表。
-- 一个带别名的典型查询 SELECT u.name AS 用户名, o.order_no AS 订单号, o.total_amount AS 订单金额 FROM users u JOIN orders o ON u.id = o.user_id WHERE u.status = 1 ORDER BY o.created_at DESC LIMIT 20;AS关键字写不写都可以。我习惯写上,纯粹是为了让语义更清楚,尤其在表达式运算的地方。
2.2 WHERE过滤条件的几个经典陷阱
WHERE子句是查询条件的主战场,这里集中了最多的实际坑位。有些问题不踩一次很难真正记住。
判空别用等号。WHERE name = NULL永远查不到数据。NULL在SQL里代表“未知值”,它不等于NULL本身,任何与NULL做等值比较的表达式结果都是UNKNOWN,而UNKNOWN会被当作不满足条件处理。正确写法是WHERE name IS NULL,或者查非空用WHERE name IS NOT NULL。这个错误我见过不止一次出现在工作了几年的人写的代码里,属于极为隐蔽又极为低级的失误。
**字符串与数值的隐式转换。**如果某张表的字段类型是VARCHAR,你却拿一个整数去比较,比如WHERE phone = 13800138000,数据库会尝试做隐式类型转换。问题在于,一旦字段被函数或者隐式转换包裹,索引通常会失效,全表扫描就来了。数据量小的时候感觉不到,等表到了几百万行,一条原本能秒回的查询可能一卡就是好多秒。我的建议是:写条件之前先搞清楚字段的真实类型,无论何时都不要依赖数据库的隐式转换。
**多个条件用AND与OR连接时注意括号。**SQL里AND优先级高于OR,但人的阅读习惯往往不这么认为。如果条件里同时出现AND和OR,最好显式加括号,否则很容易出现明明你想表达“A且(B或C)”,实际SQL却被理解成“(A且B)或C”的情况。这个错误一旦在数据量大的时候发生,查询结果对不上,排查时相当头疼。
再来一个优化向的小经验:过滤条件尽量写在JOIN之前还是之后?严格来说,对于INNER JOIN,写在WHERE还是JOIN的ON里,结果一样。但性能上,如果能在JOIN之前先把单表数据量降下来,后续参与连接的数据量就会更小,整体消耗更可控。所以我会优先把过滤条件直接放在WHERE里,让数据尽早缩小范围。
2.3 ORDER BY与LIMIT的搭配心得
排序和分页是绝大多数业务查询的收尾动作,但这里也有不少门道。
ORDER BY默认是升序ASC,降序要显式写DESC。排序字段如果有多个,从左到右依次生效:先按第一字段排,遇到相同的再按第二字段排。这一块比较直白,新手容易忽略的是汉字排序问题。在MySQL里,汉字排序跟字符集校对规则有关,默认情况下是按编码排序,不是按拼音,更不是按笔画。如果你要支持中文拼音排序,写法是ORDER BY CONVERT(name USING gbk),但这种写法大概率会让索引失效,数据量大的时候要慎重,最好在设计表时额外加一个拼音排序字段。
LIMIT分页有个经典性能问题:页码越深,查询越慢。原因是LIMIT 200000, 20这种写法,实际执行时数据库仍然要先把前面20万行都找出来,然后丢弃,再返回后面的20行。页面翻到几百上千页时,这个开销会越来越大。常见优化手段是“延迟关联”:先通过索引定位到需要的主键ID范围,再拿这批ID去关联取完整数据。举个例子:
-- 深分页的常见写法(数据量大时性能不佳) SELECT * FROM orders ORDER BY id LIMIT 200000, 20; -- 延迟关联优化写法 SELECT o.* FROM orders o JOIN (SELECT id FROM orders ORDER BY id LIMIT 200000, 20) t ON o.id = t.id;子查询里先只取主键,Order By和Limit都在那条轻量的查询里完成,再跟原表做一次关联。实测下来,在百万级数据量的场景里,这种写法往往能快上好几倍。
DISTINCT去重也经常跟ORDER BY搭配出现。注意DISTINCT作用的范围是SELECT子句中所有列的组合,而不是单列。SELECT DISTINCT name, age和SELECT DISTINCT name含义完全不同。如果只想去重一列,但还想查其他列,那DISTINCT根本满足不了你的需求,得换思路。
3. 分组统计与聚合函数的运用
3.1 聚合函数的使用边界
DQL里最出彩的一部分能力,是用一组聚合函数对大量明细数据做统计汇总。常用的聚合函数无非COUNT、SUM、AVG、MAX、MIN这几个,但用法细节值得说道。
COUNT可能是被误解最多的一个。COUNT(*)统计的是结果集行数,包括NULL行;COUNT(具体字段)统计的是该字段不为NULL的行数。我见过有人统计总数时写了COUNT(remark),结果发现有几行的remark是NULL,总数就少了,业务上死活对不上账。所以统计总行数就老老实实写COUNT(*),别搞骚操作。另外,COUNT在MyISAM和InnoDB引擎里的实现逻辑也不同,InnoDB因为要支持事务,没有单独存储行数,所以COUNT在大表上是需要实际扫描的,别指望它像计数器的表一样瞬间出结果。
SUM和AVG遇见NULL值时的表现也值得留意。SUM(字段)会自动忽略NULL,所以如果整列全是NULL,SUM的结果不是0而是NULL,这个返回类型在Java等语言里接收时容易产生空指针判断失误。AVG同理,分母是“非NULL行数”而不是“总行数”。如果你希望AVG把NULL当成0参与计算,得自己用IFNULL或COALESCE做预处理。
聚合函数里不能混用普通字段是不少新手的知识盲区。比如你写了SELECT name, MAX(salary) FROM employees,逻辑上你期望的是“拿到工资最高的那个人的名字”,但SQL标准并不保证这种结果。因为分组维度不明确,数据库到底取哪个name行为是不确定的。如果你想要的是每一组里的最高工资对应的人,就得用到窗口函数,或者子查询。MySQL的默认模式(ONLY_FULL_GROUP_BY未开启)下,甚至不报错直接给你一个随机值,这种数据拿出来就是事故。
3.2 GROUP BY分组逻辑与HAVING过滤
GROUP BY的本质是把“多行合并成一组”,然后对每一组做聚合运算。这个逻辑里最核心的认知是:**分组之后的查询结果里,SELECT后面能出现的东西只有两类——分组字段本身,以及由分组字段聚合计算得到的表达式。**其它原始列的数据在分组后已经失去了逐行的意义,因为它们可能来自组内不同的行。
举一个实际的例子。假设有一张销售流水表,里面有城市、渠道、销售额。你现在想统计每个渠道的总销售额:
SELECT 渠道, SUM(销售额) AS 总销售额 FROM 销售流水 GROUP BY 渠道 ORDER BY 总销售额 DESC;执行顺序是:先把所有行按渠道分类,然后对每个渠道桶里的销售额做求和,最后按总销售额降序排列。搞清楚这个流程后,你就明白了为什么SELECT里写非分组字段会有问题。
HAVING和WHERE的区别,前面已经提过执行时机不同。实际使用中,我的判断标准很简单:**凡是能用WHERE过滤掉的,绝不放进HAVING。**比如你想查“销售额超过1000元的渠道”,如果“销售额超过1000元”是对单行数据的过滤,就先在WHERE里干掉;如果是对分组后的汇总结果做过滤,比如“总销售额大于10万的渠道”,就必须用HAVING了。把效率不高的HAVING用到极致,也是一种典型的性能杀手。
GROUP BY还有一个常见搭配是配合WITH ROLLUP做小计汇总,MySQL和部分数据库支持这个语法。它会额外生成一行总计结果,在报表场景里很方便。但要注意,用了WITH ROLLUP之后,返回结果里会多出NULL标志的总计行,代码里要单独判断处理,不然容易跟正常数据混淆。
4. 多表关联查询的进阶应用
4.1 内连接与外连接的选型
单表查询解决的是“一张表里能回答的问题”。但业务建模为了减少数据冗余,会把数据打散到多张表里,用外键去关联。所以多表关联查询就成了DQL中绕不开的核心技能。
最常见的关联类型有INNER JOIN、LEFT JOIN、RIGHT JOIN。从集合论的角度来理解会更清楚:INNER JOIN取两张表的交集;LEFT JOIN保留左表的全部行,右表能匹配上就带上数据,匹配不上就用NULL填充;RIGHT JOIN则方向相反。不过我自己的习惯是能不用RIGHT JOIN就不用,因为它可以把逻辑对称性打破,阅读代码的人还得在心里做一次左右镜像,不如统一用LEFT JOIN实现同样效果。
写关联查询的时候,有一个思路很关键:首先确认你要以哪张表为“主表”。这是JOIN类型选择的锚点。比如你在做一个订单列表接口,需求是“展示所有订单,并带出每个订单对应的用户姓名”,那你的主表显然应该是订单表,即FROM orders,然后LEFT JOIN users ON orders.user_id = users.id。如果需求反过来是“展示所有用户,并带出他们近期的订单数”,主表就变成用户表,用LEFT JOIN把订单表带进来。
连接条件本身也讲究。ON后面写的关联字段,两边的数据类型要保持一致,避免隐式转换。并且关联字段上要有索引。这一点极其重要:关联查询性能差,十有八九是关联字段没有索引,导致每一行都要去全表扫描匹配一次。
4.2 子查询的应用与改写思路
子查询就是嵌套在SELECT、FROM或WHERE里的一整条SQL。按照出现位置的不同,可以大体分三类。
WHERE子句中的子查询,典型用法是配合IN、EXISTS做条件过滤。比如“找出近期下过订单的用户”,先查出订单表里去重后的user_id,再用IN去过滤用户表。这种写法直观易懂,但要注意IN后面的结果集如果很大,性能就会下降。EXISTS是另外一种写法,逻辑上更接近“存在性检测”,优化器处理起来往往更灵活,尤其在子查询数据量比较大的场景下,实测EXISTS往往比IN表现更好。
-- 子查询 + IN 的写法 SELECT name FROM users WHERE id IN (SELECT DISTINCT user_id FROM orders WHERE created_at > '2024-01-01'); -- 子查询 + EXISTS 的写法 SELECT name FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.created_at > '2024-01-01' );FROM子句中的子查询,可以把它理解为“先查出一个临时的结果集,再把它当成一张新表来查”。这种写法在处理多层聚合统计时非常好用。比如“统计每个渠道的总销售额,再找出其中超过平均值的渠道”——你需要先算平均数,再做比较,天然适合先搞一个临时表。
SELECT 渠道, 总销售额 FROM ( SELECT 渠道, SUM(销售额) AS 总销售额 FROM 销售流水 GROUP BY 渠道 ) t WHERE 总销售额 > (SELECT AVG(总销售额) FROM ( SELECT 渠道, SUM(销售额) AS 总销售额 FROM 销售流水 GROUP BY 渠道 ) t2);这种SQL写多了会有个问题:可读性急速下降,嵌套再嵌套会让人吐血。所以现在很多新项目里,遇到复杂逻辑我更推荐把子查询改写成公共表达式CTE,用WITH语句分成几步走。WITH有时候也被称为“临时视图”,它的最大优势是能把一坨复杂的嵌套逻辑拆成一段段有名字的步骤,每一段单独阅读都很清楚,调试起来也方便。
WITH 渠道汇总 AS ( SELECT 渠道, SUM(销售额) AS 总销售额 FROM 销售流水 GROUP BY 渠道 ) SELECT 渠道, 总销售额 FROM 渠道汇总 WHERE 总销售额 > (SELECT AVG(总销售额) FROM 渠道汇总);这一段SQL读起来就顺多了,一眼能看明白意图。CTE在可读性和维护性上的优势,在复杂统计场景里几乎碾压嵌套子查询。
4.3 多表查询中的NULL与重复数据
关联查询产生NULL值是非常正常的现象。LEFT JOIN时右表匹配不上的字段就是NULL。所以对关联查询的结果做过滤时,同样要遵守之前说的NULL判断规则,别用= NULL。另外,在连接条件里如果右表的关联字段本身可能有NULL,那这些行用ON关联时是匹配不上的,INNER JOIN不会包含它们,LEFT JOIN则会以NULL形态带出来。这个细节在做数据对齐时容易对不上数。
重复数据也是多表查询的老朋友。当左表一行数据在右表对应多行时,LEFT JOIN会把左表那行复制多份,这就是行数膨胀。举一个经典场景:一张订单表,一张订单明细表,一个订单包含多个商品。如果你SELECT * FROM orders o LEFT JOIN order_items i ON o.id = i.order_id,那一个订单有几件商品,结果集就会有几行。这在统计“订单总数”时会闹笑话——COUNT(o.id)统计出来的不是订单数,而是订单明细行数。遇到这种场景,要么明确业务需求之后在统计处做去重COUNT(DISTINCT o.id),要么把维度理清楚再关联。
多表关联时把表前缀写全也是一个值得坚持的习惯。如果表里有重名的字段,比如用户表和订单表都有created_at,不写前缀查询结果和后端取值时都会出问题。如果在SELECT里只写了字段名没写前缀,数据库会报“字段不明确”。所以从一开始就养成带表别名前缀的写法,不然后续排查问题会非常难受。
5. 查询性能问题的初步诊断
5.1 EXPLAIN执行计划基础解读
DQL写得对不对,有时候需要靠性能来验证。真正到了生产环境,一条慢查询可能拖垮整个数据库。所以我觉得这一节值得单独记录一下,尤其对初学者来说,早一点接触执行计划,后面写SQL的习惯会好很多。
MySQL等主流关系型数据库都提供了EXPLAIN命令,用法很简单:在SELECT语句前加上EXPLAIN关键字。结果里会输出一张表,里面关键字段有几个是必看的。type字段反映了访问类型,从好到坏大致是system、const、eq_ref、ref、range、index、ALL。ALL意味着全表扫描,通常是性能瓶颈的红色警报。key字段显示实际使用的索引,如果是NULL说明没用上索引。rows字段是个估算值,表示预计要扫描多少行。Extra字段里如果出现Using filesort或者Using temporary,说明排序或分组操作使用了临时表和文件排序,通常意味着大数据量下的性能问题。
我见过不少开发者的操作习惯是:SQL能跑出结果就行,压根不看执行计划。但一个能跑出结果的SQL,和能稳定扛住生产流量的SQL,中间隔着一整条执行计划的鸿沟。养成用EXPLAIN检查关键查询的习惯,对提升SQL功底非常有帮助。
5.2 慢查询的常见原因与规避方式
结合我自己的排查经验,慢查询最常见的几个原因无非下面几类。
类型不匹配导致索引失效。前面提过的隐式转换问题,这里再强调一次。字段是字符串类型,你传了整数,索引就废了;字段是日期类型,你传了字符串,同样可能废。排查这类问题,EXPLAIN一看key为NULL基本就能锁定。
函数包裹索引字段。WHERE DATE(created_at) = '2024-06-01'这种写法看起来很自然,但DATE()函数会把created_at的索引破坏掉。更优的写法是用范围条件:WHERE created_at >= '2024-06-01' AND created_at < '2024-06-02'。这既保留了索引的使用,语义也清晰。
LIKE模糊查询的前置通配符。LIKE '%keyword%'因为通配符在开头,走不了前缀索引。如果业务真的需要全文搜索,不要硬用LIKE,老老实实引入全文检索或者走专门的搜索引擎,别在SQL里死磕。
OR条件导致索引失效。多条件过滤时,OR连接的条件可能让优化器放弃索引。解决思路是把OR改写成UNION,把两个查询结果合并。尤其替换成UNION ALL时,性能改善往往很明显。
事务中执行大查询。如果在一个长事务里跑了一条大查询,哪怕SQL本身不慢,也可能因为持锁时间过长引起连锁性能问题。所以,查询类操作尽量在非事务环境下执行,或者缩短事务生命周期,也是在项目里见过的教训。
5.3 分页、排序与聚合的调优案例
再分享一个实战中的聚合调优思路。记得某次在项目里处理一张千万级的订单流水表,需要按天统计订单数并排序。最开始的SQL写法是直接对全表做WHERE过滤再加GROUP BY,结果每次查询都耗时数秒,接口直接超时。
后来优化的思路是分三步走。第一,业务上能预判的时间范围尽量收窄,减少扫描行数;第二,把按天分组结果预先通过定时任务算好,存进一张汇总表,查询直接读汇总表;第三,查询条件里对索引字段做范围过滤,让数据库多走索引少做全表扫描。改完之后,耗时从秒级直接降到了几十毫秒。
这个案例想说明的其实是一个朴素的道理:DQL查询一旦到了生产环境,就不能只停留在“查得对”的层面,必须考虑“查得快”。而查得快的基础,是对数据量的敬畏,以及对索引结构的基本理解。索引本质上就是一本“数据的目录”,写查询的时候多想想怎么让数据库更快地翻到你要的那几页。
6. DQL学习的实践建议与常见错误清单
6.1 用一套练习数据覆盖全部核心语法
DQL语法本身不算多,但如果不实际操作,看再多教程都是纸上谈兵。我给新手朋友的建议是:自己动手建一套练习用的小型数据库,比如模拟一个电商场景,包含用户表、商品表、订单表、订单明细表。表不用太复杂,每张表几十行数据就够,但设计要有意识地覆盖常用的字段类型:整数、字符串、日期时间、可空字段、外键关系。
然后拿着这套数据,把下列场景一个一个练过去:查全部字段、查指定字段、条件过滤、模糊匹配、范围查询、空值判断、排序、分页、去重、聚合函数、单字段分组、多字段分组、分组后过滤、INNER JOIN、LEFT JOIN、RIGHT JOIN、WHERE子查询、FROM子查询、CTE。每一个语法点都用同一个业务场景去套,比如“统计每个商品类别的总销售额”“找出购买次数超过3次的用户”“查询最近一周的订单明细并且带上商品名称”。二十来个练习做完,DQL的主要语感就算建立起来了。
掌握代码能力的核心从来不是看懂,而是练熟。你在自己电脑上踩过的坑,远比别人在文章里提前告诉你的坑记得牢固。
6.2 新手最容易踩的十个错误速查表
我把实际工作和带人过程中见过的典型错误汇总成一个速查表,建议保存下来,写SQL之前扫一眼。
| 错误类型 | 错误示例 | 正确写法 |
|---|---|---|
| NULL判断用等号 | WHERE name = NULL | WHERE name IS NULL |
| 统计行数用COUNT(可空字段) | COUNT(remark) | COUNT(*) |
| WHERE里用聚合函数 | WHERE COUNT(*) > 5 | 改用HAVING |
| SELECT混入非分组字段 | SELECT name, MAX(salary) | 明确分组维度或改子查询 |
| SELECT * | SELECT * FROM users | 显式列出所需字段 |
| 深分页直接LIMIT偏移 | LIMIT 1000000, 20 | 延迟关联或游标分页 |
| 关联查询不带表前缀 | SELECT id FROM users u JOIN orders o | SELECT u.id |
| LIKE前置通配符 | LIKE '%keyword%' | 能走索引的写法或用全文检索 |
| 字符串字段传数值 | WHERE phone = 13800138000 | 显式写成字符串 |
| OR连接条件 | WHERE a = 1 OR b = 2 | 改写UNION或用索引合并策略 |
6.3 一条完整DQL语句的拆解示例
最后写一个综合性的例子,把前面提到的所有语法点串起来。场景是:统计某电商平台上“2024年第一季度每个城市中消费总金额排名前3的会员用户”。
假设有会员表members(id、用户名、城市)、订单表orders(id、会员id、下单时间、订单金额)。
第一步先算每个会员在季度内的总消费:
WITH 会员消费 AS ( SELECT m.id AS 会员ID, m.用户名 AS 用户名, m.城市 AS 城市, SUM(o.订单金额) AS 总消费 FROM members m JOIN orders o ON m.id = o.会员ID WHERE o.下单时间 >= '2024-01-01' AND o.下单时间 < '2024-04-01' GROUP BY m.id, m.用户名, m.城市 )第二步在每座城市内部排序并取前3名,现代数据库普遍支持窗口函数写法,比如ROW_NUMBER:
SELECT 城市, 用户名, 总消费 FROM ( SELECT 城市, 用户名, 总消费, ROW_NUMBER() OVER (PARTITION BY 城市 ORDER BY 总消费 DESC) AS 排名 FROM 会员消费 ) t WHERE 排名 <= 3 ORDER BY 城市, 总消费 DESC;整个查询用了CTE、JOIN、WHERE范围过滤、聚合函数、GROUP BY、窗口函数、子查询和排序。一套流程走下来,你会发现自己对DQL的整体驾驭感已经和只会写简单SELECT时完全不同了。
我个人在实际操作中的一个体会是:DQL的学习曲线其实并不陡峭,卡住大多数人的根本不是语法,而是思维习惯——有没有想过数据是怎么存储的,有没有想过索引是怎么加速的,有没有想过一条SQL在数据库内部经历了什么。一旦你开始带着这几个问题去写每条查询,你的SQL水平会以超出预期的速度往上涨。最后再分享一个小技巧:遇到任何拿不准的写法,不要只在教学库上验证,尝试在EXPLAIN里看一眼执行计划,它会把很多你以为懂了、其实没懂的事情照出原形。