SQL DELETE 语句背后不为人知的黑科技

The Latency Gambler 2025-10-15 11:47:55

SQL DELETE 语句背后不为人知的黑科技

上周,我从我们的生产数据库中删除了50万行数据。查询执行成功了,但磁盘使用率却没有变化。更糟糕的是,在接下来的几天里,我们的查询性能开始下降。这并非漏洞,而恰恰是数据库的设计工作方式。

删除的假象

当你执行DELETE语句时,你会期望数据立即消失。但实际情况是这样的:

DELETE FROM orders WHERE created_at < '2024-01-01';
-- Query OK, 500000 rows affected (12.34 sec)

数据库不会从磁盘中删除任何内容。相反,它会将这些行标记为已删除,本质上是设置一个墓碑标记,表明“忽略此行”。实际数据仍物理存储在磁盘上,会占用空间并影响性能。

数据库为何这样做

现代数据库使用多版本并发控制(MVCC)来处理并发事务。当你删除一行数据时,其他事务可能仍然需要查看它。数据库无法物理删除可能被正在进行的查询引用的数据。

以下是其工作原理的简化说明:

Before DELETE:
[Row 1: Active] [Row 2: Active] [Row 3: Active]

After DELETE (Row 2):
[Row 1: Active] [Row 2: Deleted-Marker] [Row 3: Active]

Physical Storage:
[Row 1: Data] [Row 2: Data (marked)] [Row 3: Data]

现实世界影响

我在PostgreSQL 16和MySQL 8.0上运行了基准测试来衡量这种效果:

PostgreSQL测试

-- Create test table
CREATE TABLE test_data (
    id SERIAL PRIMARY KEY,
    data TEXT,
    created_at TIMESTAMP
);

-- Insert 1M rows
INSERT INTO test_data (data, created_at)
SELECT md5(random()::text), NOW() - (random() * 365)::int
FROM generate_series(1, 1000000);

-- Check table size
SELECT pg_size_pretty(pg_total_relation_size('test_data'));

-- Result: 73 MB
-- Delete 50% of rows
DELETE FROM test_data WHERE id % 2 = 0;

-- Check size again
SELECT pg_size_pretty(pg_total_relation_size('test_data'));

-- Result: 73 MB (unchanged)
-- Check dead tuples
SELECT n_dead_tup FROM pg_stat_user_tables 
WHERE relname = 'test_data';

-- Result: 500000 dead tuples

表的大小一点也没有减少。那500,000行“已删除”的记录仍然占用着空间。

清理流程

数据库提供维护命令来实际回收空间:

PostgreSQL: VACUUM

VACUUM FULL test_data;

SELECT pg_size_pretty(pg_total_relation_size('test_data'));
-- Result: 37 MB (50% reduction)

VACUUM扫描表,移除失效元组,并压缩数据。FULL选项会重写整个表,这样更彻底,但需要排他锁。

MySQL: OPTIMIZE TABLE

OPTIMIZE TABLE test_data;

-- Check table size
SELECT 
    table_name,
    ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb,
    ROUND(data_free / 1024 / 1024, 2) AS free_mb
FROM information_schema.TABLES
WHERE table_name = 'test_data';

OPTIMIZE会重建表并回收未使用的空间。对于InnoDB表,它本质上是一种ALTER TABLE操作。

性能影响

死行(Dead rows)影响查询性能。以下是一个对比:

Table with 1M rows (no deletes):
SELECT * FROM test_data WHERE status = 'active';
-- Execution time: 145ms

Same table after deleting 500K rows (no VACUUM):
SELECT * FROM test_data WHERE status = 'active';
-- Execution time: 198ms (36% slower)

After VACUUM:
SELECT * FROM test_data WHERE status = 'active';
-- Execution time: 152ms (back to normal)

数据库在进行顺序扫描时仍需遍历死行,并在索引中维护这些死行。

这对不同数据库系统有何影响

PostgreSQL:
DELETE → Marks tuples as dead → VACUUM removes them
Auto-vacuum runs periodically but may lag behind

MySQL (InnoDB):
DELETE → Marks records deleted → Purge thread cleans up
Background purge usually keeps up

MongoDB:
DELETE → Removes document → Space marked reusable
WiredTiger engine reuses space automatically

Cassandra:
DELETE → Writes tombstone → Compaction removes
Tombstones persist until compaction runs

最佳实践

1、安排维护时段

对于PostgreSQL,请在低流量时段运行VACUUM:

-- Autovacuum is enabled by default, but tune it:
ALTER TABLE high_churn_table 
SET (autovacuum_vacuum_scale_factor = 0.05);

2、对大量删除操作使用TRUNCATE

如果您要删除所有行或整个分区:

TRUNCATE TABLE temp_data;
-- Much faster, reclaims space immediately

3、分区你的表

删除整个分区而不是删除行:

ALTER TABLE orders DROP PARTITION p_2023_q1;
-- Instant, no cleanup needed

4、监控无效元组(Tuple)

-- PostgreSQL monitoring query
SELECT 
    schemaname,
    relname,
    n_live_tup,
    n_dead_tup,
    round(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 2) 
        AS dead_ratio
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;

总结

DELETE是一种逻辑操作,而非物理操作。数据会一直存在,直到维护进程将其清理掉。这种设计选择支持多版本并发控制(MVCC)和并发访问,但这意味着你需要了解并管理清理过程。

下次执行大型DELETE操作时,请记住:你并不是在删除数据,而是将其标记为最终会被移除。要做好相应规划,监控无效元组的数量,并安排维护操作以保持数据库的健康状态。

你未来的自己和你的磁盘空间都会感谢你。

原文链接:https://medium.com/@kanishks772/the-dark-secret-behind-every-delete-statement-nobody-tells-you-1e7a44615ab4

标签: MySQL
快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云
OpenSource
开源版
Flashcat
Flashcat