☰
MySQL单表查询从入门到优化:语法、执行顺序与索引实战
2026/10/1 4:02:04 网站建设 项目流程

很多人在 MySQL 上折腾了好几年,翻来覆去用的其实就那么几条 SQL,等真遇到“查得快一点”“把这几个字段拼一下”“按条件统计分组”这类需求时,就开始到处搜答案。我自己也带过不少新人,发现单表查询看起来简单,但要把 WHERE、ORDER BY、GROUP BY、LIMIT 这些组合出花来,同时还不踩性能的坑,这里面的门道其实不少。这篇文章就把单表查询整个脉络捋一遍,从基础语法到执行顺序,从条件过滤到分组聚合,再到索引优化和常见报错排查,帮你一次性搞定。

这篇文章适合两类人:一是刚入门、写 SELECT 还停留在SELECT * FROM table阶段的初学者;二是写了几年 SQL 但没系统梳理过、经常在慢查询和语义错误上栽跟头的开发。内容按“能用 → 用好 → 用快 → 用稳”的顺序展开,所有示例都是可直接复制的实战写法。

1. 单表查询的核心语法与执行顺序

1.1 SELECT 语句的完整结构

先看单表查询最完整的语法骨架:

SELECT [DISTINCT] select_expr, ... [FROM table_references] [WHERE where_condition] [GROUP BY {col_name | expr | position}] [HAVING where_condition] [ORDER BY {col_name | expr | position} [ASC | DESC]] [LIMIT {[offset,] row_count | row_count OFFSET offset}]

日常开发中 90% 的查询都跑不出这个框架。很多人写 SQL 是“想到哪写到哪”,其实 SQL 跟编程语言一样,有固定的执行顺序,搞不清这个顺序,就容易在逻辑上报错。

1.2 执行顺序才是真正的“潜规则”

SQL 的书写顺序和执行顺序不一样,这是新手最早掉进去的坑。以 MySQL 8.0 为例,一条单表查询的执行顺序是:

  1. FROM:确定从哪张表取数。
  2. WHERE:对行逐条过滤,此时 GROUP BY 还没执行,所以 WHERE 里不能使用聚合函数。
  3. GROUP BY:按指定列分组。
  4. HAVING:对分组后的结果进行过滤,这里可以使用聚合函数。
  5. SELECT:计算并投影需要的列。
  6. ORDER BY:对最终结果排序。
  7. LIMIT:截取指定行数。

这个顺序解释了三个高频疑问:

  • 为什么WHERE里不能写WHERE COUNT(*) > 1?因为执行 WHERE 时还没分组,聚合函数根本没法算。
  • 为什么HAVING既能过滤分组,又能用别名,而WHERE不能?因为 SELECT 在 HAVING 之前执行,HAVING 已经能看到计算后的列了。
  • 为什么ORDER BY后面可以写 SELECT 里的别名?因为排序发生在投影之后。

注意:MySQL 8.0.22 之前,GROUP BY 默认还允许GROUP BY后面使用 SELECT 中的别名,但从 8.0.13 开始部分场景已有变更。实际项目里建议 GROUP BY 一律写原始列名或表达式,别依赖别名,避免升级数据库后 SQL 突然报错。

1.3 写查询前的三个习惯

我在写单表查询时,无论多简单的语句,都会先问自己三个问题:

  • 我要从哪张表取数?确认表名和字段名,别想当然。
  • 取数范围是什么?即 WHERE 条件怎么限定,先粗筛还是先细筛。
  • 取回来怎么用?是要明细、要统计,还是要排序分页?

这个习惯能减少相当一部分“SQL 跑通了但结果不对”的返工。比如你想统计“每个分类的商品数”,但 FROM 后面跟错了表,后面所有逻辑全白搭。

2. WHERE 条件过滤:从基础到“花式”写法

2.1 基础比较与逻辑组合

WHERE 子句是单表查询的重中之重,它决定了你能拿回哪些行。最基础的写法就是比较运算符配合逻辑组合:

SELECT id, name, price, stock FROM product WHERE category_id = 5 AND price >= 100 AND price < 500 OR status = 1;

这里有个经典陷阱:AND的优先级高于OR。上面这条 SQL 的实际语义是category_id = 5 AND (price >= 100 AND price < 500) OR status = 1,也就是“分类为 5 且价格在区间内,或者状态为 1 的所有商品”。如果你的本意是“分类为 5 且(价格在区间内或状态为 1)”,必须加括号:

SELECT id, name, price, stock FROM product WHERE category_id = 5 AND (price BETWEEN 100 AND 499 OR status = 1);

优先级这个坑,我见过不止一次出现在生产事故里。代码评审阶段看到 OR 和 AND 混用但没加括号的 SQL,基本可以直接打回。

2.2 模糊查询 LIKE 的正确姿势

LIKE 是单表查询里使用频率极高、也最容易拖垮性能的条件之一。

SELECT id, name FROM user WHERE name LIKE '张%'; -- 前缀匹配,可能走索引 SELECT id, name FROM user WHERE name LIKE '%张'; -- 后缀匹配,索引失效 SELECT id, name FROM user WHERE name LIKE '%张%'; -- 包含匹配,索引失效

为什么前缀匹配能走索引而后缀匹配不能?MySQL 的 B+ 树索引是按键值顺序排列的,前缀匹配相当于在一个有序数组里按范围查找,索引天然支持;后缀匹配等于要遍历所有索引键做后缀判断,优化器只能放弃索引。

实际工作中,如果业务确实需要模糊搜索且数据量很大,我的建议是:

  • 少量数据的后缀/包含匹配,直接 LIKE + 扫表可以接受,别过度设计。
  • 数据量大,优先考虑全文索引(MySQL 全文索引支持中文要配分词器)或者换 Elasticsearch 这类搜索引擎,不要硬刚 MySQL LIKE。

2.3 IN、BETWEEN 与空值判断

SELECT * FROM order WHERE status IN (1, 3, 5); SELECT * FROM order WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31'; SELECT * FROM user WHERE phone IS NULL; SELECT * FROM user WHERE phone IS NOT NULL;

这里有个特别值得注意的点:NULL 是一种状态,不是值。phone = NULL和phone != NULL的结果永远是 NULL,不会命中任何行。必须用IS NULL/IS NOT NULL判断。同样的坑也出现在 NOT IN 子查询里,如果子查询结果中包含 NULL,整个 NOT IN 的结果就会是空集。比如:

SELECT * FROM user WHERE id NOT IN (SELECT user_id FROM blacklist);

如果blacklist.user_id中有 NULL,这条 SQL 一条数据也查不出来。这属于语义陷阱,很多人查了半天没结果,最后才发现是 NULL 在作怪。稳妥做法是改成:

SELECT * FROM user u LEFT JOIN blacklist b ON u.id = b.user_id WHERE b.user_id IS NULL;

2.4 条件过滤里的“不等于”写法

!=和<>都可以表示不等于,MySQL 里两者等价。但要注意,!=通常无法使用索引优化(取决于优化器和数据分布),尤其当“不等于”过滤掉的数据只占少数时,反而走全表扫描更快,优化器有自己的判断,不用强行干预。真正需要注意的,是业务上“不等于”往往还要带上 IS NULL:

SELECT * FROM user WHERE deleted != 1 OR deleted IS NULL;

很多表设计了逻辑删除位,查询有效数据时只写WHERE deleted = 0会漏掉deleted为 NULL 的记录(如果允许 NULL 的话)。在表结构设计阶段就应把这类字段定为NOT NULL DEFAULT 0,从源头规避这个坑。

3. 排序、分组与聚合:统计报表的基石

3.1 ORDER BY 的排序规则与多列排序

SELECT id, name, price, sales FROM product ORDER BY sales DESC, price ASC;

多列排序的执行逻辑是:先按第一列排,第一列相同的情况下再按第二列排。上面的例子就是“销量高的在前,销量相同则价格低的在前”。这在商品列表页、排行榜场景里非常常见。

一个隐蔽的坑是排序与字符集/排序规则的关系。MySQL 里ORDER BY默认按列的 collation 排序。如果列是utf8mb4_general_ci,排序时是不区分大小写的;如果是utf8mb4_bin,则区分。碰到“为什么我排序的结果跟字典顺序不一样”的疑问,先查列的 collation 设置。

3.2 GROUP BY 分组统计的正确打开方式

单表查询的分组统计,是数据分析的基本功。先看一个典型需求:统计每个分类下的商品数量和平均价格。

SELECT category_id, COUNT(*) AS cnt, ROUND(AVG(price), 2) AS avg_price, MAX(price) AS max_price, MIN(price) AS min_price FROM product WHERE status = 1 GROUP BY category_id ORDER BY cnt DESC;

这里要注意COUNT(*)和COUNT(col)的区别:

  • COUNT(*)统计分组内的总行数,包含 NULL。
  • COUNT(col)统计该列非 NULL 的行数。
  • COUNT(DISTINCT col)统计该列去重后的非 NULL 行数。

如果业务上需要统计“有多少个用户下过单”,用COUNT(DISTINCT user_id);如果只是统计“有多少条订单记录”,用COUNT(*)即可,两者语义完全不同,混用会导致报表数字对不上。

3.3 HAVING 与 WHERE 的分工

HAVING 是在 GROUP BY 之后对分组结果做过滤,WHERE 是在分组前对明细行做过滤。这个分工决定了两者的使用场景:

SELECT category_id, COUNT(*) AS cnt FROM product WHERE price > 0 GROUP BY category_id HAVING COUNT(*) >= 10;

先通过 WHERE 排除掉price <= 0的无效数据,再对剩余数据分组,最后用 HAVING 筛选出“商品数不少于 10 个”的分类。能用 WHERE 过滤的,绝不放到 HAVING。因为先过滤再分组,每组的数据量更小,聚合计算更快,也能减少 GROUP BY 的临时表压力。这个习惯在大表上能明显看到性能差异。

3.4 聚合函数实战细节

常用的聚合函数有COUNT、SUM、AVG、MAX、MIN。有几个细节值得单独拎出来:

  • SUM遇到全是 NULL 时返回 NULL,而不是 0。需要展示 0 可以用IFNULL(SUM(col), 0)。
  • AVG会自动忽略 NULL,但不会忽略 0。如果业务上“除数为 0 的记录”需要平均值为 0,要自己预处理。
  • MAX和MIN适用于任何可比较类型,包括字符串和日期。日期列取最大最小值很常用:SELECT MAX(create_time) FROM user就能拿到最近注册时间。

3.5 DISTINCT 的使用边界

SELECT DISTINCT category_id FROM product; SELECT COUNT(DISTINCT user_id) FROM order;

DISTINCT 在数据量小时没问题,数据量大了容易造成临时表过大甚至磁盘临时表。如果只是想去重统计数量,用COUNT(DISTINCT col)就够了,不要先用SELECT DISTINCT查出所有行再在应用层计数,那样网络传输和内存开销都大得多。

4. 单表查询的进阶玩法:表达式、函数与子查询

4.1 使用表达式与 CASE WHEN 加工列

查询不仅仅是“原样取列”,很多时候需要加工。比如计算商品折扣价:

SELECT id, name, price, discount, ROUND(price * discount, 2) AS final_price, CASE WHEN discount < 0.8 THEN '大促' WHEN discount < 0.95 THEN '普通优惠' ELSE '无折扣' END AS discount_level FROM product WHERE status = 1;

CASE WHEN 是单表查询里非常灵活的工具,可以基于多条件生成新的分类列,再配合 GROUP BY 做多维统计。比如统计“不同折扣力度的商品数量”:

SELECT CASE WHEN discount < 0.8 THEN '大促' WHEN discount < 0.95 THEN '普通优惠' ELSE '无折扣' END AS discount_level, COUNT(*) AS cnt FROM product WHERE status = 1 GROUP BY discount_level;

这里有个细节:MySQL 允许GROUP BY case_level这样的别名写法,但在其他数据库里不一定支持。为了可移植性,建议 GROUP BY 子句里把 CASE WHEN 完整写一遍,而不是用别名。

4.2 常用的字符串与日期函数

单表查询里高频使用的函数,我整理一批:

字符串函数:

  • CONCAT(a, b):拼接字符串。
  • SUBSTRING(str, pos, len):截取子串。
  • LEFT(str, n)/RIGHT(str, n):从左侧/右侧截取。
  • LENGTH(str):返回字节数,注意中文在 utf8mb4 下占 3 字节,这是初学者最容易蒙圈的地方。
  • CHAR_LENGTH(str):返回字符数,统计中文长度用这个。
  • REPLACE(str, old, new):替换字符串。
  • TRIM(str):去掉首尾空格。
  • UPPER(str)/LOWER(str):转大写/小写。

日期函数:

  • NOW():当前日期时间。
  • CURDATE():当前日期。
  • DATE_FORMAT(date, '%Y-%m-%d'):格式化日期,非常常用。
  • DATE_ADD(date, INTERVAL 1 DAY):日期加减。
  • DATEDIFF(d1, d2):两个日期相差天数。
  • YEAR()/MONTH()/DAY():提取年/月/日。

比如按月份统计订单数:

SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS order_cnt, SUM(amount) AS total_amount FROM orders WHERE create_time >= '2024-01-01' GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month;

注意:对create_time使用DATE_FORMAT后,这列的索引就基本无法利用了。如果按月份分组的数据量很大,建议在表里冗余一个month_key字段,或者查询条件用范围过滤而不是函数格式化。这是典型的“写起来省事、跑起来费劲”的写法,要权衡取舍。

4.3 把字符串转成日期的几种姿势

热词里专门提到“mysql将字符串转为日期”,实际项目里外部导入的数据常常是字符串日期。MySQL 提供了几个转换方法:

-- 严格转换,格式不对直接报错 SELECT STR_TO_DATE('2024-01-15', '%Y-%m-%d'); -- 自动解析,格式必须符合默认标准 SELECT CAST('2024-01-15' AS DATE); -- 隐式转换,比较时 MySQL 自动把字符串转日期 SELECT * FROM orders WHERE create_time > '2024-01-15';

STR_TO_DATE最灵活,因为可以指定任意格式,比如'2024/01/15 14:30:00'对应格式'%Y/%m/%d %H:%i:%s'。这里提示一句,WHERE create_time > '2024-01-15'这种写法里,字符串会被隐式转换,前提是字符串格式是'YYYY-MM-DD'之类的标准格式。如果是'2024/01/15',MySQL 8.0 里也可能出问题,最好先用STR_TO_DATE转换为 DATE 类型再比较,避免歧义。

4.4 单表子查询:当成“临时表”用

单表查询里的子查询,通常是把子查询的结果当成一个临时数据源,外层再对它查询。典型场景是求“每个分类下价格最高的商品”:

SELECT id, name, price, category_id FROM product p WHERE price = ( SELECT MAX(price) FROM product p2 WHERE p2.category_id = p.category_id );

这种写法叫作相关子查询,内层查询引用外层表的列。它的执行方式是逐行判断,数据量大时性能往往不好。更好的写法是:

SELECT p.id, p.name, p.price, p.category_id FROM product p INNER JOIN ( SELECT category_id, MAX(price) AS max_price FROM product GROUP BY category_id ) t ON p.category_id = t.category_id AND p.price = t.max_price;

先算出每个分类的最高价,再通过 JOIN 把明细行匹配出来。两者逻辑等价,但第二种写法在多数场景下性能更稳,因为子查询只跑一次,外层 JOIN 还能借用索引。这也是我日常写 SQL 的一个偏好:能用 JOIN 表达集合逻辑的,尽量少用相关子查询。

4.5 LIMIT 分页的深水区

LIMIT 的语法很简单,但分页深度是个隐藏问题:

SELECT * FROM user ORDER BY id LIMIT 0, 20; -- 第1页 SELECT * FROM user ORDER BY id LIMIT 20, 20; -- 第2页 SELECT * FROM user ORDER BY id LIMIT 1000000, 20; -- 第50001页,性能差

LIMIT 的 offset 越大,MySQL 需要扫描并丢弃的行越多。LIMIT 1000000, 20实际上要扫描 1000020 行然后丢掉前 1000000 行,代价极高。深分页的常见优化方案是延迟关联:

SELECT u.* FROM user u INNER JOIN ( SELECT id FROM user ORDER BY id LIMIT 1000000, 20 ) t ON u.id = t.id;

子查询里只查id列,扫描和排序的代价大幅下降,再用 id 关联回原表取出完整行。这在只查 id 能走覆盖索引的场景下提升非常明显。

5. 性能优化:让单表查询“飞”起来

5.1 索引是提速的第一手段

单表查询的性能瓶颈,绝大多数都能靠索引解决。索引的本质是排好序的数据结构,它让 MySQL 不再全表扫描,而是通过 B+ 树快速定位。创建索引的语法:

CREATE INDEX idx_product_category ON product(category_id); CREATE INDEX idx_product_cat_price ON product(category_id, price); CREATE INDEX idx_user_phone ON user(phone);

给查询条件列建索引是最基础的优化。但索引不是越多越好——每个索引都是“额外的一份有序副本”,写入数据时要同步维护,索引太多反而拖慢 INSERT/UPDATE。一个经验法则是:

  • 查询中频繁使用的 WHERE 条件列,优先建索引。
  • 组合索引有“最左前缀原则”,(category_id, price)能加速WHERE category_id = ?和WHERE category_id = ? AND price = ?两类查询,但不能直接加速WHERE price = ?。
  • 区分度低的列(如 status 只有 0/1)建索引意义不大,优化器大概率还是选全表扫描。

5.2 最左前缀原则与索引失效场景

组合索引(a, b, c)可以匹配的查询条件如下:

查询条件是否可用索引说明
a = ?可用走了索引第一列
a = ? AND b = ?可用走了前两列
a = ? AND b = ? AND c = ?可用全列匹配
b = ?不可用跳过了第一列
a = ? AND c = ?部分可用只用 a 列过滤,c 在索引内过滤不了
a IN (...) AND b = ?可用MySQL 8.0 对 IN 有优化,较复杂

索引失效的常见场景要背下来:

  • 对列进行函数运算,如WHERE DATE(create_time) = '2024-01-15'。
  • 隐式类型转换,如WHERE phone = 13800138000,phone 是 varchar 但用了数字比较。
  • LIKE 通配符开头,如WHERE name LIKE '%张'。
  • OR 连接非索引列,如WHERE id = 1 OR name = '张',除非两个条件都走索引。

5.3 覆盖索引与回表

覆盖索引指查询的列全部在索引中,不需要回表读取数据行,这是查询性能的重要优化方向。比如:

-- 只需要 id 和 category_id 两个列 SELECT category_id, COUNT(id) FROM product WHERE category_id = 5 GROUP BY category_id;

如果存在(category_id, id)组合索引,这条 SQL 可以完全在索引上完成,不需要回表。日常排查慢查询时,看EXPLAIN输出里的Extra字段如果出现Using index,就说明走了覆盖索引;出现Using filesort则说明排序没走到索引。

5.4 EXPLAIN 实测案例

写任何一条“感觉会慢”的查询,我都会先放EXPLAIN前面走一遍:

EXPLAIN SELECT id, name, price FROM product WHERE category_id = 5 AND price > 100;

输出结果里重点关注几列:

  • type:访问类型,从好到差依次是system>const>eq_ref>ref>range>index>ALL。看到ALL基本就是全表扫描。
  • possible_keys:可能用的索引。
  • key:实际用的索引。
  • rows:预估扫描行数,这个数越小越好。
  • Extra:额外信息,Using filesort、Using temporary都是要警惕的信号。

我自己的习惯是:单表查询如果预估rows超过表行数的 10%,或者Extra出现Using temporary,就停下来重新审视 SQL,而不是直接把线上跑挂。

5.5 什么时候不该用索引

索引不是万能药。比如表只有几百行,全表扫描比走索引还快,因为走索引要额外做一次回表,反而增加 IO。另外,频繁更新的列不适合建索引,更新索引本身的成本可能比查询收益还大。生产环境里我见过有人把每个列都建了索引,结果一张表二十几个索引,插入一次耗时翻了几倍,这就是典型的“过度索引”。

一个务实的做法:先用真实数据量压测,找到慢查询,再针对慢查询建索引,而不是凭想象提前建一堆索引“以防万一”。

6. 单表查询的常见坑与排查技巧实录

6.1 查不出数据?先把 WHERE 拆开

“为什么我查不到数据”是我被问过最多的问题。排查思路是按“层层剥洋葱”的顺序:

  1. 先不加 WHERE 查全表,确认数据在不在。
  2. 逐步添加条件,每次加一个,看哪一层过滤后结果开始为空。
  3. 重点检查字符串两边是否有空格、大小写是否精确、NULL 是否参与判断。
  4. 检查是否混淆了=与<=>,后者才是 NULL 安全等于。

我踩过最典型的一次:导入历史数据时,手机号列被存成13800138000(末尾带空格),前端查询传的是13800138000,WHERE phone = '13800138000'永远查不到。最后用WHERE TRIM(phone) = '13800138000'排查出来了。这也提醒我,入库前做好数据清洗,能省掉后面无数个“查不到”的深夜里“。

6.2 排序结果“不正常”?

如果ORDER BY出来的顺序觉得“不对”,先确认排序字段的数据类型。常见问题:

  • 列是 varchar 类型,但存的是数字,排序结果会是1, 10, 100, 2, 20这种字典序。
  • 中文排序依赖 collation,utf8mb4_general_ci下的中文排序结果可能不符合拼音预期,需要ORDER BY CONVERT(name USING gbk)才能按拼音排。
  • 多列排序里,某列升降序写反了。

有个小经验:排序字段如果有 NULL 值,ORDER BY ASC 时 NULL 排在最后,ORDER BY DESC 时 NULL 排在最前,跟大多数业务预期刚好相反。需要让 NULL 固定排最后,可以写成:

SELECT * FROM user ORDER BY ISNULL(phone), phone;

6.3 GROUP BY 查询结果只显示一部分?

SQL 模式开了ONLY_FULL_GROUP_BY时(MySQL 5.7 及以后默认开启),SELECT 的列必须是 GROUP BY 列或聚合函数。

-- 在 ONLY_FULL_GROUP_BY 下报错 SELECT id, name, COUNT(*) FROM product GROUP BY category_id;

id和name既不分组也不聚合,MySQL 会直接报错。业务上如果要“每个分类下面的一条商品信息”,就不能这么写。常见解法是用聚合函数取一个,比如MAX(id),或者用上面提到的 JOIN 子查询方案。这条限制刚上线时坑了不少老项目,因为旧版本默认没开这个模式。

6.4 慢查询从哪查起

排查慢 SQL 的完整链路我总结为四步:

  1. 打开慢查询日志,定位long_query_time,比如 1 秒。
  2. 对慢查询语句加EXPLAIN,看type、rows、Extra。
  3. 看是否缺索引或索引使用错误,按需加索引。
  4. 看是否可以把SELECT *改成只查需要的列,减少回表和网络传输。
-- 查看当前慢查询日志配置 SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time'; -- 动态开启慢查询日志(重启失效,生产慎用) SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1;

6.5 一个真实的生产排查案例

我给读者还原一个之前经手的实际场景:一张订单表,数据量约 300 万行。业务反馈某统计页面打开要 5 秒多,SQL 如下:

SELECT customer_id, COUNT(*) AS order_cnt, SUM(amount) AS total_amount FROM orders WHERE create_time BETWEEN '2024-01-01' AND '2024-06-30' AND status = 'PAID' GROUP BY customer_id ORDER BY total_amount DESC LIMIT 50;

EXPLAIN 结果里type = ALL,rows = 3000000,Extra 显示Using temporary; Using filesort。问题很清楚:

  • status列区分度低,索引价值不大。
  • create_time上有单列索引,但加上status过滤后,优化器评估后还是走了全表扫描。

最终方案:创建组合索引(status, create_time),同时把create_time BETWEEN的范围再结合业务压缩近一个月。调整后type变成range,扫描行数从 300 万降到 30 万左右,页面响应从 5 秒降到 0.4 秒。

这类优化没有玄学,就是一步步用 EXPLAIN 定位“慢在哪”,再决定“怎么改索引”和“怎么改 SQL”。

6.6 通往查询“快、准、稳”的四个检查清单

最后分享一个我自己整理的单表查询检查清单,每次上线 SQL 前过一遍:

  • 正确性:WHERE 条件的逻辑组合是否正确,括号有没有加到位,NULL 判断用没用到IS NULL。
  • 语义:COUNT(*)与COUNT(col)用得对不对,HAVING 是否能用 WHERE 替代。
  • 性能:EXPLAIN 里type是否还是ALL,Extra里有没有Using temporary/Using filesort。
  • 健壮性:对列做函数运算有没有必要,排序字段的 collation 是否符合预期,深分页有没有替代方案。

这个清单我用了很多年,配合慢查询日志,基本能把 95% 的单表查询问题拦在上线之前。MySQL 单表查询看着简单,真正写好、写快、写稳,靠的是对执行顺序、索引原理和数据类型的组合理解。少从网上复制“能跑”的 SQL,多花时间琢磨一下为什么慢、为什么错,长期下来收益非常大。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询