1. 数据库与SQL:先把地基打牢
1.1 SQL到底是什么,为什么非学不可
很多刚入行的朋友一听到"数据库""SQL"这两个词就有点发怵,总觉得这是高深莫测的东西。实际上你天天都在用数据库——刷朋友圈看到的内容存在数据库里,网购下单的订单存在数据库里,甚至点外卖时商家看到的接单信息也是从数据库里读出来的。数据库就是存放和管理数据的"超级仓库",而SQL就是操作这个仓库的通用语言。
SQL的全称是Structured Query Language,结构化查询语言。它之所以能成为这个领域的"普通话",是因为几乎所有主流数据库——MySQL、SQL Server、Oracle、PostgreSQL、达梦、SQLite——都支持SQL语法。这意味着你在MySQL上学会的SELECT语句,换到Oracle上照样能写,只是某些细节略有差异。这种通用性带来的好处是巨大的:你只需要学一套语言,就能应付绝大多数工作中的数据操作场景。
对于零基础的学习者来说,SQL可能是所有编程语言里最容易上手的一门。它的语法接近英文的自然表达,比如你想查询一张表里的所有数据,写SELECT * FROM 表名即可,翻译过来就是"从表里选出所有内容"。这种直觉性的设计让SQL的学习曲线非常平缓。但也正因为简单,很多人忽视了系统训练,导致工作中写出的SQL要么性能堪忧,要么逻辑漏洞百出。这篇文章的主旨就是用一篇的体量,把SQL最核心的操作拆开揉碎讲清楚,让你能真正上手干活。
1.2 五大类SQL语句,先建立整体框架
学习SQL最容易犯的错误就是一上来就死磕SELECT的各种花哨写法,结果连建表都建不明白。我建议你先把SQL的整体框架装进脑子里,后面所有的学习都是在往这个框架里填肉。
SQL语句按照功能可以划分为五大类,我做了张表帮你快速建立认知:
| 分类 | 全称 | 作用 | 代表关键字 |
|---|---|---|---|
| DDL | Data Definition Language | 定义和管理数据结构 | CREATE、ALTER、DROP |
| DML | Data Manipulation Language | 操作表中数据 | INSERT、UPDATE、DELETE |
| DQL | Data Query Language | 查询数据(日常用得最多) | SELECT |
| DCL | Data Control Language | 控制权限和访问 | GRANT、REVOKE |
| TCL | Transaction Control Language | 事务管理 | COMMIT、ROLLBACK |
举一个生活中的类比:DDL相当于你装修房子时"砸墙、砌墙、刷漆"这些改变房屋结构的动作;DML相当于往房间里搬家具、挪家具、扔家具;DQL则相当于走进房间"看看现在都有什么东西"。权限和事务属于更进阶的内容,这篇我们重点把DDL、DML和DQL三大块啃透,这三块覆盖了日常开发中95%以上的SQL操作。
注意:不同数据库对DDL和DML的自动提交机制略有差异。MySQL默认每条DML语句自动提交,而Oracle需要手动COMMIT。这是初学者换数据库工作时经常踩的坑,后面我们会细说。
2. 建库建表:DDL核心操作详解
2.1 数据库与表的创建:从零到一
很多教程一上来就让你CREATE TABLE,但实际工作中你大概率需要先创建一个数据库,再在库里建表。以最常用的MySQL为例,整个过程分三步走。
第一步,连接到数据库服务:
mysql -u root -p输入密码后,你就进入了MySQL的命令行环境。第二步,创建数据库并指定字符集:
CREATE DATABASE school DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;这里有个非常关键的经验:字符集务必指定为utf8mb4,而不是utf8。utf8在MySQL里实际上是utf8mb3,只能存储3字节的字符,遇到emoji或者某些生僻字就会报错。utf8mb4是完整的4字节编码,兼容所有Unicode字符,这是我在生产环境踩过坑之后强烈建议你从第一天就养成的习惯。
第三步,选中数据库并建表。假设我们要建一张学生表:
USE school; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', name VARCHAR(50) NOT NULL COMMENT '姓名', age INT COMMENT '年龄', gender CHAR(1) DEFAULT '男' COMMENT '性别', enroll_date DATE COMMENT '入学日期', score DECIMAL(5,2) COMMENT '平均成绩', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表';逐个字段拆解:id设为主键并自增,这是每张表的"身份证";name用VARCHAR(50)并加NOT NULL约束,姓名不能为空;age用INT存整数;gender用CHAR(1)存定长单字符,这里体现了CHAR和VARCHAR的核心区别——CHAR定长、VARCHAR变长,短字段用CHAR性能更好;score用DECIMAL(5,2)表示最多5位数字、保留2位小数,存金额和分数这类需要精确计算的数值时千万不能用FLOAT,否则会出现0.1+0.2不等于0.3的精度问题。
ENGINE=InnoDB是事务安全的存储引擎,支持行级锁和外键约束,是MySQL的默认推荐引擎。如果你用MyISAM,则完全不具备事务能力,这在生产环境中是致命的。初学者建表时最容易犯的错误是想当然地加字段,不加注释,等过几个月自己都看不懂这张表在干嘛。我的建议是每个字段都加上COMMENT注释,这是成本最低收益最高的好习惯。
2.2 修改表结构与删除操作:慎之又慎
建完表之后,业务一变,表结构大概率要调整。ALTER TABLE语句就是你修改表结构的手术刀。
添加字段:
ALTER TABLE student ADD COLUMN phone VARCHAR(11) COMMENT '手机号';修改字段类型和默认值:
ALTER TABLE student MODIFY COLUMN gender CHAR(1) DEFAULT '女' COMMENT '性别';修改字段名:
ALTER TABLE student CHANGE COLUMN score avg_score DECIMAL(5,2) COMMENT '平均成绩';注意MODIFY和CHANGE的区别:MODIFY只能改类型和属性,不能改字段名;CHANGE可以同时改字段名、类型和属性,但旧字段名必须写对。
删除字段:
ALTER TABLE student DROP COLUMN phone;删除表和删除数据库:
DROP TABLE student; DROP DATABASE school;这里必须强调一个血的教训:生产环境中执行DROP之前,一定要先备份。很多事故都是"手一抖,表没了"。即使是开发环境的DROP也需要再三确认表名正确。我见过有人本想删测试临时表,结果因为库没切换对,把生产环境的表删了的真实案例。建议你在执行高危操作前,先执行SELECT * FROM 表名 LIMIT 1确认一下当前操作的库和表名是否正确,再看看有没有开事务,DROP操作在MySQL里不支持回滚,删了就是真的没了。
3. 增删改查:DML操作的黄金组合
3.1 INSERT插入数据:三种写法的适用场景
INSERT是给表里塞数据的语句,看起来简单,但写法不同,适用场景天差地别。
最基础的单行插入:
INSERT INTO student (name, age, gender, enroll_date, score) VALUES ('张三', 20, '男', '2023-09-01', 88.5);这里有个值得注意的点:id字段没出现在列名列表里,因为它自增,数据库会自动生成;created_at也没写,因为它有默认值CURRENT_TIMESTAMP。这种"列出明确字段名"的写法是我强烈推荐的,它比INSERT INTO student VALUES (1, '张三', 20, ...)这种省略列名的写法更安全——即使表结构的字段顺序变了,你的SQL也不会插入错误的数据。
多行批量插入:
INSERT INTO student (name, age, gender, enroll_date, score) VALUES ('李四', 21, '男', '2023-09-01', 78.0), ('王五', 19, '女', '2023-09-01', 92.5), ('赵六', 20, '女', '2023-09-01', 85.0);批量插入的性能远高于逐条INSERT。假设你要插入1万条数据,逐条INSERT需要执行1万次网络往返,而批量插入一次搞定。这也是我在数据初始化或数据迁移时最常用的方式。
还有一种"查询结果直接插入"的高级用法,用于复制表数据:
INSERT INTO student_bak (name, age, gender, score) SELECT name, age, gender, score FROM student WHERE score > 80;这条语句把成绩大于80的学生记录复制到备份表里,整个过程不需要经过应用层,效率极高。日常工作里做数据备份、生成报表临时表时经常会用到。
3.2 UPDATE修改数据:WHERE条件是你的安全绳
UPDATE语句的语法很简单:
UPDATE student SET age = 21 WHERE id = 1;但就是这个简单的语句,几乎每天都在生产环境制造事故。原因只有一个:忘记写WHERE条件。
UPDATE student SET age = 21;这条语句会把全表所有学生的年龄都改成21,如果这不是你的本意,后果不堪设想。
在实际使用中我整理了三条经验:
第一,UPDATE在MySQL里是自动提交的,一旦执行无法简单回滚。但在执行前可以先在同一个事务里跑一下:
BEGIN; UPDATE student SET score = 90 WHERE id = 100; -- 确认影响行数正确 COMMIT;如果发现不对就执行ROLLBACK,能救回来。虽然常规写法不带BEGIN,但生产数据操作前开启事务是一个好习惯。
第二,UPDATE语句里可以配合LIMIT限制影响行数?MySQL确实支持UPDATE ... LIMIT 1,但这个行为在不同数据库里不一致,Oracle等数据库不支持,所以强烈不建议依赖LIMIT来保护自己。正确做法是:写完UPDATE后先SELECT一下同样的WHERE条件,确认你要影响的行数和内容再执行。
第三,UPDATE支持多表关联更新,但初学者往往在这里卡壳。要修改关联表的数据,MySQL的写法是:
UPDATE teacher t JOIN class c ON t.class_id = c.id SET t.salary = t.salary * 1.1 WHERE c.grade = '三年级';意思很清晰:把三年级老师工资涨10%。注意MySQL的UPDATE JOIN语法别写错顺序。
3.3 DELETE删除数据:物理删除与逻辑删除之争
DELETE的语法同样简单:
DELETE FROM student WHERE id = 1;不带WHERE的DELETE会清空全表,这个跟在UPDATE犯的错误一样致命。但DELETE比UPDATE多了一层"物理 vs 逻辑"的哲学问题。
物理删除就是DELETE语句真正把数据从存储介质里移除。逻辑删除则是给表加一个is_deleted字段,删除操作变成UPDATE student SET is_deleted = 1 WHERE id = 1,查询时统一过滤WHERE is_deleted = 0。
两种做法各有适用场景。对于用户订单、交易流水、合同记录这类业务数据,强烈建议用逻辑删除。原因有三:一是数据是资产,物理删了就没法审计追溯;二是误删可以轻松恢复;三是业务上往往需要保留历史信息。
但是逻辑删除也有代价:每个查询都要记得加is_deleted = 0条件,忘了就会出现"幽灵数据";表数据量会持续增长,查询性能会逐渐下降。作为折中的方案,可以定期把逻辑删除超过一定时限的数据做物理清理。
如果确实需要清空整张表,注意DELETE FROM student和TRUNCATE TABLE student的区别:DELETE会逐行删除,可以加WHERE条件,删除后自增ID不会重置;TRUNCATE是直接重建表,速度快得多,自增ID会重置,但TRUNCATE无法回滚且不能加WHERE条件。需要全表清空且确认数据不要了,用TRUNCATE更合适。
4. 查询是灵魂:SELECT核心操作拆解
4.1 查询基础:WHERE与过滤条件的正确姿势
SELECT是SQL里最强大也是最复杂的部分。先从基础说起,一个标准的查询长这样:
SELECT name, age, score FROM student WHERE score >= 80 AND gender = '男';WHERE的过滤条件支持多种运算符:等于=、不等<>或!=、范围BETWEEN AND、集合IN、模糊匹配LIKE、空值判断IS NULL。有一个高频踩坑点:判断NULL不能用=或=,必须用IS NULL或IS NOT NULL。因为NULL在SQL里代表"未知",任何与NULL的比较结果都是NULL,即"未知"。所以WHERE age = NULL永远查不出任何数据。
模糊匹配LIKE也有讲究。WHERE name LIKE '张%'匹配以"张"开头的姓名,%表示任意长度的任意字符;WHERE name LIKE '_三'匹配"某三"两个字的姓名,_代表单个字符。需要注意:LIKE以%开头(比如LIKE '%三')的时候,数据库无法使用索引,查询会变慢。如果业务上确实需要这种后缀模糊匹配,应该考虑改用全文索引或者搜索引擎。
还有一个容易被忽略的细节是字符串与日期的比较。在MySQL里写WHERE enroll_date = '2023-09-01'没问题,但如果表里存的是DATETIME类型且包含时间部分,这个等值比较会查不到数据,因为实际存储值是2023-09-01 10:30:00。正确写法是用日期函数截断:WHERE DATE(enroll_date) = '2023-09-01'。
4.2 多表连接JOIN:从笛卡尔积到精确关联
单表查询只是热身,实际业务中最常遇到的是多表关联查询。比如要查每个学生的班级名称和班主任,就需要把student表和class表连起来。
JOIN的核心思想是:把两张表按某种关联条件"拼"在一起。先看最常用的内连接:
SELECT s.name, c.class_name, t.name AS teacher_name FROM student s INNER JOIN class c ON s.class_id = c.id INNER JOIN teacher t ON c.teacher_id = t.id;这里我给表起了别名:student用s表示,class用c表示。表别名不仅让SQL更简洁,也是多表查询时的规范做法,否则两张表有同名字段时,SELECT后面必须用表名.字段名来区分。
误解最多的就是JOIN类型。我用一张表解释清楚:
| JOIN类型 | 含义 | 返回结果 |
|---|---|---|
| INNER JOIN | 内连接 | 只返回两表都匹配上的记录 |
| LEFT JOIN | 左连接 | 返回左表全部记录,右表无匹配则补NULL |
| RIGHT JOIN | 右连接 | 返回右表全部记录,左表无匹配则补NULL |
| FULL OUTER JOIN | 全连接 | 返回两表所有记录(MySQL不直接支持) |
用一个例子说明LEFT JOIN的行为:假设左表是学生表,右表是班级表,LEFT JOIN会把没有班级(class_id为空或班级不存在)的学生也查出来,此时右表的班级字段为NULL。这在用户管理和订单查询里非常有用:查"所有用户及其订单,即使没下过单也显示出来"。
初学JOIN时最容易出错的写法是忘记ON条件或者关联条件写错,导致产生笛卡尔积——两表记录两两组合,数据量呈爆炸式增长。比如学生表1000条记录、班级表50条记录,忘记ON条件就会查出50000条。所以写完JOIN后,第一时间检查结果数是否符合预期。
4.3 分组聚合GROUP BY与HAVING:统计报表的基石
GROUP BY是SQL中思维转变的一个分水岭:从"查明细"变成"做统计"。它的作用是按某列或多列把数据分组,然后对每组做聚合计算。
MySQL支持的聚合函数包括:COUNT(计数)、SUM(求和)、AVG(平均值)、MAX(最大值)、MIN(最小值)。例如统计每个班级的平均分、人数和最高分:
SELECT class_id, COUNT(*) AS student_count, AVG(score) AS avg_score, MAX(score) AS max_score FROM student GROUP BY class_id;这里COUNT(*)统计每组的行数,AVG(score)计算每组的平均分。注意一个铁律:SELECT后面出现的非聚合列,必须出现在GROUP BY里。上面这段SQL如果写成SELECT name, COUNT(*) FROM student GROUP BY class_id,MySQL的ONLY_FULL_GROUP_BY模式会直接报错,而且这本身就是逻辑错误——每个分组里有多个name,你到底想显示哪个?
如果要给分组加过滤条件,不能使用WHERE,而要使用HAVING。两者的区别是:WHERE在分组前过滤原始行,HAVING在分组后过滤聚合结果。比如找出平均分大于85的班级:
SELECT class_id, AVG(score) AS avg_score FROM student GROUP BY class_id HAVING AVG(score) > 85;新手常犯的错误是把别名用在HAVING里:HAVING avg_score > 85在MySQL里碰巧能用,但在Oracle和SQL Server里就会报错,因为HAVING子句的执行顺序在SELECT别名生效之前。为了跨数据库兼容,建议HAVING里使用表达式,不要用别名。
GROUP BY对多个列的分组在实际业务中同样高频,比如统计每个年级每个班级的男女生人数:
SELECT grade, class_id, gender, COUNT(*) AS cnt FROM student GROUP BY grade, class_id, gender;这会按三列的组合进行分组,每个组合生成一行汇总结果。
4.4 排序与分页:让结果可读可用
排序的关键字是ORDER BY,默认升序ASC,可以指定降序DESC,还可以多列排序:
SELECT name, score, age FROM student WHERE score >= 60 ORDER BY score DESC, age ASC;这段SQL先按分数从高到低排列,分数相同时按年龄从小到大排列。ORDER BY的执行优先级在WHERE之后,所以你可以在排序前先用WHERE缩小数据范围。多列排序时,列的先后顺序决定了主要和次要条件,这个细节在生成排行榜时特别重要。
分页是Web系统里最常见的需求。MySQL用LIMIT实现:
SELECT * FROM student ORDER BY id LIMIT 10 OFFSET 20;LIMIT 10表示返回10条记录,OFFSET 20表示跳过前20条。这对应的是第3页数据(每页10条)。分页的通用公式是:LIMIT 每页条数 OFFSET (页码 - 1) * 每页条数。
不同数据库的分页语法差异很大,我做了个对照表:
| 数据库 | 分页写法 |
|---|---|
| MySQL / PostgreSQL | LIMIT 10 OFFSET 20 |
| SQL Server | OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY |
| Oracle | FETCH FIRST 10 ROWS ONLY(12c+)或ROWNUM嵌套子查询 |
分页查询还有一个性能隐患:OFFSET越大,数据库需要扫描并丢弃的行越多,越到后面的页越慢。比如LIMIT 10 OFFSET 1000000,MySQL会先扫出1000010行再丢掉前1000000行。优化方案是改用"键集分页":记录上一页最后一条数据的ID,用WHERE id > 上次最大ID ORDER BY id LIMIT 10来取下一页,效率可以提升几个数量级。
5. 进阶利器:子查询、视图与索引
5.1 子查询的灵活运用
子查询就是嵌套在查询里的查询,它返回的结果可以作为外层查询的过滤条件或数据源。子查询解决了"一步查不出来,分两步走"的问题。
最经典的场景是"查询比平均分高的学生":
SELECT name, score FROM student WHERE score > (SELECT AVG(score) FROM student);这个括号里的子查询先算出全表平均分,再作为外层WHERE的比较值。子查询执行一次,结果固定,这种叫不相关子查询。
还有一种相关子查询,子查询会引用外层查询的别名。比如查询每个班级中成绩最高的学生信息:
SELECT s.name, s.score, s.class_id FROM student s WHERE s.score = ( SELECT MAX(score) FROM student WHERE class_id = s.class_id );外层每处理一行,子查询都重新执行一次,查询班级里最高的分数。这种写法逻辑清晰,但性能开销较大,表数据量大的时候要慎用。
子查询的另一种用法是作为临时表,放在FROM后面。比如先求出每个班的最高分,再把这个结果跟student表关联:
SELECT t.name, t.score, c.class_name FROM ( SELECT class_id, MAX(score) AS max_score FROM student GROUP BY class_id ) m JOIN student t ON t.class_id = m.class_id AND t.score = m.max_score JOIN class c ON c.id = m.class_id;这种"先聚合、后连接"的模式在统计报表中非常常见。注意子查询作为临时表必须有别名,上面例子里的m就是别名,去掉会直接报错。
5.2 视图:把复杂查询包装成"虚拟表"
视图是SQL里一个性价比极高的功能。它本质是一个保存好的SELECT查询,使用时可以像表一样去SELECT,但视图本身不存储数据,所有数据仍然来自基础表。
创建视图:
CREATE VIEW v_student_info AS SELECT s.id, s.name, c.class_name, t.name AS teacher_name FROM student s LEFT JOIN class c ON s.class_id = c.id LEFT JOIN teacher t ON c.teacher_id = t.id;创建之后,你每次查询:
SELECT * FROM v_student_info WHERE class_name = '三班';相当于把上面那串复杂的JOIN语句封装了起来,应用层只需要面对一个简单的"视图"。这对团队协作特别友好:后端写好的复杂统计逻辑,数据分析同学只需要对着视图做简单查询即可。
视图还有一个实用价值是权限控制。比如只允许业务人员看到学生的姓名、班级和成绩,但不让他们看到手机号、家庭住址等敏感字段,就可以创建只包含必要字段的视图授予查询权限。
需要注意两点:第一,视图在MySQL中通常是不可更新的,即使能更新也限制极多,不要试图通过视图去修改基础表数据;第二,如果底层表结构发生变化,视图可能失效,需要重新创建或使用CREATE OR REPLACE VIEW更新。
5.3 索引:查询提速的关键武器
数据量小的时候SQL随便怎么写都很快,但当表数据量涨到百万级,没有索引的查询会慢到让你怀疑人生。索引的本质是一种有序的数据结构,让数据库不必逐行扫描全表就能快速定位数据。
创建索引:
CREATE INDEX idx_student_name ON student(name);创建联合索引:
CREATE INDEX idx_class_score ON student(class_id, score);联合索引是索引优化里最讲究的部分。上面的联合索引(class_id, score)表示先按class_id排序,class_id相同再按score排序。查询时如果WHERE条件同时包含class_id和score,这个索引就非常高效;如果只含score而不含class_id,索引大概率用不上。这里要理解"最左前缀原则":联合索引的生效遵循最左边的字段优先原则,跳过了第一个字段直接用后面的字段,索引会失效。
哪些字段适合建索引?我的经验是:频繁出现在WHERE条件里的字段、JOIN的关联字段、ORDER BY的排序字段都值得建索引。而频繁更新的字段、重复值占比极高的字段(比如性别字段只有"男""女"两个值)则不适合建索引,建立后反而拖慢写入速度。
索引的内容很深,但对初学者来说记住两大原则就够用了:第一,索引不是越多越好,每张表的索引数量控制在5个以内,因为每次写入数据都要同时更新所有索引;第二,对SELECT频繁的表建立合理的索引,收益远大于代价。
注意:使用
SELECT * FROM student WHERE name LIKE '%三%'这种以通配符开头的模糊查询,即使有索引也无法使用。这是索引失效最常见的场景之一。
6. 实战踩坑:SQL常见问题与排查技巧
6.1 常见报错与解决方案
SQL报错是每个初学者的必经之路,不要慌,九成以上的错误原因就那么几类。我把高频报错整理成速查表:
| 报错信息 | 常见原因 | 解决方式 |
|---|---|---|
| Unknown column 'xxx' | 字段名写错 | 用DESC 表名查看真实字段名 |
| SQL syntax error | 语法错误 | 检查关键字拼写、逗号、引号是否成对 |
| Column 'xxx' in field list is ambiguous | 多表查询时同名字段未加表别名 | 使用表名.字段名明确指定 |
| You can't specify target table for update in FROM clause | 子查询引用了正在更新的表 | 改用临时表或JOIN方式 |
| Incorrect string value | 字符集不支持特殊字符 | 表和连接都使用utf8mb4 |
| Lock wait timeout exceeded | 行锁等待超时 | 检查是否有长事务未提交 |
| Data too long for column | 字段长度不够 | 使用ALTER TABLE增大长度 |
举两个真实案例拆解。有个朋友在生产环境执行UPDATE student SET score = score + 5 WHERE id IN (SELECT id FROM student WHERE score < 60),MySQL直接报错"can't specify target table for update in FROM clause"。原因是MySQL不允许在子查询里直接引用正在UPDATE的同一张表。改成临时表方案:
UPDATE student SET score = score + 5 WHERE id IN (SELECT id FROM (SELECT id FROM student WHERE score < 60) AS tmp);外面再套一层子查询把数据"搬运"一下,问题就解决了。
再比如"Lock wait timeout exceeded",这个报错出现在事务长时间占用某行记录的锁,而另一个事务等待超时。解决思路是找到并结束持有锁的事务。MySQL里可以用:
SELECT * FROM information_schema.innodb_trx;查出所有运行中的事务,找到trx_state为RUNNING且持续很久的线程,用KILL 线程ID终止。
6.2 慢SQL优化心得
在实际开发中,SQL"能用"只是第一步,生产环境对性能的要求往往更苛刻。一条慢SQL能把整个数据库拖垮,这就涉及慢SQL优化。
先说排查手段。MySQL默认开启了慢查询日志,你可以定位最耗时的SQL:
SHOW VARIABLES LIKE 'slow_query_log'; SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 2;超过2秒的查询会被记录到日志文件里。拿到慢SQL后,用EXPLAIN查看执行计划:
EXPLAIN SELECT * FROM student WHERE score > 80;EXPLAIN的结果里有一列叫type,从好到差依次是:system、const、eq_ref、ref、range、index、ALL。看到ALL表示全表扫描,这是性能杀手;range表示索引范围扫描,通常可以接受;const和ref表示精确命中了索引,是最理想的状态。
慢SQL优化的常规手段按优先级排序:
- 优化查询逻辑:减少不必要的字段返回,避免
SELECT *;把数据量大但逻辑复杂的查询拆成多个简单查询,在应用层合并 - 加索引或调整索引:针对慢SQL的WHERE和JOIN条件建立合适索引
- 避免在索引列上做函数运算:
WHERE YEAR(enroll_date) = 2023会让索引失效,应改写为WHERE enroll_date >= '2023-01-01' AND enroll_date < '2024-01-01' - 优化大表分页:用键集分页代替深分页OFFSET
- 考虑数据归档:把历史数据迁到归档表,核心表只保留热数据,查询大幅提速
一个典型的优化案例:某个报表查询需要扫描500万行数据,执行耗时8秒。排查后发现WHERE条件里对日期列使用了LEFT(enroll_date, 7) = '2023-09',这种写法让日期索引完全失效。改写为enroll_date >= '2023-09-01' AND enroll_date < '2023-10-01'之后,执行时间从8秒降到0.05秒,提升了160倍。这个案例告诉我们,很多时候性能问题不是数据库不行,而是SQL写法有问题。
6.3 新手必须要养成的几个好习惯
文章最后,分享几条我在实际工作里逐渐沉淀下来的习惯。这些习惯不一定写在教科书上,但正是它们让我少踩了很多坑。
第一,写SQL之前先搞清楚表结构。用DESC查看表结构,搞清楚每个字段的含义、类型、是否有索引。连表结构都没搞清楚就急着写查询,等于蒙着眼睛开车。我见过太多人因为没先看字段类型,拿字符串跟数字比,或者不知道时间是DATETIME还是TIMESTAMP导致查询结果为空。
第二,生产环境操作必须备份和开事务。不管是UPDATE还是DELETE,数据库默认自动提交,执行错了很难恢复。养成先查再改、改前备份的习惯,关键时刻能救命。即使你的公司有完整的备份机制,从备份恢复到止损也需要时间,这段时间里业务可能已经受到影响。
第三,写查询时先写SELECT再逐步加过滤条件。不要一步到位写一长串SQL,那样出错后极难排查。先把基础查询跑通,确认返回的数据结构正确,然后一层一层加WHERE、JOIN、GROUP BY、ORDER BY。每次加完条件都跑一遍,确认结果符合预期再继续。这个方法在调试复杂查询时能帮你节省大量时间。
第四,注意SQL注入风险。在Web应用里,永远不要用字符串拼接的方式把用户输入直接嵌进SQL语句。用户输入' OR 1=1 --这种内容时,拼接出来的SQL可能会把整个表的数据查出来,或者造成更严重的破坏。应用层应该使用参数化查询或预编译语句,这是防注入最有效的手段。
第五,选一个趁手的数据库图形化工具。命令行能用,但日常开发效率低。我常用的Navicat、DBeaver、DataGrip这些工具都支持可视化建表、执行查询、查看执行计划。特别是DBeaver开源免费,跨平台支持几乎所有主流数据库,装一个能用好几年。工欲善其事必先利其器,别在这上面省钱省时间。
最后再说一点个人体会:SQL这门技能,看十篇教程不如上手练十道题。建议你找一台机器装个MySQL或者直接用在线SQL练习平台,把文章里的示例SQL亲手跑一遍,再自己改改条件、加加字段,看看会得到什么结果。踩过的坑、跑通的心得,都会变成你自己的经验。等你能不看文档随手写出带JOIN、GROUP BY的查询时,恭喜你,SQL已经从入门到上手了。