1. 从装到连:MySQL命令的第一道门槛
1.1 命令行到底管什么
先说个现象。很多人装完MySQL之后的第一反应是打开Navicat或者MySQL Workbench,鼠标点一点就能建库建表,看起来很方便。但真到了线上环境排查问题、写自动化脚本、处理大批量数据的时候,你才会发现那些图形工具全都不好使了——服务器上根本没有图形界面,能依赖的只有那一行行的mysql命令。
MySQL命令行的全称是mysql client,它是MySQL官方自带的交互式客户端工具。你通过它发SQL语句给服务端,服务端解析执行后把结果返回给你。听起来简单,但它能管的事远不止查数据这么简单:建库、建表、改字段、授权、备份、恢复、查看性能状态、分析慢查询、调试存储过程,全都能在命令行里完成。
这篇内容不是让你背命令手册。我会把日常使用频率最高、踩坑最多、面试也常问的这些命令串起来讲,配上实际场景和参数说明,让你看完之后能直接拿去用。适合刚入门MySQL的新手,也适合用了一两年但一直在用图形工具、碰到命令行就发怵的同学。
1.2 装好之后必须先做的几件事
不管你是在Windows上装,还是在Linux服务器上装,装完之后第一步都是确认服务有没有起来。Windows上你可以在服务管理器里找MySQL服务,Linux上可以用systemctl status mysql或者service mysql status查看。
确认服务启动之后,用命令行连进去:
bash复制mysql -u root -p
输入密码之后,你会看到mysql>这个提示符,说明已经进入MySQL的命令行交互模式。这时候可以用几条命令验证一下环境:
sql复制SELECT VERSION();
SELECT NOW();
SHOW DATABASES;
如果你的Linux服务器上还没有MySQL,可以用包管理器装。以CentOS/RHEL系列为例,先下载官方yum仓库再安装:
bash复制wget https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm
rpm -ivh mysql80-community-release-el7-3.noarch.rpm
yum install mysql-community-server
Ubuntu/Debian系列则是:
bash复制apt update
apt install mysql-server
装完之后执行mysql_secure_installation,它会引导你设置root密码、删除匿名用户、禁止root远程登录。这一步别跳过,尤其是部署在公网服务器上的MySQL,不锁掉匿名用户和默认账号,等于把数据库裸奔在公网上。另外说一句,Windows上也有不少人用免安装版zip包的方式装MySQL,那种方式需要手动初始化datadir,命令是:
bash复制mysqld --initialize-insecure
执行完会在datadir目录生成初始数据文件,root账号默认密码是空的。初始化之后再通过net start mysql启动服务。这一步经常有人忘,直接用mysql命令去连,结果报ERROR 2003 (HY000): Can't connect to MySQL server,原因就是服务端根本没起来。
注意:MySQL 5.7和8.0的初始化参数略有差异。5.7用mysqld --initialize会生成一个临时密码,临时密码在错误日志里,8.0也一样。如果想用空密码初始化,用--initialize-insecure参数。生产环境不建议空密码,但本地开发图省事可以用。
关于docker部署MySQL,这里也顺便提一句。现在很多人在本地用docker跑MySQL测试环境:
bash复制docker run -d --name mysql-test \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
进容器执行命令:
bash复制docker exec -it mysql-test mysql -u root -p
用docker跑的好处是环境隔离、删除方便,适合开发测试。但生产环境我不太推荐用docker跑MySQL,除非你对容器化存储和网络运维非常有把握,否则数据卷、网络模式、性能调优这些坑会让人怀疑人生。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高频率CRUD命令:别只会SELECT * FROM
2.1 库表管理的基本命令
连接上MySQL之后,第一件事往往是建库。建库命令有几个细节值得注意:
sql复制CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
IF NOT EXISTS是防止重复执行时报错。字符集推荐用utf8mb4而不是utf8,utf8mb4是utf8的超集,能存emoji和更多特殊字符,排序规则也一并指定。很多老项目当初用了utf8,后来在业务里遇到了生僻字或者emoji,数据存不进去,还得改库改表,非常痛苦。
查看当前有哪些库:
sql复制SHOW DATABASES;
切换库:
sql复制USE demo;
建表命令需要注意字段类型的选择。比如用户表:
sql复制CREATE TABLE IF NOT EXISTS `user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户名',
`age` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '年龄',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
id用INT UNSIGNED表示只存非负整数,AUTO_INCREMENT自增主键。username加唯一索引,防止重复注册。created_at和updated_at用DEFAULT CURRENT_TIMESTAMP,这样插入和更新时不需要手动维护时间字段。
查看表结构:
sql复制DESC `user`;
SHOW CREATE TABLE `user`;
第一条是简略结构,第二条是完整的建表语句,后者在排查表结构问题时更常用。修改表结构的命令也时不时用到:
sql复制ALTER TABLE `user` ADD COLUMN `email` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '邮箱' AFTER `age`;
ALTER TABLE `user` DROP COLUMN `email`;
ALTER TABLE `user` MODIFY COLUMN `age` SMALLINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '年龄';
ADD是在表里加字段,DROP是删字段,MODIFY是改字段定义。注意ALTER TABLE操作在大表上会锁表,几百上千万行的表在执行ALTER的时候业务基本就卡住了,这个后面会单独说。
2.2 增删改查的实践细节
查询命令大概是MySQL里用得最多的。先说SELECT的基础语法:
sql复制SELECT id, username, age FROM `user` WHERE age > 18 ORDER BY id DESC LIMIT 10;
这里的执行顺序很多人容易搞混。逻辑上顺序是:FROM找表、WHERE过滤行、SELECT选列、ORDER BY排序、LIMIT限制行数。理解了执行顺序,你就能明白为什么不建议在WHERE条件里对索引列做函数运算,比如WHERE YEAR(created_at) = 2024这种写法,会导致索引失效,全表扫描。正确做法是写成范围条件:WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'。
UPDATE语法有个高频坑:
sql复制UPDATE `user` SET age = 30 WHERE id = 1;
如果漏掉WHERE条件,会把整张表的age全部改成30。这个问题我在生产环境见过不止一次。还好MySQL有个参数叫sql_safe_updates,开启后UPDATE和DELETE必须带WHERE条件或者LIMIT,否则拒绝执行:
sql复制SET sql_safe_updates = 1;
建议大家在开发环境顺手SET一下,防止手滑。
DELETE同理:
sql复制DELETE FROM `user` WHERE id = 1;
如果要清空全表,用TRUNCATE TABLE比DELETE FROM更快,因为TRUNCATE是直接重建表而不是逐行删除,也不记录行级别的binlog。但TRUNCATE不能用在有外键关联的表上,也不能加WHERE条件,属于高风险操作。
还有一个业务上经常遇到的场景:插入数据时如果主键或唯一键冲突,希望不报错而是更新。MySQL提供了ON DUPLICATE KEY UPDATE语法:
sql复制INSERT INTO `user` (id, username, age) VALUES (1, 'zhangsan', 25)
ON DUPLICATE KEY UPDATE age = VALUES(age);
这句的意思是:如果id=1这个主键已经存在,就把age更新为25,否则插入新数据。比先SELECT判断再UPDATE要优雅得多,而且减少了一次网络往返。
2.3 排序、分组和条件过滤别踩坑
排序是最容易出问题的地方。ORDER BY子句支持多字段:
sql复制SELECT username, age FROM `user` ORDER BY age DESC, id ASC;
先按age降序,age相同再按id升序。这个优先级顺序写清楚,不要想当然。
分组查询GROUP BY通常配合聚合函数使用:
sql复制SELECT age, COUNT(*) AS cnt, AVG(age) AS avg_age FROM `user` GROUP BY age HAVING cnt > 1;
注意WHERE和HAVING的区别:WHERE在分组前过滤行,HAVING在分组后过滤组。WHERE不能用聚合函数,HAVING可以。有些同学会写成WHERE COUNT(*) > 1,MySQL会直接报错。
条件过滤方面,IN、BETWEEN、LIKE这些都有使用场景。我特别想提醒的是LIKE的模糊查询:
sql复制SELECT * FROM `user` WHERE username LIKE 'zhang%';
前缀匹配可以走索引,但后缀匹配LIKE '%zhang'会让索引失效。如果业务上确实需要后缀匹配,看看能不能改表结构,用冗余字段存反转字符串,或者直接用全文索引。
3. 用户与权限:root裸奔是最大的隐患
3.1 用户创建与授权
生产环境不建业务账号、全员用root,是我见过最普遍也最危险的操作。业务代码连接数据库应该用独立账号,权限只给到对应库,不要给全局权限。
创建用户的命令:
sql复制CREATE USER 'app_user'@'192.168.%' IDENTIFIED BY 'StrongPassword123!';
这里'192.168.%'表示只允许192.168网段的主机连接。%表示所有主机,本地就写'localhost'。收紧host范围能大幅降低被外部爆破的风险。
授权命令:
sql复制GRANT SELECT, INSERT, UPDATE, DELETE ON demo.* TO 'app_user'@'192.168.%';
这条只给了demo库的增删改查权限,没有DDL权限。如果需要建表、改表结构的权限,再加ALTER、CREATE、DROP、INDEX。线上环境建议分权:应用账号只有DML权限,DBA账号才有DDL权限。
授予所有权限的命令:
sql复制GRANT ALL PRIVILEGES ON demo.* TO 'app_user'@'192.168.%';
这里不推荐直接写GRANT ALL ON .,除非这个账号真的是超级管理员。*.*代表着所有库所有表,风险极大。
注意:MySQL 8.0里CREATE USER和GRANT是分开的,8.0之前GRANT语句可以隐式创建用户。如果你在8.0里执行GRANT ALL ON xxx TO 'user'@'host',而该用户还不存在,报错信息是ERROR 1410 (42000): You are not allowed to create a user with GRANT。需要先CREATE USER再GRANT。
修改密码的命令,8.0的写法:
sql复制ALTER USER 'app_user'@'192.168.%' IDENTIFIED BY 'NewPassword123!';
5.7及以前常用:
sql复制SET PASSWORD FOR 'app_user'@'192.168.%' = PASSWORD('NewPassword123!');
设置完权限或密码后,如果是用了GRANT动态修改权限,需要执行FLUSH PRIVILEGES让权限表生效。有些教程说每改一次就要FLUSH一次,其实CREATE USER和GRANT语句会自动刷新权限,只有在直接修改mysql.user表时才需要手动FLUSH。
3.2 权限回收与密码安全
回收权限和删除用户是运维中避免不了的操作:
sql复制REVOKE DELETE ON demo.* FROM 'app_user'@'192.168.%';
DROP USER 'app_user'@'192.168.%';
回收权限的语法和GRANT几乎对称。员工离职或者项目下线,账号一定要及时删除,这种不留死账号的习惯能避免很多历史遗留风险。
查看一个用户有哪些权限:
sql复制SHOW GRANTS FOR 'app_user'@'192.168.%';
我在排查权限问题时经常用这条命令,确认到底是哪个权限缺失导致应用报错,而不是猜。
MySQL 8.0默认的认证插件是caching_sha2_password,安全性比5.7时代的mysql_native_password高很多,但兼容性差。老客户端、老驱动如果连不上,报错信息通常是Authentication plugin 'caching_sha2_password' cannot be loaded这类。解决办法有两个方向:升级客户端驱动到支持caching_sha2_password的版本;或者把用户的认证插件改回mysql_native_password:
sql复制ALTER USER 'app_user'@'192.168.%' IDENTIFIED WITH mysql_native_password BY 'NewPassword123!';
如果你在用Delphi/Firedac这类老驱动连接MySQL 8.0,大概率会遇到firedac phys mysql client does not support authentication protocol requested这个报错,处理办法同样是改认证插件。但这里我建议优先升级驱动,因为mysql_native_password在8.0里已经标记为废弃,长期依赖不是正路。
4. 索引、慢查询与状态体检:性能优化三板斧
4.1 索引命令与使用原则
索引用得对不对,直接决定SQL跑得快不快。建索引的命令很简单:
sql复制CREATE INDEX idx_username ON `user` (username);
建唯一索引:
sql复制CREATE UNIQUE INDEX uk_username ON `user` (username);
建联合索引:
sql复制CREATE INDEX idx_age_username ON `user` (age, username);
联合索引的字段顺序非常讲究,它遵循最左前缀原则。比如idx_age_username这个索引,相当于建了(age)、(age, username)两个索引的合体。如果查询条件是WHERE username = 'xx',这个联合索引是用不上的,因为在联合索引里username不是最左字段。
查看表上已有的索引:
sql复制SHOW INDEX FROM `user`;
删除索引:
sql复制DROP INDEX idx_username ON `user`;
索引的字段选择上有几个核心原则:区分度高的字段优先、WHERE和ORDER BY后面的字段考虑建索引、不要对频繁更新的列建索引、索引不是越多越好。每个索引都会拖慢写入性能,因为每次INSERT/UPDATE都要同步维护索引树。一张表上七八个索引,写入性能下个台阶是必然的。
4.2 EXPLAIN命令:看清SQL到底怎么跑
EXPLAIN是MySQL优化SQL的第一工具,用法是在SELECT前面加EXPLAIN:
sql复制EXPLAIN SELECT * FROM `user` WHERE username = 'zhangsan';
输出结果里最关键的是type列和rows列。type表示访问类型,从好到差依次是system、const、eq_ref、ref、range、index、ALL。ALL就是全表扫描,基本可以认定为烂SQL。range表示范围扫描,常见于WHERE后面跟了>、<、BETWEEN这类条件。ref和eq_ref表示用了二级索引或主键索引做等值匹配,性能很好。
还有一个容易忽略的key列,它告诉你这次查询实际用了哪个索引。如果key是NULL,说明没有走索引。Extra列里的Using filesort表示文件排序,出现这个通常意味着ORDER BY字段没有索引可用,MySQL临时起了一个排序操作。数据量小无所谓,数据量一上来就会变成性能瓶颈。
4.3 查看连接数和运行状态
生产环境排查数据库有没有出问题,第一件事是看连接数:
sql复制SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Max_used_connections';
Threads_connected是当前连接数,Max_used_connections是历史最大连接数。如果当前连接数快逼近max_connections上限,说明应用连接池配置有问题或者有慢SQL把连接占住了。
查看max_connections配置:
sql复制SHOW VARIABLES LIKE 'max_connections';
修改配置:
sql复制SET GLOBAL max_connections = 500;
这个SET GLOBAL是临时生效,重启后失效。要永久生效得改my.cnf配置文件里的[mysqld]段。
正在运行的SQL,用SHOW PROCESSLIST查看:
sql复制SHOW PROCESSLIST;
这条命令能看到每个连接当前正在执行什么语句、执行了多久。如果发现某个查询的Time字段已经几十秒甚至几分钟,大概率就是慢SQL。可以记下它的Id,然后手动终止:
sql复制KILL 12345;
这里的12345是SHOW PROCESSLIST结果里第一列的Id。KILL连接这个操作要慎用,确保它执行的是垃圾SQL而不是重要事务,否则可能导致事务回滚,应用报错。
慢查询日志定位问题SQL:
sql复制SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
long_query_time设为1表示超过1秒的SQL记录到慢查询日志。生产环境这个阈值一般设0.5到2秒,看业务对延迟的容忍度。慢查询日志文件可以用mysqldumpslow工具分析,也可以用pt-query-digest这类第三方工具。不过现在很多云数据库自带慢查询分析工具,直接用云平台的就好。
5. 备份与恢复:不会mysqldump不敢睡踏实
5.1 逻辑备份与恢复
生产环境MySQL备份这件事,我强调多少遍都不为过。数据没了,什么都没了。最简单的备份方式是mysqldump逻辑备份:
bash复制mysqldump -u root -p --single-transaction --master-data=2 demo > demo_backup.sql
--single-transaction参数对InnoDB引擎非常重要,它通过一个一致性快照实现备份过程中不影响线上读写。--master-data=2会在备份文件里记录binlog位置信息,用于搭建从库或者做数据恢复时对齐点位。如果不加这两个参数,备份大表时可能锁表,或者恢复时找不到binlog位置。
恢复方式:
bash复制mysql -u root -p demo < demo_backup.sql
注意,备份的时候如果用了--databases参数,比如:
bash复制mysqldump -u root -p --databases demo > demo_backup.sql
备份文件里会包含CREATE DATABASE和USE语句,恢复时不需要手动建库,目标实例上如果不存在demo库会自动创建。如果没加--databases,备份文件里只有建表和数据语句,恢复前必须手动建库并USE进去。
5.2 binlog日志:更细粒度的恢复手段
mysqldump解决的是定期全量备份,两次全量备份之间如果出了事故,就需要binlog来补增量。binlog是MySQL的二进制日志,记录了对数据有修改的所有操作。
确认binlog是否开启:
sql复制SHOW VARIABLES LIKE 'log_bin';
如果返回OFF,说明没开启,在my.cnf配置里加上:
ini复制[mysqld]
log-bin=mysql-bin
server-id=1
注意MySQL 8.0默认是开启binlog的,5.7默认不开启。
查看binlog列表:
sql复制SHOW BINARY LOGS;
查看当前正在写的binlog:
sql复制SHOW MASTER STATUS;
查看binlog内容可以用mysqlbinlog工具:
bash复制mysqlbinlog --no-defaults /var/lib/mysql/mysql-bin.000001
如果只想看某个时间段的操作,可以加--start-datetime和--stop-datetime参数,或者用--start-position和--stop-position指定点位。恢复用法:
bash复制mysqlbinlog --stop-datetime="2024-06-01 10:00:00" /var/lib/mysql/mysql-bin.000001 | mysql -u root -p
这里有个经验:误删数据后,第一件事是把binlog完整备份一份再操作,防止恢复过程中写入新的binlog覆盖现场。第二件事是找到误删语句的具体位点,用位点恢复而不是时间恢复,因为时间恢复可能把其他正常操作也过滤掉。
5.3 物理备份方案
mysqldump在数据量上T之后效率会很差,备份和恢复要花大量时间。数据量大的场景,一般用物理备份。MySQL企业版自带的MySQL Enterprise Backup是商业方案,社区版一般用Percona XtraBackup:
bash复制xtrabackup --backup --target-dir=/data/backup/20240601 --user=root --password=xxx
xtrabackup --prepare --target-dir=/data/backup/20240601
--backup是执行备份,--prepare是把备份文件恢复到一致状态。之后把备份目录整体拷贝到新机器,用xtrabackup --copy-back恢复。物理备份的恢复速度远快于逻辑备份,因为不需要逐条执行INSERT语句。
不过XtraBackup的版本一定要和MySQL严格对应,8.0的MySQL要用8.0版本的XtraBackup,否则备份会失败。这个兼容性问题非常让人头疼,我踩过好几次坑。
6. 存储过程、视图与事件:把逻辑写进数据库
6.1 存储过程的基本框架
存储过程是预编译的SQL语句集合,在MySQL里通过CALL命令调用。它的价值在于把复杂业务逻辑封装在数据库层,减少应用和数据库之间的网络交互。适合做批量数据处理、定时任务、报表计算。
基础结构:
sql复制DELIMITER //
CREATE PROCEDURE `sp_demo` (IN `p_age` INT, OUT `p_count` INT)
BEGIN
SELECT COUNT(*) INTO p_count FROM `user` WHERE age > p_age;
END //
DELIMITER ;
DELIMITER //的作用是临时把语句分隔符改成//,因为存储过程体内有多条SQL语句,各自以分号结尾,如果不改分隔符,MySQL会在第一个分号处就结束整个语句,导致CREATE PROCEDURE报错。执行完把分隔符改回分号。
调用:
sql复制CALL sp_demo(18, @cnt);
SELECT @cnt;
查看已存在的存储过程:
sql复制SHOW PROCEDURE STATUS WHERE db = 'demo';
删除:
sql复制DROP PROCEDURE IF EXISTS sp_demo;
存储过程里常用的条件判断和循环:
sql复制DELIMITER //
CREATE PROCEDURE `sp_batch_update` ()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE v_id INT;
DECLARE cur CURSOR FOR SELECT id FROM `user` WHERE age = 0;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
REPEAT
FETCH cur INTO v_id;
IF NOT done THEN
UPDATE `user` SET age = 20 WHERE id = v_id;
END IF;
UNTIL done END REPEAT;
CLOSE cur;
END //
DELIMITER ;
这段是典型的游标循环处理。DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1是游标遍历结束的哨兵,不加这个会死循环。存储过程调试起来比较痛苦,因为它不像应用代码那样可以打断点,建议复杂的存储过程里加日志表,每一步写一条记录,出了错好定位。
6.2 视图与事件调度器
视图是虚拟表,本质是一条保存的SQL查询:
sql复制CREATE VIEW `v_user_over_18` AS
SELECT id, username, age FROM `user` WHERE age > 18;
查询视图和查普通表一样:
sql复制SELECT * FROM `v_user_over_18`;
视图适合做权限控制,只暴露需要的字段给应用。但注意不要过度使用视图,尤其不要在视图上再套视图,层层嵌套会让MySQL优化器很吃力,执行计划的type经常变成ALL。
事件调度器相当于MySQL内置的定时任务:
sql复制SET GLOBAL event_scheduler = ON;
CREATE EVENT `ev_clean_expired_data`
ON SCHEDULE EVERY 1 DAY STARTS '2024-06-01 02:00:00'
DO
DELETE FROM `user` WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);
这个事件每天凌晨两点执行一次,清理一年前的数据。查看事件:
sql复制SHOW EVENTS;
禁用或启用事件:
sql复制ALTER EVENT `ev_clean_expired_data` DISABLE;
ALTER EVENT `ev_clean_expired_data` ENABLE;
利用事件做定时清理、定期归档是很常见的运维手段。但要注意ON SCHEDULE的时区问题,MySQL事件调度器默认使用系统时区,如果服务器时区设置不对,定时任务的执行时间和预期会差好几个小时。
7. 线上踩坑记录:几个高频报错的解决实录
7.1 认证协议导致的连接失败
MySQL 8.0默认认证插件是caching_sha2_password,这就导致一个非常经典的报错:
text复制Authentication plugin 'caching_sha2_password' cannot be loaded
或者在某些老客户端里直接提示:
text复制The server requested authentication method unknown to the client
我排查过很多次这类问题,根本原因就是客户端驱动版本太老,不认识新的认证插件。解决办法是修改用户的认证方式:
sql复制ALTER USER 'your_user'@'%' IDENTIFIED WITH mysql_native_password BY 'your_password';
FLUSH PRIVILEGES;
改完之后,老客户端重新连接就能通了。不过mysql_native_password在MySQL 8.0里已经是废弃状态,8.4版本里默认不启用。所以这不是长久之计,能升级驱动还是升级驱动。
7.2 唯一索引重复数据的处理
业务上线跑了一段时间之后,发现某个字段需要加唯一约束,但表里已经有重复数据了。直接加唯一索引会报错:
text复制ERROR 1062 (23000): Duplicate entry 'xxx' for key 'uk_xxx'
处理流程分三步。第一步先找出重复数据:
sql复制SELECT username, COUNT(*) FROM `user` GROUP BY username HAVING COUNT(*) > 1;
第二步处理重复数据,根据业务逻辑决定保留哪条。一般保留id最小的那条:
sql复制DELETE u1 FROM `user` u1
INNER JOIN `user` u2
WHERE u1.username = u2.username AND u1.id > u2.id;
这条语句用自连接删掉了所有同username下id更大的行,只保留每组里id最小的。
第三步再添加唯一索引:
sql复制ALTER TABLE `user` ADD UNIQUE INDEX uk_username (`username`);
注意:执行删除语句前一定要备份数据。重复数据的清理逻辑一旦写错,会把正常数据一起删掉。建议先把DELETE改成SELECT验证一遍结果集。
7.3 安装MySQL启动服务报错
Windows上安装MySQL时,启动服务报错是高频问题。根据我见过的案例,绝大多数是以下几种原因。
一是my.ini配置文件路径有问题。MySQL在初始化时如果指定的basedir或datadir不存在,服务启动会失败。检查一下my.ini里的路径是不是绝对路径,目录是否已创建。
二是datadir目录没有初始化。如果你改过datadir配置但没执行初始化命令,服务也起不来。解决办法:
bash复制mysqld --initialize-insecure --user=mysql
执行后再启动服务。
三是3306端口被占用。排查命令:
bash复制netstat -ano | findstr 3306
如果端口被占用,要么杀掉占用进程,要么在my.ini里改端口:
ini复制[mysqld]
port=3307
四是权限问题。Linux上如果datadir目录的属主不是mysql用户,同样启动失败。用chown改属主:
bash复制chown -R mysql:mysql /var/lib/mysql
排查启动失败问题,最重要的手段是看错误日志。Linux上通常在/var/log/mysql/error.log,Windows上在datadir目录下的err文件。日志里会明确告诉你失败原因,比网上搜一万个假设都管用。
7.4 连接数打满与Too many connections
应用突然报Too many connections,这个问题的根源是并发连接数超过了max_connections。先看当前状态:
sql复制SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';
如果Threads_connected已经接近上限,说明连接数异常增加。这时候不能只想着调大max_connections,得先搞清楚为什么会有这么多连接。
经验上有三个排查方向:第一,应用代码里有没有连接泄漏,连接池是否正常释放连接;第二,有没有慢SQL把连接长时间占住,跑到超时才释放;第三,有没有其他应用误连到了这个实例。
调大max_connections只是治标:
sql复制SET GLOBAL max_connections = 1000;
治本还是要从应用侧优化连接池配置、消灭慢SQL。另外MySQL连接超时参数wait_timeout和interactive_timeout可以设置短一点,比如60秒,让空闲连接尽快被回收,释放资源。
8. 我踩过坑之后的几点体会
最后聊点实际的体会。
我在前几年刚接触数据库的时候,也喜欢打开图形工具点点点,觉得命令行这东西是老古董才用的。直到有次线上数据库负载飙升,我对着图形界面完全无从下手,被逼着SSH到服务器上敲命令,才发现命令行才是数据库运维和排查问题的核心工具。图形工具能看到的东西,命令行全能看到,而且命令行能做的事远比图形工具多——自动化、批量处理、定时任务,样样离不开它。
几个小建议送给正在看这篇内容的朋友。
第一,别急着背命令,把常用的十几条命令练熟就够用了。MySQL命令少说几百条,但90%的场景就那么几十条。建库建表、增删改查、授权、备份、看状态,这些练熟,日常够用。碰到生僻命令,用HELP命令查:
sql复制HELP SELECT;
或者查官方文档,一查就有。
第二,命令行的输出比图形界面更准确。Workbench或者Navicat很多时候会把错误信息包装成更友好的提示,反而丢失了原始报错细节。命令行报错虽然看着丑,但信息最全。排查问题时,优先看命令行报错信息,再结合错误日志定位。
第三,凡是涉及删除、修改的语句,先跑SELECT验证结果。一个UPDATE忘了WHERE条件,一个DELETE删错了行,都是能让人掉头发的灾难。在执行危险操作之前,先把语句包一层事务:
sql复制START TRANSACTION;
UPDATE `user` SET age = 30 WHERE unit_price > 100;
-- 检查结果
SELECT * FROM `user` WHERE age = 30;
ROLLBACK;
确认无误后再COMMIT,这套动作养成习惯,能救你无数次。
第四,多练多写。命令行的熟练度只能靠实操堆出来。你可以在本地用docker起一个MySQL,把所有命令都过一遍;也可以在公司测试库里多试试EXPLAIN和慢查询分析。真到线上出问题的时候,你的反应速度和操作准确性不会骗你。
