资讯动态

DQL深度解析:从执行顺序到慢查询优化,掌握SQL查询核心逻辑

发布时间:2026/10/21 9:26:21 来源:尧图企业网站定制
说到DQL我印象特别深。刚入行那会儿我写查询就三板斧SELECT、FROM、WHERE能用一条语句查出来的绝不写第二条后来被一次慢查询事故狠狠教育了一顿才回头把DQL这条线整个啃了一遍。DQL全称是Data Query Language也就是数据查询语言日常开发里几乎天天都在用但很多人对它的理解停留在“能用就行”真到调SQL、优化报表、排查线上问题时才发现底子不够。这篇文章不打算讲那些官网上随处可见的语法清单我想把DQL里面真正影响你写查询质量的东西——执行顺序、连接与过滤的边界、聚合分组的逻辑、慢查询分析的方法——按我的理解重新捋一遍。适合刚学会写SELECT但想更进一步的同学也适合写过一段时间、还想系统补一补的开发者。1. DQL的边界它到底管哪些事又不该管哪些事1.1 SQL家族中的DQLSQL按功能划分成几块DQL数据查询、DML数据操作、DDL数据定义、DCL数据控制。DQL就是查数据的核心主角是SELECT以及跟它绑定的FROM、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT这些子句。很多人在学SQL时容易把DQL和DML混在一起其实边界很清楚SELECT只负责把数据读出来不负责改INSERT、UPDATE、DELETE属于DML负责写CREATE TABLE、ALTER TABLE属于DDL负责定义结构。搞清楚边界有一个实际好处当你的数据库账号只有查询权限时你能用的就是DQL这一套能不能把报表做好全看你对SELECT家族的理解有多深。1.2 一条完整DQL语句的骨架一条完整的查询通常长这样SELECT 字段 FROM 表 WHERE 过滤条件 GROUP BY 分组字段 HAVING 分组后的过滤条件 ORDER BY 排序字段 LIMIT 条数;很多初学者背得住这个顺序但不理解为什么HAVING在GROUP BY后面也不明白为什么GROUP BY和HAVING在某些数据库里可写可不写也能过。这里建议先把它当成“搭积木”的过程先选哪几张表再缩小数据范围再按维度汇总再对汇总结果做条件过滤最后排序截断。只要骨架搭对了后面所有优化和排错都有得聊。1.3 先跑一条最简单的查询以某业务项目里的订单表为例假设表名叫order_info字段包括order_id、user_id、amount、status、create_time。最基础的DQL就是SELECT order_id, user_id, amount FROM order_info WHERE status PAID ORDER BY create_time DESC LIMIT 10;这条语句意思很直白把已支付订单找出来按创建时间倒序取前10条。别小看这种语句它能跑对靠的是每一段逻辑各司其职。很多问题恰恰出在看起来“能跑”但结果不对比如把WHERE的条件写进HAVING或者把LIMIT放在子查询里没有外层约束这些之后我都会讲到。还要说明一点DQL不只是单表查询。多表关联、子查询、聚合、窗口函数都属于DQL的范畴。所以学DQL实际是在学一套“如何从数据库里精确取数”的思维框架。2. 执行顺序才是DQL的底层逻辑为什么WHERE用不了别名而ORDER BY可以2.1 书写顺序不等于执行顺序这是一条很重要的认知。SELECT语句的书写顺序是SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT但数据库实际执行顺序大致是FROM先确定数据来源表JOIN如果有连接先把多表拼出中间结果WHERE过滤原始行GROUP BY分组聚合函数计算HAVING过滤分组SELECT投影字段计算表达式DISTINCT去重ORDER BY排序LIMIT截断为什么必须知道这个顺序因为它直接决定了你能在哪一步引用什么。最经典的例子WHERE子句里不能直接使用SELECT里定义的别名因为执行到WHERE时SELECT的投影还没发生而ORDER BY里可以使用别名因为它靠后执行。如果不懂这一步遇到“Unknown column”报错就会一脸懵。2.2 “中间结果”概念你可以把每一步都理解为在生成一张虚拟的中间表先把所有来源表拼起来形成一张超大的行集WHERE从这张行集里一行一行挑把符合条件的留下GROUP BY把行集切块聚合函数在每个块上计算HAVING在切块之后再做条件判断保留满足条件的组SELECT决定最终输出哪些列可以按表达式加工ORDER BY在输出列上排序LIMIT最后截断行数。理解了这张中间表就能解释很多面试题也能解释很多实际怪象。比如JOIN之后行数变多是因为中间结果是一张笛卡尔积式的临时集合COUNT(*)统计的是中间结果的行数而不是原表的行数。这些用“中间结果”一想就能通。2.3 执行顺序在实际排查中的用处有一次我帮一个同学排查报表数据异常他的SQL是在FROM里先做子查询筛选再在外面套WHERE结果发现外面再过滤时数据反而少了。根源就是他把过滤条件放错了层级内层子查询里已经用GROUP BY把行数压下去了外层再过滤条件作用的行集完全变样。这种问题不靠死记而是靠执行顺序去推断每一步的行集长什么样。所以我在带人时有个笨办法让新人把一条复杂SQL拆成多步临时表写一遍再对比合并写法跑三次基本就能把执行顺序刻进脑子里。这里强调一点执行顺序不是所有数据库严格一致比如SQL标准与实际优化器的执行顺序会有差异但逻辑层的顺序可以作为分析和书写的依据。数据量一大优化器可能调整JOIN顺序但最终结果不能变。所以逻辑顺序用于理解物理顺序交给优化器。3. 条件过滤与表连接WHERE和JOIN的边界与坑3.1 三种常用JOIN怎么选INNER JOIN表示只保留两边都匹配的行LEFT JOIN保留左表全部行右表没有匹配的补NULLRIGHT JOIN反过来。实际业务里用LEFT JOIN最多因为它天然适合“以某张表为主去补其他表的属性”这种需求。比如订单表左连接用户表订单一定都保留用户信息没有就空着这是对的。3.2 JOIN条件和WHERE条件的先后差别这是很多人踩坑的地方LEFT JOIN时如果对右表的过滤条件写进WHERE会把左表中右表为NULL的行也淘汰掉导致结果“莫名变少”。比如SELECT o.order_id, u.level FROM order_info o LEFT JOIN user_info u ON o.user_id u.user_id WHERE u.level VIP;这条语句看起来是“查VIP用户的订单”但实际执行时WHERE会把右表为NULL的行全部过滤掉效果等同于INNER JOIN。如果只想保留左表所有订单、右表只有VIP信息过滤条件应该写在ON里SELECT o.order_id, u.level FROM order_info o LEFT JOIN user_info u ON o.user_id u.user_id AND u.level VIP;这个差别属于DQL里非常典型又容易忽视的知识点。正确理解是ON负责连接时的匹配条件WHERE负责结果集生成后的过滤条件。LEFT JOIN的语义是先保证左表不丢行再谈其他过滤一旦过滤发生在WHERE左表不丢行的保证就被打破了。3.3 JOIN与子查询的性能取舍有些需求能用多表JOIN实现也能用子查询实现比如“查每位用户的最近一条订单”。早期我喜欢用子查询后来发现某些数据库版本对相关子查询的优化并不好在大表上会引发逐行扫描。一个更可靠的做法是用窗口函数或JOIN去重。所以现在我的经验是能用JOIN表达的关联优先用JOIN子查询适合场景比较简单或有明确过滤语义的地方。当然没有绝对答案要以执行计划的结果为准这个后面有专门章节展开。3.4 三值逻辑与隐式转换WHERE条件里还有个隐形杀手NULL。SQL里NULL参与比较的结果是UNKNOWN不是TRUE也不是FALSE所以WHERE status NULL永远查不到数据必须写成IS NULL。再有类型转换字段是varchar条件里写数字数据库会做隐式转换可能导致索引失效。比如某系统的手机号字段是varcharWHERE phone 13800000000虽然能查到但可能走不了索引写成WHERE phone 13800000000类型一致才能稳定用上索引。这些都是DQL日常里非常容易踩的坑。我把几个最常见的条件类问题整理成了一张表方便排查时对照问题场景错误示例正确姿态原因判空WHERE name NULLWHERE name IS NULLNULL比较结果是UNKNOWN类型不匹配WHERE phone 13800000000WHERE phone 13800000000隐式转换可能让索引失效LEFT JOIN右表条件条件放WHERE条件放ON放WHERE会干掉NULL行多条件组合优先级WHERE a 1 OR b 2 AND c 3明确加括号AND优先级高于OR容易误读4. 聚合、分组与HAVING报表查询的正确打开方式4.1 聚合函数家族COUNT、SUM、AVG、MAX、MIN是最基础的五件套。使用时有个容易被忽略的点COUNT(*)统计行数COUNT(column)统计非NULL的个数两者结果可能不同。SUM(column)如果列里有NULL默认跳过NULL但不会报错AVG同理。所以算平均值时如果要去掉NULL或把NULL按0处理得提前想清楚否则报表数字会对不上。比如要算客单价有人直接AVG(amount)结果金额字段里混进几条NULL平均值就偏了更稳妥的做法是先明确SUM(amount)/COUNT(order_id)或者用COALESCE把NULL处理掉。4.2 GROUP BY的字段规则分组查询有个重要约束SELECT里出现的非聚合字段基本都要出现在GROUP BY里。这种设计不是语法洁癖而是一个逻辑保证分组后每组可能有多行你不告诉数据库“每行选谁的order_id”数据库就没法确定输出哪个值。有些数据库默认允许这么写但取到的一定是随机的某行这在实际报表里就是事故。所以我的建议是分组查询里的非聚合列要么进GROUP BY要么放进聚合函数要么根本不查。写完之后自查一遍可以省掉很多数据核对的时间。4.3 HAVING和WHERE谁管哪一段WHERE在分组之前过滤原始行HAVING在分组之后过滤组。两者分工的核心价值是先缩小数据量再分组可以大幅减少聚合计算的开销。能写在WHERE的过滤条件不要放在HAVING里。比如查“今年已支付订单按用户汇总且总金额超过1000”SELECT user_id, SUM(amount) AS total_amount FROM order_info WHERE status PAID AND create_time 2025-01-01 GROUP BY user_id HAVING total_amount 1000;这里WHERE先砍掉非支付和旧订单能让分组计算的数据量小很多HAVING只负责对聚合结果做筛选。如果你把status条件放HAVING里虽然结果可能一样但计算量和可读性都差一截。4.4 一个实际的报表拆解某业务项目要做“各城市已支付订单的客单价”写起来大概是SELECT city, COUNT(DISTINCT order_id) AS order_cnt, SUM(amount) / COUNT(DISTINCT order_id) AS avg_amount FROM order_info o JOIN user_info u ON o.user_id u.user_id WHERE o.status PAID GROUP BY city;这里有个小技巧是COUNT(DISTINCT order_id)因为订单表可能一条订单拆多条明细直接COUNT(*)会算重。如果数据不太大这个写法简洁直观如果明细量很大也可以先内层去重再外层聚合性能更好。DQL的问题很多不是“语法不会”而是“统计口径不清晰”。像客单价这种指标分子分母各是什么、要不要去重、时间范围截在哪都得在写SQL之前跟业务对齐。5. 进阶查询子查询和窗口函数的实战定位5.1 子查询的三个位置子查询可以出现在WHERE、FROM、SELECT三个位置各自作用不同。WHERE后面的子查询通常用于生成一个过滤集合比如查“购买过某类商品的用户”通常会用到IN或EXISTS。FROM后面的子查询像一个派生表把一个复杂的中间结果先算出来外面再继续查询。SELECT后面的子查询一般用来补充标量信息比如查每个订单的同时带出用户余额但这种方法在数据量大时容易产生性能问题用得克制。我见过不少同学喜欢在SELECT后挂一串子查询字段一多整个查询就变成了一次循环叠加非常伤性能。5.2 窗口函数解决排名问题“分组后取每组Top N”是高频需求典型场景是“每个品类销量排名前3的商品”。在没学窗口函数之前很多人会用一个相关子查询去数和比较写法绕性能也一般。窗口函数让这类问题变成常规操作SELECT category, product_id, sales, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn FROM product_sales;外面再包一层WHERE rn 3就能拿到每个品类的Top3。窗口函数不是把多行并成一行而是在每一行边上额外算出一个汇总或排名字段这是它和GROUP BY最本质的区别。分组函数让多行变一行窗口函数让多行还是多行只是多了个计算列。5.3 三类排名函数的区别ROW_NUMBER()强调的是位置编号不会并列RANK()和DENSE_RANK()允许并列区别在于并列之后的下一个名次是否跳过。用三行数据可以看得很清楚数据ROW_NUMBERRANKDENSE_RANK10011110021190332选择哪个取决于业务口径这个不能拍脑袋定。有一次项目里做排行榜业务方要“并列不跳号”我一开始用了RANK然后被数据核对的人找上门改成DENSE_RANK才对齐。这类问题看起来小实际直接影响页面展示名次和奖金额度务必先确认口径再选函数。5.4 用窗口函数做累计和移动平均除了排名窗口函数还能做累计、移动平均。比如查“每日累计销售额”SELECT dt, amount, SUM(amount) OVER (ORDER BY dt) AS cum_amount FROM daily_sales;这个字段是从第一行累加到当前行的结果比分组再自连接省事得多。窗口函数的框架是OVER里面可以写PARTITION BY分区和ORDER BY排序还可以写ROWS BETWEEN指定窗口边界。理解到这一层很多高级报表需求都能迎刃而解。比如要算“最近7天平均销售额”窗口边界写ROWS BETWEEN 6 PRECEDING AND CURRENT ROW就行。窗口函数值得花时间系统练一遍它是DQL从基础走向进阶的一个明显分水岭。6. 查询性能分析EXPLAIN怎么读慢SQL怎么处理6.1 先建立“查一下执行计划”的习惯我处理慢查询第一件事不是改SQL而是执行EXPLAIN看看数据库打算怎么跑。EXPLAIN不会真的执行完整查询它只是输出执行计划。养成这个习惯之后很多“跑得慢”的问题几分钟就能定位。要是连执行计划都不看就上手改SQL常常是改了半天性能没提升反而把业务逻辑改坏了。6.2 几个核心指标看到执行计划后先抓住几个关键列。type表示访问类型从好到坏大体是system、const、eq_ref、ref、range、index、ALL。看到ALL基本就是全表扫描要警惕。key表示实际用到的索引possible_keys是可能用到的索引如果key是NULL就说明没用上。rows是预估扫描行数这个值比实际执行耗时来得快是优化前后的对比利器。Extra里包含很多提示比如Using filesort表示排序没走索引Using temporary表示用了临时表这些都是优化信号。我常用的判断方法是type至少得是rangerows别太离谱Extra里的filesort和temporary能避免就避免。把EXPLAIN当成体检报告来读比盲目加索引靠谱得多。6.3 一次慢查询的处理案例模拟项目里有一张行为日志表数据量到了千万级有个统计接口慢到超时。先EXPLAIN一看type是ALLrows显示可能扫上百万行Extra还有Using temporary和Using filesort。当时的条件是这样WHERE user_id xxx AND action click单看两个条件都有过滤性但索引是(user_id, action_time)action没进索引GROUP BY一个和索引顺序不一致的字段导致临时表排序。调整思路很简单把查询频次最高的字段组合建一个联合索引并让GROUP BY字段尽量贴近索引顺序同时把查询范围用时间条件收紧。改完EXPLAIN变成range接口耗时从几秒降到几十毫秒。这个案例没有什么玄学就是把执行计划读懂了之后对症下药。6.4 SELECT *、LIMIT深分页与隐式转换SELECT *会把用不到的列也读出来增加IO和网络传输能不写就不写。LIMIT深分页比如LIMIT 100000, 20数据库要跳过前面十万行越翻越慢可以用上一页最大ID做条件SELECT order_id, amount FROM order_info WHERE status PAID AND id 100000 ORDER BY id LIMIT 20;这种写法让数据库可以顺着主键索引往后读而不是重新从头部扫一遍。这类优化其实都属于DQL的“使用姿势”不算高深但非常实用。7. 书写规范与自检清单让DQL少踩坑7.1 NULL与三值逻辑前面提过NULL的三值逻辑这里再集中说几个常见问题函数处理NULL要小心字符串拼接里遇到NULL会返回NULL经常需要COALESCE兜底COUNT(column)不统计NULL需要判断时显式加IS NULL聚合排序时NULL默认排最前还是最后不同数据库不一致写代码时最好明确处理。我在代码评审里见过太多因为NULL导致的结果偏差问题都不复杂就是写的时候没多想一层。7.2 关键字与保留字给字段起名时要避开保留字比如order、group、select这些。如果不可避免要使用大多数数据库要求加反引号或双引号包裹。我的建议是从根源上避免创建表和写查询时统一命名风格。之前见过线上SQL因为字段名是status这类普通词没问题但碰到index、rank这种半保留字不同版本兼容性不一非常容易被坑。命名规范这件事前期多花两分钟后期能省很多排查时间。7.3 格式化与可读性DQL写得一长串堆在一起出问题很难查。我自己的习惯是关键字大写、字段小写、每行一个主要子句缩进对齐。这样不管是自己回头排查还是交给同事评审都能一眼看到结构。执行顺序的知识在这种格式化方式下也会更清晰一眼扫过去就知道先FROM后WHERE还是先SELECT后FROM。代码是写给人看的顺带让数据库执行可读性从来都不是小事。7.4 发布前自检清单写复杂查询时我会习惯性地过一遍这些项有没有SELECT *能不能只选需要的字段WHERE条件字段的类型和参数类型是否一致如果用LEFT JOIN右表的过滤条件是否误放WHERE分组查询的非聚合字段是否都在GROUP BY里统计口径用COUNT(*)还是COUNT(DISTINCT)是否符合业务排序字段有没有索引支撑有没有必要加LIMIT防止结果集过大。把这些当作肌肉记忆之后DQL的踩坑率会明显下降。从我自己带人写SQL的经历来看DQL这门“手艺”的瓶颈从来不是语法记忆而是对执行逻辑的掌握和对查询条件的准确判断。上面的内容基本都是我在实际项目里被反复问过、也反复踩过的问题。如果你也正处在能写SQL但不太确定写得对不对的阶段我建议你找个周末把自己写过的最复杂的几条查询拿出来一边用执行顺序的框架去拆一边用EXPLAIN验证一下坚持两周你会有明显感受。再分享一个小技巧排查DQL问题时先在纸上写出每一步的中间结果长什么样再对照实际输出几乎所有逻辑错误都能在这个过程里现出原形。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑