☰
MySQL CRUD 核心实践:从增删改查到事务、索引与防注入
2026/10/11 2:57:03 网站建设 项目流程

“MYSQL ---CURD”,看到这个标题我就绷不住了——兄弟,这个词八成是想写 CRUD(Create、Read、Update、Delete),也就是对数据库里数据的增、删、改、查。字母顺序调换一下,意思完全变了。不过话说回来,这个拼写问题反倒让我觉得有必要认真聊聊:在实际开发里,CRUD 这四个操作几乎占据了一个业务系统 80% 以上的数据访问逻辑,但越基础的东西越容易被轻视。我见过不少项目,前期建表随意、SQL 裸拼、事务不设,等数据量上来之后,慢查询、死锁、脏数据一个接一个往外冒,最后只能半夜爬起来救火。

这篇文章我就围绕 MySQL 的增删改查,把我整理过的完整写法、参数化方案、事务处理、索引与字段设计的思路,以及踩坑记录全部摊开讲。适合刚接触后端开发的新手,也适合写了两三年业务代码但没系统梳理过 CRUD 细节的同学。你不需要一次性全看懂,可以先收藏,等敲到对应场景再回来查。

1. CRUD 的整体设计与思路拆解

1.1 为什么 CRUD 没有想象中那么简单

先明确一点:CRUD 不是四个孤立的 SQL 语句,它是一套围绕数据的“生命周期管理”方案。

  • Create:数据从无到有,要解决的是“唯一性”和“完整性”问题。谁来生成主键?哪些字段必须有值?并发插入时会不会产生重复数据?
  • Read:数据从存储到展示,要解决的是“检索效率”和“结果准确性”问题。查询走不走索引?要不要分页?多表关联怎么避免笛卡尔积?
  • Update:数据状态发生变更,要解决的是“变更范围”和“并发安全”问题。不小心把 UPDATE 语句的 WHERE 条件写漏,会直接酿成事故;高并发下多个事务同时改同一行,又容易产生覆盖写。
  • Delete:数据从存在到移除,要解决的是“永久删除”和“可恢复性”之间的权衡问题。直接 DELETE 还是软删除?TRUNCATE 和 DELETE 在行为上有本质区别。

我在模拟项目 X 的数据访问层设计阶段,会先把这四个操作分别拆成“单条操作”和“批量操作”两条线。单条操作优先保证准确性和安全性,批量操作优先保证执行效率和事务边界。这套思路的后端逻辑很简单:单条 SQL 出错影响面窄、排查快;批量 SQL 效率高但容错要求严,不能一说批量就无脑拼一个几百条 VALUES 的 INSERT,要知道 MySQL 对单条 SQL 有 max_allowed_packet 大小限制,拼过头了直接报错。

1.2 为什么选择 MySQL 作为核心存储

市面上可以选的数据库很多,PostgreSQL、SQL Server、Oracle、SQLite 都有各自定位。MySQL 之所以在绝大多数中小型乃至中大型业务系统中被作为默认选择,理由很实际:

  • 部署和维护成本低。单机安装几分钟搞定,主从复制、读写分离的配套方案非常成熟。
  • 社区生态极其庞大。无论你用什么语言,Java、Python、Go、PHP,都有稳定成熟的驱动和 ORM 框架,出问题的排查资料也是全网最多的。
  • InnoDB 存储引擎在事务支持、行级锁、崩溃恢复方面足够可靠。MySQL 5.7 和 8.0 之后的版本性能稳定性大幅提升,8.0 还引入了窗口函数、CTE 这些以前要绕行的能力。
  • 和云数据库产品的兼容度极高。业务量起来之后可以无缝迁移到云上的托管实例,不用重写 SQL。

但不代表 MySQL 没有问题。它对复杂查询的优化不如 PostgreSQL 那么激进,深分页场景容易变慢,DDL 操作在大型表上有一定代价(8.0 后的 INSTANT 算法改善了部分场景)。因此,用 MySQL 做业务存储,要求开发者在设计表结构时想得更清楚,而不是把所有压力都丢给数据库去“优化”。

在动手写 CRUD 之前,我的习惯是先把表结构彻底定下来。不是凭感觉建表,而是先用一句业务描述定义清楚:“这张表保存谁的什么数据,哪些数据必须唯一,哪些字段会被频繁检索。”想清楚这三点,表设计基本不会跑偏。然后才是 CREATE TABLE 语句,这里面有两个让新手最容易栽跟头的细节:字符集和排序规则。

  • 字符集:业务表统一用 utf8mb4。utf8mb4 是 utf8 的超集,能存 emoji 和生僻字。如果你的表用了 utf8,某些 App 昵称里带了表情符号,插入时直接报 “Incorrect string value”,这坑我踩过不止一次。
  • 排序规则:MySQL 8.0 之前默认 utf8mb4_general_ci,8.0 之后默认 utf8mb4_0900_ai_ci。排序规则决定了字符串比较方式,比如 a 和 A 是否相等,中文按拼音还是按二进制排序。一般业务表随手用默认规则就行,但如果你要存用户名且希望校验时区分大小写,就得把排序规则改成 utf8mb4_bin。

主键选择方面,优先用自增 BIGINT。它写入有序、索引占用空间小、性能稳定。如果因为分布式环境必须用雪花 ID,那在 InnoDB 下最好把主键设计成 BIGINT 类型,并用 UUID_SHORT() 这类有序生成方案,避免随机 UUID 导致的页分裂和索引碎片。

字段类型上有一张我常用的速查表:

数据类型使用场景注意事项
TINYINT状态位、枚举值范围 -128~127,定义字段时建议写明注释
INT / BIGINT普通整数、自增主键根据业务量选,不要全部无脑 BIGINT
DECIMAL(10,2)金额类禁止用 FLOAT/DOUBLE 存钱,否则精度会出问题
VARCHAR(32)用户名、昵称、状态名长度要留有业务余量,但别超大
CHAR(固定长度)定长编码如手机号相比 VARCHAR 检索更高效但占用固定空间
DATETIME业务时间无时区问题,推荐默认使用
TIMESTAMP时间戳字段受时区影响,且范围到 2038 年
TEXT / JSON大文本、复杂结构无法直接建索引,可配合虚拟列或拆表

你可能会问,为什么 CREATE TABLE 要花这么多篇幅?因为后边的每一个 SELECT、UPDATE、DELETE 都依赖这张表的结构。字段类型选错,索引建错,哪怕你 CRUD 写得再工整,依旧该慢的慢、该错的错。CRUD 的质量上限,在表设计那一刻就锁死了。

3. CRUD 核心语法与实操细节

3.1 CREATE:插入数据的四种正确姿势

新增数据是业务写入的入口,也是干净数据的源头。这里说的“干净”,指的就是入库的每条记录都符合业务规则、不重复、不冗余。基础的 INSERT 语句如下:

INSERT INTO user (username, email, age, created_at) VALUES ('张三', 'zhangsan@example.com', 25, NOW());

这段语句本身没有任何难度,真正需要讲究的是下面几个特殊场景。

第一,需要跳过已存在的数据时,使用INSERT IGNORE。它根据主键或唯一索引判断,如果数据已存在,本次插入直接忽略,不报错也不中断。适合跑批导入场景,比如每天同步一次外部数据源,同一用户重复出现在文件里,用 INSERT IGNORE 可以自动去重。

第二,需要“存在就更新,不存在就插入”时,使用INSERT ... ON DUPLICATE KEY UPDATE。这个语法非常实用,典型场景是记录用户最后一次登录时间,或者做计数累加。要注意的是,它判断冲突的依据同样是主键或唯一索引,而不是业务主键以外的任意字段。

INSERT INTO user_login_log (user_id, login_count, last_login_at) VALUES (1001, 1, NOW()) ON DUPLICATE KEY UPDATE login_count = login_count + 1, last_login_at = NOW();

第三,批量插入时不要一条一条执行,而应该用多值插入。一次插入十条记录比循环插入十条快一个数量级,原因是减少了网络往返和 SQL 解析开销。

INSERT INTO user (username, age) VALUES ('A', 20), ('B', 21), ('C', 22);

这里有个硬性限制:单条 SQL 不能无限长。受max_allowed_packet参数控制,我一般建议每次批量 500~1000 条,数据量再大就分批。而且批量插入一旦中间某条数据违反约束,整批都会被回滚。

第四,插入之后需要拿到自增主键。这里不同驱动处理方式不太一样,比如在 JDBC 里使用PreparedStatement.RETURN_GENERATED_KEYS,Python 的 pymysql 里用cursor.lastrowid,ORM 框架则会自动回填到实体对象。你要记住的原则是:主键是插入数据的“收据”,一定要能拿回来,否则后续关联写操作会非常尴尬。

3.2 READ:查询语句的执行顺序与分页优化

SELECT 是 CRUD 里使用频率最高的一项,也是优化空间最大的一项。几乎所有慢查询问题都能追溯到一条糟糕的 SELECT。一条看似简单的 SQL,MySQL 内部实际上是按照固定顺序执行的:

FROM -> WHERE -> GROUP BY -> HAVING -> SELECT 字段 -> ORDER BY -> LIMIT

理解这个顺序非常关键,否则很容易写出“看似正确实则低效”的查询。比如 HAVING 是在分组之后过滤的,所以能写在 WHERE 里的条件就不要放到 HAVING 里,WHERE 可以先缩小参与分组的数据量。

分页是查询中最容易出现性能事故的环节。最典型的写法是以偏移量分页:

SELECT * FROM orders ORDER BY id DESC LIMIT 10000, 20;

这条语句在数据量小的阶段毫无压力,但当偏移量来到百万级,MySQL 需要先扫描并排序前一百万条记录再丢弃,性能断崖式下跌。推荐的做法是“游标分页”,把偏移量换成上一次结果集最后一条记录的 ID:

SELECT * FROM orders WHERE id < 100860 ORDER BY id DESC LIMIT 20;

应用层把上一页返回的最后一条 id 存起来,下一页直接带过去,整个过程几乎不浪费扫描。很多资深开发不写 LIMIT 偏移,只用这种方式,原因就在于此:OFFSET 是“位置”,而 ID 是“数据”,按数据路标记位置永远比数据位置更可靠。

JOIN 查询在业务中也绕不开。多表关联时一定要清楚内连接(INNER JOIN)、左连接(LEFT JOIN)在语义上的区别:INNER JOIN 只保留两表匹配上的行;LEFT JOIN 保留左表全部行,右表匹配不上的字段以 NULL 填充。实际开发中我反复强调一个原则:能用 INNER JOIN 就别用 LEFT JOIN。左连接如果使用不当,很容易因为右表有多条匹配记录而把左表数据放大成笛卡尔积,导致结果集膨胀。

3.3 UPDATE:有限更新与影响行数的坑

UPDATE 语句是所有 CRUD 里最容易闯祸的。闯祸原因永远是同一个:漏掉 WHERE 条件,或者 WHERE 条件不够精确。

UPDATE orders SET status = 'PAID' WHERE order_id = 1024;

上面这条是安全写法,只更新目标行。但如果你写成:

UPDATE orders SET status = 'PAID';

MySQL 不会问你“确定吗”,它直接执行。等反应过来,所有订单都变成了已支付。虽然可以把sql_safe_updates打开来阻止不带 WHERE 的 UPDATE 和 DELETE,但最可靠的防线永远是写 SQL 的人自己。我的习惯是,UPDATE 之前先写一条 SELECT 验证 WHERE 条件选中的数据行数和范围,确认没错再改写成 UPDATE 执行。

UPDATE 语句还有一个常见认知误区:MySQL 返回的“影响行数”并不等于你试图更新的行数。默认情况下,如果某一行更新前后的值没有变化,MySQL 会把这次更新判断为“没有变化”,影响行数为 0,而不是 1。也就是说,affect rows = 0未必代表“没找到数据”,也可能是“找到但值没变”。这在做乐观锁判断时尤其要注意,不能依赖影响行数直接判断数据存在与否。

一次性更新多条记录时,可以考虑用CASE WHEN拼接成一条 UPDATE,减少 SQL 执行次数。但可读性和维护成本会上升,所以建议只用在数据量较大且更新规则固定不变的场景。

3.4 DELETE:物理删除与软删除的选择

DELETE 是 CRUD 四兄弟里最需要谨慎对待的操作。我先说结论:绝大多数业务系统,都不要物理删除数据,而是通过“软删除”的方式——给表加deleted字段(通常为 TINYINT),删除操作本质上是 UPDATE。

-- 物理删除,危险 DELETE FROM user WHERE id = 1001; -- 软删除,安全可回溯 UPDATE user SET deleted = 1 WHERE id = 1001;

选择软删除的理由很实际:线上事故一旦发生,数据删了就回不来了;即使有备份,恢复也耗时耗力。而软删除只是打一个标记,随时可以恢复,审计时也能查清楚“这条数据到底是不是被删过”。代价是每条查询都要记得带上WHERE deleted = 0,这可以通过建立视图或者在 ORM 层统一处理。

如果确实需要物理删除旧数据,也要注意 DELETE 和 TRUNCATE 的区别。DELETE 是逐条删除、返回影响行数、可带事务回滚;TRUNCATE 是重建表、重置自增主键计数、不可回滚。业务数据清理宁可分批 DELETE,也不要图快用 TRUNCATE。

大表清理还容易踩另一个坑:DELETE 大量数据会长时间持有行锁,阻塞其他业务写入。我建议的做法是分批删除,每次删 1000 条、SLEEP 一小段时间再删下一批,把锁持有时间摊平,降低对在线业务的影响。

4. 参数化查询与防注入实操

4.1 字符串拼接 SQL 是万恶之源

我先问一个直击灵魂的问题:你写 CRUD 时是怎么把用户输入的内容拼进 SQL 的?如果你还在用这样直接拼接的写法:

# 反例,严禁在生产环境使用 user_input = request.get_param("username") sql = "SELECT * FROM user WHERE username = '" + user_input + "'"

那等于把你的数据库大门敞开了。举个最简单的例子,用户在输入框里提交:

' OR '1'='1

拼接后的 SQL 变成了:

SELECT * FROM user WHERE username = '' OR '1'='1'

因为'1'='1'恒为真,这一条查询直接返回整个 user 表的数据。更狠的变体甚至可以拼接 DROP TABLE、UPDATE 权限范围内的其他表数据。SQL 注入不是远古漏洞,它在今天的真实互联网上仍然疯狂存在着,而且绝大多数是因为开发者在 CRUD 里贪图省事,直接用了字符串拼接。

4.2 Prepared Statement 为什么能防注入

要理解防注入的原理,得先知道 SQL 在 MySQL 里的执行流程:先做词法解析、语法检查、生成执行计划,再绑定参数执行。传统的字符串拼接写法,是把用户输入“直接成为 SQL 句子的一部分”,MySQL 解析的时候会认为输入就是命令;而 Prepared Statement(预编译语句)把 SQL 结构提前发送给数据库端完成解析,之后用户输入只是“参数值”,不参与 SQL 语法解析。

用 JDBC 写一个标准安全的插入:

String sql = "INSERT INTO user (username, email) VALUES (?, ?)"; try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, email); ps.executeUpdate(); }

这里的?是占位符,username 和 email 无论传什么内容进来,在数据库看来都只是字符串值,不可能变成新的 SQL 命令。Python 的 pymysql 也提供了对应的参数化接口:

sql = "INSERT INTO user (username, email) VALUES (%s, %s)" cursor.execute(sql, (username, email))

注意不同驱动的占位符样式有差异:JDBC 用?,Python 的 pymysql 用%s,Go 的 database/sql 也用?。这就引出另一个易错点:不要把?和%s混为一谈,规则跟随你使用的驱动而定。

4.3 正确绑定参数类型避免隐式转换

参数化查询的另一个好处是:能显式指定参数类型,避免隐式类型转换带来的索引失效问题。很多慢查询事故不是 SQL 写得差,而是 WHERE 条件的字段类型和传入参数类型不匹配。

举例来说,user 表的phone字段是 VARCHAR 类型,索引也建在 phone 上,但如果你在 WHERE 里传了一个整数:

SELECT * FROM user WHERE phone = 13800138000

MySQL 会把 phone 字段隐式转换为数字再比较,导致索引失效,变成全表扫描。使用参数化写法后,通过setString明确把 phone 按字符串处理,就不会触发这种问题。每次写 SQL 都问一下自己:这个字段在表里是什么类型?我传的参数和它是一伙的吗?统一类型,是防止索引失效最廉价且有效的方式。

提示:任何由外部输入参与拼装的 SQL 片段,都必须走参数化。不管是查询、新增、更新还是删除,没有例外。

5. 事务与并发控制关键点

5.1 事务能让 CRUD 真正可靠

单一一条 SQL 成功与否很好判断,但真实业务往往需要多个步骤一起成功。比如转账操作:A 账户扣钱是一条 UPDATE,B 账户加钱是另一条 UPDATE。如果第一条成功、第二条失败,钱就凭空消失了。把多条 CRUD 放进同一个事务里,保证“要么全部成功,要么全部回滚”,是数据库层面最基础的一致性保障。

事务的四个特性 ACID——原子性、一致性、隔离性、持久性,用生活类比来说就是:转账操作像一个整体包裹,要发就整体发出,要退就整体退回;快递在中途不允许被人拆开改内容;一旦包裹签收,物流记录就不能丢。MySQL 的 InnoDB 引擎默认开启自动提交,也就是每一条单独的 SQL 自带事务。

多语句事务的正确写法是:先去设置SET autocommit = 0,然后在代码里显式调用 commit 或 rollback。以 JDBC 为例:

Connection conn = dataSource.getConnection(); try { conn.setAutoCommit(false); // 执行多条 CRUD conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }

注意一个容易被忽略的问题:请把事务范围控制在真正需要一致性的操作上,事务内部不要夹带远程调用、大量计算、批量导入。事务不是越大越安全,事务越大,持锁时间越长,死锁概率越高。

5.2 隔离级别与脏读幻读

当多个事务并发执行时,隔离级别决定了一个事务能看到另一个事务的什么状态。MySQL 默认的隔离级别是REPEATABLE READ,也就是可重复读。四个级别的差异如下:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不会可能可能
REPEATABLE READ不会不会可能
SERIALIZABLE不会不会不会

理解这三个现象可以这样判断:脏读是读到别人未提交的数据,这种数据可能下一秒就被回滚;不可重复读是同一事务内两次读取同一行,结果不同,原因是其他事务已经提交了更新;幻读是同一事务内两次范围查询,结果行数不同,原因是其他事务插入了新行。MySQL 的 InnoDB 在 REPEATABLE READ 级别下通过 MVCC 和间隙锁解决了部分幻读问题,但不代表完全免疫。对于极高一致性要求的业务,可以考虑使用 SERIALIZABLE,代价是并发能力大幅下降。

5.3 并发更新:悲观锁与乐观锁

CRUD 中的并发更新是事故高发地。典型场景是库存扣减:两个请求同时读到库存剩 10 件,各自都执行UPDATE stock SET count = count - 1,但两次都基于同一个初始值“10”,可能最终库存变成 9 而不是 8。解决思路有两种。

悲观锁思路:先锁定目标行,再执行更新。

BEGIN; SELECT * FROM stock WHERE product_id = 88 FOR UPDATE; -- 业务计算 UPDATE stock SET count = count - 1 WHERE product_id = 88; COMMIT;

FOR UPDATE是行级排他锁,锁住后其他事务要更新同一行必须阻塞等待,这能保证当前事务对这条数据具备独占权。使用时要小心:锁必须在事务内生效,写完立刻提交,避免长时间持锁。

乐观锁思路:不锁数据库行,而是用版本号控制更新条件。

UPDATE stock SET count = count - 1, version = version + 1 WHERE product_id = 88 AND version = 5;

如果更新影响行数为 0,说明 version 已经发生变化,本次更新失败,由应用层决定重试或报错。乐观锁适合读多写少、冲突概率低的业务,成本低、性能好。两者没有谁绝对更好,关键看业务场景:秒杀扣库存的高冲突场景适合悲观锁或 Redis 限流前置,普通业务更新适合乐观锁。

5.4 死锁排查是一线开发必备技能

两个事务各自持有对方需要的锁,就会出现死锁。场景很常见:

  • 事务 A:先更新订单,再更新用户。
  • 事务 B:先更新用户,再更新订单。

二者都等对方先释放锁,互相卡死。MySQL 检测到死锁后会自动回滚其中一个事务,报错信息形如“Deadlock found when trying to get lock”。

死锁的排查思路要固定成套路:第一步,看错误日志,确认是哪两条 SQL 参与;第二步,检查这两条 SQL 涉及的表的锁等待情况,用SHOW ENGINE INNODB STATUS查看最近一次死锁现场;第三步,调整代码访问顺序,让所有事务都按同一顺序更新表;第四步,尽量缩小事务范围,减少锁的持有时间。

6. 常见问题与排查技巧实录

6.1 慢查询与索引失效排查

这里列一个我在实际调试中反复见到的现象速查表,每一条都来自生产环境实踩:

现象常见原因解决思路
查询越来越慢数据量增长,索引缺或失效用 EXPLAIN 分析执行计划
WHERE 条件有索引却不走函数包裹字段、隐式类型转换去掉函数或用类型一致参数
深分页很慢用了 LIMIT 大偏移量改用游标分页
关联查询慢被驱动表关联字段无索引给 ON 条件的字段建索引
COUNT 全表统计慢数据量大且无过滤条件使用计数表或从汇总表读取

索引失效的几个雷电,挨个说。

第一,在索引列上使用函数:

SELECT * FROM user WHERE DATE(created_at) = '2024-01-01'

这会导致 created_at 上的索引失效,因为 MySQL 每次都要对每行数据先执行 DATE 函数再比较。正确写法是范围查询:

SELECT * FROM user WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02'

第二,前导通配符:

SELECT * FROM user WHERE username LIKE '%zhang%'

以%开头的模糊查询无法走索引。如果业务确实需要,考虑使用全文索引或外部搜索引擎。

第三,OR 条件里有一侧没有索引。比如WHERE id = 1 OR age = 20,id 有索引但 age 没有,MySQL 可能转全表扫描。改成 UNION ALL 或确保 OR 两侧字段都有索引。

检查慢 SQL 的工具方面,先在 MySQL 里开启慢查询日志,定位到具体 SQL,再使用EXPLAIN查看执行计划。看执行计划时重点关注 type 列:从好到坏依次是 system、const、eq_ref、ref、range、index、ALL。如果看到 ALL,意味着全表扫描,必须赶紧查索引。

6.2 时区问题:TIMESTAMP 还是 DATETIME

时区问题经常在 CRUD 里表现得非常诡异。用户明明存了一个时间,查出来却发现差了 8 个小时,年底一核对全是错的。原因要追溯到字段类型:TIMESTAMP 在存储时会根据数据库服务器时区转换成 UTC,读取时再转换回当前时区;而 DATETIME 不做任何转换,存什么就是什么。

解决方案很简单也很统一:业务字段统一使用 DATETIME,并在插入时使用数据库函数 NOW() 或由应用层生成统一标准的日期格式字符串写入。如果在分布式系统里应用服务器和数据库服务器不在同一时区,更要避免使用 TIMESTAMP 当作业务时间。TIMESTAMP 还有个隐藏限制:只能存储到 2038 年,很多业务系统在推进“百年会员”之类的需求时会撞上这个边界。

6.3 整数溢出与 BIGINT 边界

MySQL 的 INT 是 32 位有符号整数,最大值为 2147483647。如果一张表的数据量或自增主键超过这个上限,继续插入会直接报“Out of range value”错误。主键溢出这个问题集中出现在快速发展的业务上,上线初期表数据量不大,用 INT 觉得够用,两年后日增百万条数据,主键逼近上限才想起来要做迁移,操作成本和时间成本都非常高。

我的建议是:所有自增主键,一律从第一天就用 BIGINT。虽然 BIGINT 占用的存储稍大一点,但可取值范围到 922 亿亿级别,几乎不可能耗尽。不要因为初期数据少就选 INT,变更主键类型的成本远超那一点点存储成本。

类似情况还有金额字段。千万别用 FLOAT 或 DOUBLE 存金额,这类浮点类型在二进制表示上不精确,0.1 + 0.2 可能等于 0.30000000000000004。金额要么用 DECIMAL,要么用整数存“分”,应用层再转换。这不是洁癖,是金融业务的基本要求。

6.4 SELECT * 的隐形危害

很多初学者习惯写SELECT *,因为省事。但它带来的问题在 CRUD 层面非常明显:

  • 多查了不需要的列,增大了网络传输和内存占用,数据量一大,性能差距就很明显。
  • SELECT *覆盖索引失效的风险高。如果表里有个很大的 TEXT 字段,查询时也不得不把这个大字段拉出来,完全无法利用覆盖索引优化。
  • 表结构变更时,SELECT *的字段顺序和数量会同步改变,代码里按位置取字段的逻辑很容易被悄然破坏。

正确的做法是显式列出需要的字段:

SELECT id, username, email, created_at FROM user WHERE id = 1001;

这样一来,执行计划能利用覆盖索引、网络传输量更小、字段变化时也不会轻易破坏代码逻辑。CRUD 看起来只是“取数”,实际上少查一列就是少一分风险。

注意:DELETE 和 UPDATE 操作前,务必先执行同条件 SELECT 确认命中范围。生产环境的“手滑”事故,九成以上是少了这一步确认。

7. 结语与我的实操习惯

这套 CRUD 的完整思维链,从上到下应该是这样:先设计表,再写插入,再写查询,然后考虑更新和删除,每次访问数据都要将通过参数化防止注入,跨多步操作包上事务,最后用索引和 EXPLAIN 保障查询性能。把它练成肌肉记忆,数据库层面的稳定性就成功了一大半。

我个人这几年最受用的一条习惯,是在所有涉及数据变更的操作里强制加上“先查后改”的步骤。不管代码写得多熟练,执行 UPDATE 和 DELETE 前,永远先跑一条 SELECT,用同样的 WHERE 条件确认影响范围,再加事务、再执行。这个习惯帮我挡掉了至少三次线上事故。

另一个想特别提一下的小技巧是数据库连接和驱动层的统一管理。不要每写一个接口就自己 new 一个连接,而是由连接池统一分配。连接池的最小空闲连接数、最大连接数、最大等待时间要结合业务并发量和数据库性能基线来配置,避免连接数耗尽或者空闲连接不合理占用资源。

CRUD 是后端开发的地基,地基不牢,上层业务做得再花哨也撑不住。如果你想系统性把 MySQL 搞扎实,可以顺着表设计、索引原理、事务隔离、慢查询优化这几条线继续往深挖。但不管学到哪一步,先把最常用的这四个动作练到不出错、不埋雷,才是真正的本手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询