侧边栏壁纸
  • 累计撰写 24 篇文章
  • 累计创建 5 个标签
  • 累计收到 5 条评论

目 录CONTENT

文章目录

MySQL-常用操作-001

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

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区