1. 为什么我们需要掌握MySQL增删改查?
作为一名数据库工程师,我经常遇到这样的场景:新入职的同事面对MySQL时,要么被各种GUI工具迷惑了双眼,要么陷入复杂的优化理论而忽略了基础。实际上,工作中80%的数据库操作都围绕着CRUD(增删改查)展开。今天,我就带大家重新审视这些"基础"操作背后的门道。
MySQL的增删改查远不止是简单的四条命令。以查询为例,同样的SELECT语句,在不同数据量级、不同索引条件下,性能可能相差百倍。而一个不当的UPDATE操作,可能导致全表锁定,引发线上事故。这些经验,都是我在处理过多次生产环境故障后积累的实战认知。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 表的基础操作全解析
2.1 创建表的艺术
创建表看似简单,但字段类型的选择直接影响后续查询性能。比如手机号字段,新手可能直接用VARCHAR(20),但我会这样设计:
sql复制CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL COMMENT '登录账号',
mobile CHAR(11) NOT NULL COMMENT '国内手机号',
status TINYINT UNSIGNED DEFAULT 1 COMMENT '1-正常 2-冻结',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_mobile (mobile),
KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
这里有几个关键设计点:
- 手机号用CHAR而非VARCHAR,因为长度固定且需要高频查询
- 状态字段用TINYINT而非VARCHAR,节省存储空间
- 自动维护创建和更新时间戳
- 为手机号添加唯一索引,避免重复注册
- 使用utf8mb4字符集支持emoji等特殊字符
2.2 修改表结构的避坑指南
线上修改表结构是高风险操作,我总结了一套安全流程:
- 先在测试环境执行
- 使用pt-online-schema-change工具(适用于大表)
- 低峰期操作
- 准备好回滚方案
比如要新增一个邮箱字段:
sql复制ALTER TABLE users
ADD COLUMN email VARCHAR(100) COMMENT '用户邮箱',
ALGORITHM=INPLACE, LOCK=NONE;
关键参数说明:
- ALGORITHM=INPLACE:尽量使用原地修改算法
- LOCK=NONE:避免锁表影响线上业务
2.3 删除表的注意事项
生产环境禁止直接DROP TABLE!正确的做法是:
- 先RENAME TABLE转移
- 观察一段时间确认无影响
- 最后再删除
sql复制-- 安全删除流程
RENAME TABLE users TO users_del_20230815;
-- 观察1周后
DROP TABLE users_del_20230815;
3. 数据操作进阶技巧
3.1 插入数据的性能优化
批量插入比单条插入效率高10倍以上。对比以下两种写法:
sql复制-- 低效写法
INSERT INTO orders(user_id, amount) VALUES(1, 100);
INSERT INTO orders(user_id, amount) VALUES(1, 200);
...
-- 高效写法
INSERT INTO orders(user_id, amount) VALUES
(1, 100),
(1, 200),
...;
对于海量数据导入,建议:
- 使用LOAD DATA INFILE(比INSERT快20倍)
- 临时关闭索引和约束
- 分批提交(每1万条一次)
3.2 更新操作的安全锁
UPDATE操作不当会导致锁全表,这个案例让我记忆犹新:
sql复制-- 危险操作:没有使用索引字段导致全表锁
UPDATE products SET stock = stock - 1 WHERE name LIKE '%手机%';
-- 安全写法:使用主键或索引字段
UPDATE products SET stock = stock - 1 WHERE id IN (1,2,3);
黄金法则:UPDATE条件必须能用上索引,否则在事务中会导致全表记录被锁定。
3.3 删除数据的正确姿势
生产环境删除数据必须加LIMIT!这是我用惨痛教训换来的经验:
sql复制-- 危险操作:可能误删整个表
DELETE FROM logs WHERE create_time < '2023-01-01';
-- 安全写法:分批删除
DELETE FROM logs WHERE create_time < '2023-01-01' LIMIT 1000;
-- 然后循环执行直到影响行数为0
对于重要数据,建议采用逻辑删除:
sql复制ALTER TABLE orders ADD COLUMN is_deleted TINYINT DEFAULT 0;
UPDATE orders SET is_deleted = 1 WHERE ...;
4. 查询优化的核心方法论
4.1 EXPLAIN执行计划详解
这个查询为什么慢?让我用EXPLAIN带你分析:
sql复制EXPLAIN SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.status = 1 AND o.amount > 1000;
关键指标解读:
- type列:ALL表示全表扫描,要优化为ref或range
- key列:显示实际使用的索引
- rows列:预估检查的行数
- Extra列:Using filesort/Using temporary需要优化
4.2 索引失效的常见陷阱
即使有索引也可能用不上,这些坑我基本都踩过:
- 隐式类型转换:
sql复制-- mobile是字符串类型,用数字查询会导致索引失效
SELECT * FROM users WHERE mobile = 13800138000;
- 使用函数操作:
sql复制-- 对索引字段使用函数会导致失效
SELECT * FROM orders WHERE DATE(create_time) = '2023-08-15';
- 前导模糊查询:
sql复制-- LIKE '%xxx' 无法使用索引
SELECT * FROM products WHERE name LIKE '%手机%';
4.3 连接查询的性能优化
多表连接时,我遵循这些原则:
- 小表驱动大表
- 确保关联字段有索引
- 避免SELECT *
优化案例:
sql复制-- 原始低效查询
SELECT * FROM large_table l JOIN small_table s ON l.id = s.lid;
-- 优化后写法
SELECT l.id, l.name, s.value
FROM small_table s
STRAIGHT_JOIN large_table l ON s.lid = l.id;
使用STRAIGHT_JOIN强制指定驱动表顺序。
5. 事务与锁的实战经验
5.1 事务隔离级别的选择
不同的隔离级别对性能影响巨大:
- 读未提交:性能最好,但会出现脏读
- 读已提交:Oracle默认,适合大多数场景
- 可重复读:MySQL默认,可能产生幻读
- 串行化:安全性最高,性能最差
线上配置建议:
sql复制-- 电商核心业务使用RC级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
5.2 死锁分析与预防
这是我遇到的一个典型死锁场景:
事务A:
sql复制UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
事务B:
sql复制UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
解决方案:
- 所有事务按相同顺序访问记录
- 减少事务持有锁的时间
- 使用SELECT ... FOR UPDATE明确加锁范围
5.3 乐观锁的实现方案
高并发更新时,我常用这种乐观锁模式:
sql复制-- 先查询当前版本
SELECT id, stock, version FROM products WHERE id = 1;
-- 更新时校验版本
UPDATE products SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 123;
如果影响行数为0,说明并发冲突,需要重试。
6. 生产环境最佳实践
6.1 数据库连接池配置
Java项目中的Druid配置经验:
properties复制# 初始连接数 = 平均QPS * 平均耗时(秒) * 2
druid.initialSize=10
# 最大连接数不超过 (核心数 * 2 + 磁盘数)
druid.maxActive=50
# 验证连接的SQL要用轻量级查询
druid.validationQuery=SELECT 1
# 获取连接超时时间(毫秒)
druid.maxWait=3000
关键点:
- 不要设置过大的maxActive
- 必须设置maxWait
- 定期监控连接数变化
6.2 SQL审核流程
我们团队的SQL上线流程:
- 开发人员在测试环境执行
- 使用pt-query-digest分析慢查询
- DBA审核执行计划
- 灰度发布观察
- 全量上线
禁止直接在生产环境执行未经审核的SQL!
6.3 监控指标关注点
我每天必看的监控指标:
- QPS/TPS波动
- 慢查询数量
- 连接数使用情况
- InnoDB缓冲池命中率
- 锁等待时间
配置报警阈值示例:
sql复制-- 慢查询报警
SET GLOBAL long_query_time = 1;
-- 连接数报警
SET GLOBAL max_connections = 500;
7. 常见问题解决方案
7.1 大表COUNT(*)优化
当表数据量上亿时,COUNT(*)会非常慢。我的解决方案:
- 使用估算值:
sql复制SHOW TABLE STATUS LIKE 'orders';
- 维护计数表:
sql复制CREATE TABLE table_counts (
table_name VARCHAR(100) PRIMARY KEY,
row_count BIGINT
);
- 用EXISTS替代COUNT:
sql复制SELECT 1 FROM orders WHERE ... LIMIT 1;
7.2 分页查询优化
经典的分页性能问题:
sql复制-- 低效写法
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
-- 高效写法
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;
对于复杂分页,我常用延迟关联技巧:
sql复制SELECT t.* FROM orders t
JOIN (SELECT id FROM orders ORDER BY create_time LIMIT 1000000, 10) tmp
ON t.id = tmp.id;
7.3 大数据量导出方案
需要导出百万级数据时,避免直接用SELECT *:
- 使用MySQL的SELECT INTO OUTFILE
sql复制SELECT * INTO OUTFILE '/tmp/orders.csv'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
FROM orders WHERE create_time BETWEEN ...;
- 分批查询后合并
- 使用专门的ETL工具如DataX
8. 开发中的实用技巧
8.1 避免隐式提交的操作
这些语句会隐式提交当前事务:
- DDL语句(ALTER/CREATE/DROP)
- 管理语句(LOCK TABLES/UNLOCK TABLES)
- 事务控制语句(START TRANSACTION)
我曾因此丢失过数据,现在一定会显式控制事务:
sql复制BEGIN;
-- 业务操作
COMMIT;
8.2 高效处理枚举值
对于状态字段,我推荐这种写法:
sql复制-- 创建表时使用注释说明枚举值
CREATE TABLE orders (
status TINYINT COMMENT '1-待支付 2-已支付 3-已取消 4-已完成',
...
);
-- 查询时清晰明了
SELECT * FROM orders WHERE status = 2; -- 已支付订单
比使用ENUM类型更灵活,比VARCHAR更节省空间。
8.3 JSON字段的妙用
MySQL 5.7+支持JSON类型,我的使用心得:
sql复制-- 存储动态属性
ALTER TABLE products ADD COLUMN specs JSON;
-- 查询JSON字段
SELECT id, name, specs->>'$.color' AS color
FROM products
WHERE JSON_EXTRACT(specs, '$.weight') > 10;
适合存储不常查询的稀疏数据,但不要过度使用。
