我入行这些年,MySQL 是我见过最“好上手、也最容易翻车”的数据库——装好了、连上了、写几条 SQL 没毛病,你以为完事了,结果 UPDATE 忘带 WHERE 把整张表清空,或者客户端突然给你抛一个“authentication protocol requested”的报错让你一脸懵。更别提那些面试题里“为什么 MySQL 默认用 RR 隔离级别”“锁表到底怎么排查”之类的问题,看着简单,真被问到细节时能卡住一大半人。
这篇东西我不打算写成一本正经的教程文档,而是把我从安装到日常使用、再到数据迁移和面试复盘这一路上踩过的坑、验证过的方法、看过的底层机制,整理成一篇能直接“抄作业”的实战笔记。覆盖范围包括 Windows/Linux/Docker 安装、连接报错、常用 SQL 陷阱、存储过程与触发器、连接池与锁表,以及 sqoop、Kettle、Navicat 这些生态工具的配合。无论你是刚接触 MySQL 的新手,还是被某个诡异报错卡住的老手,这里面大概率有一节能帮到你。
1. 安装这一步,为什么大多数人折在“启动服务”上
1.1 Windows 安装:先分清是 Installe 卡住还是服务起不来
很多人一上来就百度“mysql 下载”,结果下到一堆捆绑软件或者旧版本。这里我建议只认官网,也就是 MySQL 官方下载页,社区版(Community Server)完全够用。下载时注意区分安装包格式:一个是几百 MB 的 MySQL Installer(MSI 安装向导),一个是精简版 ZIP 包。新手用 MSI 更省心,老手往往更喜欢 ZIP,因为解压即用、不污染注册表。
但 MSI 安装有个知名卡点:“Configuration of MySQL Server is taking longer than expected”或者卡在 Check Requirements 转圈。我帮人排查过很多次,最常见的原因是:
- 系统缺少 Visual C++ Redistributable,尤其是 MySQL 8.0 之后对 VC 运行库有明确要求。
- 本机 3306 端口已经被其他程序占用,比如以前装过 MySQL 没卸干净,或者跑着别的服务。
- Windows 防火墙弹窗被忽略,导致服务无法监听到端口。
- 旧版本残留的 my.ini 或数据目录权限冲突。
遇到卡住,不要反复点“Cancel”重试,先打开“服务”面板看有没有 MySQL 相关服务的残影,再 netstat -ano | findstr :3306 查端口占用,最后去 C:\ProgramData\MySQL\MySQL Server 8.0\Data 看 .err 错误日志。日志里往往直接写着“Port 3306 is already in use”或者“Cannot create Windows service”,比你在图形界面瞎猜高效得多。
另一种情况是服务能装上,但“启动服务报错”。MySQL 8.0 常见报错是“MySQL 服务无法启动”且错误日志提示 ibdata1 相关权限问题。这时候别急着重装,先确认数据目录的写权限:右键数据目录 → 属性 → 安全,给 NETWORK SERVICE 或当前用户完整控制权限。Windows 下启动服务本质是让服务账户去读写数据目录,权限不对,一切都白搭。
1.2 Linux 与 Docker 安装:版本差异是最大的坑
Linux 下安装 MySQL,Ubuntu 系比较典型。apt install mysql-server 装出来的很可能是发行版维护的 MySQL 8.0,安装过程不会让你设置 root 密码,而是默认用 auth_socket 插件,导致你 mysql -uroot -p 输任何密码都报错。正确做法是用 sudo mysql 进入后,手动把 root 的认证插件改成 caching_sha2_password 或 mysql_native_password,再设置密码。
Docker 安装则要特别注意环境变量和初始化机制。我常用的命令类似:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=yourpassword \
-v /my/own/datadir:/var/lib/mysql \
mysql:8.0
有几个细节是网上教程不爱提的:
MYSQL_ROOT_PASSWORD只在数据目录首次初始化时生效。如果/var/lib/mysql已经存在旧数据,这个环境变量会被忽略。- 容器启动后不要马上连,先
docker logs mysql8看日志,出现ready for connections才算真正初始化完成。 - 如果宿主机 3306 被占用,改成
-p 3307:3306,但连接串也要跟着变端口。 - MySQL 8.0 在容器里跑,建议加
--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci,否则默认字符集在存中文时迟早出问题。
Linux 和 Docker 下最大的坑其实是 5.7 与 8.0 的行为差异。5.7 默认认证插件是 mysql_native_password,8.0 默认是 caching_sha2_password;8.0 默认 utf8mb4,5.7 默认 utf8mb3;8.0 的 sql_mode 默认带了 ONLY_FULL_GROUP_BY,5.7 没这么严格。如果你照着旧文章写 SQL,很可能在新的 8.0 上莫名报错,这我在后面 SQL 部分还会细说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 连不上的时候,先别怀疑密码
2.1 FireDAC 报错:caching_sha2_password 的兼容性难题
有一个报错,几乎所有用 Delphi 开发的人都会撞上:[FireDAC][Phys][MySQL] Client does not support authentication protocol requested by server; consider upgrading MySQL client。这个报错翻译成人话就是:MySQL 8.0 服务端默认用 caching_sha2_password 认证,而你用的 FireDAC 驱动版本太老,不认这个协议。
初次遇到这个报错,很多人第一反应是改密码、重建用户,其实方向错了。根本原因是客户端驱动和服务端认证插件不匹配。三种解决办法,我按推荐程度排序:
- 升级客户端驱动。FireDAC 的 MySQL 驱动依赖于底层的 libmysql.dll 或 libmariadb.dll,去官方下最新版 MySQL Connector/C,替换掉 Delphi 安装目录里的同名 DLL。这是最干净的做法。
- 修改用户认证插件。如果你暂时动不了 DLL,可以执行:
sql复制ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';
FLUSH PRIVILEGES;
把用户的认证方式降级成老插件。代价是你的密码在网络上传输时的安全性弱一点,但内网测试环境问题不大。
3. 在连接字符串里加参数。FireDAC 支持 ServerCharSet、UseUnicode,但对认证协议本身没有直接参数,所以这个方法一般不够用。
老版本的 Navicat、Workbench 连 8.0 时报 unable to load authentication plugin caching_sha2_password 也是同一个原因。当时我帮同事排查,他装了最新版 Workbench 立刻就好了,可见很多时候问题不在服务端而在客户端。
2.2 端口号、防火墙与连接池的超时假象
连接报错还有一个隐蔽来源:服务端端口根本没开、或者 bind-address 限制了监听地址。
MySQL 默认监听 3306,如果 my.ini/cnf 里有 bind-address = 127.0.0.1,那就只有本机能连。云服务器上你开了安全组、本机防火墙也放行了,但实例内部 bind-address 没改,照样连不上。排查时先 telnet 服务器IP 3306,通不通一目了然。不通的话用 mysql -uroot -p -h127.0.0.1 在服务器本机测,本机能连就继续查防火墙和监听地址;本机都连不上,就回服务端日志找问题。
连接池这块更是个“假象重灾区”。应用连接池配置了 wait_timeout 但连接空闲了 8 小时,MySQL 会按默认的 wait_timeout=28800(8小时)主动断开空闲连接。你以为连接池里的连接还是好的,一用才发现 Communications link failure。这不是密码错了,也不是端口被封,而是“连接已经死掉但连接池还在复用”。
解决思路有三个层次:
- 连接池配置里加
testConnectionOnCheckout或testWhileIdle,让连接在被取出前先做一次轻量 SELECT 1 检测。 - 把 MySQL 的
wait_timeout调大或调小,与连接池的空闲回收时间对齐,一般建议wait_timeout小于连接池的空闲超时时间。 - 应用连接串里增加
autoReconnect=true(JDBC),但这个只能缓解,不能治本。
真正治本的是理解连接生命周期:连接池负责复用 TCP 连接,但 MySQL 服务端会按空闲策略回收不活跃连接,这个回收是“单向”的,客户端并不知道。所以连接池必须有过期校验,而不能只是傻傻地复用。
3. 日常 SQL 里最容易被忽略的“小陷阱”
3.1 UPDATE 千万别忘了 WHERE:一次全表更新的代价
MySQL 的 UPDATE 语法看着简单,实际上最危险。我见过不止一次有人执行:
sql复制UPDATE user SET status = 1;
本意是更新某个用户,结果把全表状态全改了。这个在测试环境还好,线上直接事故。我自己的习惯是:写 UPDATE 和 DELETE 之前,先写对应的 SELECT 查一遍,确认 WHERE 条件命中范围,然后再加 UPDATE/DELETE。看起来多了一步,但能救命。
UPDATE 的语法还有几个细节:
- 多表更新时不能用
UPDATE t SET ... FROM ...这种 SQL Server 写法,MySQL 是UPDATE t1 JOIN t2 ON ... SET t1.col = ...。 UPDATE语句可以加LIMIT,这在某些批量变更场景很有用,但要注意不能配合多表 JOIN。- UPDATE 时若设置值为表达式,比如
SET age = age + 1,是在原值基础上加的。这和SET age + 1(不存在的写法)是两个概念。 - 如果字段是整型,读出来做加法和直接在 SQL 里做加法都可能踩“隐式转换”的坑。
热词里那个 mysql中int+5,指的就是查询时对整型字段做算术运算。比如:
sql复制SELECT price, price + 5 AS price_plus FROM goods;
这个没问题,但如果字段是字符串类型,'10abc' + 5 会得到 15,MySQL 的隐式转换会把非数字开头的字符串当作 0。这个细节在统计报表时经常产生诡异结果,排查起来特别费劲。
3.2 排序、去重与大小写:你以为的 MySQL 不一定是你以为的
ORDER BY 的坑主要体现在字符集和排序规则上。MySQL 默认的 utf8mb4_unicode_ci 排序是按 Unicode 编码的“通用权重”排的,不是按拼音也不是按笔画。中文排序结果在很多人看来“毫无规律”,其实就是因为默认排序规则不按拼音。如果你需要中文拼音排序,得用 ORDER BY CONVERT(name USING gbk),利用 GBK 编码的拼音序特性。这不是 MySQL 的 bug,而是排序规则的设计问题,但没经历过的人第一次见确实会懵。
去重的问题更经典。热搜里“mysql 的 or 能去重吗”,答案很直接:OR 不会去重。去重是 DISTINCT 或 GROUP BY 的职责,OR 只是条件连接符。很多人写:
sql复制SELECT DISTINCT name FROM user WHERE age > 18 OR age < 30;
注意这里 OR 是条件,DISTINCT 才是去重。如果你写的是 SELECT name FROM user WHERE name = 'a' OR name = 'b',那返回的必然包含重复项(如果数据里有重复的话)。要想严谨去重,得用 DISTINCT 或者 GROUP BY。
大小写的问题也常被问到。MySQL 的表名和列名在 Linux 上默认大小写敏感(lower_case_table_names=0),在 Windows 上默认不敏感。这意味着你在 Windows 上建的表叫 User,迁移到 Linux 上用 user 可能报“表不存在”。字符串比较是否大小写敏感则取决于列的 collation,_ci 后缀表示 case-insensitive,_bin 或 _cs 表示区分大小写。搞不清这个,你写 WHERE name = 'admin' 时,Admin 用户也被捞出来了。
3.3 字段名为关键字、int+5、常用函数的边界
MySQL 里有一批保留关键字,比如 order、group、select、desc。如果你的表字段恰好叫这些名字,SQL 会直接报语法错误。解决办法是加反引号:
sql复制SELECT `order`, `group` FROM `user`;
但更根本的建议是建表时避免这些字段名,尤其不要用 order 这种业务意义太强又和 SQL 撞车的词。如果已经建了,所有涉及该字段的查询都要加反引号,非常容易出现“开发环境好好的、生产环境换了个环境就报错”的情况,因为不同 MySQL 版本对保留字的处理有差异。
常用函数里,IFNULL、DATE_FORMAT、GROUP_CONCAT、ROW_NUMBER()(8.0+)是高频使用的。我补充几个实际开发中容易踩坑的边界:
IFNULL(expr, 0)只能处理 NULL,不能处理空字符串。如果字段是'',IFNULL 不会转成 0,需要CASE WHEN col = '' THEN 0 ELSE col END。GROUP_CONCAT默认最大长度是 1024 字节,超出会被静默截断。要改大可以SET SESSION group_concat_max_len = 10240;,但这个设置只对当前会话有效。DATE_FORMAT的格式符是%Y-%m-%d而不是 Java 里的yyyy-MM-dd。写错不会报错,会返回 NULL,然后你还找不到原因。
4. 存储过程与触发器:不常用,但一旦用上就要注意分隔符
4.1 为什么写存储过程总要先 DELIMITER
MySQL 存储过程的热度一直很高,因为它是面试题常客、也是老系统里常见的业务逻辑载体。初学者第一次写存储过程,大概率会遇到一个诡异问题:在命令行或客户端里执行 CREATE PROCEDURE,报语法错误。
原因很简单:MySQL 客户端默认用分号作为一条 SQL 的结束符。而存储过程内部也有分号,比如:
sql复制CREATE PROCEDURE demo()
BEGIN
SELECT 1;
SELECT 2;
END;
你没告诉客户端“BEGIN...END 之间的分号不用管”,客户端就在第一个 SELECT 1; 那里截断了。解决方式就是用 DELIMITER 临时把结束符换成别的,比如:
sql复制DELIMITER $$
CREATE PROCEDURE demo()
BEGIN
SELECT 1;
SELECT 2;
END$$
DELIMITER ;
这是一个非常经典且实用的细节。Navicat 之类的图形客户端有时会自动处理,但命令行和脚本执行时完全依赖 DELIMITER。我记得有一次在自动化部署脚本里执行存储过程脚本,一直报错,后来发现就是没写 DELIMITER。这种问题几乎不会写在官方文档的醒目位置,但只要你写过程序,迟早遇到。
4.2 触发器里的 NEW 和 OLD,以及常见的错误信息
触发器比存储过程更隐蔽。它的基本逻辑是在 INSERT/UPDATE/DELETE 事件时自动执行一段 SQL。触发器中可以用 NEW.字段 引用新值,用 OLD.字段 引用旧值。比如:
sql复制CREATE TRIGGER trg_user_insert
AFTER INSERT ON `user`
FOR EACH ROW
BEGIN
INSERT INTO user_log(user_id, action) VALUES (NEW.id, 'INSERT');
END;
这里有几个细节容易踩:
NEW在 INSERT 和 UPDATE 触发器里可用,OLD在 UPDATE 和 DELETE 触发器里可用。不要试图在 INSERT 触发器里用OLD,会报“Unknown column”。FOR EACH ROW是必写的,MySQL 不支持语句级触发器(不像 Oracle)。- 触发器内不能直接返回结果集,也不能调用存储过程太深,容易被嵌套深度限制。
- 触发器执行出错,会导致主语句失败。比如你在 AFTER INSERT 触发器里往一个不存在字段插值,整个 INSERT 都会回滚。
存储过程中的“错误信息”处理也是重点。MySQL 8.0 支持 SIGNAL SQLSTATE 主动抛错:
sql复制CREATE PROCEDURE check_status(IN p_status INT)
BEGIN
IF p_status NOT IN (0, 1) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'status 参数非法';
END IF;
END;
45000 是用户自定义异常的通用 SQLSTATE。而 GET DIAGNOSTICS 可以拿到上一条 SQL 的执行信息,用于记录错误。这些在 MySQL 5.7 之后都比较成熟了。做数据库运维脚本或业务封装时,我会习惯在存储过程里加条件判断 + SIGNAL 主动抛错,这样应用层拿到的错误信息直白很多,而不是一长串“Incorrect arguments to EXECUTE”之类的模糊提示。
5. 面试官真正想问的 MySQL 底层问题
5.1 锁表:行锁、表锁、间隙锁和元数据锁
“mysql 锁表”是热搜里很靠前的词,也是几乎每个 DBA 或后端面试官都会问的点。锁表不是一种锁,而是一类“锁等待”现象。从底层来说,MySQL 的 InnoDB 引擎支持行锁,但你在某些场景下还是会遇到“表锁”或“锁表”:
LOCK TABLES table WRITE显式加表锁,这是手动行为。- DDL 操作(ALTER TABLE、DROP TABLE)会需要元数据锁(MDL),如果此时有事务占用了相关表,DDL 会一直阻塞,表现为“锁表”。
- 间隙锁(Gap Lock)是 InnoDB 在 RR 隔离级别下,为了避免幻读而对索引范围加的锁。它锁的不是某一行,而是“一个区间”,所以即使表里没有那行数据,你插入时也可能被阻塞。
我碰到过一个真实案例:一个长事务里执行了跨表查询,事务一直没提交,导致另一条 ALTER TABLE 语句等待元数据锁超时。排查时用:
sql复制SHOW PROCESSLIST;
看到 Waiting for table metadata lock 的状态,然后去 information_schema.innodb_trx 查有没有长时间未提交的事务。事后复盘,根因是应用层某个接口在事务里调了外部 HTTP 请求,事务被拉长到几十秒,结果把 DDL 堵死了。
避免锁表的基本习惯:
- 事务尽量短小,提交要快,不要在事务里做远程调用或慢查询。
- DDL 尽量放在低峰期,并利用
pt-online-schema-change或 MySQL 8.0 的ALGORITHM=INPLACE减少阻塞。 - 监控长事务,设置锁等待超时时间(
innodb_lock_wait_timeout默认 50 秒)。
5.2 连接池:为什么不是越大越好
连接池绝对是 MySQL 进阶绕不开的话题。很多人觉得连接池越大并发越高,实测下来往往相反。原因在于:连接数越多,CPU 在线程切换上的开销越大,而且每个连接都有独立的事务和锁上下文,争抢共享资源(比如 redo log、buffer pool)反而更激烈。
一个相对合理的经验值是:连接数 = ((核心数 * 2) + 有效磁盘数),这是 PostgreSQL 圈子里流传的经验,MySQL 也可以作为初值参考。如果应用是 IO 密集型,可以适当上调;如果是计算密集或短事务,则调低。关键不是抄数字,而是通过压测观察“吞吐量随连接数的变化曲线”,找到拐点。
连接池和 MySQL 服务端参数之间的关系也很微妙:
- 连接池最大连接数如果超过 MySQL 的
max_connections,MySQL 会报Too many connections。8.0 默认是 151,很小,生产环境往往要调大。 wait_timeout和interactive_timeout是服务端断开空闲连接的阈值,连接池要配合这个周期做探活。- 连接池预热和释放要依赖合理的最小/最大设置,最好和业务负载曲线对齐。
我在一个 Java 项目里把 HikariCP 从 50 下调到 20,业务 TPS 反而提升明显。原因就是每条 SQL 特别快,50 个连接里大部分都在空转,白白增加了上下文切换。这个反直觉的结论,在面试里讲出来会让面试官眼前一亮。
5.3 事务隔离级别和主从复制
面试题里“为什么 MySQL 默认用 RR(可重复读)而 Oracle 用 RC(读已提交)”,答案要追溯到 MySQL 的复制机制。MySQL 5.0 之前的复制是基于语句(Statement)的,binlog_format=STATEMENT。如果隔离级别是 RC,会出现一个问题:同一个事务内在不同时刻读取到的数据不同,但 binlog 里记录的却是执行过的 SQL 语句,从库按顺序执行后,和主库最终结果可能不一致。
RR 隔离级别通过间隙锁和一致性视图(MVCC)保证了事务内可重复读,这就让基于语句的复制能在主从两边得到一致的结果。后来 MySQL 有了 ROW 格式的 binlog,RC 在复制上的问题不再那么严重,但默认值还是 RR,一直沿用到 8.0。
主从复制这块,还要知道 binlog 的三种格式:
| 格式 | 含义 | 优点 | 缺点 |
|---|---|---|---|
| STATEMENT | 记录 SQL 语句 | 日志量小 | 某些函数/非确定性语句在主从执行结果可能不一致 |
| ROW | 记录每行变更前后值 | 一致性最强 | 日志量大 |
| MIXED | MySQL 自动选择 | 折中 | 不可预测,排错稍麻烦 |
8.0 默认是 ROW,这个变化解决了很多历史问题,但也让 binlog 占用空间变大。做主从时如果发现磁盘涨得飞快,先看是不是 ROW 格式加一个大事务产生海量日志。
6. 数据迁移与工具链:把 MySQL 接入现有架构
6.1 sqoop 连不上 MySQL:驱动与 JDBC 的细节
大数据场景下,Sqoop 从 MySQL 导数据到 HDFS/Hive 是常见操作。热搜里“sqoop 连接不上 mysql”是一个典型问题群。我遇到过的原因基本就三类:
- 驱动缺失:Sqoop 没有 MySQL JDBC 驱动,报
ClassNotFoundException: com.mysql.jdbc.Driver。解决方法是下载mysql-connector-java-x.x.x.jar放到 Sqoop 的lib目录下。注意 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,老配置里的com.mysql.jdbc.Driver可能在新版驱动里被移除。 - 连接串参数问题:MySQL 8.0 的连接串需要带
useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai。缺少后两个参数,会因为 SSL 握手或时区问题连不上。 - 权限问题:Sqoop 使用的 MySQL 账号只给了查询权限,但 Sqoop 导入时需要读取元数据、可能在目标库建临时表,权限不足会报各种看不懂的错误。
配置驱动时还有个细节:连接串里可以用 jdbc:mysql://host:3306/db?useCursorFetch=true 等方式优化大表导出,避免一次把所有结果拉到内存。
6.2 Navicat、Workbench 与 ER 关系图
图形化工具这块,Navicat 是的口碑很好但收费,MySQL Workbench 官方免费但要忍受它的卡顿。两个工具在“导出 ER 图”这个需求上都支持。
- Navicat:在模型(Model)功能里,通过“导入数据库”可以把现有库的表结构生成 ER 图,还能反查外键关系。
- MySQL Workbench:Database → Reverse Engineer 是经典操作,可以连远程库生成 ER 图,然后导出为图片或 SQL。
如果你要导出的 MySQL 表本身没有外键约束,ER 关系图会显得稀稀拉拉。这不是工具问题,而是设计阶段没建外键。MySQL 里外键用起来有性能代价,很多团队干脆不用,只在业务层维护关联关系。这种情况下生成 ER 图,可能需要手动在建模工具里加逻辑关系。
Workbench 还有一个很实用的功能:New Connection 之后右下角有一个命令行小窗口(Command Line Interface),可以直接执行 SQL。很多人不知道“mysql workbench 如何快速用命令行新建数据表”的答案是:直接在这个命令行里写 CREATE TABLE,或者用工具栏里的“SQL Editor”写脚本。相比图形化建表,命令行快得多,而且方便复制脚本到生产环境。
6.3 Kettle 与 TaoS 数据迁移到 MySQL
Kettle 是 ETL 工具,支持从各种数据源抽取数据。热搜里“kettle 支持 taos 数据库迁移到 mysql”,说明已经有人把 TDengine(时序数据库)往 MySQL 迁了。
Kettle 连 TDengine 用的是 JDBC 驱动,官方提供了一个 taos-jdbcdriver,连接串一般是 jdbc:TAOS://host:6030/dbname。但需要注意:
- TDengine 的 JDBC 驱动版本和时序数据库服务端版本要匹配,否则连接报错或者查询超时。
- 迁移时最容易出问题的字段是时间类型。TDengine 主键一般是
timestamp,MySQL 里如果选datetime,毫秒级别会被截断,要选datetime(3)或timestamp(3)。 - 字符集方面,TDengine 默认 UTF-8,但 MySQL 表如果建成
utf8mb3,存 emoji 会直接报错或变成问号。迁移前先把目标表建成utf8mb4。 - 增量同步可以考虑在 Kettle 里用“根据时间戳过滤”的方式,但 TDengine 的
ts字段是主键,按这个字段做增量会很直观。
Kettle 拉大量数据时,建议在“表输入”步骤里加 LIMIT 分页或用基于主键的增量抽取,避免一次性加载全部数据导致内存溢出。这也是 ETL 的通用经验,不只是针对 TDengine。
关于工具链,还有一个我个人的体会:无论用哪种工具连 MySQL,都先确认默认字符集。Kettle 连接 MySQL 时如果连接串没加 characterEncoding=utf8,数据里出现中文乱码,通常不是工具的问题,而是 JDBC 连接字符串的问题。类似这种“小配置、大影响”的问题,往往比 SQL 本身的 bug 更难排查,遇到数据迁移乱码,先看连接串参数,再查表结构字符集。
回过头来看,我这些年被 MySQL 折腾的每一个坑,几乎都可以总结成一句话:先搞懂机制,再动手操作。装 MySQL 要懂服务、端口、权限;连不上要懂认证插件和连接生命周期;写 SQL 要懂 collation 和隐式转换;写存储过程要懂客户端分隔符;配连接池要懂服务端参数;做迁移要懂字符集和驱动版本。这些细节单看都不难,但组合起来就是别人口中“MySQL 是个坑”的根源。
最后再分享一个小技巧:每当你被一个 MySQL 报错卡住超过半个小时,先退出当前操作,去 SHOW VARIABLES 和错误日志里找线索,而不是反复重试。大部分问题在日志里都有明确答案,只是我们习惯性地凭经验瞎猜。保持这个习惯,能少走很多弯路。
