news 2026/10/20 8:46:16

SQL DQL查询全解:从执行顺序到索引优化的实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL DQL查询全解:从执行顺序到索引优化的实践指南

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 返回行数

但数据库引擎内部的执行逻辑,一个经典的心智模型是这样排列的:

  1. 先确定数据来源:从哪些表取数据(FROM和JOIN)
  2. 再按条件逐行过滤:把不需要的行扔掉(WHERE)
  3. 然后对剩余行做分组聚合(GROUP BY)
  4. 分组之后用HAVING再筛掉不合格的组
  5. 接着才计算SELECT后面写的表达式和列
  6. 然后做去重(DISTINCT)
  7. 再排序(ORDER BY)
  8. 最后才做分页截断(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 = NULLWHERE 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 oSELECT 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里看一眼执行计划,它会把很多你以为懂了、其实没懂的事情照出原形。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 8:46:04

URL编码本质解析:RFC 3986字符分层与工程避坑指南

1. 为什么URL编码不是“加个%就完事”的小技巧很多人第一次接触URL编码&#xff0c;是在浏览器地址栏里看到一串类似%E4%BD%A0%E5%A5%BD的字符&#xff0c;或者在调试接口时发现传过去的中文参数变成了name%E5%BC%A0%E4%B8%89。这时候第一反应往往是&#xff1a;“哦&#xff0…

作者头像 李华
网站建设 2026/10/20 8:43:53

TCP/IP协议栈实战:用数据包旅行串起网络分层与排障

我见过太多人把TCP/IP协议当成八股文来背&#xff1a;四层模型、三次握手、四次挥手&#xff0c;考试能默写&#xff0c;真到了机房排查问题的时候却完全派不上用场。这不是学习者的错&#xff0c;是学习方式出了问题——TCP/IP本身就是从实战中长出来的协议栈&#xff0c;脱离…

作者头像 李华
网站建设 2026/10/20 8:46:03

《数据结构(C语言版)》实战:从抽象类型到可运行代码的避坑指南

简介&#xff1a;这份资源是严蔚敏、吴伟民编著的《数据结构&#xff08;C语言版&#xff09;》PDF电子书&#xff0c;面向计算机专业学生、考研备考者以及需要夯实算法与数据结构基础的开发者&#xff0c;可用于课程学习、期末复习与考研专业课备考。压缩包内共1个PDF文件&…

作者头像 李华
网站建设 2026/10/16 22:22:55

Java爬虫实战:从技术选型到分布式架构的工程指南

1. 我为什么用Java写爬虫&#xff0c;而不是Python说到爬虫&#xff0c;很多人下意识就想到Python。这个问题我几乎每次分享都会被问到&#xff0c;尤其在团队内部做技术选型的时候。我的答案其实很简单&#xff1a;爬虫的核心从来不是语言&#xff0c;而是抓取、解析、存储和调…

作者头像 李华
网站建设 2026/10/16 14:25:26

高防IP实战调优:DDoS清洗策略、CC防护参数与黑洞排障全解析

业务上线第三天&#xff0c;凌晨两点半&#xff0c;监控大屏的流量曲线突然从 200Mbps 直接飙到 400Gbps。机房交换机告警、运营商封堵通知、客户投诉电话几乎同时砸过来。那一夜我盯着高防 IP 的拦截日志看了整整六个小时&#xff0c;也第一次真正体会到大流量 DDoS 攻击下&am…

作者头像 李华
网站建设 2026/10/16 5:40:48

SpringBoot2+Vue3宠物商城系统:前后端分离实战全解析

前后端分离项目写了几年&#xff0c;技术栈审美会变得异常挑剔。2020年以后我接到的商城类项目需求&#xff0c;十条里有七八条是同一套配方&#xff1a;SpringBoot2 写后端、Vue3 写前端、MyBatis-Plus 管数据访问、MySQL8 做存储。这个 Java Web 宠物商城系统项目就是很典型的…

作者头像 李华