1. 增删改查的本质与整体设计思路
聊数据库,绕不开的永远是这四个字:增删改查。说句实在话,我入行这些年,经手的业务系统少说也有几十个,从早期的单机管理软件,到后来基于微服务架构的中台系统,无论技术栈怎么换、ORM框架怎么变,最终落到数据库层面,干的事情无非就这四类——插入新数据、查询已有数据、修改旧数据、删除不需要的数据。这个组合在英文里有个专门的说法叫CRUD,对应Create、Read、Update、Delete。不管你的项目吹得多么天花乱坠,本质上都是在跟这四件事打交道。
所以我说,增删改查不是入门知识点,而是贯穿整个开发生涯的核心基本功。很多同学刚接触数据库时觉得SQL很简单,SELECT、INSERT、UPDATE、DELETE各写一遍就完事了,但真到了实际项目里,你会发现同样的操作,不同人写出来的差距非常大。差距不在语法,而在细节——有没有考虑查询走不走索引,插入要不要批量处理,更新有没有加事务,删除之前有没有备份。这些细节才是区分“会写”和“写得好”的分界线。
这篇文章我想换个角度来写:不摆教科书式的语法大全,而是站在一个实际开发者的位置,把增删改查每个操作背后真正要命的细节、原理选型、以及我踩过的一些坑,全部摊开讲一遍。内容偏重MySQL,但大部分思路放在Oracle、达梦、人大金仓这类数据库上也是通的。适合刚学完数据库基础语法、正准备上手做实战项目的同学,也适合写了两年业务代码但一直没时间系统梳理SQL细节的工程师——这篇文章能帮你把这些底层操作梳理成一套干净利落的方法论。
既然是方法论,第一件事就是把四个操作放在同一个视角下来看。增删改查不是孤立的四个动作,它们之间是有关联的。查询是核心,因为一个系统80%以上的数据库压力都来自查询;删除是风险最大的,因为一旦删错没有后悔药;插入和更新是日常操作最频繁的,但也是最容易被忽略细节的。我给自己定的原则很简单:查询要优化,插入要批量,更新要谨慎,删除要备份。这四句话就是今天全文的主线。
先从整体设计讲起。为什么增删改查值得单独拿出来系统梳理?因为它是连接业务逻辑与数据存储的唯一桥梁。你写的是订单系统、库存系统还是用户系统,前端交互无论多复杂,落到后端就是把这些数据通过增删改查写进数据库、再读出来。你对这四个操作的掌握程度,直接决定了系统的稳定性、数据的一致性和响应速度。可以说,增删改查写得有多好,你的系统就能跑多稳。
另外说一句题外话,既然提到数据库,就必须先建立起一个基本概念:数据库本质上就是一个有组织的数据仓库,而SQL(结构化查询语言)是你跟这个仓库对话的语言。增删改查所对应的这四条语句,就是这门语言里最高频的四个词。后面的所有内容,都建立在这个“对话”的基础上。
2. 查询:最常用也最容易出问题的一环
2.1 SELECT的基础结构与执行顺序
先说查询。在四个操作里面,查询是唯一不改变数据状态的操作,但它却是最考验功力的一项。项目里的实际比例我估计过,一个典型业务系统中,查询语句占SQL总量的可能达到70%到80%。你八成会写SELECT,但你是否真正理解它的执行顺序?
这一点非常关键。很多人写SQL只看语法对不对,不看执行逻辑。SELECT语句的书写顺序是SELECT→FROM→WHERE→GROUP BY→HAVING→ORDER BY→LIMIT,但数据库引擎实际执行时顺序是反过来的:先确定数据源(FROM),再过滤行(WHERE),然后分组(GROUP BY),再过滤组(HAVING),接下来才轮到SELECT挑选列和计算表达式,最后才是排序(ORDER BY)和分页(LIMIT)。
为什么要理解这个顺序?因为写SQL时容易犯一个经典错误:想要在WHERE子句里使用SELECT中定义的别名,比如SELECT age*2 AS double_age FROM users WHERE double_age > 30。这在大多数数据库里直接报错,原因就是WHERE执行时,SELECT还没跑,别名根本不存在。这一类问题如果不理解执行顺序,光靠试错,效率很低。
我在实际工作中还发现,很多人习惯把复杂业务全压在一个大查询里面,嵌套好几层子查询,然后抱怨数据库太慢。其实大多数时候,问题不在于数据量大,而在于SQL写得不合逻辑,导致引擎没法用高效的执行计划。先理解顺序,再谈优化,这是第一步。
2.2 WHERE条件的写法规范与索引利用
WHERE是查询的过滤器,决定你查询的数据范围。这里很容易出现的一个问题是:条件写得太宽松,导致扫描大量无关数据。比如查一个订单表,业务上只需要查当天的数据,有人图省事直接在代码里拼SQL,不加时间过滤条件,硬是把整个表几百万行全捞出来再在业务代码里过滤。这种操作就是典型的“数据库白学”——数据库层能过滤的,绝不放到业务层去过滤。
另一个常见的WHERE写法问题是函数包裹。比如你给某张表的某个字段建了索引,却写出WHERE YEAR(create_time) = 2024这样的条件。这个写法在MySQL、Oracle里都试图对每行数据进行计算,计算完才能比较,索引直接失效,全表扫描没商量。但如果你写成WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01',索引就能顺利用上。同样的业务目标,性能天差地别,这就是写SQL有没有考虑索引的区别。
还有一类问题在于字符集与排序规则不一致导致索引失效。之前有个项目,连接数据库时用的连接串指定了utf8字符集,但表的字段是utf8mb4,两边比较字符串时没法直接使用索引,查询明细一下子慢了几倍。排查了半天,最后发现是字符集不一致导致无法走索引。这类坑没有实际经验,光看书根本碰不到。
常年在实际业务里写查询,我总结了一个排查顺序:先看执行计划(EXPLAIN),确认是否走索引;再看扫描行数,确认过滤是否充分;最后看排序和临时表,确认是否有额外开销。这三个键位配合好,90%的慢查询都能定位到原因。
2.3 多表关联查询:JOIN的场景与选择
光查一张表显然不够,业务上大部分查询都要关联多张表。JOIN是面试必问、实操必用的内容,但很多人对它的理解只停留在一张结果图里。
先分清INNER JOIN、LEFT JOIN、RIGHT JOIN的区别:INNER JOIN取交集,LEFT JOIN左表全保留右表匹配不到就补NULL,RIGHT JOIN反过来。实操中INNER JOIN和LEFT JOIN最常见,RIGHT JOIN极少用——因为完全可以调换表位置写成LEFT JOIN,可读性还更好。
但比类型更重要的是关联列的索引设计。JOIN的本质是嵌套循环或者哈希匹配,无论哪种,关联字段上有索引都至关重要。假设你有个用户表和订单表,通过user_id关联,那么user_id这个字段在订单表上一定要建索引。否则每关联一个用户,订单表就要全表扫一遍,两张几十万行的表关联起来,慢到怀疑人生。
我踩过一个印象很深的坑:两张大表做分页查询时使用LEFT JOIN,因为关联字段没索引,一页数据返回要8秒多。当时第一反应是优化SQL,加上了索引之后,查询时间降到200毫秒以内。后面我养成了一个习惯:只要看到JOIN,立刻检查被驱动表的关联列有没有索引。这个检查动作,比任何SQL优化技巧都来得直接有效。
2.4 聚合、分组与排序的统计查询
统计类的查询是增删改查里最接近“分析”的一环。计数、求和、平均值、最大最小值,对应COUNT、SUM、AVG、MAX、MIN,加上GROUP BY做维度分组,HAVING做分组后过滤,一套组合拳下来基本能应对日常报表需求。
但是分组查询有个哲学级的问题:查出来的分组字段不一定是你要的。比如SELECT user_id, MAX(order_amount), order_no FROM orders GROUP BY user_id——这个SQL在MySQL里不报错,order_no返回的到底是哪一行的值,MySQL不保证,Oracle直接就报错。很多新人被这个坑过:分组看最大单金额,顺手把订单号也select出来,结果到线上发现返回的订单号跟最大金额根本对不上。这里必须用子查询或者窗口函数来做逻辑修正。
排序同样有坑。排序字段和时间字段组合排序时,一定要注意索引提供的顺序是否跟业务需要一致。如果发现查询里ORDER BY导致文件排序(filesort),数据量一大就会明显变慢。解决办法通常是调整索引设计,让索引顺序天然满足排序需求。比如查询条件是WHERE status = 1 ORDER BY create_time DESC,建一个(status, create_time)联合索引,让数据库直接按索引顺序扫描返回,连排序都省掉,效率翻倍。
2.5 查询性能问题与索引设计的联动关系
讲了这么多查询细节,不得不单独把索引拎出来说。索引是查询性能的灵魂,但它不是越多越好。
我见过一个极端的项目,开发同学为了让所有查询都快,每张表搞了十几个索引,结果插入和更新慢到离谱。原因很简单:索引需要维护,每插一行数据、每改一条记录,所有涉及到的索引都要同步更新。读快写慢,就是这个代价。
基本原则是这样的:一般查询频繁的字段,尤其WHERE条件里的字段,建立索引;JOIN的关联字段,必须建索引;排序字段如果固定,可以考虑放进联合索引;但索引数量控制在五六个以内,超过就停下来想想到底有没有必要。另一个原则是区分度,像性别字段只有男和女两个值,就算建了索引,查询时也可能被优化器抛弃走全表扫描——字段区分度太差,索引没意义。
查询这个话题细讲可以写一万字,但对于增删改查这条主线,掌握执行顺序、条件写法、索引利用这三件事,就已经能把查询做到80分。
3. 插入与更新:数据写入的细腻活
3.1 INSERT的几种写法与性能对比
查询讲完了,轮到写入。插入操作看起来就是往表里塞数据,但里面值得展开的细节非常密。
先说最简单的INSERT写法:
INSERT INTO users (name, age, email) VALUES ('张三', 25, 'zhangsan@example.com');这个写法大家都会。需要补充的是多行插入的写法,这在批量导入场景下非常实用:
INSERT INTO users (name, age, email) VALUES ('张三', 25, 'zhangsan@example.com'), ('李四', 30, 'lisi@example.com'), ('王五', 28, 'wangwu@example.com');到底是一次插一行还是一把梭批量插几十行,性能差距有多大?在MySQL里,每条INSERT都是一次独立的语句执行,内部有语句解析、权限检查、事务日志记录等开销。如果循环一万次执行单行INSERT,客户端与数据库之间的网络往返就是一万次,光延时就能拖垮性能。批量插入一次性提交多条记录,网络往返降到一次,速度提升不是一点半点。我在一个数据迁移项目里,把单行插入改成每500行一批,迁移耗时从原本估算的40分钟直接缩到3分钟,这个比例你感受一下。
批量插入还有个注意点是单批大小要控制。不是批越大越好,一次性插十万行,事务日志过大,内存消耗也高,锁范围大容易阻塞其他操作。我常用的经验值是500到1000行一批,再根据实际情况调整。
3.2 插入冲突与更新撞车的处理策略
真正考验插入功力的场景,是数据已经存在怎么办。业务上最常见的需求是“存在就更新,不存在就插入”,也就是UPSERT。
MySQL里有专门的语法:
INSERT INTO users (id, name, age, email) VALUES (1, '张三', 26, 'zhangsan@example.com') ON DUPLICATE KEY UPDATE age = VALUES(age), email = VALUES(email);Oracle和达梦这类数据库则用MERGE INTO语法,或者直接先UPDATE再判断影响行数。PostgreSQL有INSERT ... ON CONFLICT DO UPDATE。不同数据库语法千差万别,但核心思路一致:以唯一键或主键为判断依据,冲突时执行更新动作。
这里要提醒一句:UPSERT虽然好用,但它依赖唯一索引。如果你的表连唯一性约束都没建,那这个语法根本不会触发,冲突记录会老老实实再插一遍。曾见过清理完重复数据、准备上线UPSERT逻辑的系统,因为没有在业务字段上建唯一索引,跑了几天才发现库存数据重复计算了一倍。先建唯一约束,再谈UPSERT。
还有一种写法值得补充:REPLACE INTO。它的逻辑是先把冲突的旧记录删掉,再插入新记录。听起来差不多,但副作用很大——删掉再插意味着主键可能变化,外键关联的记录可能受影响,自增ID也会断档。我一般不太推荐生产环境用这个,除非你非常清楚它带来的“先删后插”影响。
3.3 UPDATE的WHERE子句是安全生命线
更新操作是增删改查里最容易“手滑”的一环。SQL本身很简单:
UPDATE users SET age = 26 WHERE name = '张三';但到了生产环境,这句话如果WHERE条件写错或漏写,后果就是整张表的age字段全被改成26。我曾经在一次带教时目睹过同事在测试环境漏加WHERE条件,全表被改,还好是测试库,没造成事故,但那次之后我就立了一条规矩:UPDATE和DELETE语句,写完后先数一下WHERE条件,确认有这个字眼,再按回车。
这听起来很无语对不对?但就是这样的低级错误,在真实团队里隔三岔五就会发生。防止误更新,除了细心,还有几个技术手段:
- 事务里先SELECT确认影响范围,再执行UPDATE;
- 更新前用相同WHERE条件跑COUNT看影响行数;
- 给表的字段加只读约束或触发器保护关键数据;
- 生产环境执行前,把WHERE条件拿出来单独验证。
更新的另一个关键点是更新后索引的维护成本。如果更新的字段本身是索引列,那这个更新不光是改数据,还要同步改索引。所以大量更新高频索引字段时,写入性能会明显下降。业务设计上,要谨慎把高频变化的字段设为索引。
3.4 事务与并发控制下的插入更新
插入和更新,天然落进“事务”这个范畴。为什么事务重要?因为业务上的写入几乎不可能单表完成。比如下单场景,既要插入订单表,又要扣减库存表,再把订单与用户关系写进去,任何一个步骤失败,都不能让数据库停留在“一半成功一半失败”的状态。
事务的ACID特性就是干这个的。原子性保证这批操作要么全成、要么全败;一致性保证数据前后状态符合业务约束;隔离性让并发事务互相不产生脏数据;持久性保证提交后数据不丢。实际工作中,事务用起来也很简单:
START TRANSACTION; -- 扣库存 UPDATE products SET stock = stock - 1 WHERE id = 100 AND stock > 0; -- 插订单 INSERT INTO orders (product_id, user_id, amount) VALUES (100, 1, 99.00); COMMIT;但这里必须补充一个关键细节:当UPDATE影响行数为0时,往往意味着库存已经被扣完或者条件不成立,此时要判断是否需要ROLLBACK,绝不能盲目COMMIT。实际开发中,很多超卖问题就是“UPDATE完没检查影响行数,直接继续业务流程”导致的。
事务与并发控制紧密相关。两个事务同时改同一行数据,数据库会通过锁机制保证安全,但锁也带来了死锁问题。死锁的定义是:两个事务分别持有对方需要的锁,互不相让,僵持不下。比如事务A锁了订单表再想锁库存表,事务B锁了库存表再想锁订单表,两边就卡住了。
处理死锁的常见思路:一是保持多个表的加锁顺序一致,二是在事务里尽量缩短持锁时间,三是设置合理的事务超时时间。MySQL默认会自动检测死锁并回滚其中一个事务,Oracle则通过等待超时机制处理。像这类问题,光靠背概念没用,真要遇到几次线上死锁,你对SQL执行顺序的理解会立刻上一个台阶。
事务另一个要注意的问题是“长事务”。曾经有同事在一个事务里跑了上百条耗时操作的循环,整个事务运行超过一分钟,期间一直没有COMMIT,导致相关表的锁长期不释放,整个业务模块几乎卡死。排查之后,把大事务拆成了多个小事务,问题迎刃而解。凡是涉及写入的代码,心里都要有根弦:能干完的活别拖在一个大事务里慢慢干。
4. 删除:危险系数最高的一环
4.1 DELETE、TRUNCATE、DROP三者怎么选
删除是增删改查里最后一块拼图,也是风险最高的一块。删错、删多、删光,每一种事故场景都让我这个老开发提心吊胆。但“删除”并不只有DELETE一条命令,它实际对应三种不同级别的操作,很多人混为一谈,选错就出事。
- DELETE是删除指定行,属于DML,可加WHERE条件,可以用事务回滚,删除后表结构、索引、自增序列全都在。
- TRUNCATE是清空整个表,属于DDL,不允许加WHERE,通过释放存储页的方式一次性清掉数据,速度快但无法用事务回滚。
- DROP是直接删除整张表,包括表结构、索引、约束、触发器,一步到位彻底消失。
怎么选?业务上只删除部分数据,用DELETE加WHERE;想把一张表全部清空但保留表结构,用TRUNCATE;整张表都不要了,用DROP。这个选择题做错一次,代价就大到没法承受——尤其DROP之后发现数据没备份,那就真是欲哭无泪了。
我记得之前接手过一个老系统,数据量大,归档程序每天凌晨用DELETE清理三个月前的过期数据。一开始还挺好使,但数据量涨到几千万行后,DELETE一次要执行很久,还不断产生Binlog,主从同步延迟越来越高。后来把归档方案改成“分区表 + 定期TRUNCATE分区”,速度从小时级提升到秒级。这个案例说明什么?选对删除方案,不只是安全问题,更是性能问题。
4.2 安全删除的操作规范
如果你在业务代码里写DELETE,下面这几条规范我建议你刻在脑子里:
- DELETE必须带WHERE条件,不带WHERE条件的DELETE,在MySQL里默认是全表删除。虽然可以用safe update模式挡住,但你不能依赖这个开关。
- 执行删除前先SELECT确认范围,用同样的WHERE条件先查出要删除的ID列表,看一眼数量,心里有数。
- 大批量删除要分批进行,每批几百上千行就好,避免一次锁太多行、产生超大事务日志。我实际用过的方案是循环DELETE + SLEEP,能稳定控制对生产库的影响。
- 重要数据表建议做软删除,加一个
is_deleted字段,逻辑删除,查询时默认带上WHERE is_deleted = 0。凡是业务数据涉及审计、追溯的场景,软删除都远优于物理删除。 - 删除前必须备份。生产环境的删除操作,先导出需要删除的ID集合或者整表备份文件,这是最后一道防线。
有些同学觉得加is_deleted字段查询麻烦,每个SQL都要多写一个条件,但经历过一次误删大事故之后,你就知道这多写的一个条件有多值钱。线上数据是无价的,任何额外代码成本都远低于数据恢复的成本。
4.3 误删数据的应急恢复思路
真到了误删那一刻怎么办?我的经验是:先冷静,然后按顺序行动。
如果是在事务里执行了DELETE,第一时间ROLLBACK回滚,这是最理想的情况。
如果是已经COMMIT的误删,那要分数据库来看。MySQL有Binlog,如果开启了Binlog且记录格式是ROW,就可以通过Binlog定位到被删的SQL事件,反向生成INSERT语句,把数据重新插回去。Oracle则有闪回查询功能,通过AS OF TIMESTAMP找回某个时间点之前的数据。我在一个项目里用MySQL Binlog恢复过一张被全表误删的配置表,步骤大致是:
- 先停掉所有写入操作,防止Binlog位置继续推进;
- 用
mysqlbinlog工具解析误删时间段的Binlog日志,找到DELETE事件; - 通过工具或者手写脚本,把DELETE事件的记录转换成INSERT语句;
- 在临时库重放这些INSERT语句,确认无误后再导回生产。
整个过程很考验耐心。但我要特别强调一点:误删之后发现没有备份、也没有开启Binlog,那大概率只能自认倒霉。所以备份不是“有空再做”的事,而是上生产之前就必须安排好的基础设施。
不管哪种数据库,核心原则是日常做好备份与归档策略。没有备份的数据库,就像没有安全带的赛车,快是真的快,出事也是真的出事。
4.4 软删除设计的具体实践
既然说了软删除,这里展开讲一下具体怎么落地。
最常用的方案就是增加一个状态字段:
ALTER TABLE users ADD COLUMN is_deleted TINYINT NOT NULL DEFAULT 0;删除操作变成UPDATE:
UPDATE users SET is_deleted = 1 WHERE id = 123;查询时所有业务SQL都带上is_deleted = 0,这确实烦,但安全。如果担心每次都忘加条件,可以在框架的ORM层做统一拦截,比如MyBatis的拦截器、Spring Data JPA的@Where注解,把软删除条件做成全局默认,开发同学就不需要每人记一条规则。
更细的设计是搭配删除时间字段,比如deleted_at,默认为NULL,删除时写入当前时间,查询时用WHERE deleted_at IS NULL。这个方案一方面能保留删除时间便于审计,另一方面还能通过时间字段做定期清理,比如三个月前的软删除数据可以物理批量删除。比起简单的0/1标记,这个设计更实用。
软删除也有缺点:表数据量会持续增长,唯一索引没法保证业务唯一性。比如用户表里email字段有唯一约束,用户删了之后记录还在,再注册同email就会撞上唯一索引。解决思路是把唯一约束改成“email + deleted_at”的联合唯一索引,或者删除时把email改成“原值_deleted_时间戳”。这些细节你只有踩过坑才会记得住。
5. 工具选型、常见问题与排查实录
5.1 数据库客户端与同步工具怎么选
讲完操作本身,再推荐一些我实际使用过、觉得靠谱的配套工具。增删改查写归写,但你总得有个趁手的客户端去连数据库执行这些SQL。
单说MySQL生态,我常用的客户端有两个:Navicat和DBeaver。Navicat界面友好,表数据直接双击编辑,适合日常管理和调试。DBeaver是开源的,免费,支持MySQL、Oracle、达梦、人大金仓、SQLite等几十种数据库,而且可以装插件扩展功能。如果你要同时连接管理多种数据库,DBeaver更省心。
这里说句跟热词相关的题外话:最近总看到有人问“Navicat怎么连接达梦数据库”“人大金仓数据库怎么用Docker部署”“SQLite用哪个管理工具打开”。达梦、金仓这类国产数据库这几年在政企项目里出现频率非常高,语法大体兼容Oracle和PostgreSQL,但细节处总会蹦出些小差异。DBeaver对达梦的连接支持还算可以,Navicat 16以上版本官方也开始支持达梦数据源了,具体版本要确认。SQLite则可以直接用DB Browser for SQLite这个免费工具,单文件DB,拖进去就能看数据。
数据库同步工具也经常被问到。日常开发环境与生产环境需要同步表结构、数据,或者做数据归档时,用成熟的同步工具能省很大力气。MySQL官方有mysqldump做逻辑备份和迁移,binlog-based方案可以做持续同步。开源工具方面,我之前用过DataX做异构数据源之间的批量同步,也用过SymmetricDS做双向同步。这套东西组合起来,可以应付大多数数据库同步场景。不管用什么工具,同步前后必须做数据校验,行数、关键字段抽样比对,这步不能省。
连接池同样是数据库访问的高频话题。热词里有“MySQL的数据库连接池”,这个问题跳过不去。应用连接数据库如果每次请求都建立物理连接,性能开销非常大。连接池的本质是维护一批复用连接,减少建连和断连的次数。Java生态比较常用的连接池是HikariCP和Druid,HikariCP性能好、配置简单,Druid在监控和SQL审计方面做得更全面。连接池关键参数包括最大连接数、最小空闲连接数、连接超时时间、闲置回收时间等,要根据业务并发量和数据库负载来确定,不是越大越好,连接开太多反而会把数据库压垮。
5.2 增删改查实操中的高频问题速查
实操过程中,有一类问题反复出现,我把它们整理成了速查表,方便你在遇到同样情况时直接对照定位。这上面每一条都是我实际踩过或者帮别人排查过的,不是从文档里抄来的。
| 现象 | 常见原因 | 排查思路 |
|---|---|---|
| 查询越来越慢 | 表数据量增长、索引失效或缺失 | EXPLAIN看是否走索引,检查扫描行数 |
| UPDATE或DELETE影响行数异常多 | WHERE条件漏写或条件太宽 | 立刻查看是否事务内,能回滚就回滚;确认条件范围 |
| 插入大量数据超时 | 逐条插入、事务日志过大 | 改批量插入,每批500~1000行 |
| 两个事务互相卡死 | 表加锁顺序不一致 | 统一加锁顺序,拆分长事务 |
| 死锁频繁发生 | 并发量大、索引缺失 | 抓取死锁日志,优化索引,缩短事务 |
| 主从数据不一致 | 同步工具配置错误、大事务延迟 | 检查从库状态,关注Seconds_Behind_Master |
| 连接池耗尽 | 最大连接数太小、连接泄漏 | 提高最大连接数,排查未释放连接代码 |
| 误删数据后无法恢复 | 没开Binlog、没备份 | 从备份恢复,重建Binlog点位 |
这张表覆盖了增删改查日常运维中最容易碰到的那些状况。每一行背后都是一段“血泪史”,尤其UPDATE和DELETE影响行数异常这条,我在前面的章节已经反复强调,这里不再重复,但我希望你把它当作第一优先级来警惕。
5.3 我踩过的坑与调整过程
最后聊几件我真实经历的项目现场,给大家做参考。
第一个坑就是索引过多导致的写入缓慢。那是一个订单中台项目,开发阶段图查询方便,订单表几乎每个字段都建了索引,整整建了十几个。上线之后查询其实不算慢,但插入订单和更新订单状态时,数据库CPU经常飙到90%以上,业务高峰期甚至出现超时。排查下来发现每一笔订单写入都要维护十几个索引,开销巨大。后来把索引精简到6个,保留高频查询和JOIN关联字段,写入压力立刻降下来了,查询性能也没受到明显影响。这件事教会我一个道理:索引是给查询用的,但它的成本是写入承担的,两边必须平衡。
第二个坑是死锁。库存系统并发扣减时,两条SQL分别以不同顺序更新同一批商品库存,线上出现频繁死锁告警。当时的修复方案是把所有商品更新统一按商品ID排序后再执行,确保多个事务加锁顺序一致,死锁直接消失。这个方案不需要改业务逻辑,只调整了SQL执行顺序,线上重启后死锁率降到零。所以说,死锁不一定非得靠改隔离级别、加锁等待时间去解决,先看加锁顺序,往往能四两拨千斤。
第三个坑是误删。那是帮朋友排查的一个生产事故,一张用户标签表被测试同学误执行了不带WHERE的DELETE,几百万行数据没了。由于开启了Binlog,通过解析Binlog恢复了大部分数据,但恢复过程中业务还在持续写入,最终根据恢复时间点做了数据拼接,才勉强规整回来。整个恢复过程熬了一宿,从那以后我对所有生产环境的删除操作都强制要求“先备份再执行”,哪怕只是删一行,也要批量导出一次。这个习惯救过我很多次。
5.4 个人实操中的几点心得
再多说几句个人体会。数据库的增删改查,说到底是数据系统最小的调度单元,你写出的每一条SQL都会在数据库引擎中触发一系列决策:走哪个索引、怎么关联、如何加锁、什么时候写日志。不要把它们看成“写代码而已”,它们是对数据完整性、性能和稳定性的一次次表态。
我建议所有想提升数据库实操能力的同学,给自己定个规矩:每写一条SQL,都问自己三个问题——这条语句能走索引吗?影响多少行?会不会锁太多资源?这三个问题能回答清楚,你的SQL质量就已经超过大部分同行。
增删改查之外的扩展方向也很多。比如MySQL主从复制、分库分表中间件、数据库服务托管与自动备份、向量数据库这种新兴领域、甚至Excel导入数据库之类的数据集成任务,都是在CRUD基础上生长出来的能力。核心基本功打牢之后,往哪个方向发展都有底气。
最后再补一句:把备份当成默认动作,把WHERE条件当成安全腰带,把事务边界当成职业操守。这三件事记在脑子里,比任何工具都值钱。