从第一次在服务器上敲mysql -u root -p手心冒汗,到现在带新人时让他们先背熟几十条命令,我对“命令大全”这四个字的理解一直在变。刚入行那会儿,我把命令当成字典查,遇到一个场景翻一条;做久之后才发现,真正值钱的不是记住多少条,而是理解每条命令背后的执行逻辑、适用边界,以及那些只有踩过坑才知道的细节。这篇内容我会把日常从连接、建库建表、增删改查,到索引、事务、存储过程、备份恢复、巡检排错的命令,按使用频率和重要度重新梳理一遍,该给参数的地方给参数,该讲原理的地方讲原理。它适合刚接触 MySQL 的新手当作实操手册,也适合做了一两年但命令体系比较零散的朋友拿来补全认知。文中涉及 MySQL 架构、安装后的初始密码处理、索引与排序、JOIN 语义、更新子查询等高频关注点,我都会结合真实场景讲清楚。
1. 连接、账号与权限:先把入口这一关管住
命令行的第一步永远是怎么连上去。很多人装完 MySQL 之后,用图形化客户端连得很顺,一到纯命令行环境就卡壳,问题往往出在对连接参数和认证方式不够熟。这一节把入口相关的命令讲透,后面所有操作都建立在能稳定连上去的前提上。
1.1 连接命令的几种姿势与选择依据
最基础的一条命令长这样:
mysql -h 127.0.0.1 -P 3306 -u root -p-h是主机地址,-P(大写)是端口,-u是用户名,-p(小写)表示密码在回车后交互输入,不写在命令里。这个大小写区分是新手最容易搞混的地方:小写-p是密码,大写-P是端口,敲错了会得到“Unknown MySQL server host”之类的提示,实际上根本不是主机的问题。
为什么不建议把密码直接跟在-p后面写成-proot123?因为这样密码会进入 shell 的 history 记录,history一敲就暴露了,而且在多用户机器上通过ps aux有可能被同机其他用户看到命令行参数。我见过太多测试环境因为这一条被拖库的案例,生产环境务必用交互输入,或者用配置文件。
如果你本地有个固定的测试库,每次敲一长串参数很烦,可以写一个客户端配置文件。在用户目录下建~/.my.cnf:
[client] host=127.0.0.1 port=3306 user=root password=你的密码 default-character-set=utf8mb4然后直接mysql就能连上。需要注意的是这个文件权限要设成600,否则 MySQL 客户端会报“World-writable config file is ignored”并拒绝读取,这是它保护你的方式。chmod 600 ~/.my.cnf记得加上。
还有一个高频场景是远程连接。如果连不上,先别急着改配置,按顺序排查:网络是否通(ping主机)、端口是否开(用telnet ip 端口或nc -zv测试)、账号是否允许远程来源(这个在下一节讲)、防火墙是否放行。MySQL 默认只监听127.0.0.1,想接受远程连接要改bind-address,这个改动属于安全敏感项,改动前务必确认是不是真的需要对外暴露。
提示:连接时如果报
Access denied for user,九成是密码或主机来源不匹配,不是网络问题。而报Can't connect to MySQL server,才是网络、端口、服务没起来这类问题。两类报错对应两套排查方向,分清楚能省很多时间。
进入命令行后,还有几个“元命令”值得记住。这些命令不以分号结尾,是客户端层面的指令,不是 SQL:
status或\s:查看当前连接的版本、字符集、端口等信息show databases;:列出所有库use 库名;:切换当前库source /path/file.sql:执行一个 SQL 脚本文件,做数据导入时特别常用exit或quit或\q:退出\G:把查询结果竖着显示,字段特别多的宽表用它可以避免换行乱成一团
\G这个技巧我要多提一句。当你查一张有几十个字段的表时,默认表格输出会挤成一团根本看不清,改成select * from 表名\G(注意末尾不要分号),每一行会按“字段名: 值”的格式纵向排列,可读性直接翻倍。
1.2 用户管理命令与权限模型
MySQL 的权限是“用户@主机”的二维模型,这一点极其关键。同样的用户名appuser,appuser@localhost和appuser@%是两个完全不同的账号,权限互不影响。
创建用户的命令现在推荐这种写法:
CREATE USER 'appuser'@'%' IDENTIFIED BY '强密码';老写法GRANT ... IDENTIFIED BY在 8.0 之后已经被移除,必须先用CREATE USER建账号,再单独授权。这是 8.0 升级时最常见的兼容性问题之一。
授权:
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'appuser'@'%'; FLUSH PRIVILEGES;这里shop.*表示只给shop这个库下所有表的增删改查权限,不给 DROP、不给 GRANT。最小权限原则在实际运维里不是口号,应用账号绝不能给ALL PRIVILEGES,更不能给*.*。我处理过一次线上误删事故,根源就是某个应用连的是 root 账号,一个拼错的 DELETE 语句直接清掉了一张大表。哪怕图省事,也应给应用单独建账号。
FLUSH PRIVILEGES这个命令经常被滥用。实际上直接用GRANT、REVOKE、CREATE USER这类语句修改权限时,MySQL 会自动刷新内存中的权限表,不需要手动执行。只有当你直接用INSERT、UPDATE去改mysql.user等系统表时,才需要FLUSH PRIVILEGES让改动生效。日常用授权语句的情况下,这条命令可以省掉。
查看权限:
SHOW GRANTS FOR 'appuser'@'%';回收权限:
REVOKE DELETE ON shop.* FROM 'appuser'@'%';修改密码(8.0 语法):
ALTER USER 'appuser'@'%' IDENTIFIED BY '新密码';删除用户:
DROP USER 'appuser'@'%';这里有个坑:%代表任意主机,但它不匹配localhost。在 MySQL 的匹配逻辑里,localhost走的是 socket 连接,用的是另一套匹配规则。所以经常出现“用%建了账号却在本地连不上”的情况,解决办法是额外建一个'appuser'@'localhost',或者把应用配置里的localhost换成127.0.0.1,强制走 TCP。
1.3 初始密码处理与安全加固命令
很多人安装完 MySQL 8.0,第一件事就是问“初始密码是什么”。如果是通过包管理器安装且开启过临时密码,它通常在错误日志里,可以用:
grep 'temporary password' /var/log/mysqld.log找到后用mysql -u root -p登录,系统会强制你先改密码,不改任何命令都执行不了。这是 8.0 的强制策略,不是 bug。
改完密码之后,建议顺手把几个安全项过一遍。查看密码策略:
SHOW VARIABLES LIKE 'validate_password%';如果策略太严导致测试环境设置简单密码失败,可以调整:
SET GLOBAL validate_password.policy = LOW; SET GLOBAL validate_password.length = 6;注意这条命令是全局、临时的,重启后失效。想永久生效要写进配置文件。生产环境我强烈建议保持MEDIUM以上,测试环境才放宽。
查看当前认证插件:
SELECT user, host, plugin FROM mysql.user;8.0 默认用caching_sha2_password,老版本的一些客户端连不上就是这个原因,临时把某个账号改成mysql_native_password可以应急,但长期看应该升级客户端而不是降级认证方式。
2. 库表结构命令:设计阶段就决定后面好不好过
结构命令是那种“平时用得不多,一出错就要命”的类型。改表在生产环境是有风险的,所以每条 DDL 命令我都建议先搞清楚它会不会锁表、锁多久。这一节把库表操作命令和索引命令一起讲,因为它们经常在同一个优化场景里出现。
2.1 库级操作命令清单
查看所有库:
SHOW DATABASES;只看自己关心的,可以用模糊匹配:
SHOW DATABASES LIKE 'shop%';建库时明确字符集和排序规则,不要依赖默认值:
CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;为什么强调utf8mb4?因为 MySQL 里有个历史遗留的坑:老版本所谓的utf8实际最多只存 3 字节,存不了 emoji 和一些生僻字,真正的完整 UTF-8 是utf8mb4。这个坑在涉及用户昵称、评论内容的业务里必然触发。所以从建库开始就用utf8mb4,别等到线上报“Incorrect string value”才回头改。
排序规则_ci表示大小写不敏感(case insensitive),_bin表示按二进制比较,大小写敏感。一般业务用_ci,需要精确区分的字段(比如激活码、token)再单独指定_bin。
查看建库语句:
SHOW CREATE DATABASE shop;这个命令的价值在于,当你需要在新环境复制一个库的结构时,它输出的就是可以直接执行的完整语句,比手写靠谱。
删库这件事我不想多说,只留一句:DROP DATABASE shop;没有确认提示,执行即生效。养成习惯,删之前先SHOW TABLES;看一眼,确认库名没敲错。
2.2 建表与改表命令详解
建表命令的核心不是把字段列出来,而是把类型、长度、约束、默认值一次想清楚:
CREATE TABLE `order` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` BIGINT UNSIGNED NOT NULL, `amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `status` TINYINT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status_created` (`status`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;几个细节值得展开。金额字段用DECIMAL而不是FLOAT,浮点数在金融场景会有精度误差,这是硬性要求。status设为NOT NULL DEFAULT 0,避免大量 NULL 值干扰索引和统计。created_at和updated_at用自动时间戳,能省掉应用层每次手动赋值的麻烦。
“mysql 设置默认值为 0”这个需求很常见,写法就是DEFAULT 0。但要注意,NOT NULL和DEFAULT是两回事:NOT NULL约束不能插 NULL,DEFAULT是没给值时用啥。只有DEFAULT没有NOT NULL时,显式插入 NULL 依然会写入 NULL。
改表命令ALTER TABLE在生产环境要格外小心:
-- 加字段,指定位置和不指定位置效果不同 ALTER TABLE `order` ADD COLUMN `remark` VARCHAR(255) DEFAULT NULL; -- 改字段类型(可能重建表,大表风险高) ALTER TABLE `order` MODIFY COLUMN `remark` VARCHAR(500) DEFAULT NULL; -- 改字段名 ALTER TABLE `order` CHANGE COLUMN `remark` `note` VARCHAR(255) DEFAULT NULL; -- 加索引 ALTER TABLE `order` ADD INDEX `idx_created` (`created_at`); -- 删索引 ALTER TABLE `order` DROP INDEX `idx_created`;MODIFY和CHANGE的区别记牢:MODIFY只改类型和属性,不改名字;CHANGE既能改名也能改类型,但必须把新旧名字都写上。很多人第一次用CHANGE少写了新名字,直接语法报错。
线上大表加字段、加索引,直接ALTER可能造成长时间锁表甚至拖垮业务。常规做法是用在线 DDL 工具(比如 pt-online-schema-change 或 gh-ost),或者利用 MySQL 8.0 对部分ALGORITHM=INPLACE操作的支持。执行前先用SHOW CREATE TABLE看清表大小和结构,评估影响。
查看表结构有三条命令,各有用处:
DESC `order`; -- 简洁,字段、类型、是否可空、键、默认值 SHOW CREATE TABLE `order`; -- 完整,含引擎、字符集、索引定义 SHOW FULL COLUMNS FROM `order`; -- 带注释,看字段注释最方便2.3 索引命令与创建时机的判断
“mysql 创建索引”是搜索量极高的词,但真正难的不是语法,而是判断该不该建。语法先给全:
-- 建表时建 KEY `idx_name` (`name`) -- 表建好后加 ALTER TABLE `user` ADD INDEX `idx_name` (`name`); -- 或者 CREATE INDEX `idx_name` ON `user` (`name`); -- 唯一索引 CREATE UNIQUE INDEX `uk_phone` ON `user` (`phone`); -- 复合索引 CREATE INDEX `idx_status_time` ON `order` (`status`, `created_at`); -- 前缀索引(长字符串字段) CREATE INDEX `idx_title_prefix` ON `article` (`title`(20)); -- 删除 DROP INDEX `idx_name` ON `user`;复合索引的“最左前缀”原则必须理解透。idx_status_time (status, created_at)能加速WHERE status = ?、WHERE status = ? AND created_at > ?,但单独用created_at作为条件时用不上这个索引,因为最左的status没出现在条件里。这就是为什么复合索引的字段顺序至关重要,把区分度高、又经常单独作为查询条件的字段放左边。
前缀索引适用于长字符串,比如给VARCHAR(255)的 URL 字段建索引,用前 20 个字符往往已经足够区分,能显著减小索引体积。但前缀索引不能用于覆盖索引和排序,用的时候要权衡。
那什么时候不该建索引?写多读少的表要克制;区分度低的字段(比如性别只有两三个值)单独建索引意义不大;频繁更新的字段建索引会增加写开销。我个人的判断顺序是:先看这条 SQL 在慢查询日志里出现频率高不高,再看EXPLAIN显示扫了多少行,最后才决定加什么索引。不加思考见字段就加索引,是另一种性能灾难。
查看表上的索引:
SHOW INDEX FROM `order`;看执行计划:
EXPLAIN SELECT * FROM `order` WHERE status = 1;EXPLAIN输出的type列最值得关注,从好到坏大致是system、const、eq_ref、ref、range、index、ALL。看到ALL就是全表扫描,大表上出现基本要优化。key列显示实际用了哪个索引,rows是预估扫描行数,Extra里出现Using filesort或Using temporary也是优化信号。
3. 数据操作与查询命令:每天真刀真枪在用的部分
DML 和查询命令是日常使用频率最高的。这一节我按“写”和“读”分开讲,重点放在那些看起来简单、实际处处是坑的地方。
3.1 增删改命令的边界与坑
插入单行:
INSERT INTO `user` (`name`, `age`) VALUES ('张三', 28);批量插入:
INSERT INTO `user` (`name`, `age`) VALUES ('李四', 30), ('王五', 25), ('赵六', 33);批量插入比循环单条插入快得多,因为减少了网络往返和事务提交次数。数据量大时务必批处理,一般每次几百到几千行比较合适,太大可能撞上max_allowed_packet限制。
有一个容易忽略的坑:批量插入时如果其中一条违反唯一约束,整个语句默认全部失败。想跳过错误继续插,用:
INSERT IGNORE INTO `user` (`phone`, `name`) VALUES ('13800000000', '张三');INSERT IGNORE会把错误降级为警告,重复的直接跳过。但要注意它也会吞掉其他错误,比如类型转换失败,用之前想清楚是否真的想忽略所有异常。
还有个高频需求是“存在则更新,不存在则插入”:
INSERT INTO `user` (`phone`, `name`) VALUES ('13800000000', '张三') ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);这条命令依赖唯一索引或主键冲突来触发更新分支,做计数、去重同步时特别好用。VALUES(name)取的是插入时提供的值,8.0 之后官方推荐改成别名写法AS new ON DUPLICATE KEY UPDATE name = new.name,避免和其他用法混淆。
更新命令:
UPDATE `user` SET `age` = 29 WHERE `name` = '张三';更新最大的风险是漏写WHERE,直接全表更新。我给自己立的规矩是:任何时候写UPDATE,先写WHERE把条件想好,再补SET,最后才敲回车。另外,生产环境执行前先SELECT COUNT(*)用同样的条件数一遍影响行数,心里有底才动手。
删除命令同理:
DELETE FROM `user` WHERE `id` = 100;大批量删除大表数据时,直接DELETE会锁住大量行、产生巨大 binlog、还可能撑爆 undo 日志。常规做法是分批删,比如每次删 1000 行,循环执行,中间留一点间隔让主从和存储有个喘息空间。
3.2 JOIN 的含义与写法选择
“mysql 数据库 join 含义”这个问题问得特别多,我用一个图书馆的类比来解释。把用户表想成读者名册,订单表想成借阅记录。
INNER JOIN(内连接):只保留两边都能对上的记录,也就是“有借阅记录的读者和对应的借阅记录”,没借过书的读者不出现。LEFT JOIN(左连接):保留左表全部,右表没有匹配的用 NULL 填充。“所有读者,以及他们的借阅记录,没借过书的也列出来,借阅部分为空”。RIGHT JOIN(右连接):和左连接相反,保留右表全部。实际写代码时我基本不用 RIGHT JOIN,把表顺序调换来写 LEFT JOIN 更符合阅读习惯。CROSS JOIN:笛卡尔积,两边行数相乘,除非明确需要组合,否则别用,很容易炸出几百万行。
-- 内连接:查有订单的用户 SELECT u.name, o.amount FROM `user` u INNER JOIN `order` o ON u.id = o.user_id; -- 左连接:查所有用户,包括没下过单的 SELECT u.name, o.amount FROM `user` u LEFT JOIN `order` o ON u.id = o.user_id;这里有个经典坑:在LEFT JOIN的WHERE里写右表字段的过滤条件,会把左连接“悄悄”变成内连接。比如:
-- 这样写,没下过单的用户会消失 SELECT u.name, o.amount FROM `user` u LEFT JOIN `order` o ON u.id = o.user_id WHERE o.status = 1;原因是右表匹配不上时o.status是 NULL,NULL 不等于 1,条件过滤掉了这些行。如果本意是“所有用户,有已支付订单的显示出来”,正确写法是把条件下移到ON里:
SELECT u.name, o.amount FROM `user` u LEFT JOIN `order` o ON u.id = o.user_id AND o.status = 1;这个ON和WHERE位置差异,是面试里区分度很高的一道题,也是实际排错时经常踩的点。
JOIN 的性能关键在连接字段有没有索引。ON u.id = o.user_id这条,o.user_id上必须有索引,否则每匹配一个用户就要扫一遍订单表,几万用户直接把数据库拖垮。所以设计表时,所有外键关联字段默认都建索引,这是习惯。
3.3 排序、分页与聚合命令
“mysql 排序”涉及ORDER BY:
SELECT * FROM `order` ORDER BY `created_at` DESC LIMIT 20;单字段排序简单,多字段要分清楚优先级:
SELECT * FROM `order` ORDER BY `status` ASC, `created_at` DESC;先按status升序,status相同时再按时间倒序。想让ORDER BY走索引而不是 filesort,排序字段最好和过滤字段组成一个合适的复合索引,并且方向一致。比如索引是(status, created_at),那ORDER BY status, created_at能利用索引;如果写成ORDER BY status, created_at DESC,在某些版本和场景下就可能退化成 filesort。这类细节用EXPLAIN一看便知。
分页是另一个重灾区:
SELECT * FROM `order` ORDER BY id LIMIT 0, 20; -- 第一页 SELECT * FROM `order` ORDER BY id LIMIT 100000, 20; -- 翻到很后面深分页LIMIT 100000, 20会先扫描并丢弃前 100000 行,越翻越慢。优化思路是用“游标”的方式,记住上一页最后一个 id:
SELECT * FROM `order` WHERE id > 100000 ORDER BY id LIMIT 20;这样每次只扫需要的 20 行,无论翻到多深都很快。前提是 id 单调递增且没有空洞依赖,适用于按主键翻页的场景。
聚合命令:
SELECT status, COUNT(*) AS cnt, SUM(amount) AS total FROM `order` WHERE created_at >= '2024-01-01' GROUP BY status;GROUP BY的字段和SELECT里非聚合字段要对应,否则在ONLY_FULL_GROUP_BY模式下会直接报错。这个模式是 5.7 之后默认开启的,好处是避免返回不确定的结果,坏处是很多老代码迁移过来会报错。遇到报错先别急着关这个模式,多数情况下是查询本身写得不够严谨。
HAVING和WHERE的区别也要清楚:WHERE在分组前过滤行,HAVING在分组后过滤组。想筛掉订单数少于 5 的状态,用HAVING cnt < 5。
3.4 更新子查询的经典报错与解法
“mysql 中更新子查询”是一个高频问题,因为 MySQL 不允许在UPDATE的WHERE子句里直接查同一张表:
-- 这样写会报错:You can't specify target table 'user' for update in FROM clause UPDATE `user` SET `age` = `age` + 1 WHERE `id` IN (SELECT `id` FROM `user` WHERE `city` = '北京');报错原因是 MySQL 不能在更新一张表的同时,从同一张表里读取数据用于定位,这会造成不确定性和潜在的冲突。标准解法是用一层派生表把子查询“包起来”,让中间的临时结果先物化:
UPDATE `user` SET `age` = `age` + 1 WHERE `id` IN ( SELECT `id` FROM ( SELECT `id` FROM `user` WHERE `city` = '北京' ) AS tmp );多包一层SELECT再取别名,MySQL 就会先把子查询结果算出来,再拿去做更新条件,问题解决。另一个解法是用JOIN改写:
UPDATE `user` u JOIN ( SELECT `id` FROM `user` WHERE `city` = '北京' ) t ON u.id = t.id SET u.age = u.age + 1;两种写法都行,实际经验里,数据量大时JOIN改写往往效率更好,因为它能更好利用索引。但要注意,更新涉及的记录多时,务必先事务包起来,或者先在测试库验证,确认影响范围再上生产。
4. 事务、存储过程与高级命令
前面三节覆盖了 80% 的日常操作,剩下 20% 属于“不常用但关键时刻能救场”的命令。事务保数据一致性,存储过程封装复杂逻辑,这两个是进阶路上绕不开的。
4.1 事务控制命令
MySQL 的 InnoDB 引擎支持事务,核心就是几条命令:
START TRANSACTION; -- 或 BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2; COMMIT; -- 提交 -- 或者 ROLLBACK; -- 回滚转账是最经典的例子:扣款和加款必须同生共死,中间任何一步失败都要整体回滚。不包事务的话,扣款成功加款失败,钱就凭空消失了。
事务的隔离级别决定了并发下能看到什么:
SELECT @@transaction_isolation; -- 查看当前隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;默认是REPEATABLE READ。很多互联网业务会改成READ COMMITTED,因为它在高并发下锁的范围更小、死锁概率更低。但改隔离级别有代价,涉及一致性读取的逻辑要重新评估。切换前一定在测试环境充分验证。
事务还有两个概念要记住:AUTOCOMMIT和隐式提交。默认AUTOCOMMIT = 1,也就是每条 SQL 自动提交,你不显式START TRANSACTION就没有事务保护。而像CREATE TABLE、ALTER TABLE这些 DDL 语句会隐式提交当前事务,所以别在长事务中间夹 DDL,否则前面的操作会被提前提交,回滚就回不去了。
SET autocommit = 0; -- 关闭自动提交,谨慎使用关闭自动提交之后,每条语句都要手动COMMIT或ROLLBACK,长事务会占用大量资源、阻塞其他会话,生产环境不建议这么干。
4.2 存储过程与函数命令
“mysql 存储过程”经常出现在面试题里。它本质是一组预编译的 SQL 逻辑,存在数据库里,可以像函数一样调用。
创建存储过程:
DELIMITER // CREATE PROCEDURE get_user_orders(IN uid BIGINT) BEGIN SELECT * FROM `order` WHERE user_id = uid; END // DELIMITER ;DELIMITER这一步是新手最容易迷惑的地方。命令行默认用分号作为语句结束符,但存储过程内部本身就有分号,所以必须先临时把结束符改成//或$$,写完整个过程,再改回分号。不改的话,客户端会在过程内部第一个分号处就认为语句结束了。
调用:
CALL get_user_orders(100);查看和删除:
SHOW PROCEDURE STATUS WHERE Db = 'shop'; SHOW CREATE PROCEDURE get_user_orders; DROP PROCEDURE get_user_orders;存储过程内部还能写变量、条件判断、循环:
DELIMITER // CREATE PROCEDURE stat_order() BEGIN DECLARE total INT DEFAULT 0; SELECT COUNT(*) INTO total FROM `order`; IF total > 1000 THEN SELECT '订单量很大' AS msg; ELSE SELECT '订单量正常' AS msg; END IF; END // DELIMITER ;我的个人态度是:业务逻辑尽量放应用层,存储过程不要滥用。它的可维护性、调试体验、版本管理都不如代码,团队里会写的人少,出问题排查也难。但它在批量数据处理、定时统计这类场景确实高效,能减少网络往返,该用的时候别排斥。
4.3 视图、触发器与其他实用命令
视图是把复杂查询存起来当表用:
CREATE VIEW v_user_order AS SELECT u.name, o.amount, o.created_at FROM `user` u JOIN `order` o ON u.id = o.user_id; SELECT * FROM v_user_order WHERE amount > 100;视图不存数据,每次查都实时执行背后的 SQL,适合封装频繁使用的复杂查询,让上层代码干净。但它对性能没有直接提升,用多了还可能掩盖底层 SQL 的问题。
触发器是在增删改时自动执行的逻辑:
DELIMITER // CREATE TRIGGER trg_order_insert AFTER INSERT ON `order` FOR EACH ROW BEGIN UPDATE user_stat SET order_count = order_count + 1 WHERE user_id = NEW.user_id; END // DELIMITER ;NEW代表新插入的行,OLD代表被删改前的旧行。触发器很强大,但同样不建议重度使用,因为它把逻辑藏在数据库里,应用开发者看不到,容易造成“数据莫名其妙变了”的困扰。统计类的需求用异步任务或定时统计更可控。
5. 备份、导入导出与日常巡检命令
数据安全是底线,备份命令和巡检命令看着不起眼,真出事的时候就是救命稻草。
5.1 备份与恢复命令实操
逻辑备份的主力是mysqldump,它是在命令行执行的工具,不在 MySQL 交互界面里:
mysqldump -u root -p shop > shop_backup.sql备份单个库。备份多个库用--databases:
mysqldump -u root -p --databases shop user_center > multi_backup.sql备份所有库:
mysqldump -u root -p --all-databases > all_backup.sql只备份表结构不备份数据,做环境搭建时常用:
mysqldump -u root -p --no-data shop > shop_schema.sqlmysqldump有几个参数直接影响备份可用性,务必记住:
| 参数 | 作用 | 建议 |
|---|---|---|
--single-transaction | 备份期间不锁表,保证一致性 | InnoDB 必加 |
--routines | 导出存储过程和函数 | 用到了就加 |
--triggers | 导出触发器 | 默认导出,确认一下 |
--events | 导出事件调度 | 用到了就加 |
--set-gtid-purged=OFF | 避免 GTID 信息导致的导入报错 | 主从环境注意 |
--default-character-set=utf8mb4 | 防止中文乱码 | 强烈建议加 |
一条我常用的生产备份命令:
mysqldump -u root -p \ --single-transaction \ --routines --triggers --events \ --default-character-set=utf8mb4 \ shop > shop_backup.sql恢复就是把文件导回去:
mysql -u root -p shop < shop_backup.sql或者在 MySQL 交互界面里:
USE shop; SOURCE /path/shop_backup.sql;关于备份,我踩过最深的坑是“备份文件其实没法恢复”。有次线上要恢复,发现备份命令没加--single-transaction,备份期间表在变,导出来的一致性有问题;还有一次没检查备份文件大小,结果是空的。现在我给自己立了一条铁律:备份完必须做恢复演练,哪怕只恢复到一个测试库,确认能跑通。没验证过的备份等于没有备份。
5.2 导入导出与数据迁移命令
除了mysqldump,导出数据还有大杀器SELECT ... INTO OUTFILE:
SELECT * FROM `order` INTO OUTFILE '/tmp/order.csv' FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n';注意这个命令导出的是服务器上的文件,不是本地,而且需要FILE权限和secure_file_priv配置允许的目录,否则会报权限错误。这也是它比mysqldump麻烦的地方。
反过来,导入 CSV:
LOAD DATA INFILE '/tmp/order.csv' INTO TABLE `order` FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n';这个在大批量导入时比逐条INSERT快一个数量级,做数据迁移时非常好用。但要注意字符集和字段顺序的对应,导入前最好先建好表结构,导入时用IGNORE或先清空表。
数据在不同 MySQL 版本间迁移时,版本兼容是难点。用mysqldump从低版本往高版本导通常没问题,反过来从 8.0 往 5.7 导,常常因为字符集默认规则、认证插件、保留字变化而报错。稳妥做法是导出后先用文本工具搜一遍,把不兼容的语句处理掉再导。
5.3 巡检命令与性能观察
想了解数据库当前状态,这几条命令每天看一眼:
SHOW STATUS; -- 全局状态变量,几百项 SHOW STATUS LIKE 'Threads_connected'; -- 当前连接数 SHOW STATUS LIKE 'Slow_queries'; -- 慢查询次数 SHOW VARIABLES LIKE 'max_connections'; -- 最大连接数上限 SHOW PROCESSLIST; -- 当前所有连接在干什么SHOW PROCESSLIST是排障神器。当数据库突然变慢,用它可以看有哪些会话在跑、跑了多久、在等什么。如果发现某个会话Time列很大、State是Sending data或Locked,基本就是问题所在。
-- 当前执行时间超过 10 秒的语句 SELECT id, user, time, state, info FROM information_schema.processlist WHERE command != 'Sleep' AND time > 10 ORDER BY time DESC;慢查询日志是另一个重点,先看开没开:
SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time';没开的话在配置里打开(重启生效),或者临时开:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;long_query_time是阈值,超过它的 SQL 会被记录。生产环境一般设 1 秒甚至更低,测试阶段可以设 0.1 秒来抓那些“不够慢但也不快”的语句。抓出来的慢 SQL 用EXPLAIN分析,加索引、改写法、拆查询,这是性能优化的主战场。
查看表的大小和数据量,评估存储压力:
SELECT table_name, table_rows, ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables WHERE table_schema = 'shop' ORDER BY size_mb DESC;这条挺实用,能一眼看出哪个表最大、索引比数据还大的是不是索引建多了。
6. 常见报错速查与命令使用心得
前面按功能把命令梳理了一遍,最后这一节我把平时遇到最多的报错整理成速查表,再补几条命令使用上的经验。
6.1 高频报错与对应解法
| 报错信息 | 常见原因 | 解决方向 |
|---|---|---|
Access denied for user | 密码错、主机来源不匹配、权限不足 | 核对密码,检查用户@主机,SHOW GRANTS |
Can't connect to MySQL server | 服务没起、端口不通、防火墙拦截 | 查服务状态、测端口、看防火墙 |
Unknown column in 'field list' | 字段名拼错、表别名没带对 | 核对字段名和别名 |
You can't specify target table ... in FROM clause | 更新时子查询查了同一张表 | 用派生表包一层或用 JOIN 改写 |
Incorrect string value | 字符集不是 utf8mb4,存了 emoji | 改库表字段字符集为 utf8mb4 |
Duplicate entry for key | 唯一索引冲突 | 检查重复数据,或用 INSERT IGNORE |
Lock wait timeout exceeded | 事务锁等待超时 | 查 processlist 找持锁会话,缩短事务 |
Table is marked as crashed | 表损坏 | 用REPAIR TABLE或从备份恢复 |
Too many connections | 连接数打满 | 查漏连接,调max_connections |
Data too long for column | 数据超字段长度 | 改字段长度或截断数据 |
Lock wait timeout exceeded这一条我单独说两句。它本质是某个事务长时间不提交,导致其他事务等锁等到超时。排查步骤是先SHOW PROCESSLIST或查information_schema.innodb_trx找到运行时间最长的事务,确认它是不是卡住了或者忘了提交,必要时KILL掉。根治办法是让应用尽量缩短事务,别在事务里做远程调用、别在事务里等用户输入。
Too many connections的排查重点是区分“真的是业务量大”还是“连接泄漏”。如果是应用没用连接池,每次请求新建连接又不关,那加多少max_connections都没用,迟早再打满。看Threads_connected是不是持续爬升、SHOW PROCESSLIST里Sleep状态的是不是特别多,基本能判断出来。
6.2 命令使用上我个人的几条经验
第一条,任何危险命令先在测试库跑一遍。UPDATE、DELETE、DROP、ALTER这四类操作,无论多熟,我都坚持先在测试环境验证语句和影响行数。生产环境的“手滑”代价太大,一分钟的验证能省掉一周的补救。
第二条,善用LIMIT保护自己。写UPDATE和DELETE时,实在没底可以先加LIMIT 1试跑,确认逻辑对了再放大范围。这个习惯救过我好几次,尤其是在写多表关联更新的时候。
第三条,命令行的补全和快捷键能省很多事。MySQL 客户端支持 Tab 补全表名和字段名(部分配置下需要--auto-rehash),输入长的库名表名时特别高效。还有Ctrl + R反向搜索历史命令,比按上箭头找快多了。
第四条,把常用命令整理成自己的脚本片段。比如备份、巡检、导入导出这些重复性操作,写成带参数的 shell 脚本,比每次手敲可靠。我自己的脚本库里,备份脚本就包含了备份、压缩、校验文件大小、删除过期备份几个步骤,一次写好长期受益。
第五条,理解命令背后的执行方式比记住命令本身更重要。同样是查数据,SELECT走没走索引、JOIN用了什么算法、ORDER BY会不会 filesort,这些决定了命令快不快。EXPLAIN是连接命令和性能之间的桥梁,建议养成写复杂查询先看执行计划的习惯。
最后分享一个我调试 SQL 的小方法。遇到复杂查询报错或结果不对,我习惯把语句拆开逐步执行:先单独跑子查询看结果对不对,再加 JOIN,最后加 WHERE 和 GROUP BY。这样一旦哪一步出问题,范围立刻锁定,比对着一条几百行的 SQL 干瞪眼高效得多。这个笨办法用久了,反而成了我最快定位问题的路径。