做了这么多年后端,MySQL几乎是我每天都要打交道的工具。不管新项目搭环境,还是老系统查性能问题,绕来绕去都离不开那几条MySQL命令。这篇稿子不打算写成一本文档手册式的命令大全,而是把我在实际项目里反复用过、踩过坑、最后验证有效的东西串起来讲:从Windows和Docker环境下的安装部署,到日常增删改查、执行SQL脚本,再到存储过程、锁与高并发、故障排查,一条线走下来。无论是刚入行的新人,还是已经在生产环境里摸爬过几年的老手,应该都能在里面找到点有用的东西。
1. 环境准备:从下载安装到成功连上数据库
1.1 Windows下安装MySQL的版本选择与安装细节
先聊版本选择。这几年被问得最多的就是5.7和8.0怎么选。5.7.44是5.7系列的收尾版本,稳定、生态成熟,大量老项目跑在上面,很多生产环境的备份恢复方案、监控工具都是围绕它做的。8.0这边,默认字符集改成了utf8mb4,支持窗口函数和CTE,还引入了数据字典,性能和功能都有明显提升。我的建议很简单:全新项目直接上8.0,接手老项目就跟着现网版本走,别在升级这件事上给自己加戏。
安装方式主要有两种:MSI安装包和ZIP解压版。MSI有图形向导,适合不太熟悉命令行的朋友。我更习惯用ZIP解压版,干净、可控、没有多余的服务项。比如把mysql-8.0.46-winx64解压到D:\tool\mysql-8.0.46-winx64之后,接下来是这几步:
- 在根目录新建my.ini,配好basedir、datadir和端口,路径建议用正斜杠或者双反斜杠。
- 以管理员身份打开CMD,进入bin目录,执行
mysqld --initialize-insecure,这个命令会生成data目录,并创建一个root空密码账号。 - 执行
mysqld -install注册成Windows服务,然后net start mysql启动。
很多人卡在启动这一步,报“服务正在启动...服务无法启动”。绝大多数情况是my.ini配置写错了,其中datadir路径不存在或者没初始化是最常见的。我之前遇到过同事把basedir指到8.0目录、datadir却错写成5.7路径的情况,服务死活起不来。排查的时候用mysqld --console在前台跑一下,错误日志会直接打在控制台,比翻Windows事件查看器直观得多。
顺带说一句卸载。干净卸载MySQL不是删了文件夹就完事,正确顺序是net stop mysql停服务,mysqld -remove移除服务,再删掉data目录和残留的my.ini。Windows上还要留意服务列表里有没有残留的MySQL相关服务,注册表残留会导致重装的时候各种识别异常。
1.2 Docker部署MySQL:一条命令和随后的排障
Linux服务器上部署MySQL,Docker已经是主流方案。一条命令完成部署:
docker run -d --name mysql8 -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=yourpassword \ -e MYSQL_ROOT_HOST=% mysql:8.0这里要特别提醒:-e MYSQL_ROOT_HOST=%很多人会漏掉。不加这个参数的话,默认root账号只允许localhost连接,你用Navicat这类客户端从宿主机连过去,大概率会报Host is not allowed to connect。如果容器已经启动了,也可以进容器补救:
docker exec -it mysql8 mysql -uroot -p然后执行授权语句:
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'yourpassword' WITH GRANT OPTION; FLUSH PRIVILEGES;Docker安装MySQL的另一个常见问题是容器启动几秒就退出。先看日志:
docker logs mysql8我遇到过的两类情况:一是宿主机3306端口被本机MySQL占用,把-p改成3307:3306就好;二是数据卷权限问题,容器内mysql用户没有挂载目录的写权限。解决办法是用chown把目录属主改成1000(mysql用户的UID),或者用命名卷来托管数据。
2. 日常增删改查:高频命令与脚本执行的细节
2.1 连接、库表操作与数据操作的核心命令
连接数据库是每天重复最多的操作。命令行连本地库:
mysql -uroot -p指定远程地址和端口:
mysql -h 192.168.1.10 -P 3306 -uroot -p注意小写-p是密码,大写-P是端口。这个小写和大写的区别,我见过不少新同事搞混,报错后对着命令看了半天才发现问题。
连上之后,库表管理命令基本是固定的:
SHOW DATABASES; USE database_name; SHOW TABLES; DESC table_name; SHOW CREATE TABLE table_name;特别说下SHOW CREATE TABLE,它输出的是完整建表语句,包含索引、约束、字符集设置。我排查表结构差异、给测试环境同步表结构的时候几乎必用,比DESC全貌得多。
数据操作的核心是INSERT、UPDATE、DELETE、SELECT这四类。这里只提一个重要习惯:生产环境执行UPDATE和DELETE,一定先写SELECT确认WHERE条件。少了WHERE条件就是把全表数据改掉,这不是危言耸听,线上事故里这种例子太多了。另外一个习惯是分批操作,比如:
DELETE FROM orders WHERE status = 1 LIMIT 1000;这样分批删可以避免一次删太多造成长事务和锁表时间过长。
2.2 排序、分组与执行SQL脚本
排序是日常需求里出现频率很高的功能。ORDER BY默认升序,降序用DESC:
SELECT * FROM products ORDER BY price DESC;多字段排序时,规则从左往右生效:
SELECT * FROM orders ORDER BY status ASC, create_time DESC;这条表达的是:先按status升序排,status相同的再按create_time降序排。不少人容易把顺序想反,以为是两套排序并行执行,实际上排序条件是分优先级的。
分组统计最常用的是GROUP BY配合聚合函数。比如统计每个分类下的商品数量:
SELECT category_id, COUNT(*) FROM products GROUP BY category_id;这里有个容易踩的坑:MySQL 5.7之后的版本默认开启ONLY_FULL_GROUP_BY,SELECT出来的字段必须是GROUP BY字段或者被聚合函数包裹。有人会为了省事去改sql_mode关掉这个限制,我不建议这么干,宁可把SQL写标准,让查询行为可预期。
日常还有个高频操作是执行SQL脚本。命令行方式:
mysql -uroot -p -e "source /path/to/script.sql"或者直接重定向:
mysql -uroot -p < script.sql在mysql客户端内也可以直接执行:
source /path/to/script.sql执行脚本最常见的报错是编码问题,中文字符变成乱码或者报Incorrect string value。脚本文件本身要存成UTF-8编码(不带BOM),连接时加参数:
mysql -uroot -p --default-character-set=utf8mb4 < script.sql我处理过一个JavaWeb项目的初始化脚本,就因为文件编码是GBK,导入后页面上全是乱码。后来统一转成utf8mb4,问题才彻底解决。所以初始化脚本的编码问题,建议在项目规范里就定死,省得后人重复踩坑。
3. 存储过程与函数:把业务逻辑写进数据库
3.1 存储过程的基本结构与实战案例
存储过程适合封装复杂的、涉及多次SQL交互的业务逻辑,尤其是一些历史项目里需要定时批量处理的场景。先看基本结构:
DELIMITER $$ CREATE PROCEDURE get_category_product_count(IN cat_id INT, OUT cnt INT) BEGIN SELECT COUNT(*) INTO cnt FROM products WHERE category_id = cat_id; END$$ DELIMITER ;DELIMITER的作用很多人不理解。默认SQL语句以分号结尾,而存储过程体内也有分号,如果不临时把分隔符改成其他符号,MySQL在定义过程时就会按第一个分号截断语句,直接报语法错误。这是新手写存储过程最常见的坑。
调用方式:
CALL get_category_product_count(1, @cnt); SELECT @cnt;实战里经常要用到循环和游标。比如批量把超过指定天数的待处理订单置为关闭状态:
DELIMITER $$ CREATE PROCEDURE batch_update_order_status(IN days INT) BEGIN DECLARE done INT DEFAULT 0; DECLARE cur CURSOR FOR SELECT id FROM orders WHERE DATE(create_time) < DATE_SUB(NOW(), INTERVAL days DAY) AND status = 'pending'; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; OPEN cur; read_loop: LOOP FETCH cur INTO @oid; IF done THEN LEAVE read_loop; END IF; UPDATE orders SET status = 'closed' WHERE id = @oid; END LOOP; CLOSE cur; END$$ DELIMITER ;这个例子把游标、条件处理、循环都用上了,逻辑是一行行读取满足条件的订单ID,逐个更新状态。性能不是最优,但逻辑清晰、好调试。注意游标用完一定要CLOSE,否则连接资源释放不及时,时间长了连接池会被拖垮。
3.2 常用函数:日期、字符串与聚合函数
MySQL函数库里,日期函数的使用频率最高。格式化日期用DATE_FORMAT:
SELECT DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s') FROM orders;按天分组统计:
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) FROM orders GROUP BY day;DATE_SUB、DATEDIFF这类函数常用于时间窗口计算。比如统计最近7天的订单:
SELECT COUNT(*) FROM orders WHERE create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY);有个从SQL Server转过来了的朋友可能找DATEPART函数,MySQL里没有同名函数,对应的功能用EXTRACT或者DATE_FORMAT都能实现,比如提取年份用EXTRACT(YEAR FROM create_time)。
字符串函数里,CONCAT、SUBSTRING、REPLACE是高频工具。比如手机号中间四位打码:
SELECT CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) FROM customers;聚合函数COUNT、SUM、AVG、MAX、MIN配合GROUP BY是报表查询的核心。注意COUNT(*)和COUNT(column)的区别:前者统计行数,后者统计该列非NULL值的数量。有时候两个结果对不上,差异往往就是NULL值造成的。
还有一个重要原则:在WHERE条件里对字段套函数会导致索引失效。比如:
WHERE DATE(create_time) = '2025-01-01'这种写法不会走create_time上的索引,应该改成范围写法:
WHERE create_time >= '2025-01-01' AND create_time < '2025-01-02'4. 锁与高并发:MySQL性能调优的必修课
4.1 MySQL锁的分类与隔离级别
锁是并发场景下最容易出问题的地方。MySQL的锁大致分三类:
- 全局锁:
FLUSH TABLES WITH READ LOCK,把整个库变成只读,主要用于全库备份,生产环境要谨慎使用。 - 表级锁:包括表锁和元数据锁(MDL锁)。MDL锁是执行DDL语句时自动加的,如果有个长查询一直不结束,后面的ALTER TABLE就会一直等。
- 行级锁:InnoDB引擎的核心,分共享锁(S锁)和排他锁(X锁)。普通SELECT默认不加锁,但可以手动加,
SELECT ... LOCK IN SHARE MODE加共享锁,SELECT ... FOR UPDATE加排他锁。
行级锁里有个容易被忽略的细节是间隙锁和临键锁。InnoDB在可重复读隔离级别下,为了防止幻读,会对索引记录之间的间隙也加锁。这意味着你在一个范围查询里加锁,哪怕某些记录不存在,范围内的间隙也被锁住了。高并发下,间隙锁很容易引发锁等待甚至死锁。
事务隔离级别分四种:读未提交、读已提交、可重复读、串行化。MySQL默认是可重复读,在这个级别下,同一个事务内多次SELECT结果一致,配合间隙锁解决大部分幻读问题。InnoDB默认级别比Oracle默认的读已提交更严格,这也是不少从Oracle转MySQL的DBA需要适应的点。
查看当前隔离级别:
SELECT @@transaction_isolation;MySQL 5.7里对应的变量是@@tx_isolation,8.0改成了transaction_isolation。
4.2 高并发场景下的调优思路
高并发优化是个系统工程,单靠命令行能做的事有限,但有几条思路值得展开。
第一是慢查询日志。开启了才能定位到执行慢的SQL:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';分析慢日志时重点看三个指标:Rows_examined(扫描行数)、Rows_sent(返回行数)和实际执行时间。Rows_examined远大于Rows_sent,就是典型的索引没走对,SQL需要优化。
第二是索引优化。EXPLAIN是必须掌握的命令:
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 1;重点关注type字段,常见访问类型的效率排序是:
system > const > eq_ref > ref > range > index > ALLALL是全表扫描,最忌讳。key字段表示实际用的索引,如果为空说明没走索引。Extra里出现Using filesort或Using temporary,说明排序或分组过程没用到索引,需要调整索引设计。
第三是连接数管理。查看当前连接数:
SHOW STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';连接数打满会报Too many connections,这时候要区分是应用没有释放连接还是真的并发太大。前者要检查连接池配置,比如HikariCP的最大连接数和等待超时时间;后者再考虑调大max_connections或者做读写分离。
第四是热点行更新的问题。高并发下对同一行频繁UPDATE,锁等待几乎是必然的。一个常见做法是异步化,把更新请求丢进消息队列,由消费者批量合并更新。另一个思路是拆分热点字段,比如把库存拆成多个槽位,分散到不同行,更新时随机选槽位,从根上降低同一行的竞争概率。
另外说下MyBatis项目里的经验:复杂报表查询尽量别硬怼MySQL。我参与过一个JavaWeb项目,核心报表查询关联了七八张表,线上扛不住。后来建了独立的汇总表,由定时任务维护,查询直接走汇总表,性能提升非常明显。
5. 故障排查:从启动失败到锁等待超时
5.1 服务启动失败与无法连接的排查链路
服务启动失败是最让人头疼的问题之一。我的排查链路基本是这样的:
- 先看错误日志。Windows下用
mysqld --console在前台启动,Linux下看/var/log/mysql/error.log。 - 确认目录权限和数据目录完整性。Docker场景下,数据卷权限不足是最常见原因。
- 检查端口和配置文件。3306端口被占用、basedir或datadir路径错误都是高频问题。
- 用
mysqld --validate-config验证配置语法,这是MySQL 8.0提供的能力。
实战中遇到过一个比较隐蔽的问题:服务器内存不够,MySQL启动到一半被系统杀掉了。日志里能看到内存相关的报错信息,解决办法是调低innodb_buffer_pool_size,或者重新评估给容器分配的内存大小。
无法连接的问题分两类。一类是网络层面,mysql -h连接超时,要先ping和telnet目标端口确认连通性;另一类是权限层面,报Access denied for user。权限问题按这个顺序查:用户是否存在、主机授权是否匹配、密码是否正确。
root密码丢失也算常见应急场景。解决办法是在my.ini里加skip-grant-tables,重启服务后免密登录,执行重置密码,改完必须去掉skip-grant-tables再重启。这里要特别强调:skip-grant-tables状态下数据库等于不设防,只能在内网应急时用,处理完第一时间恢复。
5.2 死锁和锁等待的定位方法
锁等待超时常见的报错是Lock wait timeout exceeded,默认超时时间50秒。定位步骤:
第一步,查看当前事务和锁状态:
SHOW ENGINE INNODB STATUS;输出里搜LATEST DETECTED DEADLOCK段落,能看到死锁涉及的事务和SQL语句。
第二步,查information_schema里的锁表:
SELECT * FROM information_schema.INNODB_TRX; SELECT * FROM information_schema.INNODB_LOCKS;第三步,找到长时间未提交的事务,用KILL命令终止:
KILL trx_mysql_thread_id;这里要多说一句:很多锁等待的根源不是锁本身,而是事务迟迟不提交。我碰到过一次线上大量锁等待,查了一圈发现是应用代码里有个事务包含了远程调用,网络超时导致事务挂起十几秒,把一行记录锁得死死的。优化方案是把远程调用移出事务,事务里只保留必要的数据库操作,锁持有时间立刻降了几个数量级。
死锁的处理原则是让InnoDB自动检测并回滚代价较小的事务,应用层做好重试机制。代码里对Deadlock found when trying to get lock这类报错做捕获,等待一小段时间后重新执行事务。
实践里还有个经验:死锁虽然不能完全避免,但通过统一SQL执行顺序能大幅降低概率。比如多个事务都要更新A表和B表,约定大家都按A、B的顺序更新,而不是有的先B后A,形成锁环的概率就会小很多。
最后分享一点个人体会。MySQL命令这东西,最忌讳死记硬背。我见过不少新人把命令大全打印出来贴在工位上,真到排查问题时还是不知道该用哪条。有效的方法是带着场景去记:安装部署踩了坑,把报错和对应的排查命令记下来;线上出现锁等待,把SHOW ENGINE INNODB STATUS的输出研究透。我的习惯是把高频命令和故障处理过程写进自己的技术笔记,每排查完一个就更新一次,半年下来就是一本很实用的排障手册。这篇内容里的每条命令和每个排查思路都是我实际用过的,希望能帮你少走点弯路。