最近在论坛和社群里看到不少人在搜“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先于SELECT,WHERE先于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而是UNKNOWN,UNKNOWN会被当作不满足条件过滤掉。很多人第一次遇到这个现象都以为是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的排序位置,不同数据库行为不一样:
- MySQL:NULL默认排在最小值之前,也就是
ASC时NULL排最前。 - SQL Server:NULL默认排最前。
- Oracle:NULL默认排最后,
ASC时排最后。 - PostgreSQL:NULL默认排最后。
这就是为什么如果你依赖默认行为写报表,换数据库后结果对不上。建议明确指定自己的意图。比如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(*)统计行数,包括NULL;COUNT(col)统计非NULL值的个数;SUM、AVG会忽略NULL,但有个坑——如果所有值都是NULL,SUM返回NULL而不是0,前端展示时可能显示空白,建议包一层COALESCE(SUM(col), 0)。
3.2 时间函数:格式化、日期差与区间判断
时间处理是SQL里另一大痛点。搜索词“sql server 时间函数”印证了这一点。不同数据库的时间函数有差异,我以MySQL为例讲核心套路,再补充说明其他数据库的差异点。
MySQL常用时间函数:
| 函数 | 作用 | 示例 |
|---|---|---|
| NOW() | 当前日期时间 | 2024-03-18 14:30:00 |
| CURDATE() | 当前日期 | 2024-03-18 |
| DATE_ADD(date, INTERVAL 1 DAY) | 日期加法 | 明天 |
| DATE_SUB(date, INTERVAL 1 MONTH) | 日期减法 | 上个月 |
| DATEDIFF(d1, d2) | 两个日期差(天) | 天数 |
| DATE_FORMAT(date, '%Y-%m-%d') | 格式化 | 2024-03-18 |
| YEAR(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_ADD,DATEDIFF(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 | 保留 | 丢弃 | 保留,左表列填NULL |
| FULL 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 );这种写法叫关联子查询,外层每处理一行,内层子查询就执行一次。数据量大了性能很差。优化思路是改成JOIN:
SELECT 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、3;RANK在遇到并列时会跳过编号,比如1、1、3;DENSE_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);批量插入时可以多组VALUES:
INSERT 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 '1'='1,你的查询就会变成WHERE name = '' OR '1'='1',返回全部数据,这就是经典注入。正确做法是用参数化查询或者预处理语句,让数据库把用户输入当数据而不是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:访问类型,从好到差大致是
const>eq_ref>ref>range>index>ALL。ALL就是全表扫描,通常需要警惕;range代表索引范围扫描,是良好状态。 - key:实际用到的索引。如果为NULL,说明没走索引。
- rows:预估扫描行数。行数越大,查询越慢。不同SQL之间的
rows差距能直观反映优化效果。 - Extra:附加信息,重点看有没有
Using filesort(文件排序,意味着排序没走索引)和Using temporary(使用了临时表,常见于GROUP BY或DISTINCT配合不当,性能很差)。
举个例子,有个慢SQL:
SELECT * FROM orders WHERE YEAR(create_time) = 2024 ORDER BY amount DESC;EXPLAIN很可能显示type=ALL和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常用语句基础大全的意义就在这里:不仅是语法,更是帮你建立从“写得出”到“写得好”的思维链路。