news 2026/9/18 19:14:36

MySQL查询语句全解析:从SELECT *到索引优化与排错实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL查询语句全解析:从SELECT *到索引优化与排错实战

写查询写了这么多年,我越来越觉得,MySQL 里最被低估的一句 SQL 其实就是SELECT * FROM。别笑,这个热搜词集合里藏着一堆真实问题:“select top 1000 * from [dbo].[dc_ods_rkyxjc_lotdatacollection]”这种 SQL Server 写法被搬到 MySQL、IN查询语句报错、字段默认值、去重查询、更新子查询、join 含义,还有各种安装配置和连接报错,几乎每一个都是新手和老手都会反复踩的坑。这篇文章我会从SELECT * FROM这个起点出发,把查询语句集合里的高频知识点、底层逻辑和排错经验完整拆一遍,适合刚接触 MySQL 的开发者、写报表写到头秃的数据分析,以及准备面试想系统梳理查询语法的同学。

1. 先弄懂 SELECT * FROM 的执行顺序,再谈写查询语句

1.1 一条查询不是从左往右执行的

很多人学 SQL 是从SELECT * FROM users开始的,语法简单,结果直观,但一旦加入 WHERE、GROUP BY、HAVING、ORDER BY、LIMIT 之后,就常常搞不清楚为什么报错、为什么明明有索引却很慢。这里最核心的问题是:SQL 并不是按书写顺序执行的。

MySQL 的逻辑执行顺序大致是:

  1. FROM:确定从哪张表取数据,包括 JOIN 的表
  2. WHERE:对 FROM 阶段的结果做行级过滤
  3. GROUP BY:按指定列分组
  4. HAVING:对分组后的结果做过滤
  5. SELECT:计算目标列、别名、聚合表达式
  6. ORDER BY:对最终结果排序
  7. LIMIT:截取指定行数

我经常拿这个顺序去解释一个经典问题:为什么WHERE子句里不能直接用SELECT里定义的别名?

SELECT name AS n, age FROM users WHERE n = '张三';

上面这句一定会报Unknown column 'n' in 'where clause',原因就是WHERESELECT之前执行,别名n此时还不存在。想过滤只能写WHERE name = '张三'。这个知识点看起来基础,但实际开发里因为别名复用导致的报错特别多,尤其是从 Excel 思维转过来写 SQL 的人,常常把 SQL 当 Excel 的列计算来理解,就会觉得“我明明定义了 n,为什么不能用”。

1.2 SELECT * 的代价到底在哪里

热搜词里出现频率最高的是select top 1000 * from [dbo].[dc_ods_rkyxjc_lotdatacollection],这句是 SQL Server 的 TOP 语法,MySQL 里对应的是LIMIT 1000。但更值得说的是那个*。SELECT * 不是不能用,而是要分场景。它的代价主要体现在三方面。

第一,*会返回表里所有列,网络传输和内存消耗都大。一张表如果有 50 个字段,其中 40 个你根本不需要,SELECT *会把它们全部查出来,在数据量大时对 IO、网络、排序临时文件都有压力。

第二,*会让覆盖索引失效。覆盖索引的意思是查询所需的列都在索引里,可以直接从索引返回,不需要回表。如果你建了一个(status, order_time)的联合索引,查询SELECT status, order_time FROM orders WHERE status = 1就能走覆盖索引,但写成SELECT *就会被迫回表读取完整行数据,性能差距在千万级表上会非常明显。

第三,*的结果集不稳定。表结构一变更,查询结果列就变,程序里按固定列名或位置取数的代码可能直接崩。我见过一个定时任务,上游表加了个字段,下游用SELECT *导数据,结果字段错位,数据全乱了,排查了很久才发现是通配符惹的祸。

那什么时候可以用SELECT *?我自己的经验是:本地调试、快速看数据、写一次性分析脚本的时候,随手SELECT *完全没问题。但凡是进入正式代码、定时任务、报表接口的 SQL,都要明确列出字段。

1.3 MySQL 8.0 下 SELECT * 的回表逻辑

MySQL 的 InnoDB 存储引擎里,数据是按 B+ 树组织的。主键索引的叶子节点存的是整行数据,二级索引的叶子节点存的是索引列的值加主键值。当你执行SELECT * FROM users WHERE phone = '13800138000',如果 phone 上有二级索引,MySQL 会先在二级索引里找到对应的主键值,再根据主键回表查完整行,这个动作叫“回表”。

回表不是每次都有问题,但回表次数多、每次回表都是随机 IO 时,查询就会变慢。这也是为什么很多性能优化建议里会说“别写 SELECT *,要写覆盖索引能够覆盖的列”。理解了回表,你就能理解为什么SELECT id, phone FROM users WHERE phone = 'xxx'可能比SELECT * FROM users WHERE phone = 'xxx'快——前者在二级索引里就能拿到全部需要的数据,连回表都省了。

2. 字段、别名与去重:SELECT 子句里藏着哪些细节

2.1 查询结果里给字段设置默认值

热搜词里有一条“sql select查询语句 给某一个字段设置默认值”,很多人的第一反应是建表时的DEFAULT约束,比如age INT DEFAULT 0。但再仔细看,这里问的是“查询语句里给字段设置默认值”,也就是查出来的时候,如果字段是 NULL,就显示一个默认值。

这种需求太常见了。用户表里 phone 允许为空,前端要展示时不想显示 null,想显示“未填写”;订单表里 pay_time 还没支付时为 NULL,报表里要显示“未支付”。SQL 里处理这个问题的标准做法是用IFNULLCOALESCE

SELECT name, IFNULL(phone, '未填写') AS phone, COALESCE(pay_time, '未支付') AS pay_status FROM users;

IFNULL(a, b)是 MySQL 特有的函数,COALESCE(a, b, c, ...)是标准 SQL,返回第一个非 NULL 值。两者在“两参数”场景下等价,但COALESCE可以接多个参数,应用场景更广。

这里要特别提醒一个坑:NULL 和空字符串是两回事。IFNULL('', '默认值')返回的是空字符串,不会变成默认值,因为空字符串不是 NULL。如果你要处理的是“空字符串也显示默认值”,就得写成:

SELECT name, CASE WHEN phone IS NULL OR phone = '' THEN '未填写' ELSE phone END AS phone FROM users;

热搜里还有一句“mysql设置默认值为0”,建表场景下就是DEFAULT 0,但如果历史表已经建好了,想给查询结果补 0,用IFNULL(amount, 0)就对了。

2.2 去重查询:DISTINCT 到底去的是什么

“sql语句去重查询”也是高频热搜。DISTINCT 的用法很简单:

SELECT DISTINCT status FROM orders;

这句会返回 orders 表里所有不重复的 status 值。多列去重也一样,SELECT DISTINCT user_id, status FROM orders去重的是(user_id, status)这个组合,不是分别去重。

但 DISTINCT 有几个容易被忽视的点。

第一,DISTINCT 不能部分去重。它作用于所有 SELECT 出的列,你不能只对某一列去重而保留其他列的值,这不符合 SQL 的集合语义。真想“按某列去重、取其他列某条记录”,得用窗口函数或 GROUP BY + 聚合,而不是 DISTINCT。

第二,DISTINCT 和 GROUP BY 都可以去重,但语义上 GROUP BY 更灵活。比如统计每个用户的订单数:

SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id;

这个用 DISTINCT 就不好写。反过来,如果只是简单去重,DISTINCT 的写法更直接。

第三,COUNT(DISTINCT col)是去重计数的标准写法,比如统计有几个人下过单:

SELECT COUNT(DISTINCT user_id) FROM orders;

这里有个性能点:COUNT(DISTINCT user_id)在亿级表上是大查询,往往会触发临时表去重。想要快,user_id 上要有索引,否则就是实打实扫描加排序。

2.3 别名、保留字与反引号的用法

给查询结果起别名时,AS 可以省略,比如SELECT name n FROM users,但我建议任何时候都写AS,可读性更好,也避免出一些莫名其妙的解析歧义。

另一个坑是字段名和保留字撞车。比如你有个字段叫order,直接写SELECT order FROM orders大概率会报语法错误,因为 ORDER 是关键字。解决办法是用反引号包起来:

SELECT `order` FROM orders;

MySQL 里反引号是默认的标识符引用符。还有个习惯问题:有人会把表名、字段名用双引号包起来,这在 MySQL 默认配置下会被当成字符串而不是标识符,导致报错。除非你改了ANSI_QUOTES模式,否则字符串用单引号,标识符用反引号,这是最稳的。

3. WHERE 与 IN 的恩怨:条件过滤里的类型陷阱和常见报错

3.1 IN 查询语句报错的最常见原因

热搜词里有“in查询语句报错”,这个我太有感触了。IN 的报错通常分两类,一类是语法级错误,另一类是逻辑级错误,后者最坑人。

先看语法级。有人写:

SELECT * FROM users WHERE id IN ();

空列表直接语法报错。更隐蔽的是程序动态拼接 SQL,比如 Java / Python 里传一个空集合进来,拼出来的 SQL 就变成了IN ()。解决方法是程序里先判空,空集合就不执行这条查询,比如 MyBatis 的<foreach>配合集合判空。

再看逻辑级。最常见的是字段类型不一致导致的隐式转换问题,我下面单独讲。还有一个很经典的是 NOT IN 和 NULL 的组合,这个也单独讲。

IN 列表过长也会有性能问题。当 IN 后面的值超过一定数量(MySQL 优化器有一个eq_range_index_dive_limit参数影响代价估算),优化器可能选择全表扫描而不是走索引。大批量数据过滤时,IN 不是好方案,可以考虑 JOIN 临时表。

3.2 NOT IN 遇上 NULL,结果让你怀疑人生

这个坑我踩过不止一次。看这句:

SELECT * FROM users WHERE id NOT IN (1, 2, NULL);

直觉上你会觉得它返回的是“id 不是 1、不是 2、不是 NULL 的所有用户”。但实际返回结果常常是空集。为什么?

因为id NOT IN (1, 2, NULL)等价于id != 1 AND id != 2 AND id != NULL。而在 SQL 的三值逻辑里,id != NULL的结果不是 TRUE 也不是 FALSE,而是 UNKNOWN。WHERE 只保留结果为 TRUE 的行,UNKNOWN 会被过滤掉,所以结果集为空。

这也是为什么很多开发规范里会明确说:IN 和 NOT IN 的列表里不要出现 NULL,如果字段本身允许 NULL,最好用 NOT EXISTS 或者显式加上AND id IS NOT NULL来处理。EXISTS 是二值逻辑,不会出这种诡异问题。

SELECT * FROM users u WHERE NOT EXISTS ( SELECT 1 FROM (SELECT 1 AS id UNION SELECT 2 UNION SELECT NULL) t WHERE t.id = u.id );

3.3 字符串字段用等值匹配,隐式转换让索引失效

热搜词里那句“select top 1000 * from [dbo].[dc_ods_rkyxjc_lotdatacollection]”对应到 MySQL 是SELECT * FROM dc_ods_rkyxjc_lotdatacollection LIMIT 1000,这种查法没什么坑,但真正常见的是查询条件里的类型不匹配。

比如 phone 字段是 varchar,你写:

SELECT * FROM users WHERE phone = 13800138000;

这里的 13800138000 是数字,MySQL 会把 phone 字段隐式转换为数字再比较。转换之后,phone 上的索引就废了,因为索引是基于原始字符串值建的,对索引列做函数或运算,优化器就没法走索引,只能全表扫描。

更严重的后果是匹配结果可能错。如果表里有一个 phone 是'13800138000abc',转换成数字时 MySQL 会尽量把开头的数字解析出来,结果'13800138000abc''13800138000'在数字比较下都等于 13800138000,你可能查出一条意料之外的记录。

解决办法很简单:字符串字段就用字符串字面量匹配,WHERE phone = '13800138000'。写 SQL 时养成“字段是什么类型就用什么类型”的习惯,能避开一半以上的性能坑。

3.4 LIKE 模糊查询的索引边界

LIKE也是查询语句里的高频词。LIKE 'abc%'可以走索引,因为前缀是确定的;LIKE '%abc'LIKE '%abc%'无法走索引,因为索引 B+ 树是按前缀排序的,前导通配符让优化器无法定位起始位置。

需要做模糊搜索又对性能有要求的,建议上全文索引或专门的搜索组件,而不是硬扛LIKE '%xxx%'。小表无所谓,大表会非常痛苦。

4. JOIN 的底层逻辑与 LEFT JOIN 的 NULL 陷阱

4.1 JOIN 的含义与三种常用连接

热搜词里“mysql数据库join含义”说明很多人对 JOIN 的理解还是停留在“把两个表拼起来”。从集合论的角度看,JOIN 的本质是基于某种关联条件,把两张表的行做组合,然后按连接类型决定保留哪些行。

  • INNER JOIN:只保留两边都匹配的行
  • LEFT JOIN:保留左表全部行,右表没有匹配就补 NULL
  • RIGHT JOIN:保留右表全部行,左表没有匹配就补 NULL
  • MySQL 8 没有直接的FULL OUTER JOIN,可以用LEFT JOIN UNION RIGHT JOIN模拟

举一个最容易理解的例子:用户表和订单表。

SELECT u.name, o.order_no FROM users u LEFT JOIN orders o ON u.id = o.user_id;

这条查询会返回所有用户,包括没下过单的用户,没下单的订单字段就是 NULL。如果换成 INNER JOIN,没下过单的用户就直接消失。

实际业务里 90% 的连接都是 INNER JOIN 和 LEFT JOIN。RIGHT JOIN 一般可以用 LEFT JOIN 倒过来写,可读性更好,我很少用。

4.2 ON 条件与 WHERE 条件的执行差异

LEFT JOIN 最容易出问题的地方,是把过滤条件写在 WHERE 里导致结果和预期不符。

比如你想统计每个用户的订单量,但只算已支付的订单:

SELECT u.name, COUNT(o.id) AS pay_cnt FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid' GROUP BY u.name;

如果写成:

SELECT u.name, COUNT(o.id) AS pay_cnt FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid' GROUP BY u.name;

看起来差不多,实际差远了。第二种写法里,WHERE 在 JOIN 完成后执行,没有已支付订单的用户,o.status 是 NULL,o.status = 'paid'不成立,整行被过滤掉,这些用户根本不会出现在结果里。这和一个没下过单的用户被“消失”是一样的道理。

所以记一条规则:LEFT JOIN 时,右表的过滤条件要写在 ON 里;如果你写在 WHERE 里,LEFT JOIN 就退化成 INNER JOIN 了。

4.3 驱动表和被驱动表:小表驱动大表

JOIN 还有一个底层优化点是“谁驱动谁”。MySQL 执行 JOIN 时,会选定一张表作为驱动表,先查驱动表,再用驱动表的结果去匹配被驱动表。被驱动表上有合适的索引时,匹配就是每次查索引,非常快。

优化器通常会选择小表作为驱动表,因为驱动表决定外层循环的次数。比如 users 有 100 条,orders 有 10 万条,用 users 驱动 orders,只需要循环 100 次去 orders 索引里查;反过来就是 10 万次。

实际开发中,你不需要每次都手动指定驱动表,但写 JOIN 时心里要清楚:被驱动表的连接字段一定要有索引。比如LEFT JOIN orders o ON u.id = o.user_id,被驱动表是 orders,o.user_id上建索引会显著提升性能。

4.4 关联字段类型不一致,索引等于白建

我之前排查过一个慢查询,两张表关联字段一个是 varchar,一个是 bigint,JOIN 的时候 MySQL 做了隐式转换,被驱动表上的索引完全没法用,查询直接变成全表扫描加嵌套循环,几万行的表跑了几秒。

排查方式就是 EXPLAIN,看 type 列从 ref 变成了 ALL,再看 key 列是 NULL,基本就能确定是关联条件上的类型问题。解决办法是统一字段类型,或者写 SQL 时显式转换,让优化器能正确用上索引。

5. 聚合、分组与 HAVING:光把数据查出来还不够,还得算出来

5.1 聚合函数的正确打开方式

MySQL 常用聚合函数就几个:COUNT、SUM、AVG、MAX、MIN。每个都有容易踩的细节。

COUNT 是最容易被误解的一个。COUNT(*)统计行数,COUNT(1)也是统计行数,两者在 MySQL InnoDB 下性能基本没差别,都遍历索引或数据行。COUNT(字段)则只统计该字段非 NULL 的行数。如果你想统计有效手机号的数量:

SELECT COUNT(phone) FROM users;

如果 phone 允许 NULL,这个数会和总行数不一致。很多人查“注册用户数”用 COUNT(phone),结果少算了没填手机号的用户,这就是逻辑错误。

SUM 的坑是空结果集返回 NULL 而不是 0。SELECT SUM(amount) FROM orders WHERE user_id = 999,如果这个用户没有订单,返回的是 NULL,不是 0。报表系统里往往希望看到 0,所以常用IFNULL(SUM(amount), 0)

AVG 会自动忽略 NULL。假如一组数是 10、20、NULL,AVG 结果是 15,不是 10。想考虑 NULL 当 0 算,得先IFNULL(col, 0)再 AVG,但要搞清楚业务上你要哪种语义。

5.2 GROUP BY 与 ONLY_FULL_GROUP_BY 模式

MySQL 5.7 之后默认开启了ONLY_FULL_GROUP_BY模式,这时候你写:

SELECT user_id, order_no, COUNT(*) FROM orders GROUP BY user_id;

大概率会报错,因为 order_no 没有出现在 GROUP BY 里,也没有被聚合函数包住。这个设计是符合 SQL 标准的:分组之后,每个组里的 order_no 可能有多条,到底取哪一条是不确定的。MySQL 8.0 里要取组内某条记录的字段,正确做法是用窗口函数或者子查询,而不是依赖“取第一条”这种非标准行为。

这个报错在热搜词里没出现,但面试基本必问。理解了它,你就理解了 GROUP BY 的语义边界。

5.3 HAVING 和 WHERE 的分工

WHERE 在分组前过滤行,HAVING 在分组后过滤组。最经典的例子是“找出下单次数超过 2 次的用户”:

SELECT user_id, COUNT(*) AS cnt FROM orders WHERE status = 'paid' GROUP BY user_id HAVING cnt > 2;

WHERE 先过滤掉未支付订单,GROUP BY 再按用户分组,HAVING 再过滤掉订单数不大于 2 的组。如果你想过滤“订单金额大于 100 的订单”,应该在 WHERE 里写,别放 HAVING,因为在分组前就能过滤掉的行,放到分组后再过滤,只会白白增加聚合计算的开销。

5.4 GROUP BY NULL 的一个冷知识

GROUP BY 会把 NULL 分成一组。比如用户表里有部分用户没有 age 字段,GROUP BY age会把 NULL 年龄归到一组,这组的数据同样参与聚合计算。很多人写报表时没注意,导致出现一行“年龄为空”的统计,看似异常,其实是因为没加WHERE age IS NOT NULL

6. 子查询、EXISTS 与更新子查询:嵌套逻辑这样写才不出错

6.1 子查询的三种位置

子查询可以出现在 SELECT 子句、FROM 子句、WHERE 子句里。

SELECT 子句里的标量子查询:

SELECT name, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_cnt FROM users u;

FROM 子句里的派生表:

SELECT * FROM ( SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id ) t WHERE t.total > 1000;

WHERE 子句里的子查询最常见,比如WHERE id IN (SELECT ...)

每种写法都有适用场景,但 FROM 子句的派生表要特别小心:MySQL 8.0 之前,派生表会被物化成临时表,不一定会用上索引,大结果集下性能很差。8.0 引入了派生表合并优化,但也不是百分百,复杂子查询仍可能生成临时表。能用 JOIN 表达的查询,优先用 JOIN,这是我一贯的经验。

6.2 EXISTS 与 IN 的选择逻辑

热搜词里虽然有“in查询语句报错”,但真正需要深入理解的是 EXISTS 和 IN 的取舍。网上流传的说法是“小表驱动大表用 IN,大表驱动小表用 EXISTS”,这个说法对,但不完整。

从语义上看,WHERE id IN (SELECT user_id FROM orders)是把子查询的结果集先算出来,再和外表做匹配;WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)是逐行判断外表记录是否满足存在条件。

当子查询结果集很小、且外表中匹配度较高时,IN 通常表现不错。当子查询结果集很大时,EXISTS 可能更优,因为它的判断可以提前终止。但 MySQL 8.0 的优化器已经会做半连接优化,很多情况下两者最终执行计划是一样的。所以真实场景里,我会这样建议:先看语义哪个清晰,再 EXPLAIN 看执行计划,不要背所谓的“铁律”。

EXISTS 里子查询的 SELECT 列表写SELECT 1SELECT *都一样,因为 EXISTS 只关心有没有行,不关心选什么列。这也是少数几个SELECT *在子查询里不背锅的场景。

6.3 MySQL 中更新子查询的一个大坑

热搜词“mysql中更新子查询”对应的典型场景是:你想根据另一张表的数据来更新当前表。比如把所有没下过单的用户标记为沉默用户:

UPDATE users SET is_silent = 1 WHERE id IN (SELECT user_id FROM orders WHERE order_time < '2024-01-01');

但如果你改成“更新同一张表的子查询”,就会遇到经典报错:

UPDATE users SET is_silent = 1 WHERE id IN (SELECT id FROM users WHERE age < 18);

MySQL 会报:You can't specify target table 'users' for update in FROM clause。原因很直接:UPDATE 时不能从目标表里直接 SELECT,因为这会带来并发一致性和执行顺序上的问题。

解决办法是套一层派生表,让子查询先物化成临时结果,再更新:

UPDATE users SET is_silent = 1 WHERE id IN ( SELECT id FROM ( SELECT id FROM users WHERE age < 18 ) tmp );

这个报错在 MySQL 里极其高频,面试里也很爱考。知道套一层临时表的解法,基本就能应付绝大多数场景。

6.4 关联子查询的性能隐患

关联子查询是指子查询里引用了外层查询的列,比如前面统计每个用户订单数的写法(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id)。这种写法逻辑清晰,但如果 users 表有几万行,orders 表有几百万行,每一行用户都要执行一次子查询,性能就很差。

我的建议是:报表统计场景尽量改写成 JOIN + GROUP BY,比如:

SELECT u.name, COUNT(o.id) AS order_cnt FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id, u.name;

JOIN 版本的执行计划更容易被优化器全局优化,而不是逐行去执行子查询。

7. 排序、分页与索引命中:从 EXPLAIN 看查询性能

7.1 ORDER BY 的 filesort 问题

“mysql排序”这个热搜词对应的核心问题是 filesort。ORDER BY排序时,如果排序字段能直接利用索引的顺序,MySQL 就不用额外排序;如果索引用不上,MySQL 就会把结果集加载到内存或磁盘临时文件排序,这个动作叫 filesort。

filesort 不一定慢,尤其在结果集只有几百行的时候。但结果集是几十万行时,filesort 会成为明显的瓶颈。让排序走索引的办法是:ORDER BY 的字段要和 WHERE 条件里的字段组成联合索引,并且顺序匹配。比如:

SELECT * FROM orders WHERE status = 'paid' ORDER BY create_time DESC;

如果建一个(status, create_time)联合索引,WHERE 用 status 定位,ORDER BY 直接按 create_time 顺序扫描,就能避免 filesort。这就是“索引即排序”的思路。

另外一个细节:MySQL 8.0 里ORDER BY可以配合DESC索引,但如果你在排序里混用 ASC 和 DESC,比如ORDER BY a ASC, b DESC,索引优化常常就会失效。

7.2 LIMIT 深分页的问题与延迟关联

LIMIT 1000000, 20这种写法非常常见,但性能很糟糕。MySQL 的执行逻辑是:扫描前 1000020 行,然后丢掉前 1000000 行,只返回最后 20 行。数据量越大,前面丢弃的行越多,查询就越慢。

优化的标准写法是“延迟关联”:先用覆盖索引查出目标主键,再回表取完整数据。假设 orders 表有千万级数据:

SELECT * FROM orders WHERE status = 'paid' ORDER BY id LIMIT 1000000, 20;

改成:

SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders WHERE status = 'paid' ORDER BY id LIMIT 1000000, 20 ) t ON o.id = t.id;

子查询在索引上只扫描主键,不走全行回表,然后再和主表 JOIN 拿 20 行完整数据。实测下来,深分页场景性能可以提升一个数量级。

7.3 EXPLAIN 快速体检:type、key、rows、Extra

想要系统排查查询性能,EXPLAIN 是基本功。我每次排查慢查询,必看这四个字段。

  • type:访问类型,从好到差大致是 system、const、eq_ref、ref、range、index、ALL。看到 ALL 就要警惕,说明是全表扫描。
  • key:实际用到的索引。NULL 表示没用索引。
  • rows:优化器预估扫描的行数。这个数是估算值,不是实际值,但数量级能指导你判断查询是否高效。
  • Extra:Using filesort、Using temporary 都是需要优化的信号;Using index 则是好信号,说明覆盖索引生效了。

下面这个表是我排查时的常用对照:

关键指标好信号危险信号
typeref / range / constALL / index
key具体索引名NULL
rows接近实际命中行数数倍甚至数十倍于预期
ExtraUsing indexUsing filesort / Using temporary

7.4 为什么说“索引”是查询语句集合的隐形主角

查询语句的写法直接影响索引能不能命中。很多人以为建了索引就万事大吉,实际上索引能不能生效,取决于你的写法。左边 LIKE、隐式类型转换、在索引列上做函数运算、OR 条件里有非索引列,都可能导致索引失效。

我之前排查过一个线上慢查询,SQL 是这样写的:

SELECT * FROM orders WHERE DATE(create_time) = '2024-06-01';

create_time 上有索引,但DATE(create_time)对索引列做了函数运算,索引直接失效,全表扫描。改成范围查询:

SELECT * FROM orders WHERE create_time >= '2024-06-01 00:00:00' AND create_time < '2024-06-02 00:00:00';

同样的业务目标,执行效率天差地别。这也是为什么我说,写查询语句不能只会套语法,得理解索引的底层结构。

8. 热搜里那些“看似查询、实则环境坑”的问题

8.1 MySQL 8.4 or later is required (found 8.0) 报错

热搜词里有一条 Django 报错:django.db.utils.NotSupportedError: MySQL 8.4 or later is required (found 8.0)。这不是 SQL 写错,而是依赖和版本不匹配的问题。

Django 某个版本开始要求 MySQL 客户端库或服务器版本达到 8.4,但服务器实际是 8.0。解决方案通常有两个:一是升级 MySQL 服务端到 8.4+;二是检查 Python 侧的 MySQL 驱动是否太旧,升级mysqlclientmysql-connector-python。我遇到的情况大多是驱动版本和 Django 版本不兼容,升级驱动就能解决,不用动数据库。

这个案例说明一个问题:很多报错看着像查询语句的问题,实际是环境层面的。排查时一定要先看完整报错栈,别一上来就盯着 SQL 本身。

8.2 MySQL 安装后初始密码和端口号

热搜词里“mysql的初始密码是什么”“mysql端口号”出现频率很高,这属于安装配置阶段的问题。MySQL 8.0 安装完成后,默认端口是 3306。如果用 tar 包或源码方式安装,初始化时系统会在日志里生成一个临时密码,常见路径是/var/log/mysqld.log,里面有一行类似A temporary password is generated for root@localhost: xxxxxx

拿到临时密码后,第一次登录建议立即修改:

ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码';

这里要提醒一下:MySQL 8.0 默认密码策略要求密码包含大小写字母、数字和特殊字符,长度至少 8 位。如果你设置简单密码,会直接报错,不是 SQL 语法问题,是密码策略问题。相关的validate_password组件可以调整策略,但线上环境不建议把密码策略调弱。

另外,Docker 安装 MySQL 时,端口映射很容易配错。比如宿主机 3306 已被占用,你映射到 3307,连接时却还在用 3306,就会一直连接失败。排查思路很简单:先看容器端口映射,再看防火墙,最后才看 MySQL 配置。

8.3 Navicat 和 Workbench 连接失败的常见原因

“navicat连接mysql”“mysql workbench使用教程”背后最常见的报错是 1045 和 1130。

1045 是 Access denied,用户名密码错误,或者该用户没有从当前主机访问的权限。1130 是 Host is not allowed to connect,意思是 root 用户默认只允许 localhost 连接,远程连不上。解决远程连接问题,需要创建一个允许指定主机访问的用户:

CREATE USER 'app'@'%' IDENTIFIED BY '密码'; GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app'@'%'; FLUSH PRIVILEGES;

这里%表示允许任意主机,生产环境建议换成具体 IP,缩小暴露面。我一般不用 root 做远程连接,单独建一个最小权限账号更安全。

8.4 general_log:查看系统到底执行了哪些查询

最后分享一个排查利器:MySQL 的通用查询日志。热搜词里“mysql logs目录下 general.log”说的就是这个。默认情况下 general_log 是关闭的,因为它会记录所有查询语句,高并发下日志量非常大。但你想知道某个程序到底执行了什么 SQL,或者怀疑有慢 SQL 在刷库时,它是唯一能看到全貌的地方。

临时开启:

SET GLOBAL general_log = ON; SET GLOBAL general_log_file = '/var/log/mysql/general.log';

看完之后记得关掉:

SET GLOBAL general_log = OFF;

线上不建议长期开启,但短时间排查“神秘查询”时非常有用。有一次我排查一个数据库负载异常,用 general_log 抓到了半夜定时任务在跑一条没有索引的SELECT * FROM orders WHERE status = 'pending' ORDER BY create_time DESC,锁和 IO 全部拉满。其实就是一条查询,但写法上既没覆盖索引,又用了SELECT *,优化之后负载直接降了一半。

这套查询语句的集合,从基础语法到执行原理再到排查手段,梳理下来其实就一条主线:写 SQL 的人,既要会用SELECT * FROM快速看数据,也要知道它背后的执行逻辑和代价。真正的高手不是背了多少语法,而是能解释每个写法为什么快、为什么慢、为什么会报错。希望这篇能帮你把 MySQL 查询这件事串成一条线,下次遇到问题,不再只是搜语法,而是能顺着思路自己定位。

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

从BI需求报告解读零售数据仓库设计与ETL分层实践

简介&#xff1a;某零售集团商业智能系统需求分析报告以Word文档形式交付&#xff0c;面向零售行业信息化规划人员、商业智能产品经理、数据仓库工程师与实施顾问&#xff0c;用于在BI二期建设中理清需求边界、功能模块与数据流转。报告系统拆解三大功能&#xff1a;日常业务报…

作者头像 李华
网站建设 2026/9/18 19:12:35

从docx到可视化:用Python解析轻食消费者调查数据全流程

简介&#xff1a;中国轻食行业消费者行为调查数据以文档形式呈现&#xff0c;适合餐饮品牌市场人员、行业分析师以及健康食品方向的学生&#xff0c;用于快速了解轻食消费市场。内容基于2023年艾媒咨询调查&#xff0c;完整记录了消费者食用轻食频率、喜欢的轻食类型、运动习惯…

作者头像 李华
网站建设 2026/9/18 19:12:15

STM32CubeMX2生成代码在Keil µVision5中编译调试全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 19:10:30

300节点大数据平台验收:TPC-DS、性能压测与量收迁移拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 19:10:12

正交试验设计原理与工业实战:从数学构造到注塑工艺优化

简介&#xff1a;本资源是一份面向高校统计学、工业工程及实验设计相关专业师生的《正交试验设计原理及实例》教学课件&#xff0c;系统讲解多因素优化试验的核心方法论&#xff0c;解决传统全面试验成本高、周期长、实施难等实际问题。课件以PPT格式呈现&#xff0c;共1个文件…

作者头像 李华
网站建设 2026/9/18 19:09:31

PyCharm 安装与更新全流程:解释器、虚拟环境与依赖管理避坑指南

Python IDE 这个说法听起来挺正式&#xff0c;实际落到日常开发里&#xff0c;它就是我们每天开门第一件事要面对的东西。我装 PyCharm 的经历算不上顺利&#xff0c;最早那台 4G 内存的笔记本&#xff0c;装完打开一个稍大的项目&#xff0c;索引转了十几分钟&#xff0c;风扇…

作者头像 李华