MySQL 常用操作手册
一句话总结: MySQL 是最流行的开源关系型数据库。这本手册汇总了运维日常最常用的 SQL 操作——从登录到备份,从查慢 SQL 到处理死锁,全部可直接复制执行。
目录
连接与登录
命令行登录
场景: 从跳板机直连 MySQL 实例,日常操作基础。
# 语法:mysql -h 主机 -P 端口 -u 用户名 -p
# -p 后面不加密码,回车后交互式输入(更安全,密码不会留在历史记录)
mysql -h 10.0.1.100 -P 3306 -u root -p
# 快速登录本地 Socket(比 TCP 更快)
mysql -u root -p -S /tmp/mysql.sock
预期输出:
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
mysql>
参数说明:
| 参数 | 含义 | 示例 |
|---|---|---|
-h |
主机地址 | -h 10.0.1.100 |
-P |
端口(默认 3306) | -P 3306 |
-u |
用户名 | -u root |
-p |
密码提示 | -p 或 -p'密码'(不推荐明文) |
-S |
Socket 文件路径 | -S /tmp/mysql.sock |
注意事项:
- ⚠️
mysql -p密码密码紧挨-p不能有空格,但不要把密码写在命令里——会被记录到~/.mysql_history - 💡 用
mysql_config_editor可以安全存储登录凭据:mysql_config_editor set --login-path=prod --host=10.0.1.100 --user=root --password
查看连接信息
-- 查看当前所有活跃连接(排查连接池爆满时必用)
SHOW FULL PROCESSLIST;
-- 只看非 Sleep 的活跃查询
SELECT id, user, host, db, command, time, state, info
FROM information_schema.PROCESSLIST
WHERE command != 'Sleep';
预期输出(节选):
+----+------+-------------+------+---------+------+-----------+------------------+
| id | user | host | db | command | time | state | info |
+----+------+-------------+------+---------+------+-----------+------------------+
| 10 | root | 10.0.1.1:22 | test | Query | 0 | executing | SELECT * FROM t |
| 11 | app | 10.0.1.2:33 | test | Sleep | 120 | | NULL |
+----+------+-------------+------+---------+------+-----------+------------------+
库表管理
查看数据库和表
-- 查看所有数据库
SHOW DATABASES;
-- 选择数据库
USE mydb;
-- 查看当前库所有表
SHOW TABLES;
-- 查看表结构(字段、类型、主键等)
DESC mytable;
-- 或
SHOW CREATE TABLE mytable\G
预期输出(DESC):
+-------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(100) | YES | | NULL | |
+-------+--------------+------+-----+---------+----------------+
创建数据库和表
-- 创建数据库(指定字符集,避免乱码)
CREATE DATABASE IF NOT EXISTS mydb
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
-- 创建表
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
name VARCHAR(100) NOT NULL COMMENT '用户名',
email VARCHAR(200) NOT NULL COMMENT '邮箱',
age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄',
status TINYINT DEFAULT 1 COMMENT '状态 1=正常 0=禁用',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_email (email),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
参数说明:
| 参数 | 含义 | 说明 |
|---|---|---|
ENGINE=InnoDB |
存储引擎 | 生产环境必须用 InnoDB,支持事务和行锁 |
utf8mb4 |
字符集 | 支持 emoji 和所有 Unicode 字符 |
AUTO_INCREMENT |
自增 | 主键自增,不需手动赋值 |
UNSIGNED |
无符号 | ID 用这个可以存更多正数 |
修改表结构
-- ⚠️ 大表加字段可能导致锁表,生产环境用 pt-online-schema-change
-- 添加字段
ALTER TABLE users ADD COLUMN phone VARCHAR(20) AFTER email;
-- 修改字段类型
ALTER TABLE users MODIFY COLUMN phone VARCHAR(30);
-- 重命名字段
ALTER TABLE users CHANGE COLUMN phone mobile VARCHAR(20);
-- 删除字段(谨慎!)
ALTER TABLE users DROP COLUMN mobile;
-- 添加索引
ALTER TABLE users ADD INDEX idx_name (name);
-- 查看表大小(排查磁盘空间)
SELECT
table_name,
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'size_mb'
FROM information_schema.tables
WHERE table_schema = 'mydb'
ORDER BY size_mb DESC;
注意事项:
- ⚠️ 在几百万行的大表上
ALTER TABLE会锁全表——业务高峰期千万别跑 - 💡 生产环境大表变更推荐用
pt-online-schema-change(Percona Toolkit)或gh-ost
数据增删改查
在正式开始之前,你需要先了解两件事:
第一:我们先看样例表
下面的 SQL 例子都基于这两张表——一张存操作日志、一张存用户信息。
-- operation_logs:记录每一次 API 请求的日志
-- 就像:每次有人访问你的网站,MySQL 都会记一条——是谁、什么时候、调了什么接口、花了多久、报错了没有
CREATE TABLE operation_logs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
trace_id VARCHAR(32) NOT NULL COMMENT '链路追踪ID(一个请求的唯一身份证号)',
app_name VARCHAR(50) NOT NULL COMMENT '哪个应用产生的日志?如 order-api(订单服务)',
log_level ENUM('DEBUG','INFO','WARN','ERROR','FATAL') NOT NULL DEFAULT 'INFO',
message TEXT NOT NULL COMMENT '日志的具体内容(报错信息/描述)',
req_ip VARCHAR(45) NOT NULL COMMENT '请求来源的 IP 地址',
user_id INT UNSIGNED DEFAULT NULL COMMENT '操作用户ID(没登录就是 NULL)',
api_path VARCHAR(200) DEFAULT NULL COMMENT '请求的接口路径,如 /api/order/create',
status_code SMALLINT DEFAULT NULL COMMENT 'HTTP 状态码(200=成功, 404=找不到, 500=服务器错误)',
cost_ms INT UNSIGNED DEFAULT 0 COMMENT '这个请求花了多少毫秒',
created_at DATETIME(3) NOT NULL COMMENT '日志产生的时间(精确到毫秒)',
INDEX idx_app_level (app_name, log_level),
INDEX idx_created (created_at),
INDEX idx_user (user_id),
INDEX idx_api_status (api_path(100), status_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';
-- users:用户基本信息
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
role ENUM('admin','ops','dev','viewer') NOT NULL DEFAULT 'dev',
status TINYINT DEFAULT 1 COMMENT '1=正常 0=禁用',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
第二:SQL 的执行顺序(非常关键,搞懂这个你就理解了 SQL 的灵魂)
很多人以为 SQL 是从上往下执行的——错了。SQL 的逻辑执行顺序是这样的:
FROM → 第一步:从哪张表拿数据?
↓
WHERE → 第二步:筛选哪些行留下?
↓
GROUP BY → 第三步:要不要分组统计?
↓
HAVING → 第四步:分组后还要不要过滤(很少用)?
↓
SELECT → 第五步:我要看哪些列?
↓
ORDER BY → 第六步:按什么排序?
↓
LIMIT → 第七步:只要前 N 条
怎么记: “饭(FROM)碗(WHERE)群(GROUP BY)会(HAVING)选(SELECT)排(ORDER BY)列(LIMIT)”
理解这个顺序后,你就知道为什么 WHERE 里面不能写 COUNT(*)——因为 WHERE 执行的时候,GROUP BY 还没跑呢,怎么知道统计结果?
场景 1|按时间范围查日志 ⭐ 最高频
场景故事
今早 10:05 收到告警:“order-api 在 09:50~10:00 之间 500 错误飙高!”
你现在要:立刻查出那 10 分钟内所有 ERROR 日志,看看是什么报错、哪个接口、耗时多久。
SQL → 逐行拆解
-- 需求:查某段时间内、某应用的 ERROR 日志
SELECT trace_id, created_at, message, api_path, cost_ms, req_ip ← 第⑤步:我只想看这 6 列
FROM operation_logs ← 第①步:从日志表里找
WHERE app_name = 'order-api' ← 第②步:只留 order-api 的记录
AND log_level = 'ERROR' ← 第②步:只留 ERROR 级别的
AND created_at >= '2026-06-23 09:50:00' ← 第②步:只要 9:50 之后的
AND created_at < '2026-06-23 10:00:00' ← 第②步:只要 10:00 之前的
ORDER BY created_at DESC; ← 第⑥步:最新的排最上面
逐行解释:
| SQL 片段 | 小白版解释 | 为什么要这么写 |
|---|---|---|
SELECT trace_id, created_at, ... |
“我只想看这些列” | 不要写 SELECT *,只挑需要的列 |
FROM operation_logs |
“从日志表里找数据” | 告诉 MySQL 去哪个表翻 |
WHERE app_name = 'order-api' |
“只看订单服务的日志” | 系统可能有很多应用,不限定会查到别人的数据 |
AND log_level = 'ERROR' |
“只看错误级别的” | 你和 INFO 混一起看是浪费时间 |
AND created_at >= '...' |
“只看 9:50 之后的” | 用 >=(大于等于)包含 9:50:00 这一秒本身 |
AND created_at < '...' |
“只看 10:00 之前的” | < 不包含 10:00:00 整 |
ORDER BY created_at DESC |
“最新的在最上面” | 排障时最新发生的往往最重要 |
思考过程:
Q:为什么用 >= 和 <,而不是 BETWEEN?
A:BETWEEN 包含两边,BETWEEN '09:50' AND '10:00' 会包含 10:00:00 整。
用 >= AND < 叫"左闭右开区间",是时间范围查询的标准写法,不会多出下一秒的数据。
Q:为什么 ORDER BY created_at DESC 而不是 ASC?
A:DESC = 倒序(最新的在前)。排障时永远先看最近的——10:00 的 ERROR 比 09:50 的更有参考价值。
预期输出:
+----------------------------------+-------------------------+--------------------------------------+-------------------+--------+-------------+
| trace_id | created_at | message | api_path | cost_ms| req_ip |
+----------------------------------+-------------------------+--------------------------------------+-------------------+--------+-------------+
| a1b2c3d4-e5f6-7890-abcd-ef123456 | 2026-06-23 09:59:58.123 | [Timeout] Redis read timeout after 3s| /api/order/create | 3210 | 10.0.1.50 |
| a1b2c3d4-e5f6-7890-abcd-ef789012 | 2026-06-23 09:55:32.456 | [DBError] Deadlock found when trying.| /api/order/pay | 5432 | 10.0.1.88 |
+----------------------------------+-------------------------+--------------------------------------+-------------------+--------+-------------+
如何读懂:
- 2 条 ERROR,都在 9:55~10:00 这 5 分钟
- 第一条:创建订单接口调 Redis 超时(3.2 秒)
- 第二条:支付接口出现死锁(5.4 秒)
- 推理:可能 Redis 先扛不住,业务降级失败导致死锁
变体一:查"过去 N 分钟"(巡检脚本用)
场景: 写一个脚本每分钟跑一次,检查过去 5 分钟哪个接口错误最多。
SELECT COUNT(*) AS error_count, api_path
FROM operation_logs
WHERE app_name = 'order-api'
AND log_level = 'ERROR'
AND created_at >= NOW() - INTERVAL 5 MINUTE -- NOW() 是"当前时间",减 5 分钟
GROUP BY api_path
ORDER BY error_count DESC;
新知识点:
| 关键词 | 小白版解释 |
|---|---|
COUNT(*) |
“数一数有多少行”——计数 |
GROUP BY api_path |
“把相同接口的日志归到一堆” |
AS error_count |
“给 COUNT(*) 起个名字” |
NOW() - INTERVAL 5 MINUTE |
“5 分钟前”——当前时间减去 5 分钟 |
什么时候用? 写监控脚本。Zabbix / Prometheus 每分钟调一次这个 SQL,如果 error_count 突增就告警。
变体二:查某个 trace 的完整调用链
场景: 告警里给了一个 trace_id,想看这个请求从头到尾发生了什么。
SELECT created_at, log_level, api_path, message, cost_ms
FROM operation_logs
WHERE trace_id = 'a1b2c3d4-e5f6-7890-abcd-ef123456' -- 限定是这个请求
ORDER BY created_at ASC; -- ASC = 正序(最早的在前)
思考过程:
Q:为什么这里用 ASC(正序)?
A:你要看的是"一个请求从开始到结束的全过程"——正序排列就像看录像带从头播到尾。
刚才场景 1 用 DESC 是因为"哪个错误最新"。
Q:trace_id 是什么?
A:每个请求进系统时就分配一个唯一 ID。这个 ID 会传递到所有下游服务。
拿着这个 ID,你就能把同一个请求在前端、网关、后端、数据库的日志全部串起来。
场景 2|聚合统计(排查拐点、看趋势)
场景故事
老板问:“昨天订单接口成功率多少?”
或者你在写周报:“这周每天 500 错误的变化趋势是什么?”你不能一条一条看日志,需要 MySQL 按小时汇总。
SQL → 逐行拆解
SELECT ← 第⑤步
DATE_FORMAT(created_at, '%Y-%m-%d %H:00') AS time_bucket, -- 把时间切成"小时块"
COUNT(*) AS total_reqs, -- 这个小时总共有多少次请求
SUM(IF(status_code >= 500, 1, 0)) AS error_5xx, -- 其中 500 以上的有多少次
ROUND(AVG(cost_ms), 1) AS avg_cost_ms, -- 平均耗时
ROUND(MAX(cost_ms), 0) AS max_cost_ms, -- 最大耗时(最慢的那次)
CONCAT(ROUND(SUM(IF(status_code >= 500, 1, 0)) * 100.0 / COUNT(*), 2), '%') AS error_rate -- 错误率
FROM operation_logs ← 第①步
WHERE app_name = 'order-api' ← 第②步
AND created_at >= NOW() - INTERVAL 24 HOUR ← 第②步:过去 24 小时
GROUP BY DATE_FORMAT(created_at, '%Y-%m-%d %H:00') ← 第③步:按"小时"分组
ORDER BY time_bucket ASC; ← 第⑥步
新函数解释:
| 函数 | 小白版解释 |
|---|---|
DATE_FORMAT(created_at, '%Y-%m-%d %H:00') |
把时间裁成整点。比如 2026-06-23 09:45:12 → 2026-06-23 09:00。9:01~9:59 都算"9 点" |
COUNT(*) |
数一数有多少行 |
SUM(IF(条件, 1, 0)) |
条件成立记 1,否则记 0,然后求和——就是数符合条件的行 |
AVG(cost_ms) |
平均耗时 = 总耗时 ÷ 请求数 |
ROUND(x, 1) |
四舍五入保留 1 位小数 |
CONCAT(a, '%') |
把数字后面加个百分号 |
错误率计算过程:
错误的请求数 × 100 ÷ 总请求数 = 错误率%
举例(10:00~10:59 这小时):
总请求 4210 次,其中 380 次返回 500
错误率 = 380 × 100 ÷ 4210 = 9.03%
预期输出:
+------------------+-----------+----------+-------------+------------+------------+
| time_bucket | total_reqs| error_5xx| avg_cost_ms | max_cost_ms| error_rate |
+------------------+-----------+----------+-------------+------------+------------+
| 2026-06-22 09:00 | 3520 | 12 | 45.2 | 2100 | 0.34% |
| 2026-06-22 10:00 | 4210 | 380 | 320.5 | 8910 | 9.03% | ← 拐点!
| 2026-06-22 11:00 | 1080 | 420 | 980.0 | 15000 | 38.89% | ← 扩大
+------------------+-----------+----------+-------------+------------+------------+
如何看懂这张表:
| 时间 | 说明 | 你该做什么 |
|---|---|---|
| 09:00 | ✅ 正常——0.34% 错误率,45ms 平均耗时 | 这是基准线 |
| 10:00 | 🚨 错误率跳到 9%,耗时升到 320ms | 拐点!去查 10:00 有没有发布 |
| 11:00 | 🔥 错误率 38%,耗时 1 秒 | 问题扩散,立即处理 |
什么时候用? 每天早晨看巡检看板、出周报、对比"现在和过去差多少"时。一眼看到拐点在哪、趋势是好转还是恶化。
变体:按天统计,比对最近两周
SELECT
DATE(created_at) AS day, -- DATE() 只取年月日
COUNT(*) AS total,
SUM(IF(log_level='ERROR', 1, 0)) AS errors,
ROUND(AVG(cost_ms), 0) AS avg_latency
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= CURDATE() - INTERVAL 14 DAY
GROUP BY DATE(created_at)
ORDER BY day DESC;
DATE() vs DATE_FORMAT():
DATE(created_at)→2026-06-23(按天)DATE_FORMAT(created_at, '%Y-%m-%d %H:00')→2026-06-23 09:00(按小时)
场景 3|搜索日志内容(关键词模糊匹配)
场景故事
线上出了故障,你知道出错日志里包含 “Redis timeout”。
问题:日志表几千万行,你怎么从海量数据里捞出这几条?
基础写法 + 性能警告
SELECT trace_id, created_at, log_level, message, api_path, cost_ms
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= '2026-06-23 00:00:00'
AND message LIKE '%Redis timeout%' -- 包含这个关键词(不管在哪)
ORDER BY created_at DESC
LIMIT 50;
LIKE 的三种模式:
| 写法 | 含义 | 走索引吗? |
|---|---|---|
LIKE 'Redis%' |
以 Redis 开头 | ✅ 走 |
LIKE '%timeout' |
以 timeout 结尾 | ❌ 不走 |
LIKE '%Redis%' |
中间包含 Redis | ❌ 不走 |
⚠️ 为什么 % 在前面就不走索引?
索引就像书的目录,按拼音顺序排好。你想找"以 R 开头的词"→ 翻到 R 那页就行(走索引)。
但你想找"词中间包含 timeout 的"→ 目录不会帮你排这个,你只能从第一页翻到最后一页(全表扫描)。
正确的做法:
情况 1:MySQL 数据不多(几百万行)+ 时间范围小 → 可以 LIKE
情况 2:MySQL 数据多 + 经常搜关键词 → 上 Elasticsearch(ELK)
情况 3:临时查一下 → 缩小时间窗口,加更多 WHERE 条件
安全版(缩小范围 + 加过滤)
SELECT trace_id, message, api_path
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= '2026-06-23 09:50:00' -- ✅ 只搜 10 分钟
AND created_at < '2026-06-23 10:00:00'
AND (message LIKE '%Redis%' OR message LIKE '%timeout%') -- 多关键词
AND log_level IN ('ERROR', 'WARN') -- ✅ 只看 WARN 以上
ORDER BY created_at DESC;
什么时候用? 知道大致时间窗口,但不知道具体错误信息——用"时间 + 关键词 + 级别"三层过滤。
另一个实用变体:找昨天最慢的接口
SELECT api_path,
COUNT(*) AS call_count,
ROUND(AVG(cost_ms), 0) AS avg_ms,
ROUND(MAX(cost_ms), 0) AS max_ms
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= CURDATE() - INTERVAL 1 DAY
GROUP BY api_path
HAVING call_count > 100 -- 冷门接口不具统计意义,排除
ORDER BY avg_ms DESC
LIMIT 10;
为什么用 HAVING 而不是 WHERE?
call_count 是统计出来的(GROUP BY 之后才有),所以不能写在 WHERE 里。
回顾执行顺序:WHERE(第②步) → GROUP BY(第③步) → HAVING(第④步)
什么时候用? 系统变慢了→先查"哪个接口最慢"→先定位再优化。
场景 4|多表关联查(JOIN)
场景故事
user_id=12345一直在报错。日志表只存了 user_id(数字),你想知道这个人的姓名和角色——需要把日志表和用户表关联起来。
JOIN 入门
日志表(左) 用户表(右)
+--------+--------+ +--------+--------+--------+
| log_id | user_id| | id | name | role |
+--------+--------+ +--------+--------+--------+
| L01 | 12345 | | 12345 | 张三 | admin |
| L02 | 12346 | | 12346 | 李四 | dev |
+--------+--------+ +--------+--------+--------+
JOIN 的意思:"把 user_id 对上 id,两边拼在一起"
结果:
+--------+--------+--------+--------+--------+
| log_id | user_id| id | name | role |
+--------+--------+--------+--------+--------+
| L01 | 12345 | 12345 | 张三 | admin |
| L02 | 12346 | 12346 | 李四 | dev |
+--------+--------+--------+--------+--------+
SQL → 逐行拆解
SELECT
l.created_at, l.log_level, l.api_path, l.message, -- 来自日志表(l)
u.name AS user_name, u.role AS user_role, u.status AS user_status -- 来自用户表(u)
FROM operation_logs l -- l 是简称
JOIN users u ON l.user_id = u.id -- 🔑 关键:把 l.user_id 和 u.id 对上
WHERE l.user_id = 12345
AND l.log_level IN ('ERROR', 'WARN')
AND l.created_at >= '2026-06-01'
ORDER BY l.created_at DESC
LIMIT 20;
| 代码 | 小白版解释 |
|---|---|
operation_logs l |
给表起了短名叫 l |
JOIN users u ON l.user_id = u.id |
“把两个表黏在一起——条件是日志的 user_id = 用户的 id” |
u.name AS user_name |
“用户表的 name,在结果里改名叫 user_name” |
JOIN vs LEFT JOIN:
JOIN(INNER JOIN):两边都有的才保留
日志的 user_id=12345,用户表有 → 保留
日志的 user_id=NULL → 丢掉这条日志
LEFT JOIN:左边表全部保留,右边有就拼上,没有就 NULL
日志的 user_id=NULL → 保留,但 user_name 显示 NULL
什么时候用? 用户投诉"我操作不了"→拿 user_id 一查,看具体报错、是不是被禁了。
变体:查某个 IP 干了什么
场景: 安全同事说 “10.0.1.50 在扫我们的接口”。
SELECT l.created_at, l.api_path, l.status_code, l.message, u.name, u.role
FROM operation_logs l
LEFT JOIN users u ON l.user_id = u.id -- 用 LEFT JOIN!
WHERE l.req_ip = '10.0.1.50'
AND l.created_at >= '2026-06-23 09:30:00'
AND l.created_at < '2026-06-23 10:00:00'
ORDER BY l.created_at ASC;
为什么用 LEFT JOIN? 刷接口的请求可能都是未登录的(user_id = NULL)。用 JOIN 就丢掉了——你看不到这些请求。用 LEFT JOIN 会保留它们。
场景 5|子查询与 CTE(分步做复杂查询)
场景故事
你要找"昨天调用次数多、且错误率超过 5% 的接口"。
这是两步操作:
- 先算每个接口的总调用数和错误数
- 再从结果里挑出错误率 > 5% 的
写法一:子查询
SELECT api_path, total, errors,
ROUND(errors * 100.0 / total, 2) AS error_rate
FROM (
-- 里面先算每个接口的总数和错误数
SELECT api_path,
COUNT(*) AS total,
SUM(IF(status_code >= 500, 1, 0)) AS errors
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= CURDATE() - INTERVAL 1 DAY
GROUP BY api_path
) AS stats -- 临时表,起名叫 stats
WHERE errors * 100.0 / total > 5 -- 在外面过滤
ORDER BY errors DESC;
执行顺序:
第①步(先执行括号里的):
FROM operation_logs → WHERE → GROUP BY → 算出临时表 stats
+---------------------+-------+--------+
| api_path | total | errors |
+---------------------+-------+--------+
| /api/order/create | 35210 | 2850 |
| /api/payment/pay | 18200 | 1200 |
| /api/user/login | 88000 | 400 |
+---------------------+-------+--------+
第②步(执行外层的):
FROM stats → WHERE errors/total > 5% → SELECT 并计算 error_rate
+---------------------+-------+--------+------------+
| /api/order/create | 35210 | 2850 | 8.09% |
| /api/payment/pay | 18200 | 1200 | 6.59% |
+---------------------+-------+--------+------------+
/api/user/login 调了 88000 次但只错 400 次(0.45%),被过滤掉
写法二:CTE(可读性更好)
WITH api_stats AS ( -- 先定义一个"临时视图"
SELECT api_path,
COUNT(*) AS total,
SUM(IF(status_code >= 500, 1, 0)) AS errors
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= CURDATE() - INTERVAL 1 DAY
GROUP BY api_path
)
SELECT api_path, total, errors,
ROUND(errors * 100.0 / total, 2) AS error_rate
FROM api_stats
WHERE errors * 100.0 / total > 5
ORDER BY errors DESC;
CTE vs 子查询:
| 对比 | 子查询 | CTE(WITH) |
|---|---|---|
| 可读性 | 括号多了眼花 | 先定义后使用,像写文章分段落 |
| 复用 | 不能用两次 | 可以多次引用 |
CTE 的复用优势:
WITH api_stats AS (...)
SELECT * FROM api_stats
WHERE errors * 100.0 / total > 5 -- 条件 1:错误率高
OR avg_ms > 1000 -- 条件 2:平均耗时 > 1 秒
场景 6|CASE WHEN 条件表达式(自定义分类)
场景故事
你想统计过去一周的日志,但不想按 MySQL 自带的
log_level分类。
你想要:FATAL+ERROR 算"严重"、WARN 算"警告"、其他的算"正常"。
SQL → 逐行拆解
SELECT
CASE -- "如果...就..."
WHEN log_level IN ('FATAL', 'ERROR') THEN '严重' -- FATAL 或 ERROR → 严重
WHEN log_level = 'WARN' THEN '警告' -- WARN → 警告
ELSE '正常' -- 剩下的 → 正常
END AS severity, -- 结果起名叫 severity
COUNT(*) AS cnt
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= NOW() - INTERVAL 7 DAY
GROUP BY severity
ORDER BY cnt DESC;
CASE WHEN 是怎么工作的:
MySQL 逐行读日志,对每一行:
第 1 行:log_level = 'INFO'
→ 检查 WHEN IN ('FATAL','ERROR') → 不是
→ 检查 WHEN = 'WARN' → 不是
→ ELSE → '正常'
第 2 行:log_level = 'FATAL'
→ 检查 WHEN IN ('FATAL','ERROR') → 是!→ '严重'(跳过后面)
第 3 行:log_level = 'WARN'
→ 检查 WHEN IN ('FATAL','ERROR') → 不是
→ 检查 WHEN = 'WARN' → 是!→ '警告'
然后 GROUP BY severity 把相同标签的归一堆,COUNT(*) 数每堆多少条。
预期输出:
+----------+-------+
| severity | cnt |
+----------+-------+
| 正常 | 285000|
| 警告 | 3200|
| 严重 | 480|
+----------+-------+
什么时候用? 周报/日报统计时——MySQL 自带的
GROUP BY log_level只能分出 DEBUG/INFO/WARN/ERROR,但业务上你可能需要"重要/不重要"等自定义分类。
变体:把 HTTP 状态码转成人话
SELECT
CASE
WHEN status_code >= 200 AND status_code < 300 THEN '2xx 成功'
WHEN status_code >= 400 AND status_code < 500 THEN '4xx 客户端错误' -- 用户的问题
WHEN status_code >= 500 THEN '5xx 服务端错误' -- 我们的问题
ELSE '其他'
END AS status_category,
COUNT(*) AS cnt,
CONCAT(ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 1), '%') AS pct
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= CURDATE()
GROUP BY status_category
ORDER BY cnt DESC;
新知识点: SUM(COUNT(*)) OVER() = “把所有 COUNT(*) 加起来得到总数”。比如三组分别是 20000、500、100,总和 = 20600,第一组占比 = 20000/20600 ≈ 97.1%。
场景 7|窗口函数(在组内排名)
❗ 小白预警: 窗口函数稍微难一些。如果刚开始学 SQL,先跳过这个场景,后面再回来看。
场景故事
你想知道:“昨天每个小时里,错误数最多的 TOP 3 接口是哪些?”
普通
GROUP BY只能算总数,算不出"每个小时内排第几名"——这需要窗口函数。
什么是窗口函数?一个例子看懂
普通 GROUP BY:
9:00 组 → /api/order/create 45次
9:00 组 → /api/order/pay 30次
9:00 组 → /api/user/login 8次
窗口函数多了一列"组内排名":
9:00 组 → /api/order/create 45次 排名 1
9:00 组 → /api/order/pay 30次 排名 2
9:00 组 → /api/user/login 8次 排名 3
格式:
ROW_NUMBER() OVER (
PARTITION BY 分组依据 ← "在每个组内"
ORDER BY 排序依据 ← "按什么排序来排名"
) AS 排名
SQL → 逐行拆解
WITH hourly_errors AS (
SELECT
DATE_FORMAT(created_at, '%Y-%m-%d %H:00') AS hour_bucket,
api_path,
COUNT(*) AS error_count,
ROW_NUMBER() OVER ( -- 🆕 窗口函数
PARTITION BY DATE_FORMAT(created_at, '%Y-%m-%d %H:00') -- 按"小时"分组
ORDER BY COUNT(*) DESC -- 错误数多的排前面
) AS rn -- 排名结果叫 rn
FROM operation_logs
WHERE app_name = 'order-api'
AND log_level IN ('ERROR', 'FATAL')
AND created_at >= CURDATE() - INTERVAL 1 DAY
GROUP BY hour_bucket, api_path
)
SELECT hour_bucket, api_path, error_count
FROM hourly_errors
WHERE rn <= 3 -- 只要前 3 名
ORDER BY hour_bucket DESC, rn ASC;
执行过程(三步):
第①步:GROUP BY 算出每小时每个接口的错误数
第②步:ROW_NUMBER() 在每个小时组内排名
→ 9:00 组:create=1, pay=2, login=3, refund=4
→ 8:00 组:create=1, refund=2
第③步:WHERE rn <= 3 → 过滤掉排名 4 以后的
预期输出:
+------------------+---------------------+-------------+
| hour_bucket | api_path | error_count |
+------------------+---------------------+-------------+
| 2026-06-22 09:00 | /api/order/create | 45 |
| 2026-06-22 09:00 | /api/order/pay | 30 |
| 2026-06-22 09:00 | /api/user/login | 8 |
| 2026-06-22 08:00 | /api/order/create | 12 |
| 2026-06-22 08:00 | /api/order/refund | 5 |
+------------------+---------------------+-------------+
什么时候用? 需要"每组里的前 N 个"——每个接口最慢的请求、每个小时的 TOP 错误、每个用户的首次登录。普通 GROUP BY 做不到。
场景 8|分页与大数据量查询的坑
场景故事
你要导出上个月所有 ERROR 日志。数据大概有几十万行。
如果你写
LIMIT 100 OFFSET 100000→ MySQL 会先查 100100 行,丢掉前 100000 行,只留 100 行——超级慢!
三种方案对比
-- ❌ 不要这样写!OFFSET 越大越慢
SELECT * FROM operation_logs WHERE log_level = 'ERROR'
ORDER BY id LIMIT 100 OFFSET 100000;
-- ✅ 方案一:游标分页(用 id 定位)
-- 第 1 页:
SELECT * FROM operation_logs WHERE log_level = 'ERROR'
ORDER BY id LIMIT 100; -- 最后一条 id = 100100
-- 第 2 页(记住上次的位置):
SELECT * FROM operation_logs
WHERE log_level = 'ERROR'
AND id > 100100 -- 🔑 直接从 100100 之后查
ORDER BY id LIMIT 100;
-- ✅ 方案二:按时间分批(导出数据最实用)
SELECT * FROM operation_logs
WHERE created_at >= '2026-06-01'
AND created_at < '2026-06-02' -- 每次拿 1 天
ORDER BY id ASC;
| 方案 | 适用场景 | 性能 |
|---|---|---|
LIMIT x OFFSET y |
小数据量管理后台 | 差 |
WHERE id > last |
数据导出、API 翻页 | ✅ 优秀 |
WHERE created_at |
日志导出脚本 | ✅ 优秀 |
插入(INSERT)
场景故事
数据导入脚本,要把日志从备份恢复到 MySQL。
-- 单条插入:一次加一行
INSERT INTO operation_logs
(trace_id, app_name, log_level, message, req_ip, api_path, cost_ms, created_at)
VALUES
('manual-001', 'order-api', 'INFO', '手动回放请求确认修复', '127.0.0.1', '/api/check', 0, NOW());
-- 批量插入:一次加多条(比逐条快 50 倍)
INSERT INTO operation_logs (trace_id, app_name, log_level, message, req_ip, api_path, cost_ms, created_at)
VALUES
('batch-001', 'order-api', 'ERROR', 'DB connection timeout', '10.0.1.1', '/api/order', 3210, NOW()),
('batch-002', 'order-api', 'WARN', 'Slow query detected', '10.0.1.2', '/api/user', 2100, NOW()),
('batch-003', 'order-api', 'INFO', 'Request completed', '10.0.1.3', '/api/ok', 45, NOW());
注意事项:
- ⚠️ 批量控制在 500~1000 条一次,太大导致 binlog 暴涨、主从延迟
- ⚠️ 日志表每秒写入量大时,建议按天/月分区,否则单表几亿行 INSERT 也会慢
更新(UPDATE)——安全第一
🚨 铁律:先 SELECT 再 UPDATE
-- ❌ 错误:忘写 WHERE → 全表更新!没有后悔药
-- UPDATE operation_logs SET log_level = 'ERROR';
-- ✅ 正确:
-- 第①步:先 SELECT 确认
SELECT id, log_level FROM operation_logs WHERE id = 12345;
-- 第②步:确认无误再 UPDATE
UPDATE operation_logs SET log_level = 'ERROR' WHERE id = 12345;
-- 批量更新(记得加时间范围)
UPDATE operation_logs
SET log_level = 'DEBUG'
WHERE req_ip = '10.0.1.100'
AND created_at >= '2026-06-01'; -- ✅ 加时间范围,避免全表锁
删除(DELETE)——最危险的操作
场景故事
日志表 500GB,磁盘告警。你要清理 90 天前的数据。
❌ 新手写:
DELETE FROM operation_logs WHERE created_at < '2026-01-01'
→ 删几千万行 → ① 锁大量行 ② 撑爆 binlog ③ 主从延迟数小时
正确做法:分批删除
-- 每次只删 1 万行,跑完等 1 秒再下一批
DELETE FROM operation_logs
WHERE created_at < '2026-01-01'
LIMIT 10000;
-- Shell 循环脚本:
-- while true; do
-- mysql -e "DELETE FROM operation_logs WHERE created_at < '2026-01-01' LIMIT 10000;"
-- sleep 1
-- done
最优方案:建表时按时间分区
-- 建分区表(按月)
CREATE TABLE operation_logs_partitioned (
id BIGINT UNSIGNED AUTO_INCREMENT,
-- ...
created_at DATETIME(3) NOT NULL,
PRIMARY KEY (id, created_at) -- 分区键必须包含在主键中!
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
PARTITION p202607 VALUES LESS THAN (TO_DAYS('2026-08-01')),
PARTITION p202608 VALUES LESS THAN (TO_DAYS('2026-09-01')),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- 清理旧分区:秒级!
ALTER TABLE operation_logs_partitioned DROP PARTITION p202606;
三种清理方案对比:
| 方案 | 速度 | 对业务影响 |
|---|---|---|
DELETE ... LIMIT 10000 循环 |
慢但安全 | ✅ 几乎无影响 |
TRUNCATE TABLE |
瞬间 | ⚠️ 需停写 |
DROP PARTITION |
秒级 | ✅ 无影响 ⭐ |
什么时候用分区? 日志表 > 100GB 或每天新增 > 100 万行时,一定要分区。清理从小时级变秒级。
日志查询思维导图(一张表总结)
| 你的需求 | 所用技术 | 核心注意点 |
|---|---|---|
| 🔴 实时排障,查最近日志 | WHERE created_at >= + ORDER BY DESC LIMIT |
必须带时间范围 + 走索引 |
| 📈 看趋势、做周报 | GROUP BY DATE_FORMAT(...) + SUM(IF()) |
超大量数据用汇总表 |
| 🔍 搜日志关键词 | LIKE '%keyword%' + 缩小时间窗 |
大日志用 ES,别硬查 MySQL |
| 👤 查某用户/IP | WHERE user_id = x / WHERE trace_id = x |
对应字段必须建索引 |
| 🐌 查最慢的接口 | GROUP BY + AVG(cost_ms) + ORDER BY DESC |
先过滤冷门接口(HAVING) |
| 🗑️ 清理过期日志 | DELETE LIMIT N 循环 或 DROP PARTITION |
永远不要一把删千万行 |
| 🔗 日志 + 业务信息关联 | JOIN / LEFT JOIN |
未登录的用 LEFT JOIN |
| 🏆 每组里的前 N 个 | ROW_NUMBER() OVER (PARTITION BY ...) |
窗口函数,普通 GROUP BY 做不到 |
| 📋 分批导出大数据 | WHERE id > last 或 WHERE created_at BETWEEN |
不要用 OFFSET |
索引管理
是什么: 索引就像书的目录——没有索引就是逐页翻书(全表扫描)。
查看索引
-- 查看表的索引
SHOW INDEX FROM users;
-- 找出没有主键的表(运维巡检用)
SELECT table_schema, table_name
FROM information_schema.tables
WHERE engine = 'InnoDB'
AND table_schema NOT IN ('mysql', 'sys', 'performance_schema', 'information_schema')
AND (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = information_schema.tables.table_schema
AND table_name = information_schema.tables.table_name
AND index_name = 'PRIMARY') = 0;
添加和删除索引
-- 添加普通索引
CREATE INDEX idx_email ON users(email);
-- 添加唯一索引
CREATE UNIQUE INDEX idx_email_unique ON users(email);
-- 添加复合索引(最左前缀原则)
-- 场景:经常按 status + created_at 查询
CREATE INDEX idx_status_created ON users(status, created_at);
-- 删除索引
DROP INDEX idx_email ON users;
-- 查看索引使用情况(排查慢查询)
SELECT * FROM sys.schema_unused_indexes;
索引优化原则
| 原则 | 说明 | 反例 |
|---|---|---|
| 最左前缀 | 复合索引 (a,b,c) 能走 (a)、(a,b)、(a,b,c) |
WHERE b=1 不走 |
| 区分度优先 | 性别等低区分度字段不适合单列索引 | WHERE gender='M' 走索引也慢 |
| 不要过多索引 | 写性能下降,占用磁盘 | 一张表超过 5~8 个索引要警惕 |
| 覆盖索引 | 查询的列都在索引里,不回表查 | — |
💡 如何确认查询是否走索引:
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- 看 type 列:const > ref > range > index > ALL(ALL 就是全表扫描)
用户与权限
创建用户
-- 创建用户(密码用强密码)
CREATE USER 'app_user'@'10.0.1.%' IDENTIFIED BY 'StrongP@ss123';
-- 查看所有用户
SELECT user, host, account_locked FROM mysql.user;
参数说明:
| 参数 | 含义 | 建议 |
|---|---|---|
'app_user'@'10.0.1.%' |
用户名@允许登录的 IP 段 | 不要用 % 允许所有 IP |
IDENTIFIED BY |
密码 | 至少 16 位,含大小写 + 数字 + 特殊字符 |
授权
-- 授予库级权限(最小权限原则)
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'10.0.1.%';
-- 授予只读账号(报表查询专用)
GRANT SELECT ON mydb.* TO 'readonly'@'10.0.1.%';
-- 授予 DBA 权限(仅限个人账号)
GRANT ALL PRIVILEGES ON *.* TO 'dba_zhangsan'@'10.0.1.%' WITH GRANT OPTION;
-- 刷新权限(修改权限后必须执行)
FLUSH PRIVILEGES;
-- 查看用户权限
SHOW GRANTS FOR 'app_user'@'10.0.1.%';
撤销权限和删除用户
-- 撤销权限
REVOKE DELETE ON mydb.* FROM 'app_user'@'10.0.1.%';
-- 删除用户
DROP USER 'old_user'@'10.0.1.%';
注意事项:
- ⚠️ 不要用 root 给应用用! 给应用的最小权限:
SELECT, INSERT, UPDATE, DELETE - ⚠️ 权限变更后一定要
FLUSH PRIVILEGES - 💡 用
SHOW GRANTS定期审计用户权限
备份与恢复
mysqldump 逻辑备份
# 备份单个库
mysqldump -h 10.0.1.100 -u root -p --single-transaction --routines --triggers --events mydb > mydb_20260623.sql
# 备份多个库
mysqldump -h 10.0.1.100 -u root -p --single-transaction --databases db1 db2 db3 > multi_db.sql
# 备份全部库
mysqldump -h 10.0.1.100 -u root -p --single-transaction --all-databases --routines > all_dbs.sql
# 只导结构(不要数据)
mysqldump -h 10.0.1.100 -u root -p --no-data mydb > mydb_schema.sql
参数说明:
| 参数 | 含义 | 必加? |
|---|---|---|
--single-transaction |
不加锁读一致性快照(InnoDB) | ✅ 一定要加 |
--routines |
导出存储过程和函数 | ✅ 推荐 |
--triggers |
导出触发器 | ✅ 推荐 |
--no-data |
只导结构不要数据 | 按需 |
--where |
条件导出部分数据 | 按需 |
恢复
# 恢复单个库
mysql -h 10.0.1.100 -u root -p mydb < mydb_20260623.sql
# 或者在 MySQL 内执行
mysql> source /backup/mydb_20260623.sql;
快速查看备份文件内容
# 只看备份中的建表语句,不看数据
grep "^CREATE TABLE" mydb_20260623.sql | head -10
# 查看备份文件大小
ls -lh mydb_20260623.sql
注意事项:
- ⚠️ 恢复前一定要确认目标库为空或已做好备份! 恢复会覆盖数据
- ⚠️ 不要在生产高峰期做 mysqldump,即使
--single-transaction也有额外开销 - 💡 数据量 > 50GB 时考虑用 XtraBackup(物理备份) 替代 mysqldump,速度和恢复时间快得多
性能排查
查慢查询
-- 开启慢查询日志(临时,当前会话有效)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2; -- 超过 2 秒的记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 查看当前正在执行的查询(排查"卡住"的情况)
SELECT * FROM sys.session
WHERE command != 'Sleep' AND time > 10;
-- 查看最近 10 条慢查询(需要 performance_schema)
SELECT * FROM sys.statement_analysis
WHERE avg_latency > 100000000000 -- 大于 100 秒
ORDER BY avg_latency DESC
LIMIT 10;
排查步骤:
-- 1. 找到慢查询的 SQL(从慢查询日志)
-- 2. 用 EXPLAIN 分析执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC;
-- 3. 看 type 和 rows 字段
-- type=ALL → 全表扫描,需要加索引
-- rows=100000 → MySQL 扫描了 10 万行,很大
-- 4. 看是否有文件排序
-- Extra: "Using filesort" → 排序字段没索引
-- 加复合索引:CREATE INDEX idx_user_created ON orders(user_id, created_at);
查看表大小和碎片
-- 查看各库磁盘占用
SELECT
table_schema AS '数据库',
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS '总大小(MB)',
ROUND(SUM(data_length) / 1024 / 1024, 2) AS '数据(MB)',
ROUND(SUM(index_length) / 1024 / 1024, 2) AS '索引(MB)'
FROM information_schema.tables
GROUP BY table_schema
ORDER BY SUM(data_length + index_length) DESC;
-- 查看碎片率(碎片率 > 30% 考虑 OPTIMIZE)
SELECT
table_name,
ROUND(data_free / 1024 / 1024, 2) AS '碎片大小(MB)',
ROUND(data_free * 100 / (data_length + index_length), 2) AS '碎片率(%)'
FROM information_schema.tables
WHERE table_schema = 'mydb' AND data_free > 0
ORDER BY data_free DESC;
查看当前正在跑的事务和锁
-- 查看当前事务(排查事务未提交导致的锁)
SELECT * FROM information_schema.INNODB_TRX\G
-- 查看当前锁等待(排查死锁)
SELECT * FROM sys.innodb_lock_waits;
-- 强制结束长时间未提交的事务(最后手段)
-- KILL 查询线程 ID(从 PROCESSLIST 查到的 id)
KILL 12345;
注意事项:
- ⚠️
KILL会回滚事务,可能导致数据不一致,确认清楚再执行 - 💡 定期巡检:
sys.session表里command != 'Sleep' AND time > 300的查询需要排查
锁与事务
事务控制
-- 开启事务
START TRANSACTION;
-- 执行操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 提交(成功)
COMMIT;
-- 或回滚(失败)
ROLLBACK;
查看事务隔离级别
-- 查看当前隔离级别(默认 REPEATABLE-READ)
SELECT @@transaction_isolation;
-- 修改隔离级别(当前会话)
SET SESSION transaction_isolation = 'READ-COMMITTED';
四种隔离级别:
| 级别 | 脏读 | 不可重复读 | 幻读 | 场景 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ | 几乎不用 |
| READ COMMITTED | ❌ | ✅ | ✅ | 大部分业务(如 Oracle 默认) |
| REPEATABLE READ | ❌ | ❌ | ✅ | MySQL 默认 |
| SERIALIZABLE | ❌ | ❌ | ❌ | 极少数强一致性场景 |
死锁排查
-- 查看最近一次死锁日志
SHOW ENGINE INNODB STATUS\G
-- 查看当前锁等待信息
SELECT * FROM sys.innodb_lock_waits;
预期输出(死锁日志关键信息):
------------------------
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION: // 事务1:等待 A 锁
*** (2) TRANSACTION: // 事务2:等待 B 锁,持有 A 锁
*** WE ROLL BACK TRANSACTION (2) // 自动回滚事务2
💡 预防死锁:
- 多个事务按相同顺序访问表(例如都先操作 account_a 再操作 account_b)
- 缩短事务执行时间,不要在一个事务里做大量查询
- 使用
READ COMMITTED隔离级别可以减少间隙锁导致的死锁
常用运维命令速查
| 用途 | 命令 | 说明 |
|---|---|---|
| 查看版本 | SELECT VERSION(); |
确认 MySQL 版本 |
| 查看运行时间 | SHOW STATUS LIKE 'Uptime'; |
数据库持续运行秒数 |
| 查看最大连接数 | SHOW VARIABLES LIKE 'max_connections'; |
默认 151,生产一般 500~2000 |
| 当前连接数 | SHOW STATUS LIKE 'Threads_connected'; |
当前使用连接数 |
| 查看字符集 | SHOW VARIABLES LIKE 'character_set_%'; |
确认是否 utf8mb4 |
| 查看 binlog 状态 | SHOW MASTER STATUS; |
确认主库 binlog 文件名和位置 |
| 查看主从状态 | SHOW SLAVE STATUS\G |
检查主从同步是否正常 |
| 查看数据库文件路径 | SHOW VARIABLES LIKE 'datadir'; |
数据文件存放目录 |
| 查看最大允许包大小 | SHOW VARIABLES LIKE 'max_allowed_packet'; |
默认 64M,大字段需调大 |
| 查看错误日志位置 | SHOW VARIABLES LIKE 'log_error'; |
排查异常的重要文件 |
快速健康检查命令
# 一键检查 MySQL 健康状态(返回关键指标,适合写入监控脚本)
mysqladmin -h 10.0.1.100 -u root -p ping
mysqladmin -h 10.0.1.100 -u root -p status
mysqladmin -h 10.0.1.100 -u root -p extended-status
# 或者 Shell 脚本方式
mysql -h 10.0.1.100 -u root -p -e "
SELECT 'Uptime' as k, VARIABLE_VALUE as v FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Uptime'
UNION ALL
SELECT 'Connections', VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Threads_connected'
UNION ALL
SELECT 'Slow_queries', VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Slow_queries';
"
附录:MySQL 运维 QA 速查
Q1:数据库连接数满了怎么办?
-- 1. 用 root 账号登录
mysql -u root -p
-- 2. 查看当前连接分布
SELECT user, COUNT(*) AS cnt FROM information_schema.PROCESSLIST GROUP BY user;
-- 3. 临时加大连接数
SET GLOBAL max_connections = 500;
-- 4. 杀掉空闲连接(注意:不要 kill 重要业务连接)
-- 杀掉 Sleep 超过 300 秒的连接
SELECT CONCAT('KILL ', id, ';') AS kill_cmd
FROM information_schema.PROCESSLIST
WHERE command = 'Sleep' AND time > 300;
-- 把输出的 KILL 语句复制执行
Q2:磁盘空间满了,MySQL 起不来?
# 1. 清理慢查询日志 / 错误日志
truncate -s 0 /var/log/mysql/slow.log
truncate -s 0 /var/log/mysql/error.log
# 2. 清理 binlog(确认从库已同步后)
mysql -u root -p -e "PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;"
# 3. 如果还不行,清理大表历史数据
# 见 DELETE 分批删除章节
Q3:主从同步延迟了怎么办?
-- 1. 查看延迟
SHOW SLAVE STATUS\G
-- 看 Seconds_Behind_Master 字段
-- 2. 常见原因:
-- - 从库写入慢 → 升级从库配置或优化慢查询
-- - 大事务 → 避免长事务,大操作分批执行
-- - 从库在做备份 → 错开备份时间
-- 3. 临时跳过错误(不推荐,仅紧急恢复用)
STOP SLAVE;
SET GLOBAL sql_slave_skip_counter = 1;
START SLAVE;
最后更新: 2026-06-23
📄 已保存 →
mysql-document/MySQL-常用操作-001.md
评论区