SQL 这玩意儿,说难也难,说简单也简单。难的是网上教程千篇一律,装个 MySQL 就能把新手卡死在第一步;简单的是只要环境起来、语句上手、面试题心里有数,后面基本就是水到渠成的事。我做了这么多年开发和数据相关工作,带过的实习生、转行过来的朋友多多少少都踩过同样的坑:环境装不上、SQL 记不住、面到数据库题脑子一片空白。这篇博文就围绕这三个痛点展开——MySQL 环境一键搭建、SQL 基础语句完整梳理、面试习题逐题拆解,目标读者是零基础想入门数据分析、后端开发,或者正在准备面试的朋友。内容不装高深,全是我实操验证过的路径,照着走就行。
1. 环境搭建:MySQL 装不上,后面全是空谈
1.1 版本怎么选:新手直接上 8.0
很多人一上来就卡在版本选择上,MySQL 5.7 和 8.0 各说各的好。我的建议很简单:新手直接装 8.0,不用纠结。
有个小知识点值得先说清楚,你在网上会看到类似“mysql 5.7.44 官方为什么之后 5.7.43 呢”这样的搜索问题,这其实是版本号看反了。版本号是按数字递增的:5.7.43 之后是 5.7.44,5.7.44 是 5.7 系列的收官版本,官方后续不再为 5.7 提供新的功能更新,只做安全补丁维护。而 8.0 是长期维护的当前主流版本,新特性都在 8.0 上,比如窗口函数、通用表表达式(WITH 语法)、更好的性能优化器。哪怕你是为了应付老系统的维护去学 5.7,我也建议先在 8.0 上把基础功练扎实,两者在基础 SQL 语法上的差异不超过 5%,学习成本几乎可以忽略。
另外还有个环境选择的问题,Windows 用户、Mac 用户、云服务器 Linux 用户都会问装哪个好。核心原则是:你的工作环境在哪,就在哪装。如果只是为了学习和练 SQL,Windows 10/11 直接装本地版最快;如果是为了模拟生产环境,Linux 服务器更贴近真实部署场景。我从最简单的 Windows 环境讲起,实操性最强。
1.2 Windows 10 安装全流程实操:从 zip 包到 net start mysql
我在 Windows 上装 MySQL 8.0 用过 MSI 安装包,也用过 zip 解压版,强烈推荐 zip 解压版。理由很简单:MSI 安装包附带一堆向导选项,新手容易选错组件,装完反而不知道东西在哪;zip 版所有文件都装在一个目录里,清晰可控,卸载也方便,直接删目录就行。
具体步骤如下,每一步都是验证过的:
第一步,去 MySQL 官网下载mysql-8.0.x-winx64.zip。下载后解压到某个路径,比如D:\mysql-8.0.46-winx64。注意路径里不要带中文、不要带空格,否则后面配置容易出各种怪问题。
第二步,在解压目录下新建一个文本文件,改名为my.ini,填入以下内容:
[mysqld] # 端口,默认3306 port=3306 # 安装目录 basedir=D:/mysql-8.0.46-winx64 # 数据存放目录 datadir=D:/mysql-8.0.46-winx64/data # 字符集 character-set-server=utf8mb4 # 默认存储引擎 default-storage-engine=INNODB [client] default-character-set=utf8mb4这里有两个关键点。第一,basedir和datadir建议写成反斜杠/,不用\,避免转义问题。第二,不要手动创建 data 目录,后续用命令初始化的时候 MySQL 会自动生成,你手动建了反而可能因为目录权限或者内容冲突导致初始化失败。
第三步,以管理员身份打开命令提示符(cmd),进入 MySQL 解压目录的 bin 目录:
cd /d D:\mysql-8.0.46-winx64\bin先执行初始化命令:
mysqld --initialize-insecure这一步会在datadir目录生成系统数据库文件,--initialize-insecure表示生成一个密码为空的 root 用户,方便第一次登录。如果不用--initialize-insecure而是用--initialize,MySQL 会生成一个随机密码写进日志文件里,对新手来说找密码这一步很容易劝退,所以还是用--initialize-insecure更省事。
第四步,安装系统服务并启动。继续在 bin 目录执行:
mysqld --install MySQL80 net start mysql注意一点:mysqld --install后面的服务名是自定义的,你可以叫 MySQL80,也可以叫 mysql。但net start后面的名字必须和安装的服务名一致。网上很多教程里写的是net start mysql,如果你安装时用了MySQL80,那启动命令就要改成net start MySQL80,否则系统会报“服务名无效”。这一步是新手报错频率最高的地方,建议把服务名统一成 mysql 最简单,安装命令直接写mysqld --install mysql。
第五步,登录 MySQL 并修改密码。启动成功后执行:
mysql -u root -p因为 root 密码为空,提示输入密码时直接回车就能进入。进去后立刻执行修改密码命令:
ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';顺手设置一个远程登录账号(按需):
CREATE USER 'admin'@'%' IDENTIFIED BY 'admin123'; GRANT ALL PRIVILEGES ON *.* TO 'admin'@'%'; FLUSH PRIVILEGES;建议把bin目录添加到系统环境变量 PATH 里,这样后续在任何路径执行mysql -u root -p都会直接生效,不用每次都切目录。
1.3 Linux 和 Docker 方案:一条命令跑起来
Linux 服务器上的安装,以 CentOS/RHEL 系为例,一般用 rpm 安装或者 yum 安装。rpm 的方式有些繁琐,要先下载mysql-community-server相关 rpm 包再挨个安装,新手容易遇到依赖缺失的问题。我更推荐在 Ubuntu/Debian 上用 apt 直接装,简单省事:
sudo apt update sudo apt install mysql-server -y sudo systemctl start mysql sudo systemctl status mysql初始用户名是 root,安装过程会提示设置密码。如果没提示,可以用sudo mysql直接进入,然后手动执行ALTER USER改密码。
如果你不想污染本机环境,或者需要快速起一个临时数据库练手,Docker 是最合适的方案:
docker run -d \ --name mysql8 \ -e MYSQL_ROOT_PASSWORD=123456 \ -p 3306:3306 \ mysql:8.0Docker 方式对配置文件的处理最干净,不适合脚本化的多环境测试场景,但对小白来说有个小门槛:镜像拉取可能失败,通常是网络源问题,换一个国内可访问的镜像源即可;--name指定的容器名不能和已有容器重复,遇到“Conflict”报错时改个名字或者先docker rm mysql8再重跑。
1.4 安装必坑清单:服务名、端口、初始化
第一次装 MySQL 的人,十个里有八个会遇到下面几个问题:
端口被占用。启动时报3306端口被占用,多半是之前装过 MySQL 残留服务,或者装了其他占用 3306 的软件。先执行netstat -ano | findstr 3306看是哪个进程占用了端口,再用服务管理器把旧的 MySQL 服务停掉,或者直接换一个端口(改my.ini里的port=3307,登录时用mysql -u root -P 3307 -p)。
data 目录无法初始化。这个坑也很常见,因为my.ini里的datadir目录已经存在且不为空。解决方法是把data目录删掉(或者换一个新路径),重新执行mysqld --initialize-insecure。注意初始化成功后会生成一个data目录,里面的auto.cnf文件是实例唯一标识,不要随意删除。
“mysqld 不是内部或外部命令”。这个问题简单,就是没进入 bin 目录或者环境变量没配置,直接cd到 bin 目录再执行。
服务启动后秒退。查看 MySQL 的错误日志,位置一般在datadir目录下的*.err文件,里面会写明具体原因。绝大多数是路径配置错误、目录权限不足或者内存不足,对着日志排查就行。
2. SQL 基础语句:从“照着敲”到“灵活写”
2.1 先把名词理清楚:SQL、MySQL、SQL Server 不是一回事
很多新手会把 SQL、MySQL、SQL Server 混在一起,这里先花半分钟理清三个词。
SQL 是一种结构化查询语言,是访问和操作关系型数据库的标准语言,包含增删改查、建表、授权等操作。MySQL 是一个具体的关系型数据库管理系统,它支持并实现了 SQL 标准,同时扩展了自己的一些语法。SQL Server 则是微软家的数据库产品,它同样支持 SQL 标准,但细节语法和 MySQL 有差异,比如分页用 TOP/OFFSET-FETCH,而 MySQL 用 LIMIT。一句话总结:SQL 是“话”,MySQL 和 SQL Server 是“说这种话的人”,学会 SQL 之后,切换数据库产品的成本会低很多。
2.2 DDL 与 DML:建表、加字段、插入、更新、删除
SQL 语句按功能分为四大类:DDL(数据定义语言)、DML(数据操作语言)、DQL(数据查询语言)、DCL(数据控制语言)。新手最常接触的是 DDL 和 DML。
DDL 里最常用的是建表语句。我拿一个用户表举例子:
CREATE DATABASE IF NOT EXISTS shop_db DEFAULT CHARSET utf8mb4; USE shop_db; CREATE TABLE `user` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键id', `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户名', `phone` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号', `age` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '年龄', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用,1启用', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';几个细节说一下:AUTO_INCREMENT表示自增主键;DEFAULT 0就是热门搜索里那种“MySQL 设置默认值为 0”的场景;DATETIME DEFAULT CURRENT_TIMESTAMP写不写都行,1 写跑不了;字段名用反引号包起来可以避免和 MySQL 关键字冲突,比如user;索引KEY idx_phone在查询手机号时能加速,这个后面索引章节细讲。
DDL 里还有一个经常用到的操作是修改表结构,比如加字段:
ALTER TABLE `user` ADD COLUMN `nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '昵称' AFTER `name`;DML 则是针对表中数据的操作。插入、更新、删除是三大金刚:
INSERT INTO `user` (`name`, `phone`, `age`) VALUES ('小王', '13900001234', 25); UPDATE `user` SET `age` = 26 WHERE `id` = 1; DELETE FROM `user` WHERE `id` = 1;这里必须强调:UPDATE和DELETE一定要带WHERE条件,否则会更新/删除全表数据。我真见过不止一个同事在测试环境顺手把整张表清空的情况。线上环境如果不确定条件是否精确,建议先SELECT同样的WHERE条件查一遍再执行。
2.3 DQL 查询核心:SELECT、去重、排序、分页
查询语句是 SQL 里使用频率最高的部分,也是面试必考的核心。基础模板是:
SELECT 字段列表 FROM 表名 WHERE 过滤条件 GROUP BY 分组字段 HAVING 分组后过滤条件 ORDER BY 排序字段 LIMIT 分页偏移量, 每页数量;WHERE 条件里最常用的几个操作符包括:=、!=、>、<、BETWEEN AND、IN、LIKE。比如查 22 到 30 岁之间的用户,可以写成WHERE age BETWEEN 22 AND 30,也可以写成WHERE age >= 22 AND age <= 30,两种写法效果一样。LIKE做模糊查询时注意性能问题:LIKE '%str%'的前置通配符会导致索引失效。
DISTINCT 去重是热词里频繁出现的需求。比如查所有不重复的状态值:
SELECT DISTINCT `status` FROM `user`;还可以配合COUNT统计去重后的数量:
SELECT COUNT(DISTINCT `phone`) AS cnt FROM `user`;再去重时有一点要知道:DISTINCT后面跟多个字段,表示这些字段的组合值去重,不是单独对每个字段去重。如果需要针对某一列去重但还要选出其他字段,DISTINCT就不太够用了,这时候需要窗口函数配合ROW_NUMBER()实现,后面专门展开。
ORDER BY 排序也很基础。比如按年龄倒序、创建时间正序排列:
SELECT id, name, age, create_time FROM `user` WHERE `status` = 1 ORDER BY `age` DESC, `create_time` ASC;默认是ASC升序,DESC是降序。多字段排序时,从左到右依次生效,先按age排,年龄一样的按create_time排。
LIMIT 分页是 MySQL 比 SQL Server 更直观的地方。每页 10 条,第 3 页的数据就是从第 21 条开始:
SELECT * FROM `user` LIMIT 20, 10;第一参数是跳过多少条,第二个参数是返回多少条。等价写法是LIMIT 10 OFFSET 20。注意OFFSET的写法在复杂 SQL 拼接时更容易读,但原理一样。
2.4 聚合与 JOIN:从单表到多表的丝滑过渡
聚合函数包括COUNT、SUM、AVG、MAX、MIN,通常配合GROUP BY按组统计。比如按状态分组统计用户数和平均年龄:
SELECT status, COUNT(*) AS total_cnt, AVG(age) AS avg_age FROM `user` GROUP BY status;HAVING是针对分组后的数据进行二次过滤。比如筛选出平均年龄大于 25 的状态分组:
SELECT status, AVG(age) AS avg_age FROM `user` GROUP BY status HAVING avg_age > 25;这里注意WHERE和HAVING的区别:WHERE在分组前过滤原始行,HAVING在分组后过滤聚合结果。能用WHERE过滤掉的不要放到HAVING,因为先分组再过滤代价更高。
多表 JOIN 是 SQL 小白到初级开发的必经之路。我用订单场景举例,两张表:orders订单表、users用户表。
SELECT u.name, o.order_no, o.amount FROM `orders` o INNER JOIN `users` u ON o.user_id = u.id;INNER JOIN只返回两边能匹配上的数据,好比合租只算双向认识的室友;LEFT JOIN返回左表全部数据,右表没有匹配就填 NULL,好比不管你认不认识,左边住的人都会出现在名单里。面试题里经常考这两者的区别,除了说起条数差别,最好再补一句“LEFT JOIN 能得到左表全量,而 INNER JOIN 只保留匹配到的交集”。
2.5 窗口函数与存储过程:面试加分项
我见过不少新人,基础查询写得很溜,一碰到窗口函数就懵。窗口函数简单理解就是“给每一行数据计算一个基于分组窗口的聚合结果,但并不合并行”。最经典的场景是排名:按年龄为每个用户排名。
SELECT name, age, ROW_NUMBER() OVER (ORDER BY age DESC) AS rank_no, RANK() OVER (ORDER BY age DESC) AS rank_val, DENSE_RANK() OVER (ORDER BY age DESC) AS dense_rank_val FROM `user`;三种排名的区别是高频面试题:ROW_NUMBER()不管有没有并列都连续编号;RANK()有并列时跳号,比如两个并列第 1,下一名是第 3;DENSE_RANK()有并列也不跳号,下一名是第 2。用月度销售排名的场景来记最合适。
窗口函数还能解决前面提到的“按某列去重取最新一条”的问题:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY phone ORDER BY create_time DESC) AS rn FROM `user` ) t WHERE t.rn = 1;PARTITION BY phone相当于按手机号分组,每组内按创建时间倒序编号,取rn=1就是每个手机号最新一条。子查询里的别名t是必须的,MySQL 要求派生表必须有别名,这是一个很多人都遇到过的报错。
存储过程在工作中用得不频繁,但面试偶尔会问。创建一个简单的存储过程:
DELIMITER // CREATE PROCEDURE GetUserById(IN userId INT) BEGIN SELECT id, name, age FROM `user` WHERE id = userId; END // DELIMITER ;DELIMITER是用来临时改变语句分隔符的,因为存储过程内部有多个分号,如果还用默认分号,MySQL 会在半路就截断执行。调用存储过程用CALL GetUserById(1);。
3. 面试进阶:事务、锁、索引与慢 SQL 优化
3.1 事务 ACID 与隔离级别:转账案例讲透
事务是数据库面试的绝对重点,也是最容易答得虚的部分。面试官问“事务的特性是什么”,如果只是把 ACID 四个字母背出来,基本等于没答。要结合场景讲:比如转账,A 给 B 转 100 元,中间涉及扣款和加款两条 SQL,要么都成功,要么都失败。
- 原子性:这个转账过程是一个不可分割的最小单元,要么全部完成,要么全部回滚。底层靠 undo log 实现。
- 一致性:转账前后总金额不变,数据库从一个一致状态到另一个一致状态。
- 隔离性:两个事务同时转账互不干扰,靠锁和 MVCC 实现。
- 持久性:事务一旦提交,修改就永久保存,即使宕机也不会丢,靠 redo log 实现。
事务的基本语法很直接:
START TRANSACTION; UPDATE `account` SET balance = balance - 100 WHERE id = 1; UPDATE `account` SET balance = balance + 100 WHERE id = 2; COMMIT;如果中间某条语句出错,执行ROLLBACK;回滚。
隔离级别是事务的高级考点,MySQL InnoDB 默认是REPEATABLE READ(可重复读)。面试时拿出下面这个对照表基本就稳了:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不会 | 可能 | 可能 |
| REPEATABLE READ | 不会 | 不会 | 可能(InnoDB 通过间隙锁解决) |
| SERIALIZABLE | 不会 | 不会 | 不会 |
解释三个概念:脏读是读到别的事务未提交的数据;不可重复读是同一事务里两次读同一行数据结果不一样;幻读是同一事务里两次范围查询得到的结果行数不一样,比如第一次查到 1 条,第二次变成 2 条。
实际开发中怎么设置隔离级别?用SET TRANSACTION ISOLATION LEVEL READ COMMITTED;或者直接改配置文件。多数互联网业务使用 READ COMMITTED 级别就够了,因为可重复读虽然能避免部分问题,但间隙锁会提高死锁概率。
3.2 MySQL 锁的分类:全局锁、表锁、行锁、间隙锁
“MySQL 锁的分类”同样是搜索热词。面试答锁的分类,建议按粒度从大到小讲。
全局锁:锁整个数据库实例,执行FLUSH TABLES WITH READ LOCK;后所有库只能读不能写,典型应用是做整库备份时保证一致性。
表级锁:锁整张表。MyISAM 引擎只支持表锁,InnoDB 主要用行锁但也存在某些情况下的表锁(比如没有索引的更新操作,只能全表扫描逐行加锁)。
行级锁:InnoDB 最核心的锁机制,分为共享锁(S 锁,读锁)和排他锁(X 锁,写锁)。日常执行的UPDATE、DELETE、INSERT都会自动加排他行锁;SELECT默认不加锁,但可以用SELECT ... FOR UPDATE加排他锁,或SELECT ... LOCK IN SHARE MODE加共享锁。
从另一个维度可以分悲观锁和乐观锁:悲观锁是“我认定你会跟我抢,所以在操作前先把数据锁住”,实现方式就是FOR UPDATE;乐观锁是“我默认没人和我抢,更新时检查版本号”,典型实现是在表里加version字段,更新时UPDATE table SET ... WHERE id = ? AND version = ?,如果影响行数为 0 则重新读取再重试。
间隙锁和临键锁比较进阶。间隙锁锁的是一个区间而不是具体行,用来解决幻读,它锁住的是两个索引记录之间的间隙。临键锁是间隙锁和行锁的组合。InnoDB 的 REPEATABLE READ 隔离级别下,范围查询会触发间隙锁,这也是为什么高并发场景下把隔离级别降到 READ COMMITTED 能降低死锁概率的原因。
3.3 EXPLAIN 与索引优化:慢 SQL 三板斧
面试官问“慢 SQL 怎么优化”,本质是考察你对索引和查询计划的理解。我一般按三步走。
第一步,先用 EXPLAIN 看执行计划。在 SELECT 前加上EXPLAIN关键字,MySQL 会展示这条 SQL 的执行路径。最核心的列是type,从好到差依次是system、const、eq_ref、ref、range、index、ALL。看到ALL就是全表扫描,意味着 SQL 有优化空间;看到ref或range就是走了索引,质量不错;key列显示了实际用到的索引名称,如果为 NULL 说明没走索引。
第二步,针对查询条件建索引。最常见的索引策略是给 WHERE 条件、JOIN 字段和 ORDER BY 字段建立索引。比如按手机号查用户:
CREATE INDEX idx_phone ON `user`(`phone`);联合索引要理解最左前缀原则:比如建立INDEX idx_name_age (name, age),那么查询条件里不包含name时,age索引无法生效。这就像查字典,必须先按拼音首字母定位,再按第二个字母筛选。
第三步,改写 SQL 避免索引失效。常见坑有:对索引列使用函数,比如WHERE DATE(create_time) = '2025-01-01',导致索引失效,改成create_time >= '2025-01-01' AND create_time < '2025-01-02';隐式类型转换,比如手机号是 varchar 类型,查询时写phone = 13900001234,MySQL 会做类型转换使索引失效,应该写phone = '13900001234';SELECT *只取需要的字段,尽量走覆盖索引(索引里已经包含要查询的字段,不需要回表)。
还有一个优化点是大分页问题。LIMIT 2000000, 20要扫前 200 万行再丢弃,性能很差。优化方式是先查主键再做连接:
SELECT * FROM `user` WHERE id > ( SELECT id FROM `user` ORDER BY id LIMIT 2000000, 1 ) ORDER BY id LIMIT 20;这个技巧效率提升非常明显,面试时能讲出来是加分项。
3.4 从 SQL 注入到防御:一句“万能密码”背后的原理
SQL 注入是个安全话题,网上搜“sql 注入万能密码绕过”能看到很多攻击载荷示例。作为开发者,我的态度很明确:这方面的知识可以了解原理,但精力应该放在防御上,而不是研究怎么绕过。有些 CTF 比赛里会有 SQL 注入题目,比如搜索词里提到的入门题,用来练习安全分析是可以的,但日常开发中你要做的是堵住漏洞。
SQL 注入的本质是程序把用户输入拼进了 SQL 语句里。比如登录场景:
String sql = "SELECT * FROM user WHERE name = '" + userName + "' AND pwd = '" + password + "'";如果用户在用户名输入框里填了:
' OR 1=1 --那拼出来的 SQL 就变成:
SELECT * FROM user WHERE name = '' OR 1=1 -- ' AND pwd = ''--把后面的密码条件注释掉了,OR 1=1恒为真,攻击者不需要知道密码就能拿到用户信息。这就是所谓的“万能密码”思路。
防御手段最核心的一条是:使用参数化查询,绝不手工拼接 SQL。
PreparedStatement ps = conn.prepareStatement( "SELECT * FROM user WHERE name = ? AND pwd = ?" ); ps.setString(1, userName); ps.setString(2, password); ResultSet rs = ps.executeQuery();PreparedStatement 会把用户输入当纯数据处理,而不是当成 SQL 语法的一部分。另外配合最小权限原则:数据库账号只给应用所需的权限,不要把 root 账号直接写在代码里。
3.5 高频面试习题速答:附答案拆解
整理几道出现频率很高的 SQL 面试题,附上解题思路,基本是“面试前背一遍,手写不出错”的级别。
第一题:查找第 N 高的工资。
SELECT DISTINCT salary FROM employee ORDER BY salary DESC LIMIT 1 OFFSET N-1;如果要求“第 3 高”,OFFSET 就是 2。这道题的变种是“没有第 N 高时返回空”,可以用子查询或者函数包一层。
第二题:统计每个部门的平均工资,并展示平均工资大于 5000 的部门。
SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id HAVING avg_salary > 5000;第三题:查找重复出现两次以上的手机号。
SELECT phone FROM user GROUP BY phone HAVING COUNT(*) >= 2;第四题:删除重复数据,只保留 id 最小的那条。
DELETE u1 FROM user u1 INNER JOIN user u2 ON u1.phone = u2.phone AND u1.id > u2.id;也可以提前给重复数据编号,然后删除编号不是 1 的:
DELETE FROM user WHERE id NOT IN ( SELECT MIN(id) FROM user GROUP BY phone );第五题:用窗口函数求每个部门工资最高的员工。
SELECT dept_id, name, salary FROM ( SELECT dept_id, name, salary, RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk FROM employee ) t WHERE t.rk = 1;第六题:MySQL 为什么要用 utf8mb4 而不是 utf8?因为 MySQL 里的utf8是阉割版,最多存 3 个字节,存不了 emoji 表情和一些生僻字;utf8mb4才是真正的 UTF-8 四字节编码,兼容更全。
第七题:char 和 varchar 的区别?char 是定长,varchar 是变长;char 适合存储固定长度的数据,比如手机号、身份证,varchar 适合长度不固定的文本。varchar 在 InnoDB 中存储时会有额外的长度前缀,读取速度快不过在排序等操作中更耗空间。
4. 实际开发中的辅助技能:从建表 SQL 到数据导入
4.1 MyBatis-Plus 根据 Java 实体类生成建表 SQL
搜索热词里有“mybatisplus 根据 java 实体类生成创建表的 sql 语句”,这确实是开发中的高频操作。MyBatis-Plus 的代码生成器可以生成实体类、Mapper、Service,但它本身并不直接支持“根据 Java 实体类生成建表 SQL”。实际工作中常用两种思路。
第一种,在实体类上写好注解标签,然后通过一个工具方法把字段反射出来拼接 DDL。示例实体类:
@Data @TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; @TableField("name") private String name; @TableField("age") private Integer age; }然后写一个简单的生成器,通过反射读取实体类的@TableField、@TableId、@TableName注解,拼出 CREATE TABLE 语句。这个方案能自动化,但我们通常更推荐第二种:直接用 MyBatis-Plus 的TableInfoHelper获取表结构信息,再把字段映射为 DDL 类型。
第二种更稳妥的思路是写一份 SQL 脚本文件放在项目的resources/db目录下,配合启动时执行。比如:
CREATE TABLE IF NOT EXISTS `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL DEFAULT '', `age` INT NOT NULL DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;在application.yml里配上spring.sql.init.mode: always和spring.sql.init.schema-locations: classpath:db/schema.sql,应用启动时自动建表。对比下来,手动写建表 SQL 更可控,字段类型映射也更精确,反射生成适合动态业务,但对字段类型、索引设定的控制力弱。
4.2 Navicat 导入 SQL 数据:三大常踩坑
Navicat 是数据库管理工具里的常青树,日常做表结构设计、数据导入导出都很方便。但导入 SQL 文件时有三个坑很典型。
第一个坑是字符集不对导致乱码。SQL 文件本身是 utf8mb4,但 Navicat 默认连接字符集可能是其他编码。解决方式是导入之前先把连接编码设置为utf8mb4,在“编辑连接 → 高级”里勾选“使用 MySql 字符集 utf8mb4”,导入时在向导的“高级”选项里明确指定编码。
第二个坑是外键依赖顺序错了导致导入失败。如果 SQL 文件同时包含父表和子表,而父表还没创建,子表的外键约束就建立不起来。处理方案有两种:要么在导入前临时关闭外键约束检查,在 SQL 文件开头加上SET FOREIGN_KEY_CHECKS = 0;,末尾加回SET FOREIGN_KEY_CHECKS = 1;;要么把建表语句按依赖顺序排好。
第三个坑是单个大 SQL 执行超时或内存不足。导入几 GB 的 SQL 备份时,Navicat 默认的超时时间比较短,并且max_allowed_packet太小会报Packet too large。在 MySQL 配置文件[mysqld]下增加max_allowed_packet=256M,重启服务,再在 Navicat 的“查询 → 高级”里调大执行超时时间。
4.3 JDBC 连接 MySQL 与参数化查询
如果是 Java 后端方向,JDBC 是绕不开的。一个基础连接代码如下:
Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/shop_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4"; String user = "root"; String password = "123456"; Connection conn = DriverManager.getConnection(url, user, password);useSSL=false在本地开发时能避免证书警告;serverTimezone=Asia/Shanghai处理时区差异;characterEncoding=utf8mb4防止中文乱码。查询时用 PreparedStatement 而不是 Statement:
String sql = "SELECT id, name FROM user WHERE age > ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, 18); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { System.out.println(rs.getInt("id") + " - " + rs.getString("name")); } } } catch (SQLException e) { e.printStackTrace(); }还有一套经典的坑是“Public Key Retrieval is not allowed”,这是 MySQL 8.0 使用 caching_sha2_password 认证插件时的问题,解决方案是在 JDBC URL 后面加allowPublicKeyRetrieval=true。
5. 常见问题与排查技巧实录
5.1 连接与认证问题
“Access denied for user 'root'@'localhost'”:密码错了,或者 root 账号的 host 限制。如果是刚初始化完忘记密码,最直接的方案是用--skip-grant-tables模式启动 MySQL 再改密码,但生产环境不建议随便用。另一种方案是重新执行初始化命令,清掉 data 目录重来,测试环境反正没有重要数据。
“Public Key Retrieval is not allowed”:上一章提过,JDBC 连接 8.0 时常见,加allowPublicKeyRetrieval=true。如果是 Navicat 连接报这个,在连接属性的高级选项卡里勾选“使用加密连接”并允许公钥检索。
MySQL 命令卡在mysql -u root -p回车后:一般不是卡住,是提示输入密码,直接敲密码回车就行(界面上不会显示任何字符)。如果你感觉回车后没反应,试一下输入密码再回车。
5.2 SQL 执行与字符集问题
“Every derived table must have its own alias”:MySQL 要求每个子查询(派生表)都必须有别名。写完子查询后忘了在后面加AS t,就会报这个错。解决方案是在子查询闭合括号后面加别名。
“Expression #1 of SELECT list is not in GROUP BY clause”:这是ONLY_FULL_GROUP_BY模式开启导致的。SQL 规范要求GROUP BY后面的列和SELECT的非聚合列必须一致。两种解决方式:要么把SELECT里不在分组里的列也加进GROUP BY,要么在查询时用ANY_VALUE()包一下不需要分组的字段。不建议直接关掉这个模式,它其实是帮你在避免代码隐患。
中文乱码。首先确认三处编码一致:数据库字符集(SHOW VARIABLES LIKE 'character_set_database';)、连接字符集(SET NAMES utf8mb4;)、表字段字符集。三处都统一成 utf8mb4,乱码基本不会再出现。另外注意 SQL 文件的编码格式,如果用 Word 之类的工具另存为带 BOM 的 UTF-8,导入时前几个字符可能被 BOM 吃掉导致首行解析失败。
5.3 性能与进程问题速查表
日常运维中问最多的几个量级问题,整理成速查表:
| 现象 | 排查命令 | 常见处理 |
|---|---|---|
| 数据库连接数被打满 | SHOW VARIABLES LIKE 'max_connections';SHOW STATUS LIKE 'Threads_connected'; | 调大 max_connections;优化长连接;检查是否有 MySQL 连接泄漏 |
| CPU 飙升 | SHOW PROCESSLIST;找 State 为 Copying to tmp table 或长时间 Running 的语句 | 杀掉慢会话KILL ID;;对慢 SQL 做 EXPLAIN + 优化索引 |
| 查询明显变慢 | EXPLAIN SELECT ...;看 type 是不是 ALL,key 是否为 NULL | 根据 WHERE/JOIN/ORDER BY 字段补索引 |
| 死锁报错 | 查看SHOW ENGINE INNODB STATUS;中的 LATEST DETECTED DEADLOCK | 让事务尽量短;统一 SQL 更新顺序;降低隔离级别到 READ COMMITTED |
| 磁盘空间不足 | SHOW TABLE STATUS;查看 Data_length;检查 binlog 大小 | 清理 binlog;清理无用表;考虑归档历史数据 |
还有一个容易被忽略的“慢”原因:表长时间没做 ANALYZE,统计信息过旧,优化器选错索引。执行一次ANALYZE TABLE user;往往立竿见影。
最后说一个我自己的习惯。带新人时我会要求他们装好 MySQL 之后,先自己把“建库、建表、插入 20 条测试数据、做 5 种查询、再改一条数据”这套流程完整走一遍,不要一上来就背面试题。环境踩过的坑和手感是背不出来的,只要这个流程能独立走通,后面无论是继续啃 SQL 进阶、看索引原理,还是刷面试题,都会顺畅很多。如果你装的是 Docker 版,我建议额外在 Windows 上把 zip 版也装一次,因为运维和生产环境大概率不是 Docker,提前把my.ini、net start、mysqld --initialize这套流程搞熟,以后遇到服务器部署会非常从容。