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
数据增删改查
先看看我们用的表结构: 下面所有例子都基于这两张表,方便你对照理解。
-- 日志表:记录系统操作日志(如 API 请求、错误、任务执行)
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(可能为空:未登录)',
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='操作日志表';
-- 用户表
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='用户表';
场景 1|按时间范围查日志(运维最高频操作)
场景故事: 今早 10:05 接到告警,说 order-api 在 09:50~10:00 之间 500 飙高。你要立刻拉出那 10 分钟的 ERROR 日志,看看到底出了什么错。
-- 🎯 需求:查某段时间内某应用的 ERROR 日志
SELECT trace_id, created_at, message, api_path, cost_ms, req_ip
FROM operation_logs
WHERE app_name = 'order-api'
AND log_level = 'ERROR'
AND created_at >= '2026-06-23 09:50:00'
AND created_at < '2026-06-23 10:00:00'
ORDER BY created_at DESC;
预期输出:
+----------------------------------+-------------------------+-----------------------------------------+----------------------+--------+-------------+
| 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 |
+----------------------------------+-------------------------+-----------------------------------------+----------------------+--------+-------------+
解释这行输出: 两条 ERROR:一条 Redis 超时(耗时 3.2 秒),一条死锁(耗时 5.4 秒)——大概率是 Redis 先扛不住,业务降级失败导致数据库死锁。
参数说明:
| 写法 | 含义 | 为什么这么写 |
|---|---|---|
>= AND < |
左闭右开区间 | 避免 23:59:59 漏掉毫秒级数据 |
ORDER BY created_at DESC |
最新在最前 | 排查问题永远先看最近的 |
created_at 有索引 |
索引下推 | 10 分钟的数据量小,秒级返回 |
💡 变体一:查过去 N 分钟(巡检脚本用)
-- 🎯 需求:每分钟跑一次的巡检脚本,只查最近 5 分钟的 ERROR
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
GROUP BY api_path
ORDER BY error_count DESC;
什么时候用? 写监控脚本时——比如用 Zabbix / Prometheus 每 1 分钟调一次这个 SQL,如果某接口错误数突增就告警。
💡 变体二:查某个 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;
什么时候用? 你在告警里看到一个 trace_id→把 ID 粘进这 SQL→按时间正序排列,就能看到这个请求从入口到每一环的完整日志,像查流水一样排查。
场景 2|聚合统计(排查拐点、看趋势)
场景故事: 老板问"昨天订单接口成功率多少?"或者你在做周报,要看这周每天 500 错误的变化趋势。
-- 🎯 需求:按小时统计某应用过去 24 小时的请求量 + 错误量 + 平均耗时
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,
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
GROUP BY DATE_FORMAT(created_at, '%Y-%m-%d %H:00')
ORDER BY time_bucket ASC;
预期输出(节选):
+------------------+-----------+----------+-------------+------------+------------+
| 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% 错误率),10:00 突然飙到 9%,11:00 升到 38%——明显是 10 点有发布或流量突变,需要结合发布记录确认。
什么时候用? 每天早晨看巡检看板、出周报、或收到告警后看"现在比过去严重多少"时。这张表一看就知道:拐点在哪、影响多大、趋势是好转还是恶化。
💡 变体:按天统计,比对最近两周
-- 🎯 需求:这周每天的错误率 vs 上周同一天
SELECT
DATE(created_at) AS day,
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;
场景 3|搜索日志内容(模糊匹配 + 全文检索)
场景故事: 线上出问题了,你只知道日志里包含关键字 “Redis timeout” 或者某个用户反馈的交易单号,你要从几千万行日志里捞出来。
-- 🎯 需求:搜索日志内容中包含某个关键词的记录(如 "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 '%xxx%' 不走索引,会全表扫描。日志表几百 GB 时这么查会直接把数据库打挂。
💡 最佳实践: 日志搜索不要直接扫数据库,应该用 Elasticsearch(ELK 套件)。只有以下情况才在 MySQL 里搜:
- 已知时间范围很小(比如只搜最近 10 分钟)
- 表不大(几百万行以内)
- 临时排查,搜完就完
-- ✅ 如果非要搜,缩小时间范围 + 加其他索引条件
SELECT trace_id, message, api_path
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= '2026-06-23 09:50:00'
AND created_at < '2026-06-23 10:00:00'
AND (message LIKE '%Redis%' OR message LIKE '%timeout%')
AND log_level IN ('ERROR', 'WARN')
ORDER BY created_at DESC;
什么时候用? 你知道大致时间窗口(比如告警说 10:05 出问题),但你不知道具体错误信息——用这种"时间 + 关键词"的组合查询。
💡 另一个变体:查 API 路径级的"最慢 TOP N"
-- 🎯 需求:找昨天哪个接口最慢(排障第一步:确定"受害"接口)
SELECT api_path,
COUNT(*) AS call_count,
ROUND(AVG(cost_ms), 0) AS avg_ms,
ROUND(MAX(cost_ms), 0) AS max_ms,
ROUND(percentile_approx(cost_ms, 0.95), 0) AS p95_ms -- 注意:MySQL 没有内置百分位函数,这是思路
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= CURDATE() - INTERVAL 1 DAY
GROUP BY api_path
ORDER BY avg_ms DESC
LIMIT 10;
什么时候用? 系统变慢了,你要快速锁定哪个接口最慢,而不是盲猜。"先定位再优化"是最省时间的排障方式。
场景 4|多表关联查(把日志和业务信息关联起来)
场景故事: 发现 user_id=12345 的用户一直报错,你要查一下这个人是哪个团队的、有没有权限问题,需要把日志表和用户表关联起来。
-- 🎯 需求:查看某个用户最近的错误日志 + 用户信息
SELECT
l.created_at,
l.log_level,
l.api_path,
l.message,
u.name AS user_name,
u.role AS user_role,
u.status AS user_status
FROM operation_logs l
JOIN users u ON 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;
什么时候用? 用户投诉"我操作不了"时——把 user_id 拿来一查,看具体报什么错,再看用户是不是被禁了、角色权限够不够。
💡 变体:查"谁在搞事"——某 IP 的所有操作轨迹
-- 🎯 需求:某个 IP 在半小时内发了大量请求,怀疑刷接口,查他做了什么
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
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;
什么时候用? 安全同事说"这个 IP 在扫我们的接口"或者你想确认某个来源到底调了什么 API。注意用了
LEFT JOIN——因为日志可能来自未登录请求,user_id 可能是 NULL,LEFT JOIN不会丢掉这些记录。
场景 5|子查询与 CTE(复杂嵌套查询)
场景故事: 你要找"昨天调用最多、且错误率超过 5% 的接口"。这是一个两层需求:先统计每个接口的总调用数和错误数,再过滤出错误率 > 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
WHERE errors * 100.0 / total > 5
ORDER BY errors DESC;
预期输出:
+---------------------+-------+--------+------------+
| api_path | total | errors | error_rate |
+---------------------+-------+--------+------------+
| /api/order/create | 35210 | 2850 | 8.09% |
| /api/payment/pay | 18200 | 1200 | 6.59% |
+---------------------+-------+--------+------------+
写法二:CTE(WITH 子句,逻辑更清晰)
-- 🎯 同上需求,用 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 而不是子查询? 当你要在一个查询里多次引用同一个中间结果时,CTE 写的更清楚。比如你要同时查"错误率>5%"和"平均耗时>1s"的接口:
WITH api_stats AS (
SELECT api_path,
COUNT(*) AS total,
SUM(IF(status_code >= 500, 1, 0)) AS errors,
ROUND(AVG(cost_ms), 0) AS avg_ms
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= CURDATE() - INTERVAL 1 DAY
GROUP BY api_path
)
SELECT * FROM api_stats
WHERE errors * 100.0 / total > 5
OR avg_ms > 1000
ORDER BY errors DESC, avg_ms DESC;
场景 6|CASE WHEN 条件表达式(灵活分类统计)
场景故事: 你要统计最近一周的日志,按严重程度分桶——FATAL+ERROR 算"严重",WARN 算"警告",剩下的算"正常"——看各级别数量。
-- 🎯 需求:按自定义严重等级分组统计
SELECT
CASE
WHEN log_level IN ('FATAL', 'ERROR') THEN '严重'
WHEN log_level = 'WARN' THEN '警告'
ELSE '正常'
END AS 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;
预期输出:
+----------+-------+
| severity | cnt |
+----------+-------+
| 正常 | 285000|
| 警告 | 3200|
| 严重 | 480|
+----------+-------+
什么时候用? 周报/日报统计时——不满足于 MySQL 自带的 enum 分组,要自定义业务含义的分桶。
💡 变体:将状态码转成易读分类
-- 🎯 需求:统计 API 响应分类(2xx / 4xx / 5xx / 其他)
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;
场景 7|窗口函数(排名、同比、移动平均)
场景故事: 你想知道"昨天每个小时内,错误数最多的接口是哪个",或者"每个 API 按耗时排名的前三名"——这就是窗口函数擅长的。
-- 🎯 需求:找出昨天每个小时错误最多的 TOP 3 接口
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
FROM operation_logs
WHERE app_name = 'order-api'
AND log_level IN ('ERROR', 'FATAL')
AND created_at >= CURDATE() - INTERVAL 1 DAY
AND created_at < CURDATE()
GROUP BY hour_bucket, api_path
)
SELECT hour_bucket, api_path, error_count
FROM hourly_errors
WHERE rn <= 3
ORDER BY hour_bucket DESC, rn ASC;
预期输出:
+------------------+---------------------+-------------+
| 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 |
+------------------+---------------------+-------------+
解释: 窗口函数 ROW_NUMBER() OVER (PARTITION BY 分组 ORDER BY 排序) 就是在每个分组内排名。rn=1 就是该小时内错误最多的接口。
什么时候用? 你要找"每组里的前 N 个"时——每个接口最慢的请求、每个小时的 TOP 错误、每个用户的首次登录时间。普通 GROUP BY 做不到这个。
💡 变体:查每个接口最近 7 天的日均耗时趋势(同比前一天的差异)
WITH daily_stats AS (
SELECT
DATE(created_at) AS day,
api_path,
ROUND(AVG(cost_ms), 1) AS avg_ms,
ROUND(AVG(cost_ms), 1) - LAG(ROUND(AVG(cost_ms), 1), 1) OVER (
PARTITION BY api_path ORDER BY DATE(created_at)
) AS diff_from_yesterday
FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= CURDATE() - INTERVAL 7 DAY
GROUP BY day, api_path
)
SELECT day, api_path, avg_ms, diff_from_yesterday
FROM daily_stats
WHERE ABS(diff_from_yesterday) > 100 -- 只看耗时波动超过 100ms 的
ORDER BY day DESC, ABS(diff_from_yesterday) DESC;
什么时候用? 你想自动化发现"哪个接口今天突然变慢了"——
LAG()拿到昨天的值相减,超过阈值就标出来,适合写入巡检脚本。
场景 8|分页与大数据量查询优化
场景故事: 你要导出上个月的所有 ERROR 日志给研发分析。直接用
LIMIT 1000000 OFFSET 0数据库会死得很惨。
-- ❌ 错误写法:OFFSET 越大越慢(OFFSET 100000 也要扫描 100000 行)
SELECT * FROM operation_logs WHERE log_level = 'ERROR' LIMIT 100 OFFSET 100000;
-- ✅ 方案一:游标分页(基于上次的最后 ID)
-- 第 1 页
SELECT * FROM operation_logs
WHERE log_level = 'ERROR'
ORDER BY id
LIMIT 100;
-- 第 2 页(把第 1 页最后一条的 id 拿过来)
SELECT * FROM operation_logs
WHERE log_level = 'ERROR'
AND id > 100100
ORDER BY id
LIMIT 100;
-- ✅ 方案二:基于时间范围分批(适合日志导出)
-- 每次拿 1 天的数据,避免大翻页
SELECT * FROM operation_logs
WHERE app_name = 'order-api'
AND created_at >= '2026-06-01'
AND created_at < '2026-06-02'
ORDER BY id ASC;
三种分页方案对比:
| 方案 | 适用场景 | 性能 | 使用频率 |
|---|---|---|---|
LIMIT x OFFSET y |
小数据量管理后台 | 差(y 越大越慢) | 用户页面翻页 |
WHERE id > last |
日志/流水导出 | ✅ 优秀(走索引) | API 数据导出 |
WHERE created_at |
按时间导出 | ✅ 优秀(走索引) | 批量处理脚本 |
什么时候用哪种? 用户在前台网页翻页可以用 OFFSET(数据量小),但你写脚本导数据一定要用游标或时间分批,否则 OFFSET 100 万行会让 MySQL CPU 100%。
插入(INSERT)——带场景
场景故事: 你写了一个数据导入脚本,要把 100 万行错误日志从备份恢复到 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());
-- 🎯 批量插入(场景:数据导入,一次插 500 条,速度是逐条的 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 IGNORE INTO operation_logs (id, trace_id, app_name, log_level, message, req_ip, api_path, cost_ms, created_at)
VALUES (100001, 'dup-001', 'order-api', 'INFO', 'replayed', '10.0.1.1', '/api/ok', 0, NOW());
注意事项:
- ⚠️ 批量插入控制在 500~1000 条一批,太大导致 binlog 暴涨、主从延迟
- ⚠️ 日志表如果每秒写入量很大,建议分表(按天/按月分区),否则单表几亿行 INSERT 也会变慢
- 💡 大批量导入时临时
SET UNIQUE_CHECKS=0; SET FOREIGN_KEY_CHECKS=0;再导,导完恢复
更新(UPDATE)——带场景
场景故事: 用户迁移了部门,你要批量更新他名下所有日志的归属。或者排查问题时发现一批日志 level 标错了要修正。
-- 🎯 基础更新:修正一条错误标记的日志
-- 🚨 安全准则:先 SELECT 确认 WHERE 条件再执行 UPDATE
SELECT id, log_level FROM operation_logs WHERE id = 12345;
-- 确认无误后:
UPDATE operation_logs SET log_level = 'ERROR' WHERE id = 12345;
-- 🎯 批量更新(场景:某个 IP 是监控探针,调大批日志级别改成 DEBUG 以免干扰统计)
UPDATE operation_logs
SET log_level = 'DEBUG'
WHERE req_ip = '10.0.1.100'
AND app_name = 'order-api'
AND created_at >= '2026-06-01';
-- 🎯 关联更新(场景:禁用一批用户后,标记他们的日志)
-- 把 status=0 的禁用用户,过去 7 天的日志全部标记
UPDATE operation_logs l
JOIN users u ON l.user_id = u.id
SET l.log_level = 'WARN',
l.message = CONCAT(l.message, ' [user_disabled]')
WHERE u.status = 0
AND l.created_at >= NOW() - INTERVAL 7 DAY;
注意事项:
- ⚠️ 生产环境执行 UPDATE 前务必先跑 SELECT 确认 WHERE 条件! 一个手滑就是全表更新
- ⚠️ 大表 UPDATE 加
LIMIT分批执行,避免长事务和主从延迟 - 💡 更新日志表注意时间范围条件,不加时间范围 = 锁全表
删除(DELETE)——带场景
场景故事: 日志表已经 500GB 了,磁盘报警。你要清理 90 天前的数据,但又不能一把全删导致主从延迟。
-- 🎯 基础删除:删一条误录的日志
DELETE FROM operation_logs WHERE id = 999999;
-- 🎯 批量删除(场景:定期清理过期日志——最常用的运维操作!)
-- ⚠️ 不要跑 DELETE FROM operation_logs WHERE created_at < '2026-01-01'
-- 一把删几千万行会:① 锁大量行 ② 撑爆 binlog ③ 主从延迟 N 小时
DELETE FROM operation_logs
WHERE created_at < '2026-01-01'
LIMIT 10000; -- 每批只删 1 万行
-- 写一个循环脚本(Shell 中用):
-- while true; do
-- mysql -e "DELETE FROM operation_logs WHERE created_at < '2026-01-01' LIMIT 10000;"
-- echo "Deleted 10000 rows..."
-- sleep 1
-- done
三种清理方案对比:
| 方案 | 速度 | 对主从影响 | 推荐度 |
|---|---|---|---|
DELETE ... LIMIT N 循环 |
慢但安全 | ✅ 影响小 | 日常清理 |
TRUNCATE TABLE |
瞬间 | ⚠️ 需停写 | 清空整表 |
按时间分区 DROP PARTITION |
瞬间(元数据操作) | ✅ 无影响 | 最优方案 |
💡 最优实践:建表时按时间分区——DROP PARTITION 秒级清理
-- 建分区表(按月分区)
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;
什么时候用分区? 日志表 > 100GB 或者每天新增 > 100 万行时,强烈建议按时间分区。清理数据变成
DROP PARTITION而不是DELETE,速度从小时级变秒级。
日志查询黄金法则(运维总结)
| 场景 | 关键技术 | 核心注意事项 |
|---|---|---|
| 实时排障查最近日志 | WHERE created_at >= + ORDER BY DESC LIMIT |
必须带时间范围 + 走索引 |
| 按时间聚合看趋势 | GROUP BY DATE_FORMAT(...) + SUM(IF()) |
数据量大时考虑用汇总表 |
| 搜索日志关键字 | LIKE '%keyword%' + 缩小时间窗 |
避免全表扫;大日志用 ES |
| 查某个用户/IP/trace | WHERE user_id = x / WHERE trace_id = x |
对应字段必须建索引 |
| 查接口最慢 TOP N | GROUP BY + AVG(cost_ms) + ORDER BY DESC |
配合 P99 计算更准 |
| 定期清理过期日志 | DELETE LIMIT N 循环 或 DROP PARTITION |
永远不要一把删千万行 |
索引管理
是什么: 索引就像书的目录——没有索引就是逐页翻书(全表扫描)。
查看索引
-- 查看表的索引
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
评论区