【计算基础|MySQL02】【上手】SQL 增删改查
上一篇把库表建好了,这篇开始天天写的那四类 SQL:INSERT、UPDATE、DELETE、SELECT。看着简单,生产事故一半都出自这四个字------UPDATE 忘了 WHERE、SELECT * 满天飞、LIKE '%xx' 慢到崩、= NULL 查不出数据。
这篇在上一篇建的 shop 库(users / products / orders 三张表)上练手,每条 SQL 给:语法 → 实例 → 真实输出 → 易错点。结尾 16 个查询练习,全部真实跑过。
环境:Docker MySQL 8.0.46,库
shop,数据见上篇。所有输出均为真实执行结果。
一、INSERT:插入数据
1.1 三种写法
sql
-- 写法一:标准批量插入(最常用)
INSERT INTO 表(列1,列2) VALUES (v1,v2),(v3,v4);
-- 写法二:SET 写法(列少时直观)
INSERT INTO 表 SET 列1=v1, 列2=v2;
-- 写法三:从另一张表查出来插入
INSERT INTO 表(列1,列2) SELECT 列1,列2 FROM 另一张表;
在临时表 t_crud 上实测:
sql
CREATE TABLE t_crud(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20), city VARCHAR(20), score INT);
INSERT INTO t_crud(name,city,score) VALUES ('a','北京',90),('b','上海',80),('c','北京',70);
INSERT INTO t_crud SET name='d', city='广州', score=60;
SELECT * FROM t_crud;
输出:
text
id name city score
1 a 北京 90
2 b 上海 80
3 c 北京 70
4 d 广州 60
🔴 重点:
- 自增主键可以不指定 ,上面没插
id,它自己从 1 往上长。 - 批量插入性能远好于逐条 。插 1 万行,一条
VALUES (...),(...),...比循环插 1 万次快几十倍------因为减少了网络往返和事务提交次数。 INSERT ... SELECT做数据迁移/备份时很好用,但注意目标表和来源表别是同一张(会死循环)。
二、UPDATE / DELETE:不带 WHERE 就是事故
2.1 语法
sql
UPDATE 表 SET 列=值, 列2=值2 WHERE 条件;
DELETE FROM 表 WHERE 条件;
实测(还是临时表,安全):
sql
UPDATE t_crud SET score=score+5 WHERE city='北京';
SELECT * FROM t_crud;
输出(北京两个用户各加 5 分):
text
id name city score
1 a 北京 95
2 b 上海 80
3 c 北京 75
4 d 广州 60
sql
DELETE FROM t_crud WHERE score<70;
SELECT * FROM t_crud;
输出(score=60 的 d 被删):
text
id name city score
1 a 北京 95
2 b 上海 80
3 c 北京 75
⚠️ 避坑(生产保命):
UPDATE/DELETE不写WHERE会操作全表 。UPDATE users SET balance=0;一行 SQL 把全库余额清零。执行前先把WHERE条件放到SELECT里查一遍,确认影响的行数对不对,再改成 UPDATE。- 大表删除别一次性
DELETE全删 ,会锁表、撑爆 undo log。用DELETE FROM t WHERE ... LIMIT 1000;循环删,每次删一小批,中间 sleep。 DELETE是 DML 可以回滚(在事务里);TRUNCATE是 DDL 不可回滚,区别见上篇。
三、SELECT 基础:查什么列
3.1 别再 SELECT *
sql
-- ❌ 生产禁止
SELECT * FROM users;
-- ✅ 显式列名
SELECT id, username, balance FROM users;
为什么生产禁止 SELECT *:
| 问题 | 后果 |
|---|---|
| 传输量大 | 多查了用不到的列(比如 TEXT 大字段),浪费网络和内存 |
| 覆盖索引失效 | 索引里没带全列,本来能走覆盖索引,逼得回表 |
| 表结构变更脆弱 | 加了列,你的代码/结果集映射就跟着变 |
3.2 列别名与表达式
sql
SELECT username AS 用户名, balance*1.1 AS 加息后余额 FROM users;
输出:
text
用户名 加息后余额
alice 1100.000
bob 550.550
carol 2200.000
david 880.000
erin 330.000
frank 110.000
AS 起别名(AS 可省略),SELECT 里可以直接写表达式、函数。别名不能在 WHERE 里用(WHERE 先于 SELECT 执行),但能在 ORDER BY / GROUP BY 里用。
四、WHERE:过滤条件的全部套路
4.1 比较与逻辑运算
sql
-- 比较:= <> > < >= <=
SELECT username,age FROM users WHERE age >= 28;
输出:
text
username age
bob 30
carol 28
david 35
🔴 AND 优先级高于 OR。下面这句不是"(北京或上海)且status=1",而是"北京的全部,加上(上海且status=1)的":
sql
SELECT username,city,status FROM users WHERE city='北京' OR city='上海' AND status=1;
输出:
text
username city status
alice 北京 1
bob 上海 1
carol 北京 1
erin 上海 1
注意 david(广州)不在内,而所有北京用户(不管 status)都进来了。想按你以为的逻辑,就老老实实加括号 :(city='北京' OR city='上海') AND status=1。
4.2 IN / BETWEEN / LIKE
sql
-- IN:等于列表中任意一个
SELECT username,city FROM users WHERE city IN ('北京','广州');
text
username city
alice 北京
carol 北京
david 广州
sql
-- BETWEEN:含边界
SELECT username,age FROM users WHERE age BETWEEN 25 AND 30;
text
username age
alice 25
bob 30
carol 28
sql
-- LIKE:% 任意多字符,_ 单个字符
SELECT name FROM products WHERE name LIKE '4K%';
text
name
4K显示器
⚠️ 三个坑:
NOT IN子查询里只要有 NULL,整个结果为空 。WHERE x NOT IN (1,NULL)永远返回空行,因为 NULL 参与的比较结果是 UNKNOWN。LIKE '%xx'(左模糊)用不了索引 。%在前面,B+树不知道从哪开始找,只能全表扫。LIKE 'xx%'(右模糊)才能走索引。BETWEEN min AND max含两头边界,25 和 30 都在。
4.3 IS NULL:NULL 只能这么查
sql
SELECT username,age,city FROM users WHERE city IS NULL;
text
username age city
frank NULL NULL
⚠️ = NULL 永远查不出东西,因为 NULL 不等于任何值,包括它自己:
sql
SELECT COUNT(*) AS wrong_count FROM users WHERE city = NULL;
text
wrong_count
0
🔴 记住 :判空用 IS NULL / IS NOT NULL,永远别写 = NULL。
五、ORDER BY 与 LIMIT
5.1 排序
sql
-- 单列降序
SELECT username,balance FROM users ORDER BY balance DESC;
text
username balance
carol 2000.00
alice 1000.00
david 800.00
bob 500.50
erin 300.00
frank 100.00
sql
-- 多字段:先按城市升序,同城市内按余额降序
SELECT username,city,balance FROM users ORDER BY city ASC, balance DESC;
text
username city balance
frank NULL 100.00
bob 上海 500.50
erin 上海 300.00
carol 北京 2000.00
alice 北京 1000.00
david 广州 800.00
🔴 两个细节:
ASC升序是默认,DESC降序。- MySQL 中 NULL 排最前(上面 frank 的 NULL city 在最上面)。
ORDER BY的字段如果没索引,会触发 filesort(额外排序),数据量大时慢------这是后面索引篇要优化的重点。
5.2 LIMIT 分页
sql
-- 前 3 条
SELECT name,price FROM products ORDER BY price DESC LIMIT 3;
text
name price
4K显示器 1299.00
耳机 599.00
机械键盘 399.00
sql
-- 跳过 1 条取 2 条(第 2、3 名)
SELECT name,price FROM products ORDER BY price DESC LIMIT 1,2;
text
name price
耳机 599.00
机械键盘 399.00
⚠️ 深分页问题 :LIMIT 1000000,10 看着只取 10 条,MySQL 却要先扫描并丢弃前 100 万行,越翻越慢。优化手段(用主键"书签")留到索引篇讲。
六、DISTINCT 去重
sql
SELECT DISTINCT city FROM users;
text
city
北京
上海
广州
NULL
sql
-- 多列是"组合去重"
SELECT DISTINCT city,status FROM users;
text
city status
北京 1
上海 1
广州 0
NULL 1
⚠️ DISTINCT 作用于所有查询列的组合 ,不是只去重第一列。DISTINCT city,status 是把 (city,status) 整个组合去重。要算个数用 COUNT(DISTINCT city),聚合篇讲。
七、最容易被忽视的坑:对列套函数会让索引失效
这是上篇铺垫、这篇必须亲眼看到的东西。orders 表给 created_at 建了索引后对比:
sql
-- 写法 A:对列套函数
EXPLAIN SELECT * FROM orders WHERE DATE(created_at)='2026-09-24'\G
text
type: ALL ← 全表扫描
key: NULL ← 没用上索引
possible_keys: NULL
sql
-- 写法 B:范围查询,列本身裸露
EXPLAIN SELECT * FROM orders WHERE created_at>='2026-09-24' AND created_at<'2026-09-25'\G
text
type: range ← 走了索引
key: idx_created
possible_keys: idx_created
🔴 结论 :WHERE DATE(created_at)=...、WHERE id+1=2、WHERE UPPER(name)='ABC' 这类对索引列套函数/运算 ,MySQL 没法直接用索引(它得先算每一行的函数值),只能全表扫。想查某一天的数据,写成范围区间 >= 当天 0 点 AND < 次日 0 点,别套 DATE()。
总结:你真正需要记住的 9 件事
- 批量插入一条
VALUES(...),(...)比逐条快几十倍;自增主键不用指定。 UPDATE/DELETE不写 WHERE 就是清全表,执行前先用 SELECT 核对影响行数。- 生产禁止
SELECT *,显式写列名,省传输、保覆盖索引、抗表结构变更。 - AND 优先级高于 OR,条件复杂就加括号,别赌自己的逻辑。
IN、BETWEEN(含边界)、LIKE(%任意、_单字符);左模糊%xx用不了索引。- 判空用
IS NULL,=NULL永远查不出东西。 ORDER BY无索引会 filesort,NULL 默认排最前;LIMIT offset,n深分页会越翻越慢。DISTINCT是对所有列组合去重,不是单列。- WHERE 里别对索引列套函数/运算 ,会让索引失效(
type=ALL),日期范围用区间写。
验证清单
- 能用一条批量 INSERT 插入 3 行,并说清为什么比逐条快
- 能复现"忘了 WHERE 会操作全表"的危险,并养成先 SELECT 核对的习惯
- 能解释 AND/OR 优先级,并用括号改对一个条件
- 能正确用
IS NULL查出 frank,并说明=NULL为什么查不到 - 能写出
LIKE 'xx%'(走索引)和LIKE '%xx'(不走索引)的区别 - 能用
ORDER BY ... DESC LIMIT 3查出价格 TOP3 - 能用 EXPLAIN 对比出"对列套函数"和"范围查询"在 type 上的差异(ALL vs range)
参考资源
- MySQL 8.0 SELECT 语法:dev.mysql.com/doc/refman/...
- 表达式运算符与函数:dev.mysql.com/doc/refman/...
- EXPLAIN 输出格式:dev.mysql.com/doc/refman/...
- DELETE / UPDATE 语法:dev.mysql.com/doc/refman/...
下一篇《【上手】多表查询与聚合》讲:INNER JOIN / LEFT JOIN 的区别与笛卡尔积、自连接与子查询(IN / EXISTS)、COUNT/SUM/AVG 聚合、GROUP BY + HAVING 的执行顺序,以及 UNION 与 UNION ALL。会在 shop 库上跑"每个用户的订单总额""从未下单的用户""销量 TOP3 商品"这些经典查询。