Skip to content

MySQL 不适用什么场景

面试官追问:MySQL 是万能的吗?什么场景下不应该用 MySQL?

问题

MySQL 是业界最流行的关系型数据库,但不是所有场景都适合用 MySQL。面试官问这个问题,不是要你背 MySQL 的缺点,而是考察技术选型的边界意识——你能不能根据业务场景综合选型,而不是"所有问题一把 MySQL"。

分析

① 海量数据 OLAP 分析

MySQL 的查询执行器是面向 OLTP 优化的,擅长单行/小批量的增删改查,不擅长大表全量扫描 + 聚合

为什么 MySQL 做 OLAP 慢?从引擎层拆开看

MySQL 的 InnoDB 存储引擎是行式存储:一行的所有列物理上连续存放。要读取 SUM(amount) 时,即使只关心 amount 这一列,也必须把整行的所有列读到内存。行式存储的磁盘 I/O 量 = 行数 × 平均行宽,而列式存储只需要读取 amount 这一列的数据。

ClickHouse 的 MergeTree 引擎是列式存储:同一列数据连续存放。读取 SUM(amount) 时,只读 amount 列的数据块。按 TPC-H 的 Lineitem 表(6 亿行)测试,列式存储的磁盘读取量只有行式存储的 1/10 到 1/30(取决于列数)。

再加上 MySQL 的查询执行器是火山模型(Volcano Iterator Model):每行数据从存储层拉上来后,逐行经过 Filter → Projection → Aggregation 算子,每行之间有大量虚函数调用和 context switch。ClickHouse 用向量化执行:一次处理一批(1024 行)数据,用 SIMD 指令并行计算,减少指令缓存 miss。

真实案例:我所在团队负责的 BI 报表系统

sql
-- 单表 5000 万行,跑在 MySQL 5.7,16 核 64G 云服务器
SELECT 
    DATE_FORMAT(order_time, '%Y-%m') AS month,
    SUM(amount) AS total_amount,
    COUNT(DISTINCT user_id) AS active_users
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_time >= '2025-01-01' 
  AND u.vip_level >= 3
GROUP BY DATE_FORMAT(order_time, '%Y-%m')
ORDER BY month DESC;

实际表现

  • MySQL:28 秒(第一次跑甚至要 40 秒,因为 buffer pool 没预热)
  • 同样的数据导入 ClickHouse 后同查询:180ms
  • 优化方向:MySQL 上用物化视图 + 定时汇总表,但需要每天凌晨跑批,数据时效性差 24 小时

踩过的坑

  • 在 MySQL 上给 orders.amount 加索引没用,因为 GROUP BY 和 SUM 要扫全表,索引失效
  • 把 timeout 从 30 秒调到 120 秒,结果 MySQL 查询把 CPU 打满,影响线上订单写入,导致业务连续 3 分钟不可用
  • 最后方案:MySQL 只存最近 7 天订单明细,历史数据迁移到 ClickHouse;BI 报表统一走 ClickHouse,MySQL 只做 TP 查询

结论:OLAP 场景应该用 ClickHouse、Doris、StarRocks 等 OLAP 引擎。


② 文档型 / JSON 频繁变更

MySQL 8.0 虽然支持 JSON 列和 JSON_TABLE 函数,但更新 JSON 内部的某个字段需要全量重写整个 JSON 列

为什么慢?从 InnoDB 页存储角度拆解

MySQL 的 JSON 是 LONGTEXT 的变体,存储为序列化的 JSON 字符串。更新一个嵌套字段时,InnoDB 的流程是这样的:

  1. 从聚簇索引读取包含该 JSON 列的数据页(通常 16KB)
  2. 从数据页中提取该行的 JSON 字符串(反序列化,全量)
  3. JSON_SET 函数在 MySQL 服务器层解析 JSON 树,找到目标路径节点
  4. 修改节点值
  5. 重新序列化整个 JSON 树为字符串(即使只改了一个布尔值)
  6. 写回 InnoDB 数据页(整列重写)
  7. 将这个完整的 JSON 字符串写入 undo log 和 redo log
  8. 写入 binlog(主从复制也要传全量 JSON)

对比 PostgreSQL JSONB:JSONB 是分解后的二进制格式,每个字段有自己的 TOAST 指针,部分更新只重写受影响的那部分数据,不涉及全量序列化。

真实案例:用户画像系统

sql
CREATE TABLE user_profile (
    id BIGINT PRIMARY KEY,
    profile JSON
);

-- 某条用户画像数据,profile 字段大小约 2.5KB
INSERT INTO user_profile VALUES (1, '{
    "name": "张三",
    "tags": ["高消费", "月活", "新客"],
    "contacts": [
        {"type": "phone", "value": "13800138000"},
        {"type": "email", "value": "zhangsan@example.com"}
    ],
    "preferences": {
        "theme": "dark",
        "language": "zh-CN",
        "notifications": {
            "email": true,
            "sms": false,
            "push": true
        }
    }
}');

-- 用户在前端改了一个开关
UPDATE user_profile 
SET profile = JSON_SET(profile, '$.preferences.notifications.sms', true)
WHERE id = 1;

踩过的坑

  • 用户画像系统每天更新 5 亿次用户标签,MySQL 的 JSON 列更新延迟从 1ms 飙升到 50ms 以上
  • binlog 体积暴涨:原来每行 binlog 只记录变更的列,但 JSON 列更新导致整行记录完整 JSON 到 binlog,binlog 从每天 50GB 涨到 800GB,主从复制延迟从 1 秒涨到 30 分钟
  • 切换方案:把用户画像从 MySQL JSON 迁移到 MongoDB(BSON 文档模型,字段级更新),写 QPS 从 3000 涨到 2.5 万,binlog 问题消失

结论:JSON 频繁更新 → 用 MongoDBPostgreSQL 的 JSONB(JSONB 是二进制格式,支持部分更新,性能比 MySQL JSON 好一个数量级)。


③ 全文搜索

MySQL 的全文索引(FULLTEXT)在数据量小(百万级以下)时可用来做简单搜索,但不适合做真正的搜索引擎

MySQL FULLTEXT 为什么不行?

MySQL 的全文索引底层是倒排索引(Inverted Index),但它的实现非常简陋:

  • InnoDB 的 FULLTEXT 索引使用辅助表(Auxiliary Table)存储词条,没有专门的索引文件结构
  • 中文分词:InnoDB 的 FULLTEXT 内置 parser 按空格分词,中文只能用 ngram 解析器(N 元分词),效果差到不可用——"分布式事务"会被切成"分布"、"布式"、"式事"、"事务"(bigram 模式)
  • 不支持 TF-IDF 或 BM25 相关度排序,只支持简单的布尔模式匹配
  • 不支持同义词扩展、拼音纠错、模糊搜索、权重自定义

真实案例:电商商品搜索

sql
CREATE TABLE products (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    description TEXT,
    FULLTEXT INDEX ft_desc (title, description) WITH PARSER ngram
);

-- 搜索
SELECT * FROM products 
WHERE MATCH(title, description) AGAINST('手机壳 硅胶' IN BOOLEAN MODE);

踩过的坑

  • 商品表 600 万条,MySQL FULLTEXT 搜索"手机壳"返回结果包含"手机"+"壳子"等无关商品,召回率 40%,精确率不到 30%
  • 搜索性能:500 万数据量下,MATCH AGAINST 查询耗时 3-8 秒,业务方无法接受
  • 切换方案:引入 Elasticsearch 7.10,用 IK 分词器 + 自定义同义词词典,搜索延迟降到 30ms 以内,召回率 85%+
  • 双写方案:写入 MySQL 后通过 Canal 监听 binlog 同步到 ES,保证 MySQL 做事务,ES 做搜索

结论:真正的搜索场景 → 用 Elasticsearch(Distributed, RESTful, 支持中文分词插件如 IK)、MeiliSearch(轻量,毫秒级响应,适合中小型应用)。


④ 时序数据

MySQL 没有时序数据特有的压缩和降采样功能。

时序数据的特点和 MySQL 的不匹配之处

时序数据有三个核心特征,MySQL 全都不匹配:

  1. 写多读少:写入是批量的顺序追加,读是最近一段时间窗口的聚合查询。MySQL 的行式存储每行都有自己的主键索引,写入时随机 B+ 树插入,产生大量随机 I/O 和页分裂。时序数据库(如 InfluxDB)用 LSM-Tree 结构,写入是顺序追加,写吞吐高 10 倍以上。

  2. 数据压缩:时序数据的相邻值之间变化很小(如连续 CPU 使用率 45.2%、45.3%、45.1%)。列式存储使用 Delta-of-Delta 编码 + 游程编码 + 简单的位压缩,压缩率可达 10:1 到 20:1。MySQL 行式存储的压缩率通常不到 3:1。

  3. 降采样和保留策略:时序数据的需求是"1 秒级数据保留 7 天,1 分钟级数据保留 30 天,1 小时级保留 1 年"。MySQL 没有内置的降采样和数据保留策略,需要自己写定时任务删数据(DELETE FROM device_metrics WHERE ts < NOW() - INTERVAL 7 DAY,这个 DELETE 在几亿行表上跑一次要锁表 10 分钟)。

真实案例:IoT 设备监控

sql
CREATE TABLE device_metrics (
    device_id INT,
    ts TIMESTAMP(6),
    cpu_usage DOUBLE,
    memory_usage DOUBLE,
    temperature DOUBLE,
    PRIMARY KEY (device_id, ts)
);

-- 10 万台设备,每 5 秒上报一次
-- 每天写入量:100000 × 86400/5 = 17.28 亿行

踩过的坑

  • 这个表在 MySQL 上跑了 3 天后写入速度从 2 万行/秒跌到 3000 行/秒,原因是 B+ 树持续页分裂,buffer pool 被时序数据撑爆,热点页频繁淘汰
  • 存储成本:MySQL 存 10 亿行时序数据约占 180GB,InfluxDB 同一数据约 15GB(压缩率 12:1)
  • 数据清理:DELETE FROM device_metrics WHERE ts < NOW() - INTERVAL 7 DAY 在 30 亿行表上跑了 47 分钟,期间大量行锁等待,影响写入
  • 切换方案:用 TDengine(国产时序数据库,压缩率 15:1,写入速度 100 万行/秒),相同查询从 5 秒降到 3ms

结论:时序数据 → 用 InfluxDB、TimescaleDB、TDengine


⑤ 高并发写入 + 强一致性

MySQL 单机写入瓶颈主要来自 innodb_log_file_sizebinlog 写盘延迟。

单机瓶颈的量化分析

MySQL 写入路径全链路耗时拆分(16 核物理机,NVMe SSD,sync_binlog=1):

阶段耗时占比
语法解析 + 权限检查5μs1%
InnoDB 行锁获取10-50μs5-15%
Buffer Pool 中检查/修改数据页3-5μs1%
写入 undo log(内存)2μs0.5%
写入 redo log(prepare,内存 → 磁盘 fsync)200-500μs50-70%
写入 binlog(磁盘 fsync,sync_binlog=1200-500μs50-70%
InnoDB 提交(redo log commit)5μs1%

瓶颈在磁盘 fsync:redo log 和 binlog 各做一次 fsync,一次 fsync 约 200-500μs。两个 fsync 意味着单事务提交延迟的 1ms 硬下限。这就是 MySQL 单机 QPS 天花板约 5 万的原因。

为什么 sync_binlog=0innodb_flush_log_at_trx_commit=2 可以提速但危险

  • sync_binlog=0:binlog 不主动刷盘,靠 OS 自己刷,延迟从 1ms 降到 0.1ms,但 MySQL 进程崩溃时可能丢失最后 N 秒的 binlog
  • innodb_flush_log_at_trx_commit=2:redo log 每秒刷一次,崩溃时可能丢失最近 1 秒的数据
  • 生产环境双 1 配置(sync_binlog=1 + innodb_flush_log_at_trx_commit=1)是刚需,不能为了速度牺牲 D 属性

跨节点分布式事务 XA 的坑

python
# 两阶段提交跨两个 MySQL 实例
# 库存扣减 + 订单创建
try:
    # 第一阶段:prepare
    execute_on_db1("XA START 'xid1'; UPDATE inventory SET stock = stock - 1 WHERE product_id = 1001 AND stock > 0; XA END 'xid1'; XA PREPARE 'xid1'")
    execute_on_db2("XA START 'xid2'; INSERT INTO orders (user_id, product_id, amount) VALUES (42, 1001, 99.00); XA END 'xid2'; XA PREPARE 'xid2'")
    
    # 第二阶段:commit
    execute_on_db1("XA COMMIT 'xid1'")
    execute_on_db2("XA COMMIT 'xid2'")
except Exception as e:
    # 如果 prepare 成功但 commit 失败,需要人工介入查表
    # 因为 XA PREPARE 后的资源已经锁定,不能直接回滚
    # 生产上遇到过 XA 幽灵事务,prepare 后事务悬挂了 3 天
    rollback_xa()

踩过的坑

  • 某个库存系统用 XA 跨 3 个 MySQL 节点,preprare 阶段网络超时,导致 3 个节点上各有一个悬挂的 XA PREPARE 事务,锁住了库存表的多行,线上订单连续 30 分钟无法扣减
  • 排查:XA RECOVER 查看悬挂事务,手动 XA ROLLBACKXA COMMIT,但部分事务不确定是 commit 还是 rollback,需要业务日志逆向确认
  • 结论:XA 在跨节点时,prepare 阶段需要两阶段提交的网络往返,单次事务延迟从 1ms 飙升到 10-20ms,QPS 从 5 万降到 3000 以下。而且悬挂事务是生产灾难

结论:高并发写入 + 强一致性 → 用 TiDB(分布式 NewSQL,自动分片,兼容 MySQL 协议)或 CockroachDB


总结

场景不适合 MySQL 的原因推荐替代
OLAP 海量聚合行式存储,无向量化执行,火山模型逐行处理ClickHouse / Doris / StarRocks
JSON 频繁变更全量重写,无部分更新,binlog 暴涨MongoDB / PostgreSQL JSONB
全文搜索ngram 中文分词效果差,无 BM25 排序Elasticsearch / MeiliSearch
时序数据无压缩/降采样,B+ 树写入随机 I/O,存储成本高InfluxDB / TimescaleDB / TDengine
高并发写入+强一致性双 fsync 1ms 硬下限,XA 悬挂事务灾难TiDB / CockroachDB

但是——面试官真正想听的不是 MySQL 的缺点,而是你能否根据业务场景综合选型。P7 级别的回答应该提到:

  • MySQL 的舒适区上限:单表 5000 万行,QPS 约 5 万(单机 SSD + 16 核,双 1 配置)
  • 冷热分离架构:热数据(最近 30 天)在 MySQL,冷数据迁移到 TiDB 或 ClickHouse。用 pt-archiver 或自研迁移工具定时搬数据,注意迁移期间数据一致性
  • 读写分离变体:写 MySQL,读 MySQL + ES 双写(ES 做搜索和聚合,MySQL 做事务和精确查询)。通过 Canal 监听 binlog 增量同步到 ES,监控同步延迟,延迟超过 5 秒触发告警
  • "先优化,再换库":90% 的"MySQL 不够用"问题加个合适索引就解决了。比如加联合索引、改写 SQL 避免临时文件排序、用覆盖索引消除回表,这些优化做完再考虑换库。千万别一上来就上 TiDB 或 ClickHouse,运维成本翻 3 倍起步
  • 选型决策树:先问三个问题——数据模型是结构化还是文档?查询模式是 TP 还是 AP?一致性要求多高?结构化 + TP + 高一致性 → MySQL 再优化;文档型 + 高写入 → MongoDB;AP + 大表聚合 → ClickHouse;时序 + 高写入 → InfluxDB/TDengine

MySQL 不是全能的,但也不是废物。知道它的边界,才能在合适的场景用合适的工具。面试官问这个问题,他想听到的不是"MySQL 不行",而是"基于什么特征判断什么场景用什么",以及"你踩过的坑教会你什么"。

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。