作为一名数据库工程师,我经常遇到这样的场景:新同事在操作MySQL时,要么写出一堆性能低下的查询,要么在数据修改时引发各种意外问题。CRUD(Create, Read, Update, Delete)看似简单,但真正掌握其精髓需要多年的实践积累。今天,我就来分享MySQL中CRUD操作的那些"艺术"。
MySQL作为最流行的关系型数据库之一,其CRUD操作是每个开发者必须掌握的基本功。但很多人停留在"能用"层面,远未达到"精通"的程度。一个简单的SELECT查询,在不同场景下可能有数十种优化写法;一条UPDATE语句,处理不当可能导致全表锁死。理解这些操作背后的原理和最佳实践,才能真正发挥MySQL的威力。
基础的INSERT语法大家都很熟悉,但实际工作中我们经常需要处理更复杂的插入场景。比如批量插入时,这样写效率更高:
sql复制INSERT INTO users (username, email, created_at)
VALUES
('user1', 'user1@example.com', NOW()),
('user2', 'user2@example.com', NOW()),
('user3', 'user3@example.com', NOW());
相比多次执行单条INSERT,批量插入可以减少网络往返和SQL解析开销。实测在插入1000条记录时,批量插入比单条插入快10倍以上。
注意:MySQL对单个INSERT语句的长度有限制(默认4MB),超大数据量需要分批插入。
INSERT IGNORE和REPLACE是另外两个实用的变种:
当需要导入大量数据时,LOAD DATA INFILE是最快的方式:
sql复制LOAD DATA INFILE '/path/to/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n';
这个命令比INSERT快20-100倍,因为它直接读取文件而不需要解析SQL语句。我曾经用这个方法在5秒内导入了100万条记录。
另一个技巧是禁用索引和约束。在大数据量插入前执行:
sql复制ALTER TABLE users DISABLE KEYS;
-- 执行大量插入操作
ALTER TABLE users ENABLE KEYS;
这可以显著提升性能,因为MySQL不需要在每次插入时更新索引。记得操作完成后重新启用索引。
一个常见的误区是使用SELECT *。这会导致:
应该始终明确指定需要的列:
sql复制SELECT id, username, email FROM users WHERE status = 'active';
EXPLAIN是你的好朋友。分析查询执行计划可以揭示性能瓶颈:
sql复制EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'paid';
重点关注type列(ALL表示全表扫描)、possible_keys和key列(是否使用了合适的索引)。
窗口函数是MySQL 8.0引入的强大特性:
sql复制SELECT
user_id,
order_date,
amount,
SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS running_total
FROM orders;
这个查询计算每个用户的订单金额累计值,比用应用程序处理高效得多。
CTE (Common Table Expressions) 可以提高复杂查询的可读性:
sql复制WITH active_users AS (
SELECT id FROM users WHERE last_login > DATE_SUB(NOW(), INTERVAL 30 DAY)
)
SELECT COUNT(*) FROM orders
WHERE user_id IN (SELECT id FROM active_users)
AND status = 'completed';
UPDATE语句最危险的错误是忘记WHERE条件,这会导致全表更新。我建议:
sql复制BEGIN;
-- 先确认
SELECT * FROM products WHERE stock < 10 AND status = 'active';
-- 再更新
UPDATE products SET need_restock = 1 WHERE stock < 10 AND status = 'active';
COMMIT;
更新大量记录时,单个大事务可能导致锁等待和性能问题。更好的方式是分批更新:
sql复制SET @rows_affected = 1;
WHILE @rows_affected > 0 DO
UPDATE large_table
SET processed = 1
WHERE processed = 0
LIMIT 1000;
SET @rows_affected = ROW_COUNT();
COMMIT;
DO SLEEP(1); -- 给其他查询机会
END WHILE;
这种方法每次只更新1000条记录,减少锁持有时间。
直接DELETE在生产环境风险很高。建议采用"软删除"模式:
sql复制ALTER TABLE users ADD COLUMN deleted_at DATETIME DEFAULT NULL;
-- 删除变为更新
UPDATE users SET deleted_at = NOW() WHERE id = 123;
-- 查询时排除已删除记录
SELECT * FROM users WHERE deleted_at IS NULL;
这样数据可以恢复,也便于审计。
删除大表中大量数据时,DELETE操作会记录大量日志并可能锁表。替代方案:
sql复制CREATE TABLE new_users LIKE users;
INSERT INTO new_users SELECT * FROM users WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 YEAR);
RENAME TABLE users TO old_users, new_users TO users;
这种方法几乎瞬间完成,对业务影响最小。
事务不是越长越好。长时间运行的事务会:
最佳实践是:
sql复制-- 不好的做法
BEGIN;
-- 执行多个耗时操作
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
-- 这里可能有网络请求等耗时操作
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT;
-- 更好的做法
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT;
-- 然后执行其他耗时操作
MySQL有多种锁类型,常见问题包括:
排查锁问题可以使用:
sql复制SHOW ENGINE INNODB STATUS;
-- 查看当前锁信息
SELECT * FROM performance_schema.data_locks;
我曾经遇到一个案例:一个简单的UPDATE导致整个系统卡死,原因是该查询没有使用索引,导致锁定了整张表。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 查询缓慢 | 缺少合适索引 | 使用EXPLAIN分析,添加必要索引 |
| INSERT变慢 | 单个事务太大 | 分批提交,每1000条COMMIT一次 |
| UPDATE卡住 | 锁等待 | 检查锁情况,优化事务大小 |
| DELETE耗时 | 触发器或外键约束 | 暂时禁用约束,分批删除 |
处理死锁的模板代码:
sql复制DELIMITER //
CREATE PROCEDURE safe_transfer(
IN from_user INT,
IN to_user INT,
IN amount DECIMAL(10,2)
)
BEGIN
DECLARE retry INT DEFAULT 3;
DECLARE success INT DEFAULT 0;
WHILE retry > 0 AND success = 0 DO
BEGIN
DECLARE EXIT HANDLER FOR 1213 BEGIN
SET retry = retry - 1;
IF retry = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Failed after 3 retries';
END IF;
END;
START TRANSACTION;
UPDATE accounts SET balance = balance - amount WHERE user_id = from_user;
UPDATE accounts SET balance = balance + amount WHERE user_id = to_user;
COMMIT;
SET success = 1;
END;
END WHILE;
END //
DELIMITER ;
这个存储过程在发生死锁时会自动重试最多3次。
原始查询:
sql复制SELECT * FROM products
WHERE name LIKE '%手机%'
OR description LIKE '%手机%'
ORDER BY created_at DESC
LIMIT 20;
问题:
优化方案:
sql复制ALTER TABLE products ADD FULLTEXT(name, description);
SELECT * FROM products
WHERE MATCH(name, description) AGAINST('手机')
ORDER BY created_at DESC
LIMIT 20;
原始方式:在应用层循环查询
优化方案:使用单个查询完成
sql复制SELECT
user_id,
COUNT(*) AS total_orders,
SUM(amount) AS total_amount,
MAX(order_date) AS last_order_date
FROM orders
WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY user_id
HAVING COUNT(*) > 5
ORDER BY total_amount DESC
LIMIT 100;
这个查询一次性完成过滤、分组、排序和限制,比应用层处理高效得多。
sql复制ANALYZE TABLE orders;
这更新索引统计信息,帮助优化器选择更好的执行计划。
sql复制SELECT * FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;
sql复制SELECT * FROM sys.schema_unused_indexes;
删除无用索引可以提升写入性能。
sql复制SHOW STATUS LIKE 'Qcache%';
sql复制SHOW STATUS LIKE 'Innodb_buffer_pool%';
sql复制SHOW STATUS LIKE 'Innodb_row_lock%';
我曾经通过调整innodb_flush_log_at_trx_commit参数,在允许少量数据丢失风险的场景下,将写入性能提升了5倍:
sql复制SET GLOBAL innodb_flush_log_at_trx_commit = 2;
警告:这会在服务器崩溃时丢失最后1秒的事务,仅适用于可以容忍数据丢失的非关键业务。