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 报表系统
-- 单表 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 的流程是这样的:
- 从聚簇索引读取包含该 JSON 列的数据页(通常 16KB)
- 从数据页中提取该行的 JSON 字符串(反序列化,全量)
- 用
JSON_SET函数在 MySQL 服务器层解析 JSON 树,找到目标路径节点 - 修改节点值
- 重新序列化整个 JSON 树为字符串(即使只改了一个布尔值)
- 写回 InnoDB 数据页(整列重写)
- 将这个完整的 JSON 字符串写入 undo log 和 redo log
- 写入 binlog(主从复制也要传全量 JSON)
对比 PostgreSQL JSONB:JSONB 是分解后的二进制格式,每个字段有自己的 TOAST 指针,部分更新只重写受影响的那部分数据,不涉及全量序列化。
真实案例:用户画像系统
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 频繁更新 → 用 MongoDB 或 PostgreSQL 的 JSONB(JSONB 是二进制格式,支持部分更新,性能比 MySQL JSON 好一个数量级)。
③ 全文搜索
MySQL 的全文索引(FULLTEXT)在数据量小(百万级以下)时可用来做简单搜索,但不适合做真正的搜索引擎。
MySQL FULLTEXT 为什么不行?
MySQL 的全文索引底层是倒排索引(Inverted Index),但它的实现非常简陋:
- InnoDB 的 FULLTEXT 索引使用辅助表(Auxiliary Table)存储词条,没有专门的索引文件结构
- 中文分词:InnoDB 的 FULLTEXT 内置 parser 按空格分词,中文只能用
ngram解析器(N 元分词),效果差到不可用——"分布式事务"会被切成"分布"、"布式"、"式事"、"事务"(bigram 模式) - 不支持 TF-IDF 或 BM25 相关度排序,只支持简单的布尔模式匹配
- 不支持同义词扩展、拼音纠错、模糊搜索、权重自定义
真实案例:电商商品搜索
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 全都不匹配:
写多读少:写入是批量的顺序追加,读是最近一段时间窗口的聚合查询。MySQL 的行式存储每行都有自己的主键索引,写入时随机 B+ 树插入,产生大量随机 I/O 和页分裂。时序数据库(如 InfluxDB)用 LSM-Tree 结构,写入是顺序追加,写吞吐高 10 倍以上。
数据压缩:时序数据的相邻值之间变化很小(如连续 CPU 使用率 45.2%、45.3%、45.1%)。列式存储使用 Delta-of-Delta 编码 + 游程编码 + 简单的位压缩,压缩率可达 10:1 到 20:1。MySQL 行式存储的压缩率通常不到 3:1。
降采样和保留策略:时序数据的需求是"1 秒级数据保留 7 天,1 分钟级数据保留 30 天,1 小时级保留 1 年"。MySQL 没有内置的降采样和数据保留策略,需要自己写定时任务删数据(
DELETE FROM device_metrics WHERE ts < NOW() - INTERVAL 7 DAY,这个 DELETE 在几亿行表上跑一次要锁表 10 分钟)。
真实案例:IoT 设备监控
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_size 和 binlog 写盘延迟。
单机瓶颈的量化分析
MySQL 写入路径全链路耗时拆分(16 核物理机,NVMe SSD,sync_binlog=1):
| 阶段 | 耗时 | 占比 |
|---|---|---|
| 语法解析 + 权限检查 | 5μs | 1% |
| InnoDB 行锁获取 | 10-50μs | 5-15% |
| Buffer Pool 中检查/修改数据页 | 3-5μs | 1% |
| 写入 undo log(内存) | 2μs | 0.5% |
| 写入 redo log(prepare,内存 → 磁盘 fsync) | 200-500μs | 50-70% |
写入 binlog(磁盘 fsync,sync_binlog=1) | 200-500μs | 50-70% |
| InnoDB 提交(redo log commit) | 5μs | 1% |
瓶颈在磁盘 fsync:redo log 和 binlog 各做一次 fsync,一次 fsync 约 200-500μs。两个 fsync 意味着单事务提交延迟的 1ms 硬下限。这就是 MySQL 单机 QPS 天花板约 5 万的原因。
为什么 sync_binlog=0 或 innodb_flush_log_at_trx_commit=2 可以提速但危险:
sync_binlog=0:binlog 不主动刷盘,靠 OS 自己刷,延迟从 1ms 降到 0.1ms,但 MySQL 进程崩溃时可能丢失最后 N 秒的 binloginnodb_flush_log_at_trx_commit=2:redo log 每秒刷一次,崩溃时可能丢失最近 1 秒的数据- 生产环境双 1 配置(
sync_binlog=1+innodb_flush_log_at_trx_commit=1)是刚需,不能为了速度牺牲 D 属性
跨节点分布式事务 XA 的坑
# 两阶段提交跨两个 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 ROLLBACK或XA 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 不行",而是"基于什么特征判断什么场景用什么",以及"你踩过的坑教会你什么"。