MySQL CRUD 操作
本章将详细介绍 MySQL 中的数据操作:插入(INSERT)、查询(SELECT)、更新(UPDATE)和删除(DELETE)。
知识速查
| 操作 | 语法示例 | 说明 |
|---|---|---|
| INSERT | INSERT INTO tbl (col) VALUES (val); | 插入新行 |
| SELECT | SELECT col FROM tbl WHERE cond; | 查询数据 |
| UPDATE | UPDATE tbl SET col=val WHERE cond; | 更新现有行 |
| DELETE | DELETE FROM tbl WHERE cond; | 删除行 |
| REPLACE | REPLACE INTO tbl (col) VALUES (val); | 替换(删除+插入) |
| MERGE | INSERT ... ON DUPLICATE KEY UPDATE ... | 存在则更新,否则插入 |
演示数据
为了便于演示,我们先创建示例表和数据:
-- 创建用户表
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
age TINYINT UNSIGNED,
salary DECIMAL(10, 2),
status ENUM('active', 'inactive') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 创建订单表
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
product_name VARCHAR(100) NOT NULL,
quantity INT DEFAULT 1,
price DECIMAL(10, 2) NOT NULL,
total_amount DECIMAL(12, 2),
status ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled') DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT(插入数据)
INSERT 语句用于向表中插入新数据。理解 INSERT 的各种用法对于数据操作至关重要。
基本语法
INSERT INTO 表名 (列1, 列2, ...)
VALUES (值1, 值2, ...);
这个语法清晰地说明了:我们需要指定目标表名、要插入数据的列名,以及对应的值。列名和值的顺序必须一一对应。
插入单行数据
-- 指定列名插入
INSERT INTO users (username, email, age, salary)
VALUES ('张三', '[email protected]', 25, 8000.00);
-- 省略列名(按表定义顺序)
INSERT INTO users
VALUES (NULL, '李四', '[email protected]', 30, 9000.00, 'active', NOW());
-- 使用 DEFAULT 关键字
INSERT INTO users (username, email, age)
VALUES ('王五', '[email protected]', DEFAULT);
要点说明:
NULL或不指定自增列,让 MySQL 自动生成 ID。自增列会自动分配一个唯一的递增值。- 未指定的列会使用默认值(如果有)或 NULL。如果列定义为 NOT NULL 且没有默认值,插入时会报错。
- 推荐始终指定列名,使代码更清晰且不依赖列顺序。当表结构变化时,指定列名的语句更不容易出错。
插入多行数据
-- 一次插入多行(推荐,性能更好)
INSERT INTO users (username, email, age, salary) VALUES
('赵六', '[email protected]', 28, 7500.00),
('孙七', '[email protected]', 35, 12000.00),
('周八', '[email protected]', 22, 6000.00);
多行插入的优势:
- 减少网络往返次数:客户端与服务器之间只需要一次通信
- 减少 SQL 解析次数:MySQL 只需解析一次语句
- 性能优于多次单行插入:批量插入的效率通常是单行插入的数倍
在实际应用中,当需要插入大量数据时,应该优先考虑多行插入或使用 LOAD DATA INFILE。
INSERT ... SET 语法
-- MySQL 特有语法,更清晰
INSERT INTO users
SET username = '吴九',
email = '[email protected]',
age = 27,
salary = 8500.00;
这种语法是 MySQL 的扩展,不是标准 SQL。它的优点是每个字段和值对应清晰,特别适合字段较多的场景。缺点是只能插入单行数据。
INSERT ... SELECT
从其他表复制数据:
-- 从另一个表复制数据
INSERT INTO users_backup (username, email, age)
SELECT username, email, age FROM users WHERE status = 'active';
-- 结合条件复制
INSERT INTO vip_users (username, email)
SELECT username, email FROM users WHERE salary > 10000;
INSERT ... SELECT 非常适合数据迁移、数据备份、数据汇总等场景。需要注意的是,SELECT 查询的列数和数据类型必须与目标表的列匹配。
INSERT IGNORE
忽略重复键错误:
-- 如果 email 重复,忽略此条记录,不报错
INSERT IGNORE INTO users (username, email, age)
VALUES ('张三', '[email protected]', 25);
当表中存在唯一索引或主键约束时,插入重复值会报错。使用 INSERT IGNORE 可以让 MySQL 静默忽略这些错误,继续执行后续操作。这在批量导入数据时很有用,可以跳过已存在的记录。
需要注意的是,INSERT IGNORE 不仅忽略重复键错误,还会忽略其他非致命错误(如数据类型转换警告),使用时要谨慎。
ON DUPLICATE KEY UPDATE
存在则更新,不存在则插入:
-- 如果 email 存在,则更新用户名和年龄
INSERT INTO users (username, email, age, salary)
VALUES ('张三', '[email protected]', 26, 8500.00)
ON DUPLICATE KEY UPDATE
username = VALUES(username),
age = VALUES(age),
salary = VALUES(salary);
-- MySQL 8.0.20+ 可以使用别名
INSERT INTO users (username, email, age, salary)
VALUES ('张三', '[email protected]', 26, 8500.00) AS new
ON DUPLICATE KEY UPDATE
username = new.username,
age = new.age,
salary = new.salary;
这是实现 "upsert"(update or insert)操作的标准方式。当插入的记录与现有记录的主键或唯一键冲突时,执行 UPDATE 操作;否则执行 INSERT 操作。
VALUES() 函数返回的是 INSERT 语句中尝试插入的值。在 MySQL 8.0.20+ 中,推荐使用别名语法,更加清晰易读。
REPLACE
替换数据(先删除再插入):
-- 如果 email 存在,先删除原记录,再插入新记录
REPLACE INTO users (username, email, age, salary)
VALUES ('张三', '[email protected]', 25, 8000.00);
REPLACE vs ON DUPLICATE KEY UPDATE:
| 特性 | REPLACE | ON DUPLICATE KEY UPDATE |
|---|---|---|
| 原有 ID | 会被删除,生成新 ID | 保持不变 |
| 触发器 | 触发 DELETE 和 INSERT | 触发 UPDATE |
| 性能 | 较低(两次操作) | 较高 |
| 影响 | 影响自增ID和关联数据 | 仅更新指定字段 |
大多数情况下,推荐使用 ON DUPLICATE KEY UPDATE 而不是 REPLACE,因为它不会改变记录的主键值,也不会影响关联的外键数据。
SELECT(查询数据)
SELECT 是 SQL 中最常用的语句,用于从数据库中检索数据。掌握 SELECT 的各种用法是数据库操作的基础。
基本语法
SELECT 列1, 列2, ...
FROM 表名
[WHERE 条件]
[GROUP BY 分组]
[HAVING 分组条件]
[ORDER BY 排序]
[LIMIT 限制];
SELECT 语句的执行顺序是:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。理解这个顺序对于编写复杂查询很重要。
基础查询
-- 查询所有列
SELECT * FROM users;
-- 查询指定列
SELECT username, email FROM users;
-- 使用别名
SELECT
username AS 用户名,
email AS 邮箱,
salary AS 薪资
FROM users;
-- 计算字段
SELECT
username,
salary,
salary * 12 AS 年薪,
CONCAT(username, ' - ', email) AS 用户信息
FROM users;
-- 去重
SELECT DISTINCT status FROM users;
-- 限制行数
SELECT * FROM users LIMIT 5; -- 前 5 条
SELECT * FROM users LIMIT 5 OFFSET 10; -- 跳过 10 条,取 5 条
SELECT * FROM users LIMIT 10, 5; -- 从第 11 条开始,取 5 条
SELECT * 会返回表中的所有列,在生产环境中不推荐使用,因为:
- 当表结构变化时,查询结果会改变
- 返回不需要的数据会浪费网络带宽
- 可能暴露敏感字段
WHERE 条件
WHERE 子句用于过滤数据,只返回满足条件的行。
-- 比较运算符
SELECT * FROM users WHERE age = 25;
SELECT * FROM users WHERE age > 25;
SELECT * FROM users WHERE age >= 25;
SELECT * FROM users WHERE age < 30;
SELECT * FROM users WHERE age != 25;
SELECT * FROM users WHERE age <=> NULL; -- NULL 安全比较
-- 逻辑运算符
SELECT * FROM users WHERE age > 25 AND salary > 8000;
SELECT * FROM users WHERE age < 25 OR age > 30;
SELECT * FROM users WHERE NOT status = 'inactive';
-- BETWEEN 范围
SELECT * FROM users WHERE age BETWEEN 25 AND 35;
SELECT * FROM users WHERE salary NOT BETWEEN 5000 AND 10000;
-- IN 列表
SELECT * FROM users WHERE age IN (25, 30, 35);
SELECT * FROM users WHERE status IN ('active', 'pending');
-- LIKE 模糊匹配
SELECT * FROM users WHERE username LIKE '张%'; -- 以张开头
SELECT * FROM users WHERE email LIKE '%@gmail.com'; -- 以 @gmail.com 结尾
SELECT * FROM users WHERE username LIKE '%三'; -- 包含三
SELECT * FROM users WHERE username LIKE '_三'; -- 两个字符,第二个是三
-- NULL 处理
SELECT * FROM users WHERE age IS NULL;
SELECT * FROM users WHERE age IS NOT NULL;
LIKE 通配符说明:
| 通配符 | 说明 |
|---|---|
% | 匹配任意多个字符(包括零个字符) |
_ | 匹配单个字符 |
需要注意的是,NULL 值在 SQL 中是一个特殊的值,表示"未知"。任何与 NULL 的比较(如 = NULL、<> NULL)都会返回 NULL(不是 true 也不是 false)。必须使用 IS NULL 或 IS NOT NULL 来判断,或者使用 <=> 进行 NULL 安全比较。
ORDER BY 排序
-- 单列排序
SELECT * FROM users ORDER BY age; -- 升序(默认)
SELECT * FROM users ORDER BY age ASC; -- 升序
SELECT * FROM users ORDER BY age DESC; -- 降序
-- 多列排序
SELECT * FROM users ORDER BY status, salary DESC;
-- 按表达式排序
SELECT * FROM users ORDER BY salary * 12 DESC;
-- 按列位置排序
SELECT username, salary FROM users ORDER BY 2 DESC; -- 按第 2 列排序
多列排序时,MySQL 先按第一列排序,如果第一列的值相同,再按第二列排序,以此类推。按列位置排序的语法虽然方便,但可读性较差,不推荐在生产代码中使用。
GROUP BY 分组
GROUP BY 用于将数据按指定列分组,通常与聚合函数一起使用。
-- 基本分组
SELECT status, COUNT(*) AS count
FROM users
GROUP BY status;
-- 多列分组
SELECT status, age, COUNT(*) AS count
FROM users
GROUP BY status, age;
-- 聚合函数
SELECT
status,
COUNT(*) AS 总数,
AVG(salary) AS 平均薪资,
SUM(salary) AS 薪资总和,
MAX(salary) AS 最高薪资,
MIN(salary) AS 最低薪资
FROM users
GROUP BY status;
常用的聚合函数包括:
COUNT(*):统计行数COUNT(列名):统计非 NULL 值的数量SUM(列名):求和AVG(列名):求平均值MAX(列名):求最大值MIN(列名):求最小值
HAVING 子句
HAVING 用于过滤分组后的结果(类似 WHERE,但作用于分组):
-- 查询平均薪资大于 8000 的状态组
SELECT status, AVG(salary) AS avg_salary
FROM users
GROUP BY status
HAVING avg_salary > 8000;
-- WHERE 与 HAVING 结合
SELECT status, COUNT(*) AS count
FROM users
WHERE age >= 25
GROUP BY status
HAVING count > 2;
WHERE vs HAVING:
| 子句 | 作用阶段 | 说明 |
|---|---|---|
| WHERE | 分组前 | 过滤原始行 |
| HAVING | 分组后 | 过滤分组结果 |
WHERE 在分组前执行,不能使用聚合函数;HAVING 在分组后执行,可以使用聚合函数。在实际使用中,应该优先使用 WHERE 过滤数据,这样可以减少参与分组的数据量,提高查询效率。
多表连接查询
在实际应用中,数据通常分散在多个表中,需要通过连接操作将数据组合起来。
INNER JOIN(内连接)
返回两表中匹配的记录:
SELECT
users.username,
orders.product_name,
orders.total_amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;
INNER JOIN 只返回两个表中都有匹配的记录。如果某用户没有订单,或者某订单没有对应的用户,这些记录都不会出现在结果中。
LEFT JOIN(左连接)
返回左表所有记录,右表无匹配则为 NULL:
-- 查询所有用户及其订单(包括没有订单的用户)
SELECT
u.username,
o.product_name,
o.total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
LEFT JOIN 返回左表的所有记录,即使右表中没有匹配的记录。这在查找"没有关联数据"的记录时很有用,比如找出没有订单的用户。
RIGHT JOIN(右连接)
返回右表所有记录,左表无匹配则为 NULL:
SELECT
u.username,
o.product_name
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
RIGHT JOIN 与 LEFT JOIN 相反,返回右表的所有记录。实际上,RIGHT JOIN 可以通过交换表的位置用 LEFT JOIN 实现,所以实际使用中 RIGHT JOIN 较少使用。
多表连接
SELECT
u.username,
o.product_name,
p.category
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN products p ON o.product_id = p.id
WHERE o.status = 'completed';
多表连接时,需要注意连接的顺序和条件。每个 JOIN 都需要指定连接条件,否则会产生笛卡尔积。
子查询
子查询是嵌套在其他查询中的查询,可以出现在 WHERE、FROM、SELECT 等子句中。
标量子查询
返回单个值:
-- 查询薪资高于平均薪资的用户
SELECT * FROM users
WHERE salary > (SELECT AVG(salary) FROM users);
标量子查询返回单个值(一行一列),可以像使用常量一样使用它。如果子查询返回多行,会报错。
列子查询
返回单列多行:
-- 查询有订单的用户
SELECT * FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders);
-- 使用 EXISTS
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
IN 和 EXISTS 都可以用于检查子查询的结果。EXISTS 通常在子查询返回大量数据时性能更好,因为它只需要判断是否存在匹配,不需要返回所有值。
行子查询
返回单行多列:
-- 查询与张三同部门同职位的用户
SELECT * FROM users
WHERE (department, position) = (
SELECT department, position FROM users WHERE username = '张三'
);
FROM 子句子查询
-- 统计每个用户的订单数
SELECT
u.username,
o.order_count
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
) o ON u.id = o.user_id;
FROM 子句子查询也称为"派生表",必须在派生表上使用别名。这种方式适合需要在子查询中进行复杂计算后再与主表连接的场景。
UPDATE(更新数据)
UPDATE 语句用于修改表中已有的数据。
基本语法
UPDATE 表名
SET 列1 = 值1, 列2 = 值2, ...
[WHERE 条件];
更新示例
-- 更新单个字段
UPDATE users SET salary = 9000 WHERE username = '张三';
-- 更新多个字段
UPDATE users
SET salary = 9500, age = 26
WHERE username = '张三';
-- 使用表达式更新
UPDATE users SET salary = salary * 1.1 WHERE status = 'active';
-- 基于其他列更新
UPDATE users SET salary = salary + age * 100;
-- 更新为 NULL
UPDATE users SET age = NULL WHERE username = '李四';
多表更新
-- 根据另一个表的数据更新
UPDATE users u
INNER JOIN orders o ON u.id = o.user_id
SET u.status = 'vip'
WHERE o.total_amount > 10000;
多表更新允许基于另一个表的数据来更新当前表。这在需要关联多个表的数据进行更新时非常有用。
ORDER BY 和 LIMIT
-- 更新薪资最低的 3 个用户
UPDATE users
SET salary = salary + 1000
ORDER BY salary ASC
LIMIT 3;
ORDER BY 和 LIMIT 可以控制更新的顺序和数量。这在进行批量更新时很有用,可以分批次更新数据,避免长时间锁表。
执行 UPDATE 时,务必确保 WHERE 条件正确。没有 WHERE 条件会更新所有行!
在执行 UPDATE 前,建议先用 SELECT 验证 WHERE 条件是否正确:
-- 先验证
SELECT * FROM users WHERE status = 'inactive';
-- 确认无误后再执行
UPDATE users SET status = 'deleted' WHERE status = 'inactive';
安全更新模式
MySQL 默认启用安全更新模式,防止误操作:
-- 查看安全模式状态
SHOW VARIABLES LIKE 'sql_safe_updates';
-- 临时禁用(不推荐)
SET sql_safe_updates = 0;
安全更新模式要求:
- UPDATE 和 DELETE 必须有 WHERE 条件
- WHERE 条件必须使用键列或 LIMIT
这是一个重要的保护机制,在生产环境中应该保持启用。
DELETE(删除数据)
DELETE 语句用于删除表中的数据。
基本语法
DELETE FROM 表名 [WHERE 条件];
删除示例
-- 删除指定记录
DELETE FROM users WHERE id = 5;
-- 删除符合条件的记录
DELETE FROM users WHERE status = 'inactive';
-- 删除所有数据(不推荐)
DELETE FROM users;
多表删除
-- 删除没有订单的用户
DELETE u FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
-- 从多个表删除
DELETE users, orders
FROM users
INNER JOIN orders ON users.id = orders.user_id
WHERE users.status = 'deleted';
多表删除可以一次删除多个表中的相关数据。这在处理关联数据时很有用,但需要谨慎使用。
ORDER BY 和 LIMIT
-- 删除最早创建的 10 条记录
DELETE FROM logs
ORDER BY created_at ASC
LIMIT 10;
TRUNCATE vs DELETE
-- TRUNCATE:快速清空表
TRUNCATE TABLE users;
-- DELETE:逐行删除
DELETE FROM users;
| 特性 | TRUNCATE | DELETE |
|---|---|---|
| 速度 | 快(DDL) | 慢(逐行) |
| 条件 | 不支持 | 支持 WHERE |
| 回滚 | 不支持 | 支持 |
| 自增 ID | 重置 | 不重置 |
| 触发器 | 不触发 | 触发 |
TRUNCATE 是 DDL 语句,它会删除表中的所有数据并重置自增 ID,但不能回滚。DELETE 是 DML 语句,可以指定条件、可以回滚,但速度较慢。
事务处理
事务是一组数据库操作的逻辑单元,要么全部成功,要么全部失败。事务保证了数据的完整性。
基本事务操作
-- 开始事务
START TRANSACTION;
-- 或使用
BEGIN;
-- 执行操作
INSERT INTO users (username, email) VALUES ('测试用户', '[email protected]');
UPDATE users SET status = 'active' WHERE username = '测试用户';
-- 提交事务
COMMIT;
-- 回滚事务
ROLLBACK;
事务的 ACID 特性:
- 原子性(Atomicity):事务中的操作要么全部执行,要么全部不执行
- 一致性(Consistency):事务执行前后,数据库状态保持一致
- 隔离性(Isolation):并发事务之间互不影响
- 持久性(Durability):事务提交后,数据永久保存
保存点
START TRANSACTION;
INSERT INTO users (username, email) VALUES ('用户A', '[email protected]');
-- 设置保存点
SAVEPOINT point_a;
INSERT INTO users (username, email) VALUES ('用户B', '[email protected]');
-- 回滚到保存点
ROLLBACK TO SAVEPOINT point_a;
-- 提交
COMMIT; -- 只有用户A被插入
保存点允许在事务中创建多个回滚点,可以部分回滚而不影响整个事务。
事务隔离级别
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
隔离级别说明:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 可能 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
MySQL 默认使用 REPEATABLE READ 隔离级别,通过 MVCC(多版本并发控制)和 Next-Key Lock 实现了在 RR 级别下防止幻读。
最佳实践
查询优化建议
-
**避免 SELECT ***
-- 不推荐
SELECT * FROM users;
-- 推荐:只查询需要的列
SELECT id, username, email FROM users; -
合理使用索引
-- 为常用查询条件创建索引
CREATE INDEX idx_status ON users(status);
CREATE INDEX idx_created_at ON orders(created_at); -
分页查询优化
-- 传统分页(数据量大时性能差)
SELECT * FROM users LIMIT 10000, 20;
-- 优化:使用覆盖索引
SELECT u.* FROM users u
INNER JOIN (SELECT id FROM users LIMIT 10000, 20) tmp
ON u.id = tmp.id;
-- 或使用 WHERE 条件
SELECT * FROM users WHERE id > 10000 LIMIT 20;
批量操作建议
-- 批量插入(推荐)
INSERT INTO users (username, email) VALUES
('user1', '[email protected]'),
('user2', '[email protected]'),
('user3', '[email protected]');
-- 大批量数据导入
LOAD DATA INFILE '/path/to/data.csv'
INTO TABLE users
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
(username, email, age);
LOAD DATA INFILE 是导入大量数据最快的方式,比 INSERT 语句快 20-100 倍。
小结
本章我们学习了:
- INSERT:插入数据,包括单行、多行、从其他表复制
- SELECT:查询数据,包括条件、排序、分组、连接、子查询
- UPDATE:更新数据,包括表达式更新、多表更新
- DELETE:删除数据,包括条件删除、多表删除
- 事务处理:事务的开始、提交、回滚和隔离级别
练习
- 向 users 表插入多条数据
- 查询年龄在 25-35 之间的活跃用户
- 统计每个状态的用户数量和平均薪资
- 使用连接查询获取用户及其订单信息
- 使用事务完成一个转账操作(从一个账户扣款,向另一个账户存款)