资讯动态

MySQL表的增删改查:从表设计到索引事务的完整指南

发布时间:2026/10/13 7:43:33 来源:尧图企业网站定制
提到MySQL表的增删改查不少人觉得没什么好讲的建表、insert、select、update、delete谁还不会写几条SQL。但真正在线上跑过的人都知道“会写”和“写好”完全是两回事。上周我帮某团队排查一个线上问题业务反馈订单列表打开要十几秒查下来是一条UPDATE语句没走索引把整张表锁了大半后面所有写入都在排队。问题不复杂但暴露出来的恰恰是对表结构、索引和增删改查三者关系的理解不到位。这篇文章想把这个主题“5-MySQL_表的增删改查”系统梳理一遍。这里有个关键前提表的增删改查其实有两个层面一是表结构本身的建、改、删、查也就是DDL操作二是表里数据的增删改查也就是INSERT、SELECT、UPDATE、DELETE这些DML操作。很多人只盯着第二个层面忽略了第一个层面结果上线后才发现字段长度不够、字符集混乱、索引缺失只能反复改表越改越乱。这篇文章会把两个层面都讲透适合刚开始学MySQL的开发者、天天写业务SQL的工程师以及准备转岗数据方向的同学参考。1. 先把表设计想清楚增删改查才不会乱1.1 为什么先谈表设计而不是直接写SQL“增删改查”这四个字听起来是操作但操作的质量高低往往在动手之前就已经被表设计决定了。你可以把一张表理解成一个Excel表格的加强版列是字段有类型、有长度、有默认值行是记录每行数据都必须符合列的约束。插入一条数据如果某个字段是NOT NULL且没有默认值你不填就会直接报错如果给唯一索引字段插入了重复值这条记录也会被拒之门外。更新数据时如果字段类型定义得不合理隐式转换可能导致索引失效整张表被扫一遍。删除数据时如果外键或索引设计不好删除一条记录可能要连带锁住一大片数据。这些事后问题大多不是SQL写得不对而是表结构本身埋下的坑。我在实际项目里见过一个非常典型的例子某系统做用户登记手机号字段被设计成了INT类型。听起来没问题手机号全是数字但INT最大只能存到2147483647而11位手机号早就超过了这个范围。结果插入时MySQL做了隐式转换后面几位直接变成了0数据彻底坏了最后只能重建表、清数据、重新导入。这个教训说明写增删改查之前至少要想清楚三件事这张表要存什么业务对象每个字段用什么类型哪些约束和索引必须提前建立1.2 决定增删改查效率的三个关键设计细节第一是字符集和排序规则。现在建表我基本只用utf8mb4它支持中文、表情符号兼容性最好。但字符集选对了排序规则同样不能忽视。同样一张用户表如果用了utf8mb4_unicode_ci这种不区分大小写的排序规则那么WHERE username Admin可能会匹配到admin对用户名唯一性要求严格的系统来说这就是一个潜在的事故点。更麻烦的是字符集不一致还会导致JOIN时关联字段无法走索引。曾经有过一张表字段是utf8mb4另一张表字段是gbk两张表用手机号关联查询跑了半小时没出来原因就是字符集不同导致索引失效。检查时用SHOW CREATE TABLE看一眼字段字符集再统一成utf8mb4问题立刻消失。第二是主键类型和写入模式。自增主键在InnoDB里是顺序写入的新数据总是追加在B树叶子节点的最后页分裂概率小写入性能高。但如果用UUID做主键它的值是随机无序的每次插入都可能落在索引树的中间位置频繁触发页分裂插入速度和后来查询性能都会明显下降。有些场景确实需要对外暴露业务ID那也建议保留一个自增主键用于内部关联和排序再把UUID或业务编号作为普通唯一索引而不是上来就把UUID设为主键。这个决策会直接影响后续所有插入和更新的稳定性。第三是索引数量。索引能加速SELECT但也会拖慢INSERT、UPDATE、DELETE因为每次数据变更都要同步维护索引结构。索引越多写入路径越长。常见误区是一口气给表加上六七个单列索引查询的时候哪个也用不上写入却越来越慢。更合理的方式是走联合索引比如查询条件是status created_at那就建一个(status, created_at)联合索引而不是分开建两个索引。另外字段长度也值得关注InnoDB对单表行大小是有限制的传统行格式下所有非TEXT/BLOB列的长度总和不能超过65535字节。utf8mb4下每个字符最多占4字节一个VARCHAR(200)字段理论上最多占800字节如果一张表里堆了几十个超长字段很可能会在建表阶段就报row size too large或者在数据达到一定长度后插入失败。所以设计阶段就要对字段长度做减法别指望后面靠UPDATE去补救。2. 表结构的增删改查建表、改表、看表、删表2.1 建表CREATE TABLE 的完整写法和设计习惯大多数业务表的结构都逃不开这几个部分主键、业务字段、状态字段、时间字段。我常用的用户表建表语句长这样CREATE TABLE users ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(64) NOT NULL COMMENT 用户名, email VARCHAR(128) NOT NULL DEFAULT COMMENT 邮箱, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常0禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT用户表;这个写法里有两个容易被忽略的细节。首先是id BIGINT UNSIGNED很多系统用INT做主键通常够用但如果业务增长快、数据量大INT很容易到上限。改成BIGINT UNSIGNED虽然占用8字节但换来的是更长的寿命代价非常小。其次created_at和updated_at直接交给数据库维护用DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP这样应用层就少了很多手动管理时间的代码也避免不同机器时钟不一致的问题。ON UPDATE CURRENT_TIMESTAMP只有在行的内容真正发生变化时才会更新不是每次查询都会变。字段注释也要养成习惯。没有COMMENT的表三个月后再看就跟没有文档的代码一样只能靠猜。联合索引idx_status_created对应的是WHERE status ? ORDER BY created_at DESC这类高频查询这是一个很典型的查询前置设计。我经常看到有人先建表上线后业务查询慢然后才开始加索引其实建表时就能预判大部分查询需求一次性把索引放好。2.2 改表ALTER TABLE 的常用操作与节奏项目开发到中途加字段、改类型、删字段都是常态。下面是几个最常用的ALTER写法-- 新增字段指定位置 ALTER TABLE users ADD COLUMN avatar VARCHAR(255) NOT NULL DEFAULT COMMENT 头像 AFTER email; -- 修改字段类型和默认值 ALTER TABLE users MODIFY COLUMN email VARCHAR(200) NOT NULL DEFAULT COMMENT 邮箱; -- 重命名字段 ALTER TABLE users CHANGE COLUMN email email_address VARCHAR(200) NOT NULL DEFAULT COMMENT 邮箱; -- 删除字段 ALTER TABLE users DROP COLUMN avatar; -- 加索引、删索引 ALTER TABLE users ADD INDEX idx_status(status); ALTER TABLE users DROP INDEX idx_status; -- 重命名表 ALTER TABLE users RENAME TO app_users;这里有几个实操经验。第一CHANGE COLUMN和MODIFY COLUMN都能改字段但CHANGE COLUMN必须写新字段名哪怕你只想改类型也要把旧名字再写一遍容易手误只改类型和默认值时MODIFY COLUMN更安全。第二新增字段时用AFTER email控制顺序但字段顺序不能完全依赖这个习惯查询时永远要显式写字段名不要用SELECT *。第三删除字段和修改类型之前最好先确认有没有代码在引用它很多低级事故就是删了字段后应用启动报错。更重要的是改表这个动作本身对线上服务的影响。MySQL 5.6以后有了在线DDL很多ALTER操作不用再长时间锁表但“不锁表”不等于“没有代价”。在线改表过程中还是会占用IO和CPU资源会产生大量的临时文件和日志在主从架构下还可能造成很大的复制延迟。我的习惯是小表随便改超过百万行的表改结构前必须走变更评审把操作窗口放到凌晨低峰期并且在测试环境先跑一遍同样的语句确认执行时间和影响行数。如果表非常大可以考虑用成熟的在线表结构变更工具但无论如何都要有回滚方案。2.3 删表与查表DROP、TRUNCATE 与信息查看删表的操作看起来简单后果却可能最严重。-- 删除整张表 DROP TABLE IF EXISTS users; -- 清空表数据保留表结构 TRUNCATE TABLE users;DROP TABLE把表结构和数据一起删除删除后如果发现误删只能靠备份恢复。TRUNCATE TABLE和DELETE虽然都是清数据但差别非常大TRUNCATE是DDL会重置自增主键计数不逐行触发删除操作速度极快但在事务里通常不能回滚DELETE是DML可以带WHERE条件可以配合事务回滚但删除大量数据时速度慢、日志大。业务上如果需要清空一张临时表或日志表TRUNCATE很合适如果需要删除部分数据只能用DELETE。还有一点别踩坑TRUNCATE会重置AUTO_INCREMENT如果业务上依赖主键连续清空后重新插入的数据ID会从1开始可能导致和历史数据出现主键冲突操作前一定确认好。查看表信息是排查问题的基础功。用SHOW TABLES看库里有哪些表用DESC users看字段结构用SHOW CREATE TABLE users看建表语句。尤其是SHOW CREATE TABLE它能直接展示当前表的真实字符集、存储引擎、索引定义排查线上问题基本第一件事就是跑它。我见过太多环境差异问题测试环境能跑通的SQL线上报错多半就是两边表结构不一致最直接的排查方式就是对比两个环境的SHOW CREATE TABLE输出。3. 数据的增删改查上INSERT 与 SELECT 的实操要点3.1 插入数据从单行到批量INSERT的几种形态INSERT是最常见的写入操作写法看起来简单但实际应用里有不少讲究。最基本的单行插入INSERT INTO users (username, email, status) VALUES (alice, aliceexample.com, 1);批量插入时一条SQL带多组VALUESINSERT INTO users (username, email, status) VALUES (bob, bobexample.com, 1), (carol, carolexample.com, 1), (dave, daveexample.com, 1);为什么批量比循环单条插入快很多因为每一条INSERT都要经过词法解析、权限校验、事务日志写入、索引更新等流程如果应用层写一个for循环逐条插入几千条数据就是几千次网络往返耗时极长。而一条多值INSERT只需要一次解析和提交整体效率能提升一个数量级。但批量插入也要控制单条SQL的大小我通常控制在500到1000行一次太大反而会导致锁时间过长、binlog过大甚至超出max_allowed_packet限制。插入时还有几种常见需求。比如插入的数据可能和已有唯一索引冲突这时有两种选择。INSERT IGNORE遇到冲突直接跳过不报错适用于“有就拉倒”的场景ON DUPLICATE KEY UPDATE则在冲突时改成更新指定的列。举个例子INSERT INTO users (username, email, status) VALUES (alice, alice_newexample.com, 1) ON DUPLICATE KEY UPDATE email alice_newexample.com, status 1;还有一种REPLACE INTO它遇到主键或唯一索引冲突时会先删除旧记录再插入新记录。很多人把它当成“UPSERT”用但这其实是个陷阱。REPLACE INTO带来的副作用是自增主键ID会重新分配如果这条记录被其他表的字段引用比如订单表里的user_id指向用户表的id一次REPLACE就可能让这个user_id指到另一条新记录上关联关系直接错乱。所以我在业务核心表上基本不用REPLACE需要幂等写入时优先考虑ON DUPLICATE KEY UPDATE。插入后需要拿到自增主键通常用LAST_INSERT_ID()INSERT INTO users (username, email) VALUES (eve, eveexample.com); SELECT LAST_INSERT_ID();要注意这个函数返回的是当前连接最近一次插入生成的自增ID不会和其他连接混淆但如果一次插入了多行它返回的往往是这一批中的第一个ID而不是最后一个所以依赖它逐个拿批量结果是不现实的。3.2 查询数据SELECT的过滤、排序与分页SELECT是使用频率最高的SQL同时也是问题最多的地方。一个比较稳妥的写法是先只取需要的列再考虑条件和排序SELECT id, username, email FROM users WHERE status 1 ORDER BY created_at DESC LIMIT 20;很多人喜欢SELECT *在小表上问题不大但表一旦有几十个字段尤其包含大文本字段时SELECT *会把不需要的数据也传输回来增加网络开销还可能让覆盖索引失效。查询时养成显式列出字段的习惯对线上性能和代码可读性都有好处。WHERE条件写得好不好直接影响能否走索引。常见几个坑对索引列使用函数比如WHERE DATE(created_at) 2024-01-01会让索引失效字符串字段和数字比较比如手机号字段是VARCHAR类型却写成WHERE phone 13800138000MySQL会把字符串转成数字再比较很容易导致索引失效前置通配符合并LIKE %abc%同样无法利用索引。这些写法不是绝对不走索引但至少在数据量大时要重点警惕。分页是查询里的重灾区。LIMIT 100000, 20这种写法在很多老代码里非常常见它的问题在于数据库要先把前100020行都查出来再跳过前100000行返回最后20行翻页越深越慢。解决办法是改成基于主键的游标分页SELECT id, username, email FROM users WHERE id 100000 AND status 1 ORDER BY id LIMIT 20;如果业务场景必须支持任意页码跳转也可以用延迟关联的思路先在子查询里用主键索引拿到当前页的ID再回表取完整数据这样能明显减少回表次数。需要随机取一条记录时不要直接用ORDER BY RAND()它会把整张表排序一边简单粗暴但代价很高。聚合查询也属于增删改查中“查”的一部分。统计不同状态下的用户数SELECT status, COUNT(*) AS cnt, MAX(created_at) AS last_time FROM users GROUP BY status HAVING cnt 100;这里有一个容易被业务误解的点COUNT(*)统计的是行数COUNT(列名)会跳过该列为NULL的行。如果某列允许NULL两个结果可能不同汇报数据前要先想清楚你要的是哪一种。GROUP BY和HAVING配合使用时HAVING里的条件是在分组之后过滤的和WHERE的作用阶段完全不同不能用WHERE替代。3.3 查询进阶JOIN 和 EXPLAIN 的经验单表查询只是基础真实业务里更多是表与表之间的关联。一个典型的JOIN查询SELECT u.username, o.order_no FROM users u JOIN orders o ON o.user_id u.id WHERE o.status 1;JOIN的核心在于关联字段能不能高效走索引。比如orders.user_id上如果有索引MySQL就能通过索引快速找到匹配行如果关联字段类型不一致比如一边是INT一边是VARCHAR或者两边字符集不同索引很可能失效。所以建表时就要保证相同语义的字段使用完全一致的类型和字符集这是写JOIN的前提。LEFT JOIN的坑也很经典。下面这条语句看起来是想返回所有用户包括没有订单的用户SELECT u.username, o.order_no FROM users u LEFT JOIN orders o ON o.user_id u.id WHERE o.status 1;但WHERE o.status 1会把o.status为NULL的记录过滤掉而LEFT JOIN中没有匹配订单的用户o.status正是NULL。结果这个查询等价于INNER JOIN和“返回所有用户”的预期完全不同。要保留无订单用户过滤右表的条件应该写在JOIN的ON里或者改成专属的“无订单用户”查询SELECT u.username FROM users u LEFT JOIN orders o ON o.user_id u.id WHERE o.id IS NULL;这种看起来很简单的问题在线上的出现频率并不低写完SQL最好先跑一遍再说“符合预期”。写复杂查询时不要靠猜性能直接用EXPLAIN看执行计划EXPLAIN SELECT u.username, o.order_no FROM users u JOIN orders o ON o.user_id u.id WHERE o.status 1;重点关注几列type从好到差大概是const、eq_ref、ref、range、index、ALL看到ALL基本就是全表扫描要警惕key表示实际用到的索引rows是优化器估算的扫描行数不一定精确但能反映量级Extra如果出现Using filesort或Using temporary通常意味着排序或分组没走好需要优化索引。养成写完复杂查询就跑EXPLAIN的习惯胜过事后线上救火。4. 数据的增删改查下UPDATE、DELETE 与事务控制4.1 修改数据UPDATE 的安全边界UPDATE的基本语法很简单UPDATE users SET status 0 WHERE id 123;但越是简单越容易出事。我在排查线上问题时见过最危险的SQL就是不带WHERE的UPDATE比如有人想做一个全局重置直接执行UPDATE users SET status 0如果没有事务没有备份整个表的数据瞬间改完恢复代价极高。所以只要涉及UPDATE和DELETE我现在的习惯是先写一条等价的SELECT确认影响范围再执行更新。哪怕只是改一行也要在脑子里过一遍条件。条件字段能不能走索引同样关键。如果一条UPDATE用WHERE username alice而username上没有索引MySQL就要扫描全表找到匹配行扫描期间会对扫描过的行加锁大量并发写入会立刻被阻塞。这也是文章开头那个线上事故的根因。所以好的UPDATE条件应该尽量落到有索引的字段上比如主键ID、唯一键或者至少是普通索引列。批量更新大批量数据时最推荐的方式是分批次而不是一条UPDATE干掉几百万行。MySQL的UPDATE支持LIMIT可以这样分批执行UPDATE users SET status 0 WHERE status 1 LIMIT 1000;反复执行这条语句直到影响行数为0。每次只改一小批事务短、锁持有时间短对主从复制带来的延迟也小得多。千万不要图省事一次性更新一百万行长事务除了锁问题还会产生大量的undolog严重时会把线上拖垮。多表更新在MySQL里可以写成JOIN形式UPDATE users u JOIN orders o ON o.user_id u.id SET u.status 0 WHERE o.status 1;这种语句看起来方便但影响行数往往不如单表更新直观执行前最好先改成SELECT看下匹配数量。如果只是想把某个状态同步到关联表我一般建议拆成两步先查ID列表再按ID更新虽然多几条SQL但可控制、可审计出问题了也容易定位。4.2 删除数据DELETE 的注意点与软删除设计DELETE同样有一批经典坑。DELETE FROM users WHERE id 123;最直接的坑是忘记WHERE导致整个表被清空。以及更隐蔽的坑删除逻辑本身没问题但删除量太大。一次DELETE FROM logs WHERE created_at 2023-01-01如果这条条件命中几百万行MySQL会在执行期间持有大量行锁事务日志暴涨主从延迟飙升甚至可能把生产库CPU打满。正确做法同样是分批删除DELETE FROM logs WHERE created_at 2023-01-01 LIMIT 1000;一遍一遍执行直到影响行数为0。有人会问为什么不能等它自己跑完因为一个大事务长时间不提交后续所有对这个表的写入都会等锁业务直接卡死这在线上是不可接受的。DELETE之后还有一个很容易误解的行为自增ID不会回退。即使删掉了当前最大ID的那一行下一条插入数据的ID也会继续往上涨而不是复用那个被删掉的ID。如果业务误以为“删掉最大ID下次从最大ID开始”就会得出错误的结论。所以不要依赖自增ID连续它只是唯一标识不承担业务序号的功能。更稳妥的删除设计是软删除也就是在表里加一个is_deleted之类的标记字段删除时只做UPDATEUPDATE users SET is_deleted 1, deleted_at NOW() WHERE id 123;查询时统一带WHERE is_deleted 0。这种方式的好处是数据还在误删可以恢复历史记录也可以追溯。但软删除也有自己的坑如果表上有唯一索引比如username唯一被软删除的记录还占用着这个唯一值再注册同名的用户会插入失败。我的处理办法是删除时将唯一字段改写成一个带标记的值比如username CONCAT(username, #del_, id)这样既能保留历史痕迹又能释放唯一索引占用。4.3 事务、锁与一致性增删改查可靠运行的基石单条DML语句发出去MySQL会自动提交但真实业务往往需要多条语句同时成功或同时失败这时候就必须用事务。START TRANSACTION; UPDATE inventory SET stock stock - 1 WHERE product_id 100 AND stock 0; INSERT INTO order_items (order_no, product_id, quantity) VALUES (ORDER20240101001, 100, 1); COMMIT;只要有一个环节失败比如库存不足导致UPDATE影响行数为0就可以通过ROLLBACK回滚避免出现“订单创建了但库存没扣”的脏数据。这就是事务的原子性和一致性价值。InnoDB默认隔离级别是可重复读普通SELECT是快照读不加锁但UPDATE、DELETE会对涉及的行加排他锁直到事务提交或回滚。理解锁模型后有两个线上问题特别值得注意。第一个是锁等待。一个事务长时间不提交更新了某一行后一直挂在那里其他事务再更新同一行就会一直等待直到innodb_lock_wait_timeout超时。排查时可以看information_schema.innodb_trx找到长时间运行的事务和对应线程再决定是等待还是回滚。第二个是死锁。死锁的典型场景是两个事务按不同顺序更新多行比如事务A先更新行1再更新行2事务B先更新行2再更新行1两边都卡在对方持有的锁上。解决思路是让所有事务都按同一顺序更新比如都按主键从小到大更新并在代码里对死锁异常做重试。绝大多数死锁都不是数据库的问题而是业务代码没有统一操作顺序。事务范围的控制也很重要。有些人写一个循环每处理一条数据就开一个事务事务粒度太小浪费性能有些人则把几小时的批量任务塞进一个事务事务粒度太大日志和锁都扛不住。我的经验是普通请求尽量保证单个事务只处理必要的几条DML批量任务按1000行左右一批提交一次这样既保证一致性又不至于拖垮数据库。另外要记住DDL语句会隐式提交事务比如在事务里执行ALTER TABLE前面的修改会被直接提交别再指望用ROLLBACK把DDL撤回来。还有一个非常实用的一致性技巧像库存扣减这种高并发场景不要把“查库存、判断足够、扣库存”拆成三条语句而是用一条原子UPDATE完成UPDATE inventory SET stock stock - 1 WHERE product_id 100 AND stock 0;这条语句返回的影响行数如果是1说明扣减成功如果是0说明库存不足。它避免了多个请求同时读到相同库存、同时扣减造成超卖的问题。类似的思路可以用在很多需要“先判断再操作”的业务上把判断条件直接写进WHERE让数据库帮我们保证并发安全。5. 高频踩坑与排查技巧实录5.1 线上高频问题速查表这一节把平时最容易遇到的问题整理成一张速查表方便大家直接对照排查。现象可能原因排查动作解决方向查询突然变慢WHERE条件没走索引、数据量大、锁等待执行EXPLAIN看type和rowsSHOW PROCESSLIST看等待状态补索引、优化SQL、拆分查询INSERT或UPDATE卡住行锁被长事务占用查information_schema.innodb_trx看事务开始时间等待或终止长事务优化事务粒度中文乱码客户端与表字符集不一致查SHOW CREATE TABLE确认各字段字符集连接后执行SET NAMES utf8mb4统一字符集删除大量数据后磁盘没下降InnoDB空间不会自动收缩查看表文件大小变化确认页复用情况低峰期执行表重建或碎片整理操作自增ID跳变INSERT IGNORE、REPLACE、事务回滚消耗自增ID查看表自增计数不要依赖ID连续改业务判断方式JOIN结果比预期多关联字段存在重复值对关联字段执行SELECT COUNT(DISTINCT col)检查数据质量必要时先聚合再去JOIN这张表覆盖的场景几乎都是“SQL语法没问题但上线后出问题”的典型。每一条背后都对应一句经验写SQL之前先确认索引事务尽量短字符集统一。别等到线上卡了再去补课。5.2 三个线上排查场景实录先分享一个真实遇到过的情况。某系统晚上执行定时任务把用户表一批数据的状态改成禁用结果任务跑完后正常用户下单开始报锁等待超时。排查过程是这样先用SHOW PROCESSLIST查看当前会话发现大量写操作都在等一个状态接着跑到那个定时任务使用的WHERE条件上对条件字段做了EXPLAIN发现type是ALL全表扫描。原来条件字段没有索引UPDATE任务在扫描过程中锁住了大量行业务写入全被堵住。解决方式很简单给条件字段补一个索引同时把批量更新改成按主键分批执行。这个案例再次说明UPDATE的WHERE条件和SELECT一样需要索引而且批量大事务比想象中更容易拖垮业务。第二个场景和DELETE有关。某团队删除了一张日志表超过千万条历史数据但过了一天后发现表文件大小几乎没有变化运维开始怀疑删除没生效。实际原因是InnoDB删除数据后空间不会自动归还给操作系统而是保留在表空间里供后续复用。数据确实没了但表文件并没有缩小。要真正压缩空间需要低峰期对表做重建或碎片整理。这个案例提醒我们判断“删除是否生效”不要只看文件大小要看逻辑删除的行数而执行大表空间回收前一定要评估磁盘和线上影响千万别在高峰期直接操作。第三个场景是自增ID跳变引发的业务误解。某业务方发现用户表的主键ID不连续中间空了很多怀疑数据丢失。查下来发现插入逻辑用了INSERT IGNORE当唯一索引冲突发生时虽然插入失败但自增ID已经分配并且不会回滚。也就是说跳号是正常行为不是丢数据。这个案例的关键结论是自增ID只是内部标识不能承担“连续序号”的业务语义。如果业务必须展示连续序号应该在应用层或查询层单独生成而不是依赖主键。5.3 几条防呆习惯长期受益经验都是在坑里攒出来的但有几条防呆习惯可以帮助减少踩坑概率。第一条任何UPDATE和DELETE执行之前先跑一条等价的SELECT看影响范围。比如要更新WHERE status 1 AND created_at 2024-01-01的数据就先执行SELECT COUNT(*) FROM 表 WHERE status 1 AND created_at 2024-01-01。如果这个数字明显超出预期说明条件可能写错了赶紧停下来检查。第二条生产环境禁止不带WHERE的UPDATE和DELETE也禁止裸的DROP TABLE。哪怕只是想清空一张临时表也要先确认表名和环境防止在测试库和线上库之间来回切换时手滑点错。我见过有人在线上执行了本该在测试环境执行的清表语句原因就是连接串贴错了。对此类高风险操作最好有双人复核机制或者至少在执行前把SQL复制出来再读一遍。第三条所有写得稍微复杂一点的SQL都跑一遍EXPLAIN。看到ALL、Using filesort、Using temporary就要想清楚是不是有更好的写法。很多慢查询问题在开发阶段就能发现完全没必要上线后再靠监控报警去捞。第四条大表结构变更和批量数据操作必须安排在低峰期并且拆分批次。哪怕是一条ALTER语句也可能因为表数据量大而在执行期间产生巨大负载。拆分批次不是浪费时间而是在保护数据库的可用性。第五条多表JOIN时注意关联字段的类型和字符集一致否则索引失效不会报错只会默默扫描全表直到某一天数据量增大才突然爆发。这是最难排查的一类问题因为SQL逻辑看起来完全正确。这几条防呆习惯听起来都很基础但每次线上事故复盘根因往往就出在这些“基础”上。如果能把表设计、索引和增删改查当成一个整体去理解而不是背几条SQL语法你在这个方向上的成长会快很多。下次写完增删改查不妨多问一句这条SQL执行计划合理吗事务够短吗条件走索引了吗这些问题想清楚大多数线上数据库事故都能提前避免。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑