📖 本篇简介:开发规范看起来是"软要求",但每一条规范背后都是一次真实的线上事故。本章汇总日常 CRUD、事务使用、ORM 写法、DDL 与权限管控中最容易踩的 11 个规范类坑,覆盖 SELECT *、隐式类型转换、N+1、大事务、大表 DDL 等高频问题。所有 SQL 示例基于 MySQL 8.0 语法。读完你能在 Code Review 阶段就拦住那些"看起来能跑、上线就炸"的写法。
本章适合已经写完业务代码、准备上线前做自我检查的工程师,也适合 Tech Lead 拿来做 Code Review Checklist。建议配合第 44、45、46、47、53、54、55 章一起阅读,那里有更细的索引与事务专题。
目录
- 坑 1:随手写 SELECT 拉出整张大表
- 坑 2:事务里夹带 RPC 与外部 HTTP 调用
- 坑 3:把事务开成"长事务"不拆分
- 坑 4:WHERE 字段隐式类型转换导致索引失效
- 坑 5:LIKE 左前缀模糊查询把索引废掉
- 坑 6:批量 UPDATE 或 DELETE 不带 LIMIT
- 坑 7:ORM 循环查库制造 N+1
- 坑 8:连接用完不归还,连接池被占满
- 坑 9:大表 DDL 不带 ALGORITHM 与 LOCK
- 坑 10:开发账号直连生产库执行写操作
- 坑 11:字段名见名不知意,上线三个月没人敢改
- 常见注意事项:规范不是凭空定的
- 工具链:把规范焊死在流程里
- 一句话总结
- 速查表:开发规范红线一览
- 自测:你能在 Code Review 拦住它们吗
坑 1:随手写 SELECT 拉出整张大表
场景 / 错误示范:
-- 后台管理系统:运营要"看一下最近的订单"
SELECT * FROM orders
WHERE user_id = 10086;
-- 错误:SELECT * 把订单表所有列(含 2KB 的收货地址快照、JSON 备注)全拉回应用
-- 没有 LIMIT,订单量大时一次返回几十万行
为什么是坑:SELECT * 会把聚簇索引里所有列、甚至溢出页的 TEXT/BLOB 全部读出来,回表次数与 IO 被放大数倍;没有 LIMIT 时,网络包与应用内存都会被瞬间打爆。慢查询日志里 90% 的大 SQL,第一行都是 SELECT *。更隐蔽的是,将来加列后老 SQL 结果集会悄悄膨胀,性能问题会在无人改动的情况下突然爆发。
正确做法:
-- 正确:只取需要的列,强制分页
SELECT order_id, status, amount, created_at
FROM orders
WHERE user_id = 10086
ORDER BY created_at DESC
LIMIT 20; -- 后台列表一律分页,单页不超过 100 行
加分点:团队规范里应明令禁止线上代码出现 SELECT *;如果是联表查询,列名必须写 t1.col, t2.col,避免将来加列后结果集悄悄膨胀。
坑 2:事务里夹带 RPC 与外部 HTTP 调用
场景 / 错误示范:
-- 一段典型的"下单"伪代码(Python + pymysql 风格,本示例基于 MySQL 8.0)
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
-- 下面这步是外部支付网关 RPC,平均耗时 800ms,超时还要重试
call_payment_gateway(1, 100);
INSERT INTO order_log(user_id, amount) VALUES (1, 100);
COMMIT;
为什么是坑:事务期间持有的行锁、undo log、MVCC 版本链都会一直挂着,外部 RPC 一旦超时变慢,整个事务被拖到几秒甚至几十秒,阻塞其他请求。高并发下锁等待堆积,连接池被打满,整站雪崩。
正确做法:
-- 正确:事务内只做"短、快、纯数据库"的操作
-- 1) 事务内扣库存、写订单主表
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
INSERT INTO order_main(user_id, amount, status) VALUES (1, 100, 'PROCESSING');
COMMIT;
-- 2) 事务提交后,再调用外部支付网关
-- 3) 支付结果通过回调/消息队列异步落库,更新订单状态
加分点:事务内严禁出现 HTTP/RPC/发 MQ/写 Redis 之外的任何远程调用;事务目标时长应控制在 200ms 以内,超过就要考虑拆分。
坑 3:把事务开成"长事务"不拆分
场景 / 错误示范:
-- 运营一次性把 10 万条老订单从'PAID'改成'ARCHIVED'
BEGIN;
UPDATE orders SET status = 'ARCHIVED'
WHERE status = 'PAID' AND created_at < '2024-01-01';
-- 这条 UPDATE 跑了 40 秒,期间持锁 10 万行
-- 期间 undo log 暴涨,purge 线程无法清理旧版本,从库延迟跟着飙升
COMMIT;
为什么是坑:长事务持锁时间长、阻塞面广,还会让 purge 线程无法回收旧版本,undo log 表空间只增不减,最终拖慢全库。从库回放时同样会卡,主从延迟从毫秒涨到几分钟。
正确做法:
-- 正确:分批小事务,每批 500~1000 行,批与批之间 sleep 一下
-- Python 伪代码(本示例基于 MySQL 8.0)
for batch in range(200):
cur.execute("""
UPDATE orders SET status='ARCHIVED'
WHERE status='PAID' AND created_at < '2024-01-01'
LIMIT 500
""")
conn.commit()
time.sleep(0.2) -- 给主从复制和 purge 留喘息时间
加分点:线上应开启 innodb_print_all_deadlocks 与 information_schema.innodb_trx 监控,事务时长超过 5 秒就告警。
坑 4:WHERE 字段隐式类型转换导致索引失效
场景 / 错误示范:
-- user 表 phone 字段是 VARCHAR(32),建了索引
CREATE INDEX idx_phone ON user(phone);
-- 错误写法:应用层把 phone 当数字传进来
SELECT * FROM user WHERE phone = 13800001111;
-- MySQL 被迫把 phone 列隐式转成 DOUBLE 再比较:WHERE CAST(phone AS DOUBLE) = 13800001111
-- 函数包裹列 = 索引失效,全表扫描
为什么是坑:当列类型与传入参数类型不一致时,MySQL 会按"数值优先"规则对列做 CAST,相当于在索引列上套了函数,B+ 树索引直接废掉。VARCHAR 与 BIGINT 混用在订单号、手机号上最常见,数据量一大就是慢查询。
正确做法:
-- 正确:传入参数类型与列严格一致,手机号用字符串
SELECT * FROM user WHERE phone = '13800001111';
-- 建表时就把这类字段统一成 VARCHAR
-- phone VARCHAR(32) NOT NULL,
-- order_no VARCHAR(32) NOT NULL,
加分点:Code Review 时凡是 WHERE 字符串列 = 数字 的写法一律打回;ORM 框架也要检查参数绑定类型,避免 Jackson 把字符串反序列化成 Long。
坑 5:LIKE 左前缀模糊查询把索引废掉
场景 / 错误示范:
-- 商品搜索:用户输入关键词"手机",希望搜商品标题
SELECT * FROM product
WHERE title LIKE '%手机%';
-- 错误:前后都带 %,B+ 树索引无从定位起点,退化为全表扫描
为什么是坑:B+ 树索引按前缀有序,LIKE '手机%' 还能走索引区间扫描,LIKE '%手机%' 必须逐行比较,千万级表就是慢查询。商品、文章、日志搜索一旦这么写,数据库很快被打挂。
正确做法:
-- 正确 1:业务上能改前缀就改前缀
SELECT * FROM product WHERE title LIKE '手机%';
-- 正确 2:真正的全文搜索交给 ES / OpenSearch,MySQL 只做主键回查
-- 搜索请求先打 ES,命中 product_id 列表后再回 MySQL 取详情
SELECT product_id, title, price FROM product
WHERE product_id IN (101, 102, 103);
加分点:MySQL 8.0 虽然支持全文索引 FULLTEXT 与 ngram 分词,但中文模糊搜索的召回与排序远不如专用搜索引擎,生产环境不要硬扛。如果只是后台管理系统偶尔按手机号、订单号前缀查一查,正常 B+ 树索引就够了;一旦面对用户输入的关键词模糊搜索,毫不犹豫上 ES。
坑 6:批量 UPDATE 或 DELETE 不带 LIMIT
场景 / 错误示范:
-- 数据清理脚本:删除 90 天前的浏览记录
DELETE FROM browse_log
WHERE updated_at < '2024-07-01';
-- 错误:一次删了 800 万行,产生巨大 undo/redo,主从延迟 30 分钟
-- 期间该表上的所有写入全部被阻塞
为什么是坑:批量 DELETE/UPDATE 是一个大事务,锁行数多、binlog 事件大、主从回放慢;更可怕的是一旦 WHERE 条件写错(比如少写一个 AND),就是全表锁、全表改、全表删,连回滚都来不及。
正确做法:
-- 正确:小批量循环,每批带 LIMIT,控制单次影响行数
-- 本示例基于 MySQL 8.0
DELETE FROM browse_log
WHERE updated_at < '2024-07-01'
LIMIT 1000;
-- 应用层反复执行直到 affected_rows = 0,每批之间 sleep 200ms
加分点:线上执行任何不带 LIMIT 的写操作前,先用 SELECT COUNT(*) 评估影响行数,超过 1 万行必须走分批脚本,禁止直接在客户端跑。
坑 7:ORM 循环查库制造 N+1
场景 / 错误示范:
# Python + SQLAlchemy 伪代码,本示例基于 MySQL 8.0
users = session.query(User).limit(100).all()
for u in users:
# 每个用户都再发一次 SQL 查订单
orders = session.query(Order).filter_by(user_id=u.id).all()
print(u.name, len(orders))
# 实际发出 1 + 100 = 101 次 SQL
# 列表分页 100 条,接口 QPS 100 时,数据库瞬间承受 1 万次 QPS
为什么是坑:N+1 是 ORM 时代最经典的性能杀手,单次接口看似没问题,一上量就把数据库连接池打满。慢查询日志里同一形态的 SQL 重复出现几百次,CPU 都耗在网络往返上。
正确做法:
# 正确 1:用 JOIN 一次取齐
users = session.query(User, Order).join(Order, Order.user_id == User.id).limit(100).all()
# 正确 2:用 IN 批量取
user_ids = [u.id for u in users]
orders = session.query(Order).filter(Order.user_id.in_(user_ids)).all()
# 在内存里按 user_id 分组拼装
from collections import defaultdict
by_user = defaultdict(list)
for o in orders:
by_user[o.user_id].append(o)
加分点:Code Review 看到 for 循环里出现数据库查询,一律要求改成批量;ORM 开启 echo=True 打印 SQL,压测前先数一下 SQL 条数。
坑 8:连接用完不归还,连接池被占满
场景 / 错误示范:
# Python 伪代码(pymysql),本示例基于 MySQL 8.0
def query_user(uid):
conn = pymysql.connect(host='127.0.0.1', user='r', password='x', db='app')
cur = conn.cursor()
cur.execute('SELECT * FROM user WHERE id = %s', (uid,))
row = cur.fetchone()
# 错误:忘记 conn.close(),也没写 finally
return row
# 高并发下,连接不断泄漏,MySQL 端 Threads_connected 飙升到 max_connections 后,新连接全部报错
为什么是坑:MySQL 每个连接对应一个线程,连接数有上限(默认 151)。连接泄漏后,新请求拿不到连接,接口全部报 "Too many connections",整个服务对外不可用。
正确做法:
# 正确:用上下文管理器 / 连接池,保证连接一定归还
from contextlib import closing
def query_user(uid):
# 假设 pool 是预先建好的连接池(如 DBUtils、sqlalchemy.pool)
conn = pool.get_conn()
try:
with closing(conn.cursor()) as cur:
cur.execute('SELECT * FROM user WHERE id = %s', (uid,))
return cur.fetchone()
finally:
pool.put_conn(conn) # 无论异常与否都归还
加分点:线上监控 Threads_connected 与连接池等待队列长度;开发规范里禁止业务代码自己 pymysql.connect,必须走统一封装的数据源。
坑 9:大表 DDL 不带 ALGORITHM 与 LOCK
场景 / 错误示范:
-- 一张 5000 万行的订单表,运营要求加一列 remark
ALTER TABLE orders ADD COLUMN remark VARCHAR(255);
-- 错误:MySQL 默认行为可能 COPY 整张表,期间锁表 30 分钟
-- 业务写入全部 hang 住,告警刷屏
为什么是坑:早期 MySQL 的 ALTER TABLE 是 COPY 方式,整表锁读写。8.0 虽然默认走 INSTANT/INPLACE,但加列类型、加全文索引、改列类型仍可能触发 COPY 或 metadata lock。不显式指定 ALGORITHM 和 LOCK,DBA 无法预判风险。
正确做法:
-- 正确:显式声明算法与锁级别,让 MySQL 校验能否满足,不满足直接报错而不是悄悄锁表
ALTER TABLE orders
ADD COLUMN remark VARCHAR(255) NULL,
ALGORITHM=INPLACE, -- 不复制整表;若实际必须 COPY 则直接报错,提前暴露
LOCK=NONE; -- 允许并发读写;若实际需要 DDL,则直接报错
-- 大表加索引同理
ALTER TABLE orders ADD INDEX idx_user_status(user_id, status),
ALGORITHM=INPLACE, LOCK=NONE;
加分点:超过千万行的表,DDL 一律走 pt-online-schema-change 或 gh-ost 这类在线 DDL 工具,业务低峰执行,并且先在从库上观察一遍。
坑 10:开发账号直连生产库执行写操作
场景 / 错误示范:
-- 开发小哥本地 navicat 里,图省事选了"生产"这个连接
-- 本来只想查一下 user_id=1 的信息,手滑把 SELECT 写成了 DELETE
DELETE FROM user WHERE id = 1;
-- 又手抖没加 WHERE 校验,回车瞬间整表没了
为什么是坑:生产库写权限一旦放到个人手里,任何一次手抖、任何一次脚本 bug 都是不可逆事故。权限应该按"最小可用"原则收敛:开发只读、测试在测试库、生产 DDL 必须走工单 + DBA 执行。
正确做法:
-- 正确 1:生产账号分角色
-- app_rw 应用账号,只允许业务库的 DML
-- ro_readonly 只读账号,开发/DBA 排查用,带 SESSION 级 sql_log_bin=0 权限
-- dba_admin DDL/高权账号,禁止远程登录,只能跳板机 + 工单
-- 正确 2:强制 SQL 审核平台拦截高危语句
-- DELETE/UPDATE 不带 WHERE、DROP/TRUNCATE、ALTER 大表 -> 直接拒绝
加分点:生产库开启 binlog_rows_query_log_events 与审计日志,谁在什么时间执行了什么 SQL 全部可追溯;开发环境一律用脱敏后的从库快照。
坑 11:字段名见名不知意,上线三个月没人敢改
场景 / 错误示范:
-- 一张订单表,字段名看得人一头雾水
CREATE TABLE t1 (
id BIGINT PRIMARY KEY,
a1 VARCHAR(64), -- 这是啥?订单号?
a2 INT, -- 状态?类型?
a3 DECIMAL(12,2), -- 金额?运费?
a4 DATETIME -- 创建时间?支付时间?
);
-- 半年后接手的开发不敢加索引、不敢改字段,只能不停 SELECT * 盲猜
为什么是坑:命名规范是协作成本的第一道防线。a1/a2/foo/bar 这种名字在 Code Review 阶段看不出问题,三个月后新人接手就是灾难:没人知道字段含义、没人敢动表结构、bug 定位要花几倍时间。
正确做法:
-- 正确:字段名见名知意,统一前缀与命名风格
CREATE TABLE trade_order (
order_id BIGINT PRIMARY KEY,
order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号',
order_status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成',
total_amount DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '订单总金额,单位元',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 每张表必须有 created_at / updated_at,每个字段必须 COMMENT
加分点:团队应有统一的命名规范文档:表名用业务下划线小写、布尔字段用 is_/has_、状态字段用 _status、时间字段统一 _at;建表语句评审时把 COMMENT 是否齐全作为硬指标。
常见注意事项:规范不是凭空定的
| 注意点 | 说明 |
|---|---|
| 规范要可执行 | 每条规范必须能翻译成 Code Review 时可勾选的检查项,不能只写"注意性能"这种空话 |
| 规范要有出处 | 每条规范背后至少能对应一次真实事故或一次慢查询案例,新成员培训时讲事故比讲条文有效 |
| 规范要配套工具 | 比如 SQL 审核平台、慢查询日志告警、ORM SQL 打印,把规范"写进工具"而不是"写进 PPT" |
| 规范要随版本演进 | MySQL 8.0 的 INSTANT 加列、直方图、CUBE 等新特性出现后,老规范要及时更新,不能刻舟求剑 |
| 规范要区分场景 | 后台运营脚本与核心交易链路容忍度完全不同,不要一刀切;但"红线"类规范必须一刀切 |
| 规范要有人背 | 每条规范要有 owner,通常是 DBA 或技术委员会,新规范提案要走评审 |
| 规范要有版本号 | 规范文档本身要版本化,重大变更要发通知并组织培训 |
| 规范要考核 | 上线前 SQL 审核拦截率、慢查询占比等指标纳入团队绩效,不然规范就是一纸空文 |
落地建议:每个季度把线上慢查询 TOP20、死锁 TOP10、主从延迟 TOP5 拉出来复盘,看看是哪条规范没被执行,再决定是补规范、补工具还是补培训。
工具链:把规范"焊死"在流程里
规范只写在 Wiki 上是没用的,必须落到工具链上才能强制执行。下面这套组合是中大型团队的常见做法,本示例基于 MySQL 8.0。
-- 1) SQL 审核平台拦截:上线前把 SQL 提交到审核平台,自动扫描
-- 规则示例(用正则/抽象语法树实现):
-- - SELECT 是否带 *
-- - DELETE/UPDATE 是否带 WHERE
-- - WHERE 列类型与参数是否一致
-- - 是否出现左前缀 LIKE
-- - 是否在大表上直接 DDL
-- 2) 开启慢查询日志,把超过 200ms 的 SQL 全部采集
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.2; -- 200ms 即记录
SET GLOBAL log_queries_not_using_indexes = ON;
-- 3) 定期巡检没用过的索引
SELECT OBJECT_SCHEMA, OBJECT_NAME, INDEX_NAME
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE INDEX_NAME IS NOT NULL
AND COUNT_STAR = 0
AND OBJECT_SCHEMA NOT IN ('mysql','sys','performance_schema')
ORDER BY SUM_TIMER_WAIT DESC;
# 4) 应用层统一封装数据源,禁止业务代码直连
# Python 伪代码:所有 DAO 必须走 BaseDAO,连接从连接池取
# 压测时打印每条 SQL 的耗时,P99 > 50ms 就告警
把这四件事做扎实,规范就不再靠"人自觉",而是靠系统兜底。
下面再给一个最小可落地的巡检 SQL,DBA 可以每周跑一次,把命中结果发给对应业务负责人:
-- 巡检 1:找出最耗时的 TOP10 SQL 形态(基于 performance_schema)
-- 本示例基于 MySQL 8.0
SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT/1e12 AS total_sec,
AVG_TIMER_WAIT/1e9 AS avg_ms
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME NOT IN ('mysql','sys')
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
-- 巡检 2:找出从未被使用过的冗余索引
SELECT t.TABLE_SCHEMA, t.TABLE_NAME, t.INDEX_NAME
FROM performance_schema.table_io_waits_summary_by_index_usage t
WHERE t.INDEX_NAME IS NOT NULL
AND t.INDEX_NAME <> 'PRIMARY'
AND t.COUNT_STAR = 0
AND t.TABLE_SCHEMA NOT IN ('mysql','sys')
ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME;
跑出来的结果不要急着 DROP,先观察一个月,确认业务确实没用到再删。
一句话总结
开发规范类踩坑,本质上是"把个人习惯升级成团队约束"。一个人写代码图省事,后果由自己承担;一个团队没有规范,后果由全公司承担。本章列的 11 个坑,没有任何一条是"技术做不到",全都是"图省事"。记住三句话:线上慢查询大多是 SELECT *、没 LIMIT、没索引;线上事故大多是长事务、批量写、权限过大;线上维护成本爆炸大多是命名随意、缺注释、缺审计字段。把这三句话贴在工位上,能躲开本章 80% 的坑。
速查表:开发规范红线一览
下面这张表是本章 11 个坑的浓缩版,建议直接打印贴在工位上。每一行都对应一次真实事故,不是凭空想象的"最佳实践"。
| 类别 | 红线规范 | 违反后果 |
|---|---|---|
| 查询 | 禁止 SELECT *,必须显式列名 | 回表放大、结果集膨胀 |
| 查询 | 必须 LIMIT,单页 ≤100 行 | 大结果集打满内存 |
| 查询 | 禁止左前缀 LIKE '%xx%' | 索引失效全表扫 |
| 事务 | 事务内禁止 RPC/HTTP/MQ | 长事务锁等待雪崩 |
| 事务 | 单事务目标 <200ms,大改分批 | undo 暴涨、主从延迟 |
| 写入 | UPDATE/DELETE 必须带 WHERE 与 LIMIT | 误删全表、大事务 |
| 类型 | WHERE 列与参数类型严格一致 | 隐式转换索引失效 |
| ORM | 禁止循环里查库,必须 IN/JOIN | N+1 打满连接 |
| DDL | 大表 ALTER 必带 ALGORITHM/LOCK | 锁表事故 |
| 权限 | 生产写权限收敛到应用账号 | 误操作不可挽回 |
| 连接 | 禁止业务裸建连接,走连接池 | 连接泄漏 Too many connections |
| 命名 | 字段必加 COMMENT、见名知意 | 维护成本爆炸 |
| 字符集 | 库/表/连接统一 utf8mb4 | 乱码、emoji 截断 |
| 时间 | 统一 DATETIME 或 TIMESTAMP,注明时区 | 跨时区业务时间错乱 |
| 排序 | ORDER BY 字段必须有索引或走覆盖索引 | filesort 大结果集落盘 |
自测:你能在 Code Review 拦住它们吗
题 1:为什么 WHERE phone = 13800001111(phone 是 VARCHAR)会让索引失效?
<details>
<summary>参考答案</summary>
MySQL 会把字符串列 CAST 成 DOUBLE 再与数字比较,相当于在索引列上套了函数 CAST(phone AS DOUBLE) = 13800001111,B+ 树索引无法用于函数结果,退化为全表扫描。应传入字符串 '13800001111'。
</details>
题 2:事务里调用外部支付网关为什么危险?
<details>
<summary>参考答案</summary>
事务期间持有的行锁、undo log 版本、MVCC 读视图都会一直挂着。外部 RPC 一旦变慢或超时,事务被拖到几秒甚至几十秒,阻塞其他业务线程,高并发下连接池打满、锁等待雪崩。正确做法是事务只做纯数据库短操作,外部调用放到事务提交之后,结果通过回调异步落库。
</details>
题 3:批量 DELETE 为什么必须分批 + LIMIT?
<details>
<summary>参考答案</summary>
一次删几百万行是一个大事务,持锁时间长、binlog 事件巨大、主从回放慢,期间该表写入全部阻塞;一旦 WHERE 写错就是全表事故。分批 DELETE ... LIMIT 1000 循环执行,每批之间 sleep,把大事务拆成小事务,控制锁范围与主从延迟。
</details>
题 4:ORM 的 N+1 问题怎么识别与修复?
<details>
<summary>参考答案</summary>
识别:开启 ORM 的 SQL 日志,压测时发现同形态 SQL 重复出现几百次。修复:把循环里的单条查询改成 IN (id 列表) 一次批量取,或用 JOIN 一次取齐,再在内存中按外键分组拼装。
</details>
题 5:大表 ALTER TABLE 为什么要写 ALGORITHM=INPLACE, LOCK=NONE?
<details>
<summary>参考答案</summary>
显式指定后,MySQL 会校验本次 DDL 是否真的支持在线变更;如果实际必须 COPY 或必须锁表,语句会直接报错而不是悄悄跑几十分钟锁死业务。这是把"静默风险"变成"显式失败"的工程手段,避免大表 DDL 锁表事故。
</details>
题 6:线上一条慢查询把主库 CPU 打满,你的第一反应是什么?
<details>
<summary>参考答案</summary>
第一步不要急着 kill,先用 SHOW PROCESSLIST 与 information_schema.innodb_trx 定位是哪条 SQL、来自哪个应用账号、已经跑了多久;第二步评估影响面,是否需要临时 kill 这条会话止损;第三步拉慢日志看执行计划,确认是索引失效还是数据量突增;第四步补索引或改写 SQL,并把这条 SQL 加进 SQL 审核平台规则,防止再次出现。
</details>
题 7:为什么生产库不允许开发直连?
<details>
<summary>参考答案</summary>
个人账号一旦有生产写权限,任何手抖、任何脚本 bug 都可能造成不可逆数据事故。正确做法是按角色分权:应用账号跑业务 DML、只读账号给开发排查、DDL 高权账号只允许跳板机 + 工单执行;同时开启 binlog 审计,所有操作可追溯。
</details>
下一篇预告:第 72 章 业务建模踩坑
文章回复
0 条公开回复