
最近在论坛和社群里看到不少人在搜“sql语句去重”“sql去除空值”“sql server 时间函数”“慢sql优化 explain主要看哪些信息”这类问题说实话挺感慨的。很多同学写SQL已经能跑通业务了但遇到去重、NULL判断、时间比较这种基础操作还是会卡住或者写出看似正确实则埋雷的语句。这篇SQL常用语句基础大全我不打算给你罗列一堆官方文档式的语法而是站在实际开发的角度把日常最高频、面试最容易问、踩坑最多的一批语句掰开揉碎讲清楚。适合刚学数据库的入门者也适合写了半年一年仍然对某些细节模棱两可的人。看完这篇你会发现很多“奇怪问题”其实都是基础没夯实。1. 先懂执行顺序再谈SQL常用语句1.1 为什么很多人把SQL写成“黑盒”我见过不少开发同学写SQL全靠肌肉记忆SELECT后面跟一堆列FROM表WHERE过滤GROUP BY分组ORDER BY排序看起来都有。但一旦出错只能瞎试一会儿把条件放WHERE一会儿放HAVING一会儿在SELECT里用别名做筛选发现报错就懵了。根源在于他从没理解SQL的执行顺序把SQL当成了“从上往下翻译”的普通编程语言。SQL是一门声明式语言核心逻辑是“你告诉我想要什么结果数据库自己决定怎么取”。这个思维和命令式语言完全不同。你去问MySQL优化器“这条语句该怎么跑”它内部会基于统计信息、索引情况生成一个执行计划。但优化器再怎么变逻辑上的执行顺序是相对固定的。如果你不知道这个顺序写出来的SQL可能慢得离谱甚至结果都是错的。1.2 逻辑执行顺序FROM先于SELECTWHERE先于GROUP BY我直接给结论SQL逻辑执行顺序大致如下FROM确定数据源如果是多表先做笛卡尔积或联表WHERE对FROM的结果做行级筛选GROUP BY把筛选后的数据分组HAVING对分组后的结果做过滤SELECT投影出你要的列此时可以计算别名等表达式ORDER BY对最终结果排序LIMIT / OFFSET截取部分行很多人不理解为什么WHERE里面不能用SELECT的别名因为SELECT的别名是在第5步才生成的WHERE在第2步执行那时候别名根本不存在。同理很多人问为什么WHERE和HAVING看着都能过滤差别在哪差别就在于执行时机WHERE先于GROUP BY所以它过滤的是“分组前的原始行”HAVING后于GROUP BY过滤的是“分组后的聚合结果”。你可以在WHERE里写amount 100但如果想过滤“总金额大于100的客户”就必须用HAVING因为SUM(amount)这种聚合值是在GROUP BY之后才出来的。理解了这个顺序还能解释一个常见现象为什么某些SQL在数据量小的时候跑得飞快数据量一大就卡死。比如你在WHERE里用了UPPER(name) ABC或者YEAR(create_time) 2024这类对列做函数运算的写法会破坏索引优化器只能全表扫描。基础语句好写但写好需要你对“索引对查询的影响”有意识。1.3 掌握执行顺序后很多“怪问题”会自然消失我举个例子。有次帮同事排查一个报表SQL他写的是SELECT department, COUNT(*) AS cnt FROM employee WHERE cnt 10 GROUP BY department;这条语句执行直接报错他一度怀疑是数据库版本问题。实际上就是执行顺序问题cnt这个别名在SELECT阶段才生成WHERE阶段根本访问不到。正确写法是SELECT department, COUNT(*) AS cnt FROM employee GROUP BY department HAVING COUNT(*) 10;另一个常见坑是ORDER BY和LIMIT的顺序。有人写LIMIT 10 ORDER BY score DESC在大多数数据库里这种语法不会报错但含义完全不对优化器会先取前10行再排序结果乱七八糟。正确顺序一定是先ORDER BY再LIMIT。类似这类看似小到不能再小的问题恰恰是基础不牢的表现。我的建议是每写一条SQL都在心里过一遍它的逻辑执行顺序这对后续调优、排查都会有质的帮助。2. 查询三件套条件筛选、排序与去重的细节2.1 WHERE条件运算符、优先级与LIKE细节条件筛选是SQL里最常用的部分但细节决定成败。先看一个完整示例SELECT emp_id, emp_name, salary, dept_id, hire_date FROM employee WHERE dept_id 10 AND salary 5000 AND hire_date 2023-01-01 ORDER BY salary DESC;这里需要注意几个运算符的优先级问题。AND的优先级高于OR所以WHERE a 1 OR a 2 AND b 3实际是a 1 OR (a 2 AND b 3)不是你以为的(a 1 OR a 2) AND b 3。这种隐晦的优先级很容易让结果出乎意料。我的习惯是只要条件组合超过两层一律加括号。括号不会让SQL变慢但能让阅读的人和你自己少掉很多头发。IN和NOT IN也很常用但有个细节当IN列表里含NULL时行为会变得诡异。比如WHERE dept_id NOT IN (10, 20, NULL)这条语句不会返回任何行。原因是NOT IN遇到NULL时比较结果不是TRUE而是UNKNOWNUNKNOWN会被当作不满足条件过滤掉。很多人第一次遇到这个现象都以为是bug其实是三值逻辑的必然结果。如果你确实要排除某些值且列表可能含NULL要么加AND dept_id IS NOT NULL要么用NOT EXISTS。再说LIKE。基础用法是%表示任意多个字符_表示一个字符。比如WHERE name LIKE 张%是姓张的人WHERE name LIKE _张%是第二个字为“张”的人。但LIKE的坑在于通配符放在开头%abc会导致索引失效全表扫描数据量大时性能堪忧。如果你要匹配的文本本身包含%或_需要转义LIKE 50\% ESCAPE \。我在实际开发里见过太多因为LIKE性能问题导致的慢SQL“慢sql优化”搜索词常年上榜不是没原因的。判断一个筛选条件能不能走索引可以简单看一点条件左侧是不是对列做了运算或类型转换。WHERE name LIKE abc%可以走索引WHERE name LIKE %abc%走不了这句话背下来能解决你30%的SQL慢问题。2.2 ORDER BY排序多列排序与NULL位置排序看起来简单ORDER BY column DESC谁都会写但多列排序和NULL值的排序位置往往被忽略。看这个例子SELECT emp_id, emp_name, salary, dept_id FROM employee ORDER BY dept_id ASC, salary DESC;这种写法表示先按dept_id升序部门相同再按salary降序。你要理解的是排序优先级是从左到右不是让你先单独按dept_id排一次再按salary排一次。如果想按“每个部门里工资最高的人”这个语义筛选那需要窗口函数或GROUP BY不是简单ORDER BY能解决的后面第5章会展开。关于NULL的排序位置不同数据库行为不一样MySQLNULL默认排在最小值之前也就是ASC时NULL排最前。SQL ServerNULL默认排最前。OracleNULL默认排最后ASC时排最后。PostgreSQLNULL默认排最后。这就是为什么如果你依赖默认行为写报表换数据库后结果对不上。建议明确指定自己的意图。比如MySQL中想强行把NULL放最后可以写SELECT emp_id, emp_name, salary FROM employee ORDER BY ISNULL(salary) ASC, salary DESC;这里ISNULL(salary)为1的NULL行排在最后。排序这东西写清楚比写花哨重要因为你永远不知道看这条SQL的下一个人是什么基础。2.3 DISTINCT去重的本质与三个容易踩的坑去重是搜索高频词“sql语句去重查询”里的核心操作。先明确一点SELECT DISTINCT col1, col2去重的单位是“整行组合”不是单个列。很多人疑惑为什么SELECT DISTINCT department FROM employee明明有重复结果还是出现了重复。如果你写的是SELECT DISTINCT department FROM employee那确实会去掉完全相同的行但如果后面多跟了一个id列DISTINCT会基于id, department的组合判断这时department重复就正常了。我遇到过三个比较典型的坑第一DISTINCT和COUNT的搭配。SELECT COUNT(DISTINCT department) FROM employee是统计不重复的部门数这个没问题。但SELECT COUNT(department) FROM employee统计的是非NULL的行数不是不重复数。如果你是先SELECT DISTINCT department出来再数有几个那等价于前者。别再犯“先查出来再数行数”这种低级错误了。第二DISTINCT和ORDER BY的冲突。比如SELECT DISTINCT department FROM employee ORDER BY emp_name这经常直接报错因为emp_name没有出现在SELECT列表中排序无法进行。逻辑上也能理解去重后的结果集里根本没有emp_name这个列。解决方法是把排序列也放入SELECT中或改用GROUP BY。第三DISTINCT和GROUP BY的选择。很多人不知道SELECT DISTINCT a, b FROM t其实等价于SELECT a, b FROM t GROUP BY a, b。理解到这一层之后遇到“去重后还要和其他表联表”的场景你就会更倾向于用GROUP BY的写法因为它能顺带带上聚合或子查询结果扩展性更强。DISTINCT适合简单去重要实现“保留每组最新一条记录”这类需求得靠窗口函数单纯DISTINCT搞不定。3. 空值与时间两类极易翻车的数据处理场景3.1 NULL不是空字符串IS NULL与COALESCE的正确用法搜索热词里常年有“sql去除空值”说明NULL处理是很多人的痛点。我反复跟团队强调一句话NULL不是值它表示“未知”。它既不是0也不是空字符串更不等于NULL本身。所以在SQL里写WHERE name NULL永远是查不到数据的必须用WHERE name IS NULL。处理NULL的常用手段有IS NULL/IS NOT NULL判断是否为空。COALESCE(col1, col2, 0)返回第一个非NULL的值适合给NULL填默认值。IFNULL(col, 0)MySQL专用两个参数等价于COALESCE(col, 0)。NULLIF(a, b)如果a等于b返回NULL否则返回a。这个在除法运算里防除零特别好用。举个实际例子统计员工工资但奖金字段可能为NULL想算“总收入”SELECT emp_name, salary COALESCE(bonus, 0) AS total_income FROM employee;如果直接写salary bonus只要bonus为NULL整个表达式结果就变成NULL因为“某个数加上一个未知数结果仍是未知”。我见过有人的报表里收入合计莫名少了几行排查半天就是这里出的问题。聚合函数对NULL也有讲究COUNT(*)统计行数包括NULLCOUNT(col)统计非NULL值的个数SUM、AVG会忽略NULL但有个坑——如果所有值都是NULLSUM返回NULL而不是0前端展示时可能显示空白建议包一层COALESCE(SUM(col), 0)。3.2 时间函数格式化、日期差与区间判断时间处理是SQL里另一大痛点。搜索词“sql server 时间函数”印证了这一点。不同数据库的时间函数有差异我以MySQL为例讲核心套路再补充说明其他数据库的差异点。MySQL常用时间函数函数作用示例NOW()当前日期时间2024-03-18 14:30:00CURDATE()当前日期2024-03-18DATE_ADD(date, INTERVAL 1 DAY)日期加法明天DATE_SUB(date, INTERVAL 1 MONTH)日期减法上个月DATEDIFF(d1, d2)两个日期差天天数DATE_FORMAT(date, %Y-%m-%d)格式化2024-03-18YEAR(date) / MONTH(date)提取年/月2024 / 3比较实用的写法场景按天分组统计时create_time是DATETIME类型你如果直接GROUP BY create_time会把同一个“天”里的不同时间点分成多组。正确做法是格式化到天再分组SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS cnt FROM orders GROUP BY DATE_FORMAT(create_time, %Y-%m-%d);查询“最近7天”的数据不能用WHERE create_time 2024-03-11这种写死的日期正确姿势是SELECT * FROM orders WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY);这里要特别提醒一个性能点不要在条件左侧对时间列做函数运算。比如WHERE YEAR(create_time) 2024这会破坏索引全表扫描。更好的写法是WHERE create_time 2024-01-01 AND create_time 2025-01-01既走索引语义也更清晰。这是个很容易被忽视、却对慢SQL优化至关重要的细节。在SQL Server里函数名不同对应关系大概是GETDATE()对应NOW()DATEADD(DAY, 1, date)对应DATE_ADDDATEDIFF(DAY, d1, d2)对应DATEDIFF但参数顺序相反。我建议写跨库兼容代码时把时间函数封装在视图或数据层里避免后面换库时到处改。3.3 类型转换CAST的显式用法与隐式转换陷阱类型转换是个“用到时才想起用完就忘”的知识点。显式转换用CAST(expr AS type)比如SELECT CAST(123 AS SIGNED INTEGER);如果你面对的是“字符串形式的数字参与比较或排序”建议显式转换。比如用户表里存了手机号字段是VARCHAR你要按手机号排序时ORDER BY phone是按字典序排的结果是“139...”排在“150...”后面符合预期但如果你要按某个数字字符串字段排序字典序就会出问题比如“10”会排在“9”前面。这时候ORDER BY CAST(phone_number AS UNSIGNED)才能得到数值序。隐式转换的坑更隐蔽。比如你有一个VARCHAR字段里面存的是001、002但你和数字1比较时数据库会尝试把字符串转成数字结果001变成1然后匹配成功。这有时候是好事有时候会误伤。更危险的是如果一边是字符串一边是日期不同数据库的隐式转换规则不同同样的SQL在MySQL和Oracle里结果可能不一样。我的习惯是跨类型比较时永远写显式转换别让优化器替你做决定。有同事问我排查线上问题时最怕什么我排第一的就是隐式转换导致的索引失效——你明明建了索引WHERE varchar_col 123这种写法照样全表扫描因为数据库要先把每行的字符串转成数字才能比较。4. 从单表到多表联表查询、子查询与聚合统计4.1 JOIN的本质与INNER/LEFT/RIGHT的选择逻辑联表查询是SQL进阶的第一道坎。很多人靠背“LEFT JOIN就是左表全部保留”去写SQL能对付简单场景但一旦遇到一对多、多对多就乱了。先搞清楚JOIN的本质它是把两张表按某个关联条件做“行的配对”配对不上怎么办取决于JOIN类型。JOIN类型匹配行左表不匹配行右表不匹配行INNER JOIN保留丢弃丢弃LEFT JOIN保留保留右表列填NULL丢弃RIGHT JOIN保留丢弃保留左表列填NULLFULL OUTER JOIN保留保留右表列填NULL保留左表列填NULL记忆方法INNER JOIN是“只要两边都对得上”LEFT JOIN是“左边是老大无论如何都要留在结果里右边有就对上是缘分没有就补NULL”。写JOIN时最容易踩的坑是关联条件写在WHERE里还是ON里。看下面两种写法-- 写法A SELECT e.emp_name, d.dept_name FROM employee e LEFT JOIN department d ON e.dept_id d.dept_id AND d.status 1; -- 写法B SELECT e.emp_name, d.dept_name FROM employee e LEFT JOIN department d ON e.dept_id d.dept_id WHERE d.status 1;这两种写法的结果可能完全不同。写法A中d.status 1是JOIN配对条件的一部分配对不上时仍保留左表行d.dept_name为NULL。写法B中WHERE d.status 1是在JOIN完成之后做的过滤干净利落地把d.status为NULL或非1的行全删掉了相当于把LEFT JOIN“降级”成了INNER JOIN。这个坑非常隐蔽我建议写LEFT JOIN时凡是跟右表相关的过滤条件默认先放ON里想清楚语义再决定要不要挪到WHERE。4.2 子查询WHERE内子查询与FROM派生表的差异子查询分两种常见形态一是放在WHERE里作为筛选条件二是放在FROM里作为一张临时表派生表。两者思路不同。WHERE内子查询典型例子是“找工资高于部门平均工资的员工”SELECT emp_id, emp_name, salary, dept_id FROM employee e WHERE salary ( SELECT AVG(salary) FROM employee WHERE dept_id e.dept_id );这种写法叫关联子查询外层每处理一行内层子查询就执行一次。数据量大了性能很差。优化思路是改成JOINSELECT e.emp_id, e.emp_name, e.salary, e.dept_id FROM employee e JOIN ( SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id ) d ON e.dept_id d.dept_id AND e.salary d.avg_salary;这里用GROUP BY先算每个部门的平均工资再和外层JOIN。子查询只执行一次性能大幅提升。这个思路值得重点记能先聚合缩小数据集的就先把数据集缩小再联表。FROM派生表的另一种常见场景是“取每个部门工资最高的员工”。很多人会想用GROUP BY MAX但这样只能得到部门和最高工资拿不到员工ID和姓名。正确做法是在FROM里放一个“取最近一条”的子查询配合窗口函数。我记得这是一个非常经典的面试题后面第5章会给出完整写法。关于EXISTS和IN的选择子查询结果集很大时EXISTS通常效率更高因为它只需要判断是否存在不必构造完整结果集。反过来子查询结果集很小时IN可读性更好。但要注意NOT IN和NOT EXISTS不是等价的NOT IN遇到NULL值会出问题前面讲过NOT EXISTS则不会。写代码时我基本倾向于用NOT EXISTS。4.3 GROUP BY聚合与HAVING过滤时机决定结果GROUP BY是SQL里最贴近“统计报表”思维的语句。它的执行时机在WHERE之后作用是把行按某几列分成若干组然后对每组做聚合计算。理解这个时机你就能明白WHERE不能访问聚合函数但是GROUP BY之前可以先缩小要参与分组的数据范围。常见的聚合函数SELECT dept_id, COUNT(*) AS emp_cnt, COUNT(salary) AS has_salary_cnt, AVG(salary) AS avg_salary, MAX(salary) AS max_salary, MIN(salary) AS min_salary, SUM(salary) AS total_salary FROM employee WHERE status active GROUP BY dept_id;上面这条SQL的语义是先筛出在职员工再按部门分组统计各部门的人数、有薪水的人数、平均薪水等。注意COUNT(*)和COUNT(salary)的区别前者统计组内行数后者统计组内salary非NULL的行数。HAVING的典型场景是“筛选分组后的结果”。比如找出平均工资超过8000的部门SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id HAVING AVG(salary) 8000;注意HAVING后面也可以使用别名部分数据库支持MySQL支持但推荐直接写聚合表达式兼容性更好。GROUP BY的一个重要易错点SELECT中出现的非聚合列必须出现在GROUP BY中。这是SQL标准的要求否则结果不确定。虽然MySQL默认配置下对这种写法不会报错但从逻辑和规范角度都不对。比如SELECT dept_id, emp_name, MAX(salary) FROM employee GROUP BY dept_id;这条语句在MySQL里能跑但emp_name到底取哪一行是不确定的——在同一部门里取的是工资最高那个人的名字还是随机的某个人完全取决于执行计划和数据分布。很多人拿这个去依赖MySQL的“隐式逻辑”结果上线后某天数据顺序一变结果就错了。要“每组里某个字段最大对应的整行记录”老老实实用窗口函数不要依赖这种非标准行为。5. 进阶但常用的窗口函数与增删改操作5.1 窗口函数入门ROW_NUMBER、RANK与SUM OVER窗口函数是近几年面试高频词也是“sql窗口函数”搜索量居高不下的原因。它和GROUP BY的核心区别是GROUP BY会把多行合并成一行窗口函数不会——它保留每一行同时给你一个窗口范围内的计算结果。你可以把窗口函数理解为“在每一行旁边开了一扇窗户透过窗户能看到自己所在分组的信息”。最常见的几个窗口函数SELECT emp_id, emp_name, dept_id, salary, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn, RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk, DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS drk FROM employee;ROW_NUMBER是连续编号1、2、3RANK在遇到并列时会跳过编号比如1、1、3DENSE_RANK不跳过1、1、2。三者用哪个取决于业务语义只要唯一序号用ROW_NUMBER比赛排名那种“并列后留空位”用RANK并列后连续排名用DENSE_RANK。经典的“取每个部门工资最高员工”用窗口函数实现SELECT emp_id, emp_name, dept_id, salary FROM ( SELECT emp_id, emp_name, dept_id, salary, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employee ) t WHERE t.rn 1;这个写法的逻辑是先在窗口内给每个部门按工资从高到低编号再在外部筛出编号为1的行。注意子查询别名t必须有不然会报语法错误。窗口函数还可以做累计统计。比如统计每个部门截至当月的累计销售额SELECT dept_id, month, sales_amount, SUM(sales_amount) OVER (PARTITION BY dept_id ORDER BY month) AS cumulative_sales FROM sales_table;这个写法的执行机制是以部门为窗口按月份排序从窗口起点到当前行做累加。理解窗口函数的关键是抓住“PARTITION BY窗口怎么分 ORDER BY窗口内怎么走 聚合函数对走到哪算哪的窗口做什么计算”。5.2 INSERT、UPDATE、DELETE写操作的风险控制写操作虽然基础但风险比查询大得多。一次手滑的UPDATE没有WHERE可能就把整张表数据改没了。我见过不止一个事故开发同学在测试环境写UPDATE employee SET salary salary * 1.1忘加WHERE然后连到了生产库结果全公司工资都涨了10%。INSERT基本语法INSERT INTO employee (emp_id, emp_name, dept_id, salary) VALUES (1001, 张三, 10, 8000);批量插入时可以多组VALUESINSERT INTO employee (emp_id, emp_name, dept_id, salary) VALUES (1002, 李四, 10, 9000), (1003, 王五, 20, 7500);UPDATE的正确姿势是先用SELECT验证范围再改写成UPDATE。这是我给所有新人的铁律-- 先查 SELECT * FROM employee WHERE dept_id 10; -- 再改 UPDATE employee SET salary salary * 1.05 WHERE dept_id 10;DELETE同理DELETE FROM employee WHERE emp_id 1001之前先SELECT确认这条记录没删错。如果表数据量很大删除时最好加上LIMIT分批删避免一次锁太多行、产生巨大的事务日志。还有一个容易被忽略的点UPDATE和DELETE的WHERE条件里的ONLY_FULL_GROUP_BY之类的SQL模式会影响行为吗不会但会影响安全。在MySQL里如果忘了WHERE它会“礼貌地”更新所有行——没有任何提示。想在MySQL里防止这种事故可以在启动参数里加--safe-updates它会强制要求UPDATE和DELETE带WHERE或LIMIT否则拒绝执行。SQL Server和Oracle也有类似的事务保护机制关键是你要养成习惯写操作之前想清楚影响行数有条件就放到事务里执行确认无误后再提交。关于SQL注入多说一句。写动态SQL时千万不要直接拼接用户输入。比如WHERE name userInput 这种写法一旦用户输入 OR 11你的查询就会变成WHERE name OR 11返回全部数据这就是经典注入。正确做法是用参数化查询或者预处理语句让数据库把用户输入当数据而不是SQL代码。这个基础意识能帮你省掉很多安全账单。6. 建表、索引与慢SQL优化基础语句之上的性能意识6.1 CREATE TABLE与字段类型选择很多人学SQL只关注查询语句忽略建表但表结构设计不合理后面怎么调SQL都救不回来。建表基础语句很简单CREATE TABLE employee ( emp_id INT PRIMARY KEY, emp_name VARCHAR(50) NOT NULL, dept_id INT NOT NULL, salary DECIMAL(10, 2), hire_date DATE, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_dept_id (dept_id), INDEX idx_create_time (create_time) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4;字段类型选择有几个基本讲究能用INT就不用BIGINT能用BIGINT就不用VARCHAR存储空间直接决定查询性能。手机号、身份证号这类看起来像数字的建议用VARCHAR因为不会参与数值计算而且可能含有前置0或X。金额使用DECIMAL(10, 2)不要用FLOAT/DOUBLE浮点数是近似值做财务计算会有精度问题。时间字段用DATE或DATETIME不要用VARCHAR存字符串否则没法高效比较大小。索引的选择也是建表时就要想的。索引的核心价值是“减少扫描行数”但索引不是越多越好因为每次INSERT/UPDATE都要维护所有索引写性能和存储空间都会受影响。基本原则高频查询的WHERE条件和JOIN关联字段建索引区分度低的字段比如性别建索引意义不大联合索引要考虑最左前缀原则比如(dept_id, salary)联合索引能支持dept_id ? ORDER BY salary但单独WHERE salary ?用不上。6.2 索引为什么加索引后查询还是慢“为什么加了索引查询还是慢”是慢SQL优化里最经典的问题。我列几个高频原因索引失效条件左侧做了函数运算、隐式类型转换、LIKE前置通配符、OR连接的条件含非索引列这些都可能导致索引失效。回表开销大InnoDB中非主键索引存的是主键值回表查询需要二次定位。如果索引的区分度不高查询优化器觉得“回表太多还不如全表扫描”就会放弃索引。数据量的量级变了索引能不能用优化器会基于基数估算。百万行和十亿行优化器的选择可能完全不同。小数据量测试时SQL快不代表大数据量也一样。联合索引顺序不对(a, b, c)联合索引你只查b ?用不上索引查a ? AND c ?能用到a但c那部分用不上。排序列没进索引WHERE dept_id 10 ORDER BY salary DESC如果联合索引是(dept_id, salary)那排序可以直接用索引顺序如果只有(dept_id)索引还需要文件排序。排查这类问题不要靠猜直接看执行计划。6.3 EXPLAIN的核心指标type、rows、Extra怎么读搜索热词“慢sql优化 explain主要看哪些信息”指向的正是这个环节。EXPLAIN是SQL优化最重要的工具它展示一条SQL的执行计划。基本用法是EXPLAIN SELECT ...MySQL还会额外显示一些计算成本信息。EXPLAIN SELECT e.emp_id, e.emp_name, d.dept_name FROM employee e LEFT JOIN department d ON e.dept_id d.dept_id WHERE e.salary 5000;输出结果里我主要看四列type访问类型从好到差大致是consteq_refrefrangeindexALL。ALL就是全表扫描通常需要警惕range代表索引范围扫描是良好状态。key实际用到的索引。如果为NULL说明没走索引。rows预估扫描行数。行数越大查询越慢。不同SQL之间的rows差距能直观反映优化效果。Extra附加信息重点看有没有Using filesort文件排序意味着排序没走索引和Using temporary使用了临时表常见于GROUP BY或DISTINCT配合不当性能很差。举个例子有个慢SQLSELECT * FROM orders WHERE YEAR(create_time) 2024 ORDER BY amount DESC;EXPLAIN很可能显示typeALL和Using filesort因为YEAR(create_time)破坏了索引排序也没走索引。优化后SELECT * FROM orders WHERE create_time 2024-01-01 AND create_time 2025-01-01 ORDER BY amount DESC;再看执行计划type会变成range如果amount也在联合索引里Using filesort也可能消失。这就是“从执行计划出发做优化”和“盲猜优化”的区别。最后想分享一个我自己的排查经验拿到一条慢SQL我的固定动作是先看EXPLAIN再看rows和Extra如果出现全表扫描就去查这列的索引情况如果索引存在但没用上基本就是函数运算或类型转换的问题如果索引用上了但rows还是很大就要考虑加条件缩小范围或重新设计联合索引。这套流程说起来不复杂但能解决大部分线上慢SQL问题。SQL常用语句基础大全的意义就在这里不仅是语法更是帮你建立从“写得出”到“写得好”的思维链路。