1. 开篇:为什么我把“增删改查”单独拆出来讲
新手学MySQL,最容易陷入的误区是急着去背复杂的JOIN、子查询、索引优化,但真正到了写业务代码或者做数据分析的时候,发现很多基础的操作反而磨磨蹭蹭搞不定——插入的数据格式不对,查出来的结果不是自己想要的那几行,更新操作把整张表都改了,删数据的时候犹豫半天不敢下手。这些问题的根源,都是对最核心的增删改查(CRUD)理解不够扎实。
CRUD是C(Create)、R(Read)、U(Update)、D(Delete)四个操作的合称,对应到SQL语句就是INSERT、SELECT、UPDATE、DELETE。这四个操作覆盖了日常开发中超过80%的数据库交互场景:前端页面提交一个表单,后端要往表里INSERT一条记录;列表页面要展示数据,就要SELECT查询;用户改了昵称,要UPDATE更新;一个商品下架了,要DELETE删除。可以说,应用系统的每一次响应背后,都在执行某种形式的增删改查。
这篇文章的目标读者很明确:刚接触MySQL的学生、要做课设或者毕设的同学、以及工作里经常写SQL但没系统梳理过基础语法的开发者。我会把每一类操作从语法结构、执行细节、注意事项三个层面掰开揉碎地讲,配合完整的代码示例和实际操作中踩过的坑,确保你看完就能直接上手用。
2. 动手前最重要的准备:建好库、建好表
2.1 字符集和排序规则,选错了一排雷
很多人习惯直接执行CREATE DATABASE,完全不指定字符集,觉得能用就行。但实际上字符集没选对,后面遇到的麻烦会成串出现:中文存进表里变成问号、模糊查询时大小写行为跟预期不一致、甚至导入数据直接报错。国内业务几乎都绕不开中文,所以我强烈建议从建库这一步就用utf8mb4。
utf8mb4兼容utf8,但比utf8多支持了四字节字符,像emoji表情这类特殊字符只有utf8mb4能存。MySQL 8.0默认字符集已经是utf8mb4,但很多老版本的默认还停留在latin1或utf8mb3,所以显式声明最稳妥。
CREATE DATABASE IF NOT EXISTS shop_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;COLLATE是排序规则,utf8mb4_unicode_ci基于通用Unicode排序算法,对各国语言的支持更准确,utf8mb4_general_ci的排序速度稍快但准确性不如前者。日常业务用unicode_ci就够了。这里的_ci后缀表示大小写不敏感(case insensitive),也就是说查询时WHERE name = 'apple'能匹配到'Apple'。如果你需要区分大小写,字段级别可以单独指定COLLATE utf8mb4_bin。
2.2 字段类型怎么选,直接影响后面的增删改查
建表时字段类型选得随意,后面CRUD处处受限制。举个最常见的例子:把手机号设计成INT类型,一旦号码超过2^31-1就会溢出报错,而且丢失前导零;把价格设计成FLOAT,算金额时会出现0.1 + 0.2不等于0.3这类精度问题;日期字段设计成VARCHAR,排序就变成了字典序排序,12月会排在1月前面。下面的对照表是我总结的字段类型选择速查思路:
| 业务场景 | 推荐类型 | 不推荐类型 | 原因 |
|---|---|---|---|
| 用户ID、数量 | INT 或 BIGINT | VARCHAR | 数值类型占用空间小,排序和比较效率高 |
| 用户名、标题、邮箱 | VARCHAR(长度) | TEXT | VARCHAR可以建索引,TEXT做索引有额外限制 |
| 价格、金额 | DECIMAL(10,2) | FLOAT/DOUBLE | 浮点有精度误差,金额类必须用定点数 |
| 状态码(0/1/2) | TINYINT | VARCHAR | TINYINT占用1字节,查询效率高 |
| 创建时间 | DATETIME | VARCHAR | DATETIME可直接排序和日期函数计算 |
| 图片路径、长文本 | VARCHAR 存路径或链接 | BLOB | 不要把文件内容存进数据库,存路径即可 |
另外,还有两个容易被忽略的隐藏细节:第一,VARCHAR长度不是越大越好,VARCHAR(255)和VARCHAR(191)在utf8mb4下占用不同字节数,当超过一定阈值后会导致无法建立完整索引;第二,每一行数据的总字节数受到表限制,字段过多过宽时要考虑是否需要拆分表。对增删改查来说,最直观的影响是:合理设计字段类型,INSERT时省去很多格式报错的烦恼,查询时索引能高效命中,排序和比较的结果也符合直觉。
2.3 第一次建表,就把约束和注释写完整
下面是模拟项目X中的一个用户表结构,我在实际教学和开发里经常用这个例子:
CREATE TABLE user_info ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', username VARCHAR(50) NOT NULL COMMENT '用户名,登录用', nickname VARCHAR(50) DEFAULT '' COMMENT '昵称,可空', age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄,0表示未填写', balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '账户余额', level TINYINT NOT NULL DEFAULT 1 COMMENT '会员等级,1-普通 2-白银 3-黄金', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';几个关键点拆开解释:
id字段使用INT UNSIGNED配合AUTO_INCREMENT,避免负数浪费空间,最大支持到4亿多,日常业务完全够用。username加上UNIQUE唯一索引,是为了防止同一用户名重复注册。DEFAULT CURRENT_TIMESTAMP让建表时自动记录创建时间,ON UPDATE CURRENT_TIMESTAMP让每次数据更新时自动维护update_time,省去应用层手动赋值。
字段注释一定要写,而且写中文描述。项目里表一多,没有注释的字段很快就变成天书,两个月后连你自己都猜不出LOGIN_STATUS这个字段到底有几种取值。COMMENT不仅是为了给别人看,更是为了给未来的自己省时间。
3. 插入数据:从单条INSERT到批量插入
3.1 INSERT的两种标准写法
INSERT语句有两条路径:指定列名插入和全列名省略插入。我建议永远使用指定列名的写法,句法如下:
INSERT INTO user_info (username, nickname, age, balance) VALUES ('alex', '亚历克斯', 28, 100.50);指定列名最大的优势是和表结构的演进解耦。如果哪一天你在user_info表里新增了一个字段,旧的全列插入语句直接报错,但指定列名的语句完全不受影响。另一个好处是可读性强,VALUES里的值到底对应哪个列一目了然。
全列省略写法也可以正常工作,但必须严格按照建表时的字段顺序,一旦记错顺序就出大问题。尤其是id这种自增字段,你得在VALUES里显式填NULL让系统自动生成,格式极其容易出错:
INSERT INTO user_info VALUES (NULL, 'alex', '亚历克斯', 28, 100.50, 1, '2024-01-01 12:00:00');两条路径还有一个典型的坑在这里顺便说一下:省略列名的INSERT如果有一个NOT NULL字段没有默认值,插入时直接报错Field 'xxx' doesn't have a default value。所以建表时给字段设置合理的DEFAULT,不仅能防御这类错误,也让INSERT语句变得简洁。
3.2 批量插入到底比循环单插快多少
业务开发里特别容易出现一个低效场景:用户注册时批量导入名单、订单拉取后批量写入明细,代码里写一个FOR循环,循环几百上千次,每次执行一条INSERT。我实测过,在本地环境插入5000条数据,逐条循环要耗时接近3分钟,而用一条批量INSERT几乎秒级完成。
INSERT INTO user_info (username, nickname, age, balance) VALUES ('bob', '鲍勃', 24, 0.00), ('cindy', '辛迪', 31, 85.20), ('david', '大卫', 19, 22.00);批量插入的本质是多次网络交互压缩成一次,数据库端虽然还是逐行执行,但减少了大量的客户端与服务器之间的往返通信。批量插入时注意两点:一是单条SQL的长度不要超过数据库的max_allowed_packet限制,默认通常是64MB,实际业务用几千条一批比较安全;二是有些ORM框架对批量插入的支持依赖数据库驱动配置,例如JDBC URL上必须开启rewriteBatchedStatements=true才能真正启用优化,否则还是伪批量。
3.3 插入时的引号、格式与类型陷阱
我整理了几个高频报错的来历:
字符串类型必须使用单引号包裹。数字类型加不加引号都能插入,但加上引号后可能触发隐式类型转换,导致索引命中失败。日期时间类型推荐使用标准格式'2024-06-01 12:30:00',MySQL会自动转换为DATETIME类型。插入超出DECIMAL精度范围的值时,MySQL默认会四舍五入而不是报错,所以295.999存进去会变成296.00,这在金额场景里是严重的事件,应用层必须先做好精度控制。
还有一个隐蔽的问题:字段设置了UNIQUE约束,插入重复值就会报Duplicate entry。很多新手遇到这个报错的第一反应是改数据,实际上有时候业务逻辑恰恰希望“存在就忽略、不存在才插入”,此时用INSERT IGNORE可以静默跳过重复行,用INSERT ... ON DUPLICATE KEY UPDATE则可以实现“插入或更新”的幂等操作。这类进阶写法后续文章会专门展开,这里你先有个印象。
4. 查询数据:SELECT不是只有“SELECT *”
4.1 理解SELECT的执行逻辑顺序
新手最容易犯的错误,是把SELECT语句的书写顺序和执行顺序混为一谈。写的顺序是先SELECT后FROM再WHERE,但实际上数据库引擎先定位到表(FROM),再过滤行(WHERE),最后才决定返回哪些列(SELECT)。
SELECT username, nickname, age FROM user_info WHERE age >= 18;这条语句的执行逻辑是:先从user_info表中取所有数据,然后筛选出age>=18的行,最后只输出username、nickname、age这三个列。许多新手在WHERE条件里引用SELECT里起的别名,然后发现报错Unknown column,原因就是执行顺序上WHERE先跑,SELECT里定义的别名还没生效。
基础运算符这边,除了常见的=、!=、>、<,还有一个特别容易用错的范围判断。比如查询年龄在18到30之间,新手第一反应是写age >= 18 AND age <= 30,更简洁且不易出错的写法是:
SELECT username, age FROM user_info WHERE age BETWEEN 18 AND 30;模糊查询时使用LIKE配合通配符。%表示任意多个字符,_表示任意单个字符。查询所有姓“李”的客户,可以写WHERE nickname LIKE '李%'。这里注意:LIKE查询以%开头时会放弃索引,数据量大的场景下全表扫描会比较慢,这时可以改用全文索引或其他方案。
4.2 ORDER BY排序与LIMIT分页
排序的坑主要集中在多字段排序上。比如“先按等级从高到低排,同级之间按注册时间新的在前”,这个需求看起来不复杂,但很多人写了一堆代码,实际上只用一句话:
SELECT username, level, create_time FROM user_info ORDER BY level DESC, create_time DESC;多个排序条件之间用逗号隔开,每个条件各自指定ASC(升序,默认)或DESC(降序)。对中文排序默认按字符集编码排序,想按拼音排序需要额外的转化,实际项目中遇到再说。
分页查询是几乎所有列表接口都需要的能力,语法是LIMIT offset, count。offset表示跳过的行数,count表示取多少行。第一页取10条,LIMIT 0, 10;第二页取10条,LIMIT 10, 10。注意第一页的offset是0不是1,这一点无数生产事故都源于此。分页对应的查询语句:
SELECT username, age FROM user_info ORDER BY id LIMIT 10, 10;深分页是个经典性能问题。LIMIT 1000000, 10看起来只取10条,但数据库要先把前1000000行全部扫描后扔掉才返回目标数据,百万人次之后性能急剧下降。优化方案通常是延迟关联或基于游标,在数据量大的真实生产环境中推荐后者,具体内容我们后续单独讲。
4.3 让查询结果更可读:AS别名与DISTINCT去重
写SQL时别把原始列名原封不动返回给前端。nickname字段在表里叫nickname,但前端展示可能希望字段名是displayName,此时用AS重命名:
SELECT username AS user, nickname AS display_name, age FROM user_info WHERE age < 35;AS别名在JOIN多表查询和子查询里几乎是必需品,否则结果集里会出现多个同名列,应用层取值就乱了。别名也可以省略AS直接写别名,如SELECT username user,但可读性差,容易和老手误判成字段名,建议保留AS。
DISTINCT用于去除结果集中的重复行,你得明确它是作用于“整个查询结果的组合”,不是单独一列。SELECT DISTINCT level FROM user_info是获取所有不重复等级;但SELECT DISTINCT level, age则是level和age两列的组合去重,即使level重复,只要age不同就会保留多行。如果目标是“每个等级取一个用户”,用DISTINCT就不合适,应该用GROUP BY加合适的聚合函数。
5. 更新与删除:功能越简单,风险越致命
5.1 UPDATE最致命的坑:忘了WHERE条件
我把UPDATE单独列出来强调,是因为太多惨痛教训都来自这条语句。正确写法:
UPDATE user_info SET balance = balance - 50, update_time = CURRENT_TIMESTAMP WHERE username = 'alex';SET子句负责指定要修改的字段和新值,可以一次更新多个字段,用逗号隔开。WHERE子句限定要更新的行。问题来了:如果漏掉WHERE条件,这条语句会把user_info表里所有行的balance全部减掉50,而且影响行数是全部记录数。这类操作没有弹窗提醒,执行完就完事了,生产环境一旦发生,恢复的唯一希望就是备份。
我的经验是:执行UPDATE之前,先用同条件的SELECT查一遍,确认将要影响的行数和目标范围一致,再执行UPDATE。像下面的对照写法:
-- 先确认 SELECT id, username, balance FROM user_info WHERE username = 'alex'; -- 再更新 UPDATE user_info SET balance = balance - 50 WHERE username = 'alex';另外一个容易被忽略的细节:UPDATE的SET表达式里,右边的字段引用的是旧行的值。比如SET age = age + 1是把当前age加1;如果写SET age = age + 1, level = age,这个level赋值用的是更新前的age,不是更新后的。MySQL在这点上的行为和某些编程语言多变量赋值不同,建议赋值多个字段时只在一个字段上做依赖计算,避免理解偏差。
5.2 DELETE与TRUNCATE,别把这两个混为一谈
DELETE删除行的标准语法:
DELETE FROM user_info WHERE id = 10086;和UPDATE一样,DELETE的WHERE子句决定删除哪些行,忘记WHERE会清空全表。安全习惯同样适用:先SELECT确认目标,再执行DELETE。另外一个关键认知是:DELETE是逐行删除,每删一行都会在事务日志里做记录,执行速度相对较慢,但可以配合WHERE条件精确删除,且删除的行数可用ROW_COUNT()获知。
TRUNCATE是另一种清空数据的方式,它直接将整个表重置,不了解的人经常用错。两者的整理对比表:
| 对比项 | DELETE | TRUNCATE |
|---|---|---|
| 有无WHERE控制 | 可以有 | 只能整表清空 |
| 是否记录行级日志 | 是,可精确回滚 | 否,最低级日志 |
| 自增ID行为 | 从当前值继续 | 重置为初始值 |
| 执行速度 | 较慢(逐行删) | 极快 |
| 事务回滚 | 可配合事务回滚 | DDL操作,通常不可回滚 |
| 触发器 | DELETE触发器会触发 | 不触发DELETE触发器 |
对需要清空一张表的数据,TRUNCATE的实际执行速度远快于无WHERE的DELETE,因为后者还要逐条解析和记录。但它不能恢复主键自增计数,也无法按条件选择留一部分数据,所以决定使用前必须想清楚:你到底要不要保留表结构?要不要保留自增ID?要不要留备份?三种需求对应三种不同方案。
5.3 外键约束下的“删不掉”与“改不动”
外键约束是保护数据一致性的重要体现在CRUD上。假设有订单表和订单明细表,明细表的order_id外键指向订单表的id。此时删除父表的某条订单记录会出现两种结果:如果外键约束是ON DELETE RESTRICT(默认行为),直接报错,因为存在引用它的明细数据;如果设置的是ON DELETE CASCADE,那么删除父记录时会连带删除子表中所有关联的明细记录。设计表的初期就要想清楚这个行为:
CREATE TABLE order_detail ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_name VARCHAR(100), FOREIGN KEY (order_id) REFERENCES order_info(id) ON DELETE CASCADE );ON DELETE CASCADE非常危险,你今天只想删掉一笔作废订单,结果明细数据也被“连带”删得干干净净。更稳妥的设计是“软删除”:给表增加一个is_deleted字段,删除时执行UPDATE is_deleted = 1,查询时WHERE is_deleted = 0,这样数据永远在库里,不会真正消失,既规避了外键连带问题,又为后续数据分析和恢复留了后路。我实际负责的系统里几乎所有核心业务表都采用软删除策略,硬删除只用在临时表或日志表。
6. 完整实操流程:从建库到数据清理的一站式演示
6.1 创建一套完整的示例环境
为了让你直观看到增删改查的全过程,我用模拟项目X的人事管理系统做演示,先建库建表。表结构包含员工信息和部门信息两张表,部门表作为父表,员工表包含外键dep_id指向部门表:
CREATE DATABASE IF NOT EXISTS hr_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hr_system; CREATE TABLE department ( dept_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, dept_name VARCHAR(50) NOT NULL UNIQUE, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表'; CREATE TABLE employee ( emp_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, emp_name VARCHAR(50) NOT NULL, gender TINYINT NOT NULL DEFAULT 0 COMMENT '0-保密 1-男 2-女', salary DECIMAL(10,2) NOT NULL DEFAULT 0.00, dep_id INT UNSIGNED, hire_date DATE NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_emp_dep FOREIGN KEY (dep_id) REFERENCES department(dept_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表';6.2 完整走一遍增删改查五连操作
第一步,插入基础数据。先给部门表插入三条部门,再插入员工数据:
INSERT INTO department (dept_name) VALUES ('技术部'), ('产品部'), ('运营部'); INSERT INTO employee (emp_name, gender, salary, dep_id, hire_date) VALUES ('张三', 1, 18000.00, 1, '2022-03-15'), ('李四', 2, 16000.00, 1, '2023-06-01'), ('王五', 1, 14000.00, 2, '2021-11-20'), ('赵六', 2, 12000.00, 3, '2024-01-08');插入后要验证数据是否写入成功,用SELECT查询一下:
SELECT emp_id, emp_name, salary, dep_id, hire_date FROM employee;输出结果能直观看到四行数据,每行都有一个自增的emp_id,这验证了AUTO_INCREMENT的作用。接下来执行一个典型的分页查询,找出工资排前两名的员工:
SELECT emp_name, salary FROM employee ORDER BY salary DESC LIMIT 2;再来演示条件更新。把技术部所有员工的工资统一上调10%,但注意这里需要先知道技术部的dept_id是1,然后更新:
UPDATE employee SET salary = salary * 1.1 WHERE dep_id = 1;执行后影响行数是2,表示只有两位技术部员工被更新。这里我用WHERE dep_id = 1限定了范围,避免了全表被改动。最后演示删除操作,把入职日期在2023年之前的运营部员工删除,或者按员工维度删除特定人员:
DELETE FROM employee WHERE emp_id = 4;删除后查询一下,赵六这条记录已经不存在了。需要注意的是,被删掉的员工的emp_id不会再被复用,后续新插入的员工会从5开始编号,这是自增ID的正常行为。
6.3 用事务包裹增删改查,提高安全性
事务是确保一批CRUD操作要么全部成功、要么全部失败的机制。工资调整过程里,财务部门往往要求“技术部涨薪10%”和“产品部涨薪5%”两个操作作为一个整体执行,任何一个失败都要整体回滚。演示如下:
START TRANSACTION; UPDATE employee SET salary = salary * 1.1 WHERE dep_id = 1; UPDATE employee SET salary = salary * 1.05 WHERE dep_id = 2; -- 如果一切正常,提交事务 COMMIT; -- 如果中间发现异常,回滚事务 -- ROLLBACK;事务开启后,所有CRUD操作都只在“当前会话可见”,其他连接查询不到未提交的修改。只有执行COMMIT后,数据才对全局生效;执行ROLLBACK的话,一切修改都被丢弃。这个机制对生产环境的重要性怎么强调都不过分。真实项目中,批量的增删改操作必须包在事务里,并且要捕获异常后主动回滚,否则断网、程序崩溃等意外都会造成“只改了一半”的脏数据。
7. 高频报错与排查技巧实录
7.1 Column count doesn't match value count at row 1
这个报错发生在插入时列数量与值数量不匹配,比如表有5个列但你只给了4个值,或反过来给了6个值。解决方法不是猜,而是先用DESC查看表结构:
DESC employee;这一招能瞬间定位问题:看看字段列表里到底有哪些列、哪些有默认值、哪些是自增。从经验来看,这个报错大多发生在表结构经过ALTER TABLE增删字段后,应用的INSERT语句没同步更新。
7.2 Incorrect date value: '2024/01/08' for column
日期值报错的第一检查点是格式,MySQL默认接受的是'YYYY-MM-DD'或'YYYY-MM-DD HH:MM:SS',斜杠并不能直接使用。如果你在业务里习惯了用斜杠格式,往里传的时候要先用程序转换或者用STR_TO_DATE函数:
INSERT INTO employee (emp_name, hire_date) VALUES ('测试员', STR_TO_DATE('2024/01/08', '%Y/%m/%d'));解决后要检查第二个检查点:字段本身的类型到底是不是DATE/DATETIME。如果类型是VARCHAR,系统只会把它当字符串存储,后续所有的日期排序、日期函数都会失效。
7.3 UPDATE或者DELETE忘了WHERE,影响行数异常大
这个错误最隐蔽。执行完UPDATE后,MySQL客户端会返回Query OK显示影响的行数,如果发现影响的行数远超预期,说明WHERE条件失效了。此时如果还没提交事务,立即执行ROLLBACK;如果已提交,只能靠备份恢复。这套应急流程我建议你刻在脑子里:
-- 发现影响行数不对,如果还在事务里 ROLLBACK; -- 如果已提交,立刻从备份恢复日常预防的最好方式,就是给数据库账号最小权限。生产库的写权限只给应用专用账号,业务里禁止使用root这类超级账号执行DML语句。有了这层保障,就算命令写错,也不至于造成不可挽回的全局数据灾难。
7.4 中文乱码或者无法插入特殊字符
插入中文后出现乱码,99%的原因是从客户端到服务端某层字符集不统一。排查思路从服务端逐步检查:
SHOW VARIABLES LIKE 'character_set_%';重点看character_set_server、character_set_database和character_set_connection三项。命令行的临时处理方式可以在会话开始执行SET NAMES utf8mb4;持久化方案是修改my.cnf配置文件确保全局统一。连接串层面也要注意:使用JDBC时URL后拼接characterEncoding=utf8mb4;使用Python连接时确保charset='utf8mb4'。三个环节任何一层不统一,都可能出现字符错乱。
7.5 字段名与保留字冲突
表里有个字段叫desc或者order,直接查询时SQL引擎会把它当成关键字解析而报语法错误。最佳方案是建表时避坑,别用这类词做列名;如果表结构已经定了,用反引号包裹字段名:
SELECT `desc`, `order` FROM some_table WHERE id = 1;同样的道理也适用于表名。MySQL保留字列表随版本不断更新,建议实际规划表结构时查询官方文档的保留字清单,或者避开几乎所有抽象的英文短词,用user_name、order_info这种下划线组合来减少冲突。
7.6 空值判断用 = NULL 查不出任何数据
这是SQL语言最反直觉的一个点。NULL表示“未知值”,它既不等于任何值,也不等于NULL本身。所以WHERE name = NULL是永远查不到数据的,必须写成:
SELECT * FROM employee WHERE emp_name IS NULL; SELECT * FROM employee WHERE emp_name IS NOT NULL;哪怕业务表里几乎所有行都有值,但是只要有NULL出现,= NULL的写法就是查不到的。做条件筛选时也要特别小心:WHERE salary < 100000会排除所有salary为NULL的行,如果业务上需要包含空值行,条件里要单独用OR salary IS NULL。
8. 最后分享几条实战心得体会
CRUD写来写去就那么几条语法,但真正能稳的,都是对细节足够敏感的人。我自己在实际项目里养成了几个习惯:凡是UPDATE和DELETE,执行前永远先跑一遍等价的SELECT;凡是INSERT批量写入,永远先小批量试执行再全量跑;凡是涉及金额和数量的修改,永远显式开启事务并在代码里捕获异常回滚。这几个习惯看起来“多此一举”,但它们在两年来帮我挡住了至少三次足以影响生产数据的操作失误。
还记得我第一次负责核心业务表的数据维护时的场景:一条标准到不能再标准的DELETE语句,因为少了一个WHERE条件,把整张业务表清空了。万幸当时刚好开通了备份策略,全量恢复用了将近一个小时。那一小时的每一分钟都让我更确信,基础语法的同时,对风险的敬畏和流程的遵守才是真正的分水岭。
关于“增删改查”这个话题的延展空间其实很大,这一篇只打了地基。下一篇可以围绕WHERE条件的复杂应用展开:多条件组合查询、IN与EXISTS的取舍、LIKE的模糊匹配优化、聚合函数与GROUP BY分组统计等。把今天的这些基础操作先练熟了,理解每一类语句的执行逻辑和边界,再往进阶走,就不会在知识树上长歪了。