
1. DQL单表查询的定位与SELECT基础语法1.1 为什么这一章是所有SQL查询的地基MySQL的DQLData Query Language也就是数据查询语言在整套SQL技能树里占据的位置非常特殊。很多人一上来就想着学多表联接、复杂子查询结果写出来的SQL效率低、逻辑乱归根结底是单表查询的基本功没有打牢。单表查询听起来简单实际上一套完整的SELECT语法覆盖了条件过滤、分组聚合、排序分页、去重过滤这些核心操作。任何复杂的业务查询最终拆解下来都是这些基础操作的组合。比如我们常见的统计各部门平均薪资超过8000的部门这个需求在Excel里是一个透视表在MySQL里就是GROUP BY加上HAVING的组合。如果你能把单表DQL练到位后面再去学JOIN、子查询时思维方式会顺很多。这一章适合谁正在系统学习MySQL的初学者写了几个月SQL但对自己写出来的东西没有把握的开发人员还有准备面试需要快速梳理查询知识体系的人。我会用一张员工表的示例数据贯穿全文把SELECT的每个核心子句拆开讲透。1.2 SELECT基本语法的执行顺序很多教材喜欢上来就罗列SELECT各种子句的语法格式但我想先讲一个很多人忽略的问题——SELECT语句的逻辑执行顺序。这不是死记硬背的考点而是你排查SQL问题时的基本思路。完整的一条单表查询语句大概是这样的SELECT DISTINCT 字段或表达式 FROM 表名 WHERE 过滤条件 GROUP BY 分组字段 HAVING 分组后的过滤条件 ORDER BY 排序列 LIMIT 偏移量, 行数;看起来是一条完整的语句但数据库引擎在执行时并不会按照书写的顺序从上到下处理。它内部的实际执行顺序是FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT。这个顺序解释了非常多的现象比如WHERE条件里不能使用SELECT中定义的别名因为WHERE在SELECT之前执行别名此时还不存在再比如ORDER BY可以使用别名因为排序发生在SELECT之后。我自己见过不止一次有人写WHERE salary * 1.1 8000时疑惑为什么报销了实际上这个写法没有任何问题只是WHERE阶段做的是原始行的过滤判断还没有聚合的概念而已。1.3 一张示例表贯穿全文操练为了让后面的内容不悬空我创建一张员工信息表模拟某公司员工数据CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 员工编号, name VARCHAR(20) NOT NULL COMMENT 员工姓名, dept VARCHAR(20) COMMENT 所属部门, job VARCHAR(20) COMMENT 岗位, salary DECIMAL(10,2) COMMENT 月薪, hire_date DATE COMMENT 入职日期, performance TINYINT COMMENT 绩效评分 1-5 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO employee (name, dept, job, salary, hire_date, performance) VALUES (张三, 技术部, 后端工程师, 12000.00, 2022-03-15, 4), (李四, 技术部, 前端工程师, 11000.00, 2022-07-01, 3), (王五, 技术部, 架构师, 25000.00, 2020-01-10, 5), (赵六, 产品部, 产品经理, 15000.00, 2021-05-20, 4), (孙七, 产品部, 产品助理, 8000.00, 2023-02-01, 2), (周八, 运营部, 运营专员, 7000.00, 2023-06-15, 3), (吴九, 运营部, 运营总监, 18000.00, 2019-11-01, 5), (郑十, 技术部, 测试工程师, 9500.00, 2022-10-08, 2), (钱十一, 产品部, 产品经理, 16000.00, 2021-08-12, 4), (刘十二, 运营部, 运营专员, 7500.00, 2023-09-01, 3);所有示例我都会基于这张表来说明你可以直接复制建表语句和插入语句在自己电脑上同步敲一遍。有条件的建议准备一台本地MySQL 8.0环境Windows、macOS、Linux都行版本差异不影响这些基础语法。2. 条件过滤WHERE子句的完整拆解2.1 运算符不只是等于和大于说到WHERE初学者往往只记住、、这些最基础的比较符但实际工作中条件过滤的复杂度远不止这些。我把常见的条件写法归成几类方便你对照实践。比较运算符、、、、、或!。需要注意!和在MySQL里是等价的都可以表示不等于。逻辑运算符AND、OR、NOT。这三个组合起来可以表达非常复杂的业务条件但优先级很容易出错。AND的优先级高于OR所以WHERE dept 技术部 OR dept 产品部 AND salary 10000实际含义是技术部所有人加上产品部中薪资过万的人。如果你本意是技术部和产品部中薪资过万的人就必须写成WHERE (dept 技术部 OR dept 产品部) AND salary 10000。这个优先级问题我在实际开发中见过太多次了有些还是上线后才发现查出来的数据不对。区间判断BETWEEN a AND b包含边界值。比如查薪资在10000到20000之间直接WHERE salary BETWEEN 10000 AND 20000。这个写法等价于salary 10000 AND salary 20000但可读性好很多。集合判断IN (...)用于匹配一组候选值。比如WHERE dept IN (技术部, 产品部)。IN在MySQL底层优不优化、是否走索引情况比较复杂但语法层面它非常直观。我不建议用IN接一个特别长的子查询列表性能风险高单表查询时直接用还是没问题的。模糊匹配LIKE配合通配符使用。%表示任意多个字符_表示任意单个字符。查所有姓张的员工WHERE name LIKE 张%。这里有个容易被忽略的细节LIKE 张%以指定前缀开头时理论上可以使用索引而LIKE %张这种后缀匹配基本无法走索引数据量大时会全表扫描。2.2 NULL值的经典陷阱WHERE条件里最容易被坑的地方就是NULL的判断。SQL里有一个特殊的三值逻辑真TRUE、假FALSE、未知UNKNOWN。NULL参与比较运算时结果通常是UNKNOWN而WHERE子句只保留TRUE的结果。所以WHERE performance 0查不出performance为NULL的行WHERE performance 0同样也查不出NULL行。NULL值不参与任何普通比较运算你必须显式处理WHERE performance IS NULL查出空值行WHERE performance IS NOT NULL查出非空行。我还见过有人用WHERE salary NULL查没有薪资的员工结果一行都查不出来这其实就是三值逻辑没有理解透。顺带提一个研发规范上的建议设计表结构时尽量让字段保持NOT NULL实在有可选值就给默认值。NULL值在查询、索引、聚合函数中会带来很多额外的心智负担能用空字符串或0代替的场景尽量不要引入NULL。2.3 隐式类型转换容易翻车MySQL在比较不同类型的值时会触发隐式类型转换。经典的坑出现在字符串和数字比较上。假设你的dept字段是VARCHAR类型写入的数据是123、456这样的字符串此时用WHERE dept 123MySQL会尝试把字符串转换成数字再比较——但这里有一个重大问题字符串转数字时并不是精确匹配而是截断转换。具体来说像123abc这样的字符串在数字语境下会被转换成123。如果你只想精确匹配字符串WHERE dept 123才是对的。凡是写WHERE dept 123的语法不报错但数据匹配的逻辑已经不是你以为的那个逻辑了。更隐蔽的坑出现在查询条件里字段类型是字符串但你传了数字的场景。我在帮人排查问题时就遇到过某张表里存的是用户手机号VARCHAR查询条件写成WHERE phone 13800138000少量数据时看着没问题数据量一大全表扫描跑不动了。原因就是类型转换导致索引失效。字符串字段和数字比较时MySQL对字段值做转换索引也随之失效。核心经验是字段是什么类型条件就写什么类型别依赖隐式转换。2.4 条件过滤的实操示例回到员工表看几个组合条件的实战-- 查询技术部薪资高于10000的员工 SELECT name, job, salary FROM employee WHERE dept 技术部 AND salary 10000; -- 查询绩效评分在3到5之间含边界的员工 SELECT name, performance FROM employee WHERE performance BETWEEN 3 AND 5; -- 查询产品部和运营部的员工 SELECT name, dept FROM employee WHERE dept IN (产品部, 运营部); -- 查询姓名以张开头的员工 SELECT name FROM employee WHERE name LIKE 张%;我在教学时发现一个现象很多人写复杂WHERE条件时喜欢一次性把所有条件堆上去查不出数据就开始怀疑数据有问题。我的习惯是先去掉一部分条件逐步加回来。先跑最核心的条件确认数据存在再一个个叠加条件定位是哪个条件把结果过滤没了。这不只是调试技巧也是排查线上问题时缩小范围的基本方法。3. 排序与去重ORDER BY和DISTINCT3.1 ORDER BY不只是order by字段名排序是查询结果展示最常用的功能。基础写法就是ORDER BY salary默认升序ASC想降序就写DESC。但我见过不少项目里的排序需求比这复杂得多多字段排序ORDER BY dept ASC, salary DESC的含义是首先按部门升序排同一个部门内部再按薪资降序排。注意它和ORDER BY salary DESC, dept ASC语义完全不同。这在业务中非常常见比如先按部门分组展示部门内薪资从高到低。按表达式或别名排序比如按年薪排序ORDER BY salary * 12 DESC。这里salary * 12其实是一个表达式MySQL允许直接写在ORDER BY里。如果SELECT中定义了别名ORDER BY中也可以直接使用别名SELECT name, salary * 12 AS annual_salary FROM employee ORDER BY annual_salary DESC。刚才说过ORDER BY在SELECT之后执行所以别名在这里可用但WHERE里不行。中文排序的问题MySQL默认的排序规则与字符集和排序规则collation相关。在我的示例表中name字段用的是utf8mb4字符集默认collation是utf8mb4_general_ci或者utf8mb4_0900_ai_ciMySQL 8.0默认。中文排序在这种规则下一般是按Unicode编码排序不是按拼音也不是按笔画。如果你要按拼音排序得在查询里指定排序规则ORDER BY name COLLATE utf8mb4_pinyin_ci需要数据库支持该collation。这个细节开发中很少注意但做中文通讯录、姓名列表排序时就会冒出来。3.2 排序与NULL值的顺序NULL值在排序中的默认位置也是一个容易忽略的点。MySQL中ORDER BY column ASC时NULL值默认排在最前面ORDER BY column DESC时NULL值默认排在最后面。换句话说NULL永远被当作最小的值来处理。这在某些场景下不是我们想要的比如绩效评分没有填的人我们希望他们排在最后那直接用ORDER BY performance ASC反而让NULL排在最前面需要特殊写IFNULL处理-- 把NULL绩效视为0让NULL排最后 SELECT name, performance FROM employee ORDER BY IFNULL(performance, 0) ASC;这类需求不复杂但不知道NULL排序规则的人往往要调试半天才反应过来。3.3 DISTINCT去重的正确理解DISTINCT的作用是去除重复行。但要特别注意它是对所有SELECT的字段整体去重不是对某一个字段去重。SELECT DISTINCT dept FROM employee返回的是所有不同的部门这没问题但SELECT DISTINCT dept, job FROM employee返回的是部门和岗位的不同组合部门可能有重复值岗位也可能有重复值它是组合唯一。这个特性经常被误解有人想查出去重后的部门列表照抄网上写法时发现结果里还是有重复部门就是因为整体去重的语义导致。如果只想对单个字段去重可以配合GROUP BY使用SELECT dept FROM employee GROUP BY dept。另外DISTINCT和聚合函数组合时要注意语义。SELECT COUNT(DISTINCT dept) FROM employee统计的是不同部门个数这在统计报表里非常常用。而SELECT DISTINCT COUNT(dept) FROM employee完全不是一回事它先算出dept非空的总行数再对这单个数值去重结果就是一个数字。这两种写法差别巨大写之前先想清楚你要的是什么。3.4 排序与去重的执行顺序把执行顺序串起来分析执行顺序是FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT。DISTINCT在SELECT之后、ORDER BY之前执行。这意味着给DISTINCT排序时MySQL先去掉重复行再对结果排序。可以推理SELECT DISTINCT dept FROM employee ORDER BY salary这个语句是有问题的因为dept经过DISTINCT后结果里可能存在多行的salary值对哪个值排序就不明确了。MySQL在某些SQL_MODE下会直接报错实测时你会发现语法上过不去。这又回到执行顺序的重要性——理解了它很多报错信息你就能自己推断原因了。4. 聚合函数与分组统计从明细到汇总4.1 聚合函数的基本行为和NULL处理聚合函数是把多行数据汇总成一个值。MySQL里最常用的五个COUNT()、SUM()、AVG()、MAX()、MIN()。看名字大家都懂但NULL值的处理逻辑是新手最容易出错的COUNT(字段)和COUNT(*)的区别COUNT(*)统计的是行数不管字段是否为NULLCOUNT(字段)统计的是该字段非NULL的行数。比如COUNT(hire_date)和COUNT(*)在示例表中结果一样因为hire_date没有空值。但如果换成performance字段把某个员工的performance设为NULL后COUNT(performance)会比COUNT(*)少一行。这就是NULL和聚合函数之间的微妙关系。SUM和AVG自动忽略NULLSUM(salary)只会对非NULL值求和AVG也是基于非NULL值计算平均。注意AVG(salary)和SUM(salary)/COUNT(*)结果可能不一样因为AVG除以的是非NULL行数而后者除以的是总行数。如果字段有NULL值这两个结果就有差异。MAX/MIN同样忽略NULL取最大最小值时空值不参与比较这通常符合预期但好奇心重的人可以想想如果某一列全是NULL那MAX的返回值是NULL不是空字符串也不是0。4.2 GROUP BY的分组逻辑GROUP BY的作用是把相同的值归到一组分组之后SELECT列表里通常只出现分组字段和聚合函数。这里有一个初学者最容易踩的雷区分组后SELECT的非分组字段没有意义。什么意思呢看这个错误示范-- 这个写法在MySQL严格模式下会报错 SELECT name, dept, AVG(salary) FROM employee GROUP BY dept;按部门分组后每组可能有多个员工姓名那name应该显示哪个这个问题的答案在标准SQL里是不确定的。MySQL在ONLY_FULL_GROUP_BY模式下会直接拒报错这也是MySQL 5.7以后默认开启的模式。很多人升级MySQL版本后发现自己以前能跑的SQL突然报错了大概率就是这个原因。那能不能既分组又看明细可以但要用GROUP_CONCAT()把组内的多个值拼成一个字符串-- 查看每个部门的员工名单和平均薪资 SELECT dept, GROUP_CONCAT(name), AVG(salary) FROM employee GROUP BY dept;GROUP_CONCAT是MySQL特有的聚合函数在统计会话里很好用比如查出一个部门的所有订单号拼成一串。默认拼接长度上限是1024字节超长会截断需要的时候可以用GROUP_CONCAT(... SEPARATOR | )指定分隔符。4.3 HAVING与WHERE边界线WHERE是分组之前过滤行的HAVING是分组之后过滤组的。这个边界必须清晰。举个例子-- 先过滤掉薪资低于8000的人再按部门分组看平均薪资 SELECT dept, AVG(salary) AS avg_salary FROM employee WHERE salary 8000 GROUP BY dept; -- 按部门分组算平均薪资只保留平均薪资超过12000的部门 SELECT dept, AVG(salary) AS avg_salary FROM employee GROUP BY dept HAVING AVG(salary) 12000;业务上经常有两阶段过滤先按条件排除明细行再按聚合结果排除组。实战中的组合写法-- 排除绩效低于3分的员工后统计各部门平均薪资只看平均薪资过万的部门 SELECT dept, AVG(salary) AS avg_salary FROM employee WHERE performance 3 GROUP BY dept HAVING AVG(salary) 10000;注意HAVING里使用聚合函数WHERE里不能使用因为WHERE执行时聚合还没有计算。WHERE里不能写WHERE AVG(salary) 10000这是逻辑不通的写法。另外HAVING可以在某些数据库里使用SELECT别名MySQL支持但为了可移植性和可读性我建议HAVING里直接写聚合表达式。4.4 分组统计的典型业务场景结合员工表给你几个可以直接上手的练习-- 各部门人数和平均薪资 SELECT dept, COUNT(*) AS cnt, AVG(salary) AS avg_salary FROM employee GROUP BY dept; -- 各部门最高最低薪资 SELECT dept, MAX(salary) AS max_salary, MIN(salary) AS min_salary FROM employee GROUP BY dept; -- 各岗位的入职年份分布按入职年份分组 SELECT YEAR(hire_date) AS hire_year, COUNT(*) AS cnt FROM employee GROUP BY YEAR(hire_date) ORDER BY hire_year DESC;最后一个例子相当典型GROUP BY后面不一定要写字段名也可以写表达式。YEAR(hire_date)把日期转为年份再按照年份分组统计。这是报表统计里最常见的需求之一实际业务里还会配合日期格式化、条件聚合等技巧使用。5. 分页查询与LIMIT的实战细节5.1 LIMIT语法和分页公式LIMIT是用来限制返回行数的。基础写法两种-- 返回前3行 SELECT * FROM employee LIMIT 3; -- 跳过前2行返回接下来的4行 SELECT * FROM employee LIMIT 2, 4;第二种写法的两个参数第一个是偏移量第二个是行数。很多教材说LIMIT offset, count但我建议你换个更清晰的角度理解偏移量表示跳过多少条行数表示最多取多少条。分页时标准的公式是当前页码page从1开始每页显示size条那么LIMIT (page-1)*size, size。比如每页3条第1页是LIMIT 0, 3第2页是LIMIT 3, 3第3页是LIMIT 6, 3。有人喜欢用LIMIT 3 OFFSET 0的另一种语法两者等价MySQL 8.0都支持。我觉得哪个习惯用哪个没有本质差别。5.2 分页查询的性能隐患LIMIT的偏移量越大性能问题越明显。LIMIT 100000, 10不是从第100001条开始取10条这么轻松的MySQL实际上会先扫描前100010条记录然后扔掉前100000条只返回最后10条。数据量大时这个偏移的成本相当高。我给出的优化思路有两个方向第一利用主键索引做条件分页。如果表有自增主键在已知上一页最大id的情况下直接用WHERE id 1000 ORDER BY id LIMIT 10来代替大偏移量。第二使用覆盖索引延迟关联先利用索引定位到需要的id范围再回原表取完整数据。后者稍微复杂一些也是很多人接手千万级数据量项目后接触到的第一课。还有一点务必养成习惯LIMIT几乎总是配合ORDER BY使用不写排序的分页没有实际意义。数据库返回数据的顺序是不保证稳定的不排序的话你可能看到第一页第1条是张三第二页第一条还是张三。分页丢了排序结果就不确定。5.3 LIMIT在分页之外的两个妙用工作中LIMIT不只是分页。一个很实用的场景是取前N条-- 查询薪资最高的前3名员工 SELECT name, salary FROM employee ORDER BY salary DESC LIMIT 3;再加一个常见的需求各分组取TopN。比如每个部门取薪资最高的前2名。仅用基础DQL单表查询做这需求是有难度的需要用到窗口函数或者相关子查询基础的GROUP BY实现不了。不过我可以给你一个思路先把数据按部门、薪资排序再用变量或者窗口函数ROW_NUMBER()生成组内序号最后过滤序号 2。这算单表查询的一个进阶延伸等到学完窗口函数再来回头解决这个问题你会觉得整个SQL体系打通了。6. 综合实战一个完整的统计查询场景6.1 从业务需求到SQL的拆解过程最后来一个综合练习把前面所有语法组合到真实场景里。假设产品经理提了一个需求统计各部门中2022年之后入职的员工平均薪资要求排除绩效低于3分的员工按平均薪资降序排列只看平均薪资超过9000的部门每页显示2条记录。别急着写SQL先过一遍逻辑顺序数据范围2022年之后入职包含2022年即hire_date 2022-01-01。排除条件绩效评分大于等于3也就是performance 3。分组统计按部门分组计算AVG(salary)。组过滤只保留AVG(salary) 9000的部门。排序按平均薪资降序。分页每页2条看第1页。按执行顺序写SQL如下SELECT dept, COUNT(*) AS emp_cnt, AVG(salary) AS avg_salary FROM employee WHERE hire_date 2022-01-01 AND performance 3 GROUP BY dept HAVING AVG(salary) 9000 ORDER BY avg_salary DESC LIMIT 0, 2;结果会返回哪些部门我们可以推演一下符合条件的数据中技术部有张三、李四等平均薪资约10833左右产品部有赵六和钱十一等平均薪资约15500左右运营部符合条件的平均薪资约7000多不到9000。经过HAVING过滤后产品部和技术部保留按降序排列产品部排第一、技术部排第二。如果这个需求第二页就要把LIMIT 0, 2改成LIMIT 2, 2。这个拆解过程就是单表DQL最核心的思维方式先想清楚数据流再转化成语法。每写一个子句都问一下自己它在执行顺序中的位置对不对。6.2 常用函数的补充应用实际业务中查询很少只用原生字段经常要配合MySQL内置函数做格式化、转换和计算。我把单表查询中最常用的一组函数列出来函数作用示例CONCAT拼接字符串CONCAT(name, - , dept)IFNULL空值替换IFNULL(performance, 0)YEAR / MONTH / DAY提取日期部分YEAR(hire_date)DATE_FORMAT日期格式化DATE_FORMAT(hire_date, %Y-%m)ROUND四舍五入ROUND(AVG(salary), 2)IF条件判断IF(salary 10000, 高薪, 普通)看一个综合使用的例子-- 查看每个员工的年薪万元以易读格式展示 SELECT name, CONCAT(ROUND(salary * 12 / 10000, 2), 万) AS annual_salary FROM employee;有人会疑惑这些函数是不是该等到学完单表DQL再记我的看法恰恰相反函数是单表查询里最灵活的调味料字段本身是数据但函数能把它变成人话。报表、导出、消息模板拼接都离不开这些函数。6.3 查询结果的判空与边界情况任何一条查询写完后都要想一想空结果的情况。我自己坚持的习惯是写完SQL先尝试回答三个问题第一这条SQL在最极端的数据条件下会不会返回空表空表往往不是SQL错误而是业务上的无数据状态前端拿到的空集合和NULL完全不同所以很多公司在工程规范里明确要求查询接口返回数组而不是NULLSQL里的处理通常是IFNULL配合聚合函数-- 没有符合条件的记录时COUNT返回0AVG返回NULL需要IFNULL兜底 SELECT COUNT(*) AS total_cnt, IFNULL(AVG(salary), 0) AS avg_salary FROM employee WHERE dept 不存在第二聚合结果会不会为NULL刚才说了AVG在无数据时返回NULL这在Java、Python等后端语言里拆值时常引发空指针或解析异常提前用IFNULL兜底是防御式编程的习惯。第三LIMIT取不到数据时结果集是空数组而不是NULL。这一点在写接口时也要提醒自己判断list.isEmpty()而不是list null如果SQL查询层面返回的是空结果集不能用一个非空判断去校验。7. 常见报错与排查技巧实录7.1 高频报错现象速查表平时辅导别人调SQL发现很多报错其实是重复出现的。我整理了一张速查表都是单表DQL阶段的高频问题报错或异常现象常见原因解决方案Expression #1 of SELECT list is not in GROUP BY clause非严格模式下漏了ONLY_FULL_GROUP_BY校验SELECT列表里只保留分组字段或聚合函数别选无关字段Unknown column xxx in where clause字段名写错或WHERE里用了SELECT别名检查字段拼写WHERE里不能使用别名改成原始字段或表达式WHERE salary NULL误解NULL语义改为IS NULL或IS NOT NULL查询结果排序不对中文排序规则问题指定COLLATE如ORDER BY name COLLATE utf8mb4_pinyin_ci分页结果重复或遗漏没有ORDER BY或排序字段不唯一排序时加上主键兜底ORDER BY salary DESC, id ASCBETWEEN边界值查不到边界数据类型不匹配或边界写反确认BETWEEN a AND b是闭区间a必须小于等于bLIMIT 100000, 10查询越来越慢偏移量过大的自身缺陷改用条件分页或延迟关联不要盲目加大偏移量字符串字段查数字结果异常隐式类型转换条件类型和字段类型保持一致字符串加引号第二行提到的WHERE表别名是新手写复杂条件时经常碰到的例如SELECT name AS n, salary AS s FROM employee WHERE s 10000;这个写法在MySQL会直接报Unknown column s in where clause。WHERE先于SELECT执行别名还没有生成这是执行顺序的具体体现。你要过滤的话写SELECT name AS n, salary AS s FROM employee WHERE salary 10000;7.2 一个隐藏很深的编码问题有些同学在自己电脑上跑示例表发现中文条件查不到数据比如WHERE dept 技术部结果为空但表里分明有技术部的数据。这通常不是SQL问题而是客户端连接的编码设置问题。连接参数里没有指定utf8mb4导致中文条件在传输过程中变成了乱码到了数据库端比对时自然匹配不上。排查时先在MySQL命令行里手动查一下SHOW VARIABLES LIKE character_set_connection;如果显示的不是utf8mb4就要在连接字符串里加上characterEncoding和编码参数。很多开发框架在配置数据源时默认编码就处理好了但命令行或脚本直连时很容易踩这个坑。8. 调试查询的思维方式与经验总结8.1 从结果反推SQL的调试路径SQL不报错但结果不对比报错更让人头疼。调试的逻辑其实和破案类似我常用的路径是先确认数据本身是否存在再逐层剥离条件最后检查排序和分页有没有干扰。第一步不加任何WHERE直接查SELECT * FROM employee。确认表里确实有数据并且字段值和你预期的一致。这一步能排除数据本来就没有和字段值理解错误两个因素。第二步把WHERE条件逐个拆开单独执行。比如发现WHERE dept 技术部 AND performance 3结果为空先单独跑WHERE performance 3如果结果正常则问题集中在dept 技术部这个条件上如果这个单独条件也为空转去检查编码或字段内容。第三步加了GROUP BY或HAVING之后结果不对就先用聚合函数单独确认数据比如SELECT dept, AVG(salary) FROM employee GROUP BY dept能直接看到每个部门的平均值再比对加了WHERE过滤后的结果差异找出是哪一步把部分行过滤掉了。8.2 强制认真对待SQL_MODEMySQL的sql_mode是一个经常被忽略但影响很大的配置。5.7之后默认开启了ONLY_FULL_GROUP_BY、STRICT_TRANS_TABLES等模式。前者让不规范的GROUP BY直接报错后者让数据类型不合法时放弃写入而不是截断。线上和本地环境如果sql_mode不一样行为就会不一致本地跑通了一天的SQL线上同一条SQL可能报错。查看当前模式SELECT GLOBAL.sql_mode; SELECT SESSION.sql_mode;我的建议是不要试图关闭ONLY_FULL_GROUP_BY来迁就不规范的SQL而是适应严格模式写出符合标准的查询语句。规范的值不在于应付检查而在于以后面对更复杂的业务时你的SQL能在任何环境上稳定运行。8.3 实践建议把单表DQL当内功修炼很多人学SQL追求会写但更高级的目标是写得对、写得稳、写得快。我对单表DQL的学习建议是这样的不要只刷增删改查的练习题要把每个子句的边界条件主动测一遍。比如今天学了LIKE就去测一下LIKE %abc%、LIKE abc_、LIKE abc\%转义今天学了GROUP BY就把ONLY_FULL_GROUP_BY开和关各跑一条SQL看看报错差异。我自己带新人时经常让他们做的练习是在一张万行级的表上分别测试LIMIT 10、LIMIT 10000, 10、WHERE id 10000 LIMIT 10的执行时间差异亲眼看到数据量带来的性能差距比背十遍索引原理都管用。这一章如果吃透了后面多表联接、子查询、窗口函数都是在这个基础上叠加复杂度。写SQL不是背语法是理解数据的流动从表中取出原始行一层层过滤、分组、计算、排序最后展示结果。把这条主线内化成自己的思维方式你会突然发现所有复杂的查询都不再需要死记硬背了。