平时我打开图形化数据库客户端的第一件事,基本是快速确认三件事:连接通不通、账号有没有权限、SQL 有没有写错。剩下的查询、改数据、导入导出、建视图建存储过程,本质上都是在这三件事的基础上叠加。很多刚接触 MySQL 的朋友会误以为“装个数据库客户端就能用”,结果卡在连接不上、权限不足、存储中文报错、导入乱码这些地方,来回折腾一整天。这篇文章就把客户端里那些绕不开的 MySQL 常用操作,按实际动手的顺序梳理一遍:从连接参数怎么填,到建表选类型,再到增删改查的安全习惯、视图与存储过程的写法、权限备份和报错排查。不管你是刚学会 select 的新手,还是天天在客户端里点来点去的老手,下面这些内容应该都能找到对你有用的部分。
1. 连接这一步,比你想的更容易卡住
图形化客户端本身不存任何数据,它只是一个把 SQL 语句包装成按钮和表格的外壳,真正干活的永远是 MySQL 服务端。所以“客户端需要哪些 MySQL 内容”这个问题,拆开就是三件事:连得上、有权限、SQL 写得对。三个环节里任何一个出问题,你在界面上看到的就是一个一直在转圈的连接图标,或者一句看不懂的英文报错。我见过太多人在这第一步就把时间耗光,然后开始怀疑软件装坏了、网络有问题、防火墙坏了,其实往往只是某个输入框填错了。
先把底层逻辑讲清楚:客户端连数据库,走的是 MySQL 官方的网络协议,默认端口 3306,握手过程分两步——先建立 TCP 连接,再交换用户名、认证方式、密码哈希。第一步失败是网络层的问题(端口没开、地址不通),第二步失败是账号层的问题(用户名、密码、认证插件、来源主机)。你把这两层分开看,报错信息就不会那么吓人了。
1.1 那五个输入框,到底分别在填什么
新建连接的时候,界面上要你填的东西看着多,其实核心就五个:主机地址、端口、用户名、密码、默认数据库。主机地址填的是 MySQL 服务端所在机器的 IP 或域名,本机就填127.0.0.1(用 IP 比用localhost更稳,因为localhost在部分环境下会走本地 socket 而不是 TCP,排查问题时容易误导)。端口默认 3306,如果服务端改过端口,这里必须跟着改,很多人连接超时就是因为服务端跑在 3307 上而客户端还填着 3306。
用户名和密码是成对出现的,注意 MySQL 的用户标识是“用户名 + 来源主机”的组合,不是单纯的用户名。默认数据库这一栏可以留空,连上之后再选;但如果留空,部分客户端在打开时不会自动展开库结构,需要你手动刷新一次。字符集那一栏如果界面上有,建议直接选utf8mb4,理由后面会专门讲。
提示:连接配置里还有“高级”选项,可以设置连接超时、保持连接(Keep Alive)间隔。做长时间的数据处理任务时,把保持连接打开,能避免操作到一半连接被服务端超时踢掉。
1.2 为什么本机能连、隔壁工位却连不上
这是新手最常见的困惑。同样是用户名密码,自己电脑上连得好好的,同事那边一连接就提示 “Host 'xxx.xxx.xxx.xxx' is not allowed to connect to this MySQL server”。这不是防火墙把包丢了,如果防火墙拦了,报错会是超时或者“连接被拒绝”,而不是这种明确的“主机不被允许”。这句报错来自 MySQL 服务端本身,意思是:这个来源 IP 对应的账号,我在权限表里找不到。
MySQL 的权限表里,每条用户记录都绑定了来源主机。'app'@'localhost'只允许本机连接,'app'@'192.168.1.%'允许某个网段,'app'@'%'允许任意来源。你在本机用localhost登录,匹配的是第一条;同事从别的机器连过来,要找%或他那个具体网段的记录,找不到就直接拒绝。解决办法是补一条带正确 host 的账号,而不是改 root 的 host(那是给自己埋雷)。
另一种情况是服务端只监听了本地回环地址。配置文件里的bind-address如果设成127.0.0.1,服务端根本不会在外部网卡上开端口,这时无论账号怎么加都连不上,表现是整个 TCP 连接都建立不起来。检查方式是在服务端机器上执行netstat -tlnp | grep 3306或ss -lntp | grep 3306,看监听地址是0.0.0.0:3306还是127.0.0.1:3306。改这个参数需要重启服务,属于影响面较大的操作,动手前最好确认没有别的业务在跑。
1.3 账号后面那个 @ 才是权限的关键
接着说权限,因为这是新手最容易忽略、老手最容易偷懒的地方。MySQL 的授权语法是GRANT 权限 ON 库名.表名 TO '用户'@'来源主机',箭头顺序很重要:先库后表,先用户后来源。很多人写授权的时候只写用户名不写主机,客户端帮你在后台补成'用户'@'%',看起来省事,实际把账号开放给所有来源,这在多人共用的开发机上是个不小的隐患。
更细的一个坑:GRANT ALL ON *.*这种写法会把整个实例的权限都给出去,包括建库、删库、管理用户。日常开发给的权限按最小集来就行,比如只读账号给SELECT,业务账号给SELECT, INSERT, UPDATE, DELETE,需要改结构的时候再单独临时提权。这不是洁癖,是真能救命——我见过误操作把测试库整张表删掉然后找不到备份的情况,事后追责的时候发现账号权限给得太大,本来只要读权限的应用居然能执行 DROP。
授权完记得执行一次FLUSH PRIVILEGES。严格来说用GRANT语句授权会自动刷新内存里的权限表,但如果你直接改mysql.user表(不推荐,但在老项目里常见),就必须手动刷一次,否则改完看着生效了,重启之后又变回去。
2. 建表之前先定字符集,这一步偷懒后面全是债
在客户端里右键“新建表”,界面会给你一堆默认值。这些默认值大部分时候能跑,但一旦碰上中文、emoji、多语言混合的内容,或者未来要做数据迁移,当初偷的懒会变成后面几天的加班。我的习惯是:建任何一张表之前,先把字符集和字段类型这两件事想清楚,再动手点界面。
2.1 字符集与排序规则,一次说清楚
MySQL 里的字符集分三层:服务端默认、数据库默认、表和字段级。三层里任何一层配成窄的,最终落到字段上就是窄的。老版本 MySQL 的默认字符集是latin1,后来的版本改成了utf8,但这里的utf8其实是utf8mb3的别名,最多只支持 3 个字节,存不下 emoji 和部分生僻字。你往utf8的字段里写一个四字节字符,就会出现经典的报错:“Incorrect string value: '\xF0\x9F\x98\x80' for column ...”。
正确做法是全程用utf8mb4。建库的时候写CREATE DATABASE db_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;,建表的时候也显式带上,别指望继承。排序规则(collation)决定了字符串怎么比较和排序,结尾带_ci的是大小写不敏感,带_cs或_bin的是敏感。业务上的用户名、邮箱这类字段,如果想严格区分大小写,用utf8mb4_bin;一般的中文内容用utf8mb4_0900_ai_ci(8.0 版本默认)就够了。
| 字符集/排序规则 | 字节上限 | 适用场景 | 注意点 |
|---|---|---|---|
latin1 | 1 字节 | 纯西文老系统 | 中文必乱码 |
utf8/utf8mb3 | 3 字节 | 仅中文、无 emoji | 存 emoji 直接报错 |
utf8mb4+_0900_ai_ci | 4 字节 | 绝大多数新项目 | 8.0 默认,推荐 |
utf8mb4+_bin | 4 字节 | 需要严格区分大小写 | 排序结果与_ci不同 |
有个细节值得单独提:改了表字符集不等于改了已有字段的字符集。ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4会连带转换所有字符型字段,而ALTER TABLE ... DEFAULT CHARACTER SET utf8mb4只改表的默认值,新加字段才生效,老字段原样不动。这两个语句长得很像,效果天差地别,执行前一定要看清楚。
2.2 常用字段类型的选型清单
字段类型选错,往往不是“跑不起来”,而是“跑起来但数据悄悄变形”,这种问题最难查。我按实际使用频率从高到低捋一遍。
整数类型,主键统一用BIGINT UNSIGNED加自增。有人觉得INT够用了,21 亿条上限,一般业务确实撑不到,但真到了要扩容的那天,改主键类型是个几乎不可能在线完成的操作。起点高一点,成本几乎为零。
金额字段,一律DECIMAL,绝不用FLOAT或DOUBLE。浮点数是二进制近似存储,0.1 + 0.2在浮点里不等于0.3,做对账的时候能对出几分钱的差额,找半天找不到原因。DECIMAL(10,2)表示总共 10 位数字、其中 2 位小数,最大能存到 99999999.99,一般订单金额够用了。
时间字段,DATETIME和TIMESTAMP的区别主要在两点:TIMESTAMP会随时区转换,存进去的时候按当前会话时区转成 UTC,取出来再转回去;DATETIME存什么就是什么,不做转换。另外TIMESTAMP的范围只到 2038 年,这是个硬限制。我现在的习惯是:业务时间一律DATETIME,避免跨时区读取时出现莫名其妙的偏差;只有需要自动记录“最后更新时间”的场景才用TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,图它省事。
字符串字段,VARCHAR(n)的 n 是字符数不是字节数,在utf8mb4下VARCHAR(255)最大占 1020 字节。这个 255 不是随便定的,早期 InnoDB 的行格式里,长度前缀 1 字节最多表示 255,超过就变 2 字节,所以 255 成了一个约定俗成的分界线。现在行格式早就升级了,但习惯保留了下来。真正需要注意的是索引:给VARCHAR(255)建索引,在utf8mb4下是 1020 字节,超过了老版本 767 字节的索引前缀限制,会报 “Specified key was too long”。要么缩短字段长度,要么只对前 N 个字符建前缀索引,要么确认行格式是DYNAMIC(8.0 默认,索引前缀上限 3072 字节)。
大文本用TEXT系,但别滥用。TEXT类型的字段不能有默认值,排序时只取前若干字节比较,而且查询时如果SELECT *会把大字段一起拉出来,白白消耗带宽。内容长度不确定但不会特别长的,用VARCHAR(2000)往往比TEXT更好控制。JSON 结构的数据可以用JSON类型,MySQL 会校验格式合法性,还能直接用->>取值,比存成字符串再在应用层解析干净得多。
2.3 索引:客户端里最容易点错的一个地方
在客户端的表设计界面里,点一下“索引”标签就能加,太方便了,方便到很多人给每个字段都加上。索引不是免费的,每个二级索引都是一份额外的B+树,写入时要同步维护,字段更新频繁的表索引越多写入越慢。
真正需要索引的是出现在WHERE、ORDER BY、JOIN ON里的字段,而且要看区分度。性别这种只有两三个值的字段,建了索引优化器也大概率不用,因为回表的成本比全表扫还高。联合索引要遵守最左前缀原则:(a, b, c)这个索引能支持a、a AND b、a AND b AND c的查询,但单独查b或c用不上。所以联合索引的字段顺序不是随便排的,把区分度最高、查询最频繁的放前面。
还有一个容易忽略的点:索引列上做函数运算或隐式类型转换,会导致索引失效。比如字段是VARCHAR类型的手机号,查询时写WHERE phone = 13800000000(不带引号),MySQL 会把字段转成数字再比较,索引直接废掉。这种问题在客户端里看不出来,只有用EXPLAIN看到type变成ALL才会暴露。
3. 增删改查:把客户端用成安全工具而不是危险工具
客户端最大的价值不是让你少打几个字,而是把危险操作挡在一层确认框后面。会用它的人和不会用它的人,差别不在写 SQL 的速度,在于有没有踩过“手一抖全表更新”的坑。
3.1 查询从限制条数开始
在客户端里点“新建查询”,很多人习惯直接写SELECT * FROM 大表然后回车。数据量小的时候没事,几百万行的表能直接把客户端卡死,内存吃满,甚至触发连接超时。我的习惯是任何查询先带LIMIT 100,看清楚返回的字段和数据结构,再决定要不要放开。
深翻页是另一个经典问题。LIMIT 1000000, 20这种写法,MySQL 要先扫描前 100 万行再丢掉,越往后越慢。优化思路是用游标式分页:记住上一页最后一条的 ID,下一页写WHERE id > 上一页最后的ID ORDER BY id LIMIT 20,走主键索引直接定位,速度稳定。代价是不能跳页,只适合“下一页”式的列表。
-- 传统分页,越翻越慢 SELECT id, title, created_at FROM article ORDER BY id LIMIT 1000000, 20; -- 游标分页,速度稳定 SELECT id, title, created_at FROM article WHERE id > 1000000 ORDER BY id LIMIT 20;查询结果的导出也很实用。客户端一般支持把结果集直接导出成 CSV、Excel 或 INSERT 语句,做数据核对的时候非常方便。但要注意导出的编码,默认可能是 UTF-8 带 BOM 或者 GBK,用表格软件打开时如果中文变成乱码,多半是编码没对上,导出时手动选一下就行。
3.2 更新和删除的两条保命习惯
第一条:先 SELECT 再 UPDATE。写UPDATE语句之前,把同样的WHERE条件套到SELECT里跑一遍,确认影响的行数和内容都对,再把SELECT换成UPDATE。这个动作只多花十秒钟,但能挡掉百分之九十的误操作。
第二条:开启客户端的安全更新模式。大部分图形化客户端都有一个“安全更新”选项,开启之后,UPDATE和DELETE语句如果不带WHERE,或者WHERE里没有用到主键或索引,客户端会直接拒绝执行并给出提示。这个机制救过我两次,一次是手滑选中了一行结果然后按了删除键,一次是写WHERE的时候漏了条件。生产库上强烈建议打开。
如果确实需要批量修正数据,用事务包起来,改完先SELECT验证,没问题再COMMIT:
START TRANSACTION; UPDATE account SET status = 0 WHERE last_login_at < '2024-01-01' AND status = 1; -- 先看一眼影响范围对不对 SELECT COUNT(*) FROM account WHERE last_login_at < '2024-01-01' AND status = 0; -- 确认无误再提交,有问题就 ROLLBACK COMMIT;注意:
UPDATE语句本身也可以带LIMIT,比如UPDATE ... LIMIT 1000,在清理历史数据时能避免一次锁太多行导致业务卡顿。分批跑比一口气跑完安全得多。
3.3 导入导出:客户端最值钱的功能
数据迁移、环境同步、给同事一份测试数据,这些场景下客户端的导入导出功能比手写命令快得多。常用的有几种形式:SQL 文件(结构和数据都包含)、CSV/Excel(纯数据)、结构同步(只对比表结构差异)。
导入大文件时最容易踩的坑是文件大小限制和超时。SQL 文件几百 MB 的时候,客户端可能跑到一半断开,原因是服务端的max_allowed_packet设得太小,或者执行超时。前者可以在服务端配置里调到 64M 或更大,后者在客户端的连接设置里把超时时间延长。更稳妥的方式是绕开客户端,直接用命令行工具导入,几 GB 的文件也能稳稳跑完。
CSV 导入要注意三件事:编码统一成 UTF-8、字段分隔符和引号规则对上、日期格式匹配。Excel 另存为 CSV 的时候默认可能是 GBK 编码,导进去中文全是问号,这种问题排查起来最快的方式就是拿文本编辑器打开文件看一眼编码。
结构同步功能值得单独说。开发库改了一堆字段,要同步到测试库,手动一个个改容易漏。客户端的“结构同步”会把两个库的表结构做 diff,生成变更脚本给你预览,确认后再执行。这个功能在多人协作的项目里非常好用,但执行前一定要看脚本,尤其是涉及DROP COLUMN的语句——diff 工具只负责找差异,不负责判断这个字段是不是还有数据在用。
4. 视图、存储过程、事件:让重复动作固化下来
天天写同样的多表 JOIN,不如把它做成视图;每天凌晨要跑的数据汇总,不如做成事件。这些功能在客户端里都有图形化的编辑入口,但真正写起来还是得懂 SQL 语法,界面只是帮你省了拼字符串的功夫。
4.1 视图解决的是“看”的问题
视图本质是一条被保存下来的查询语句,查视图等于查它背后那条 SELECT。适合的场景是把复杂的多表关联、字段重命名、权限过滤封装起来,让调用方只需要SELECT * FROM v_order_detail就能拿到想要的数据。
建视图的语法很简单,CREATE VIEW v_name AS SELECT ...。但有两个坑要注意。一是DEFINER:视图创建时会记录创建者的账号,默认是SECURITY DEFINER,意思是任何人查这个视图,都以创建者的权限执行。这在做数据隔离时是好事(普通账号看不到原始表,只能看视图过滤后的数据),但迁移到别的环境时,如果创建者账号不存在,查视图会直接报错。稳妥的做法是指定一个稳定的账号作为 definer,或者改成SQL SECURITY INVOKER,让权限跟随查询者。
二是性能。视图不会物化数据,每次查询都要重新执行底层 SQL。如果视图里套了视图、还带了聚合和排序,一层层展开之后执行计划可能非常难看。我的经验是视图嵌套不要超过两层,超过就该考虑用物化表或者直接在应用层拼 SQL 了。
4.2 存储过程与函数:DELIMITER 的那些事
存储过程适合封装一批有逻辑顺序的操作,比如“插入订单主表 → 扣减库存 → 写流水”,放在一个过程里,一次调用完成。写存储过程躲不开DELIMITER这个问题:MySQL 默认用分号作为语句结束符,而存储过程内部也有分号,服务端会提前截断。所以要先用DELIMITER $$把结束符临时改成$$,过程写完再用DELIMITER ;改回来。
DELIMITER $$ CREATE PROCEDURE sp_deduct_stock(IN p_goods_id BIGINT, IN p_qty INT, OUT p_result INT) BEGIN DECLARE v_stock INT DEFAULT 0; START TRANSACTION; SELECT stock INTO v_stock FROM goods WHERE id = p_goods_id FOR UPDATE; IF v_stock >= p_qty THEN UPDATE goods SET stock = stock - p_qty WHERE id = p_goods_id; SET p_result = 1; COMMIT; ELSE SET p_result = 0; ROLLBACK; END IF; END$$ DELIMITER ;这里的关键点是SELECT ... FOR UPDATE,它在事务里锁住了这一行,防止并发时两个请求同时读到相同库存、都判断为充足、最后超卖。这类并发问题在单机测试时几乎复现不出来,一上量就出事,所以库存、余额这类字段的扣减逻辑,必须在事务里加行锁。
在客户端里执行存储过程有个小细节:不要用“全选执行”,因为DELIMITER是客户端识别的命令,不是服务端语法,全选执行时客户端可能把它当普通语句发给服务端导致报错。正确的做法是用专门的存储过程编辑器,或者手动选中一段执行。另外调试存储过程时,SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足';可以主动抛出自定义错误,比返回一个错误码再在应用层判断要直观。
4.3 事件调度:让数据库自己按点干活
事件(EVENT)相当于数据库内置的定时任务,按EVERY 1 DAY这种周期执行一段 SQL,适合做数据归档、统计表刷新、过期数据清理。用它的前提是服务端的event_scheduler参数是ON,默认是OFF,需要执行SET GLOBAL event_scheduler = ON;打开,并且写进配置文件才能持久化。
CREATE EVENT IF NOT EXISTS ev_archive_log ON SCHEDULE EVERY 1 DAY STARTS '2024-06-01 03:00:00' DO DELETE FROM operation_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY) LIMIT 10000;注意最后那个LIMIT 10000。一次性删几十万行会长时间锁表,影响线上业务,分批删、多次执行才安全。事件调度虽好,但它运行在数据库进程内,出问题时排查手段有限,而且主从架构下只在主库执行。更复杂的调度需求,交给专门的定时任务系统更合适,数据库只做存储。
5. 权限、备份与性能初筛:上线前必须做的三件事
开发阶段大家关注的都是“功能跑通没有”,到部署上线那一刻,权限、备份、性能这三件事才浮出水面。而这三件事,恰好都能在客户端里完成。
5.1 权限按“够用就行”来给
给应用分配账号的时候,别图省事直接用 root。按职责拆成几个账号:只读账号给报表和查询使用,读写账号给业务逻辑使用,DDL 账号只在发布变更时临时启用。这样即便应用被攻破,或者代码里写了错误的 SQL,影响范围也是可控的。
-- 只读账号 CREATE USER 'app_read'@'10.0.0.%' IDENTIFIED BY '强密码'; GRANT SELECT ON biz_db.* TO 'app_read'@'10.0.0.%'; -- 读写账号,不含 DROP 和 ALTER CREATE USER 'app_rw'@'10.0.0.%' IDENTIFIED BY '强密码'; GRANT SELECT, INSERT, UPDATE, DELETE ON biz_db.* TO 'app_rw'@'10.0.0.%'; FLUSH PRIVILEGES;密码这块,MySQL 8.0 默认的认证插件是caching_sha2_password,比老版本的mysql_native_password安全得多,但一些老版本的客户端或驱动不支持,连接时会报 “Authentication plugin 'caching_sha2_password' cannot be loaded”。解决办法是升级客户端或驱动,而不是把账号改回旧插件——旧插件在后续版本里已经被移除了,改回去等于给未来埋一个必踩的坑。
5.2 备份要能在别的机器上恢复才算数
备份这件事有个残酷的判定标准:没在另一台机器上成功恢复过的备份,等于没有备份。客户端里有“备份”和“还原”的按钮,背后调用的通常是导出工具,配置项不多,但每一项都影响结果。
用命令行导出更可控,参数也透明:
mysqldump -h 127.0.0.1 -u root -p \ --single-transaction \ --routines --triggers --events \ --default-character-set=utf8mb4 \ biz_db > biz_db_20240601.sql--single-transaction保证导出时的一致性快照,不会锁表(只对 InnoDB 有效),这是生产环境在线备份的关键。--routines --triggers --events这三个参数不加的话,存储过程、触发器、事件都不会被导出,恢复完了功能缺失,还找不到原因。--default-character-set=utf8mb4则避免恢复时中文变乱码。
恢复的时候,如果库里表之间有外键约束,导入顺序可能导致失败。可以先执行SET FOREIGN_KEY_CHECKS = 0;关掉检查,导完再打开。这个操作会暂时失去约束保护,所以要在业务停写的时间窗口做,或者导入到独立的临时库再迁移。
5.3 EXPLAIN:客户端里那个免费的体检按钮
任何一条觉得慢的 SQL,前面加个EXPLAIN,就能看到优化器打算怎么执行。重点看几个字段:type表示访问方式,从好到坏大致是const、eq_ref、ref、range、index、ALL,出现ALL基本就是全表扫描;key是实际用到的索引,如果是NULL说明没走索引;rows是预估扫描行数,数字越大越慢;Extra里出现Using filesort或Using temporary说明有额外排序或临时表,通常是ORDER BY或GROUP BY没走索引。
EXPLAIN的rows是估算值,有时候和真实情况差很远,可以用EXPLAIN ANALYZE(8.0.18 之后支持)看实际执行耗时。定位慢查询的源头,还可以打开慢查询日志,设定long_query_time = 1,跑一段时间之后再看日志里哪些语句超时,比凭感觉猜准得多。
6. 报错排查实录:这些错误信息我都见过
6.1 连接类报错速查表
| 报错信息 | 大概率原因 | 处理方式 |
|---|---|---|
Can't connect to MySQL server on ... | 端口不通、服务未启动、监听地址限制 | telnet IP 3306测通,检查bind-address |
Host 'x.x.x.x' is not allowed to connect | 账号的来源主机不匹配 | 补建'user'@'对应网段'账号 |
Access denied for user ... (using password: YES) | 密码错误或权限表未刷新 | 重置密码,检查mysql.user |
Authentication plugin ... cannot be loaded | 客户端/驱动版本太旧 | 升级客户端,别降级认证插件 |
Too many connections | 连接数打满 | 查max_connections与连接池配置 |
The server time zone value 'CST' is unrecognized | 时区参数有歧义 | 连接参数显式指定时区 |
时区那个问题值得展开一句。CST这个缩写在不同语境下可能指中国标准时间,也可能指北美中部时间,驱动解析不了就报错。解决方式是在连接串里明确写serverTimezone=Asia/Shanghai,别用缩写。这类问题在客户端界面里可能表现成“连接成功但查询报错”,容易被误判成别的原因。
6.2 SQL 与数据类报错
Duplicate entry 'xxx' for key 'PRIMARY',主键或唯一索引冲突。批量插入时经常遇到,要么改成INSERT ... ON DUPLICATE KEY UPDATE,要么先查再插。后者在并发下不安全,两个请求同时查到不存在然后同时插入,照样冲突,所以唯一索引才是最后一道防线。
Data too long for column 'xxx',字段长度不够。注意VARCHAR(n)里的 n 是字符数,一个中文字算一个字符,但如果表的字符集是latin1,一个中文会被当多个字节处理,表现就是明明没超长度却报错。检查字符集,八成是这里的问题。
Lock wait timeout exceeded; try restarting transaction,行锁等待超时,默认 50 秒。通常是有个事务开了没提交,一直占着锁。查information_schema.innodb_trx能看当前有哪些活跃事务,找到那个跑了很久的,确认业务影响后 kill 掉。
Cannot delete or update a parent row: a foreign key constraint fails,外键约束阻止了删除。要么先删子表数据,要么设计时就把外键改成逻辑关联。互联网业务里物理外键用得越来越少,主要就是因为它会在分库分表、批量操作时带来额外的耦合。
6.3 导入导出乱码与时区问题
导入 CSV 中文变问号,九成是文件编码和连接编码不一致。三层要统一:文件的物理编码、客户端读取文件时选的编码、数据库表和字段的字符集。排查顺序是从文件开始,用文本编辑器确认文件本身的编码,再确认导入向导里的编码选项,最后确认表结构的字符集。
时间字段导入后整体偏移 8 小时,是时区问题。TIMESTAMP类型会做时区转换,如果导出时是东八区、导入的目标会话是 UTC,数据就会平移。解决办法是导出导入全程显式指定时区,或者干脆把这类字段改成DATETIME,从根上避开转换逻辑。
7. 几个反复踩的坑,和现在我打开客户端的固定动作
7.1 那些年反复踩的坑
第一个坑是在生产库上开着自动提交改数据。客户端默认每条语句自动提交,一个UPDATE敲下去回车,事务就已经落地了,后悔都来不及。我现在的习惯是,只要连的不是本地库,第一件事就是把自动提交关掉,改完手动确认手动提交。
第二个坑是把结构同步当成数据同步。客户端的结构对比只比对表定义,不比对数据。有一次我以为同步过去数据也就跟着过去了,结果目标库是空表,上线前测试才发现。数据同步要用单独的“数据传输”功能,而且要注意它默认可能是覆盖模式,目标表已有数据会被清掉,执行前一定看清楚选项。
第三个坑是给大表加索引没考虑锁。在老版本 MySQL 上给千万级表加索引会锁表,业务直接卡住。现在的版本支持在线 DDL,但仍有前提条件,操作前查一下当前版本的在线 DDL 支持矩阵,或者干脆放到低峰期做。
7.2 我现在打开客户端的固定顺序
连上之后先看一眼当前库和当前账号,避免在错误的库上操作;再确认自动提交是关的;然后跑一个SELECT NOW(), @@character_set_database, @@sql_mode,确认时区、字符集、SQL 模式都符合预期。sql_mode这一项经常被忽略,严格模式和非严格模式下,超长截断、除零、非法日期这些行为完全不同,从别的环境导过来的 SQL 在这个环境报错,很多时候就是sql_mode不一致。
最后分享一个小技巧:把常用的查询、建表语句、授权语句整理成一个自己的 SQL 片段库,客户端一般都支持保存常用语句或者代码片段。看起来很土,但真到了紧急排查的时候,能省下大量翻文档的时间。数据库操作这件事,稳比快重要,每一次点击之前多想一秒“这条语句影响多少行”,比事后找回数据容易得多。