PostgreSQL DELETE
PostgreSQL 使用 DELETE 删除表中的行:
DELETE FROM 表名
WHERE 条件;
删除操作的核心不在于会写 DELETE,而在于确认:
- 删除哪张表
- 删除哪些行
- 是否有关联数据
- 能否恢复
- 是否会产生大锁、WAL 和表膨胀
- 应该物理删除还是软删除
完整语法见 PostgreSQL 18 DELETE。
股票基本信息:
CREATE TABLE market.security (
security_id bigint
GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
exchange_code varchar(8) NOT NULL,
code varchar(6) NOT NULL,
name text NOT NULL,
is_active boolean NOT NULL DEFAULT true,
delisted_on date,
deleted_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT uq_security_exchange_code
UNIQUE (exchange_code, code)
);
日线行情:
CREATE TABLE market.daily_quote (
security_id bigint NOT NULL,
trade_date date NOT NULL,
open_price numeric(18,4) NOT NULL,
high_price numeric(18,4) NOT NULL,
low_price numeric(18,4) NOT NULL,
close_price numeric(18,4) NOT NULL,
volume bigint NOT NULL,
amount numeric(24,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT pk_daily_quote
PRIMARY KEY (security_id, trade_date),
CONSTRAINT fk_daily_quote_security
FOREIGN KEY (security_id)
REFERENCES market.security (security_id)
ON DELETE RESTRICT
);
最安全、最明确的删除方式,是根据主键删除:
DELETE FROM market.security
WHERE security_id = 100;
执行成功后通常显示:
DELETE 1
表示删除了一行。
如果显示:
DELETE 0
表示没有行满足条件,不属于 SQL 错误。
DELETE FROM market.security
WHERE exchange_code = 'SSE'
AND code = '600519';
只有在以下约束存在时,才能确定最多删除一行:
UNIQUE (exchange_code, code)
如果字段不唯一,同样的条件可能删除多行。
删除某个日期之前的行情:
DELETE FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01';
删除日期范围内的数据:
DELETE FROM market.daily_quote
WHERE trade_date >= DATE '2020-01-01'
AND trade_date < DATE '2021-01-01';
推荐使用左闭右开的时间范围:
[start, end)
这样处理日期、时间和分区边界时更清晰。
DELETE FROM market.daily_quote
WHERE security_id = 100
AND trade_date >= DATE '2026-01-01'
AND trade_date < DATE '2027-01-01';
使用括号明确逻辑:
DELETE FROM market.security
WHERE exchange_code = 'SSE'
AND (
is_active = false
OR deleted_at IS NOT NULL
);
AND 的优先级高于 OR。删除语句中应主动加括号,不要依赖记忆判断优先级。
DELETE FROM market.daily_quote;
它会删除表中所有行,但保留:
- 表结构
- 字段
- 索引
- 约束
- 权限
- Identity 序列
这是一条完全合法的 SQL,不会因为没有 WHERE 而提醒确认。PostgreSQL 删除数据教程
执行删除前,建议先把 DELETE 改成同条件的 SELECT:
SELECT
security_id,
trade_date,
close_price
FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01'
ORDER BY trade_date
LIMIT 100;
再检查数量:
SELECT count(*)
FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01';
最后才执行:
DELETE FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01';
重要数据建议使用以下流程:
BEGIN;
SET LOCAL lock_timeout = '3s';
SET LOCAL statement_timeout = '30s';
SELECT count(*)
FROM market.daily_quote
WHERE security_id = 100
AND trade_date < DATE '2020-01-01';
SELECT *
FROM market.daily_quote
WHERE security_id = 100
AND trade_date < DATE '2020-01-01'
ORDER BY trade_date
LIMIT 100;
DELETE FROM market.daily_quote
WHERE security_id = 100
AND trade_date < DATE '2020-01-01'
RETURNING security_id, trade_date, close_price;
检查正确后:
COMMIT;
发现问题:
ROLLBACK;
第一次执行高风险删除时,可以先明确使用:
ROLLBACK;
完成一次“演练”,确认受影响行数,再正式执行并 COMMIT。
需要注意:在默认的 READ COMMITTED 隔离级别下,预览查询和真正删除之间可能有并发变化。关键业务还应考虑:
- 根据主键删除
- 锁定目标行
- 使用更严格的事务隔离级别
- 在业务低峰期执行
删除后可以直接返回被删除的行:
DELETE FROM market.daily_quote
WHERE security_id = 100
AND trade_date = DATE '2026-09-14'
RETURNING *;
返回部分字段:
DELETE FROM market.daily_quote
WHERE security_id = 100
AND trade_date = DATE '2026-09-14'
RETURNING
security_id,
trade_date,
close_price;
这比删除后再查询可靠,因为删除完成后,原记录已经不存在。
RETURNING 返回的是实际被删除的行;如果没有匹配数据,就不会返回记录。PostgreSQL RETURNING
PostgreSQL 18 可以明确引用删除前后的值:
DELETE FROM market.daily_quote
WHERE security_id = 100
AND trade_date = DATE '2026-09-14'
RETURNING
OLD.security_id,
OLD.trade_date,
OLD.close_price,
NEW.close_price;
对于普通 DELETE:
OLD:被删除前的行NEW:通常为NULL
因此普通删除中:
RETURNING close_price
与:
RETURNING OLD.close_price
含义相同。
如果删除行数很多,不要 RETURNING *,否则会把大量数据发送到客户端。
可以统计实际删除数量:
WITH deleted AS (
DELETE FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01'
RETURNING 1
)
SELECT count(*) AS deleted_count
FROM deleted;
PostgreSQL 支持在 DELETE 中关联其他表。
例如删除所有上交所股票在2020年以前的行情:
DELETE FROM market.daily_quote AS q
USING market.security AS s
WHERE q.security_id = s.security_id
AND s.exchange_code = 'SSE'
AND q.trade_date < DATE '2020-01-01';
执行逻辑:
daily_quote q 与 security s 关联
↓
筛选上交所股票
↓
只删除 daily_quote 中匹配的行
USING 中的 security 只提供筛选条件,不会被删除。
DELETE FROM market.daily_quote AS q
USING market.security AS s
WHERE q.security_id = s.security_id
AND s.exchange_code = 'SSE'
AND q.trade_date < DATE '2020-01-01'
RETURNING
s.code,
s.name,
q.trade_date,
q.close_price;
指定别名后,应始终使用别名:
DELETE FROM market.daily_quote AS q
后面应使用:
q.security_id
而不是:
market.daily_quote.security_id
因为别名会隐藏目标表原来的名称。
DELETE FROM market.daily_quote
WHERE security_id IN (
SELECT security_id
FROM market.security
WHERE exchange_code = 'SSE'
);
DELETE FROM market.daily_quote AS q
WHERE EXISTS (
SELECT 1
FROM market.security AS s
WHERE s.security_id = q.security_id
AND s.exchange_code = 'SSE'
);
EXISTS 更直接地表达:
只要存在满足条件的关联记录,就删除当前行。
删除找不到股票主表记录的孤立行情:
DELETE FROM market.daily_quote AS q
WHERE NOT EXISTS (
SELECT 1
FROM market.security AS s
WHERE s.security_id = q.security_id
);
如果存在外键,这类孤立记录正常情况下不应该出现。
不推荐:
DELETE FROM market.daily_quote
WHERE security_id NOT IN (
SELECT security_id
FROM market.security_import
);
如果子查询返回结果中含有 NULL,NOT IN 的比较结果可能变成未知,导致一行也不删除。
更安全的写法:
DELETE FROM market.daily_quote AS q
WHERE NOT EXISTS (
SELECT 1
FROM market.security_import AS i
WHERE i.security_id = q.security_id
);
错误写法:
DELETE FROM market.security
WHERE deleted_at = NULL;
任何值与 NULL 使用普通等号比较,都不会得到 TRUE。
正确写法:
DELETE FROM market.security
WHERE deleted_at IS NULL;
删除非空记录:
DELETE FROM market.security
WHERE deleted_at IS NOT NULL;
需要把 NULL 当作可比较值时,可以使用:
DELETE FROM market.security
WHERE industry IS DISTINCT FROM '银行';
它会匹配:
- 行业不是银行
- 行业为
NULL
而普通写法:
WHERE industry <> '银行'
不会匹配 NULL。
日线表定义了:
FOREIGN KEY (security_id)
REFERENCES market.security (security_id)
ON DELETE RESTRICT
如果股票已经存在行情,再删除股票:
DELETE FROM market.security
WHERE security_id = 100;
会因为外键约束失败。
这是合理保护:不能因为误删股票主记录,就让历史行情失去归属。
| 策略 | 删除父记录时的行为 |
|---|---|
RESTRICT |
立即禁止删除 |
NO ACTION |
默认策略,在约束检查时验证 |
CASCADE |
自动删除关联子记录 |
SET NULL |
子表外键设为 NULL |
SET DEFAULT |
子表外键改成默认值 |
完全依附于父记录的中间表:
CREATE TABLE market.security_concept (
security_id bigint NOT NULL
REFERENCES market.security (security_id)
ON DELETE CASCADE,
concept_id bigint NOT NULL
REFERENCES market.concept (concept_id)
ON DELETE CASCADE,
PRIMARY KEY (security_id, concept_id)
);
删除股票时,股票与题材的关联关系可以自动删除。
历史数据通常不应自动删除:
- 行情
- 财务报表
- 订单
- 交易记录
- 审计日志
删除主表前,先检查外键关系:
SELECT count(*)
FROM market.daily_quote
WHERE security_id = 100;
不要为了让删除成功而随意修改为 ON DELETE CASCADE。
DELETE FROM market.security
WHERE security_id = 100;
记录从业务表中移除,适合:
- 临时数据
- 错误导入的数据
- 可重新生成的缓存
- 明确不再需要的数据
- 法规要求必须清除的数据
软删除不真正执行 DELETE,而是执行 UPDATE:
UPDATE market.security
SET
is_active = false,
deleted_at = now(),
updated_at = now()
WHERE security_id = 100
AND deleted_at IS NULL;
查询有效数据:
SELECT *
FROM market.security
WHERE deleted_at IS NULL;
恢复:
UPDATE market.security
SET
is_active = true,
deleted_at = NULL,
updated_at = now()
WHERE security_id = 100;
一只股票退市后,历史数据仍然有研究价值。因此更适合:
UPDATE market.security
SET
is_active = false,
delisted_on = DATE '2026-09-14',
updated_at = now()
WHERE security_id = 100;
不建议:
DELETE FROM market.security
WHERE security_id = 100;
因为这可能影响历史行情、财务数据和题材历史。
软删除并非只有优点,它会带来:
- 所有查询都要过滤
deleted_at IS NULL - 唯一约束设计更复杂
- 数据长期累积
- ORM 容易误查已删除记录
- 统计查询需要区分有效与已删除数据
可以创建有效数据视图:
CREATE VIEW market.active_security AS
SELECT *
FROM market.security
WHERE deleted_at IS NULL;
如果只要求“有效记录”业务键唯一,可以使用部分唯一索引:
CREATE UNIQUE INDEX uq_active_security_exchange_code
ON market.security (exchange_code, code)
WHERE deleted_at IS NULL;
但对于股票代码这种需要永久保留历史归属的业务键,通常不应允许软删除后重新创建另一条同代码记录。
可以把被删除数据先写入归档表。
归档表:
CREATE TABLE archive.daily_quote (
security_id bigint NOT NULL,
trade_date date NOT NULL,
open_price numeric(18,4) NOT NULL,
high_price numeric(18,4) NOT NULL,
low_price numeric(18,4) NOT NULL,
close_price numeric(18,4) NOT NULL,
volume bigint NOT NULL,
amount numeric(24,2) NOT NULL,
archived_at timestamptz NOT NULL DEFAULT now()
);
删除并归档:
WITH deleted AS (
DELETE FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01'
RETURNING
security_id,
trade_date,
open_price,
high_price,
low_price,
close_price,
volume,
amount
)
INSERT INTO archive.daily_quote (
security_id,
trade_date,
open_price,
high_price,
low_price,
close_price,
volume,
amount
)
SELECT
security_id,
trade_date,
open_price,
high_price,
low_price,
close_price,
volume,
amount
FROM deleted;
这是一条完整语句:
- 删除成功但归档失败:整条语句失败,删除回滚。
- 删除和归档都成功:一起提交。
如果需要完整审计,还应记录:
- 删除时间
- 操作用户
- 删除原因
- 请求编号
- 原始记录
PostgreSQL 不支持:
DELETE FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01'
LIMIT 10000;
可以先选出一批主键,再关联删除:
WITH delete_batch AS (
SELECT
security_id,
trade_date
FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01'
ORDER BY trade_date, security_id
FOR UPDATE SKIP LOCKED
LIMIT 10000
)
DELETE FROM market.daily_quote AS q
USING delete_batch AS b
WHERE q.security_id = b.security_id
AND q.trade_date = b.trade_date
RETURNING q.security_id, q.trade_date;
重复执行,直到返回:
DELETE 0
对于有稳定主键的表,优先使用主键批处理,而不是依赖内部字段 ctid。
官方文档也给出了使用 CTE 模拟分批删除的方法。
一次删除几千万行可能导致:
- 事务持续时间过长
- 大量 WAL
- 复制延迟
- 长时间持有行锁
- 表和索引膨胀
- 自动清理压力增大
- 回滚时间很长
- 数据库I/O突增
分批删除可以控制每次事务规模:
选择 10,000 行
↓
删除
↓
提交
↓
继续下一批
批量大小没有固定答案,应根据以下条件测试:
- 每行大小
- 索引数量
- 磁盘性能
- 主从复制情况
- 业务并发量
- 可接受的锁等待和事务时间
PostgreSQL 使用 MVCC。普通 DELETE 通常不会立刻把表文件缩小,而是让旧行版本逐渐变成可清理、可复用空间。
自动清理通常由 autovacuum 完成:
VACUUM market.daily_quote;
同时更新统计信息:
VACUUM (ANALYZE) market.daily_quote;
普通 VACUUM 的主要作用包括:
- 回收并复用已删除或更新行占用的空间
- 更新可见性信息
- 配合查询规划
- 防止事务ID回卷
官方建议大多数数据库让 autovacuum 持续工作,并根据实际负载调整配置。PostgreSQL 日常 VACUUM
VACUUM FULL market.daily_quote;
它会重写表,并可能把空间归还操作系统,但代价较高:
- 需要额外磁盘空间
- 需要强锁
- 阻塞时间可能很长
- 大表执行风险较高
不要每次删除数据后都执行 VACUUM FULL。
一般原则:
少量日常删除 → 交给 autovacuum
大量删除后继续使用表 → 评估 VACUUM (ANALYZE)
删除绝大多数数据且必须缩小文件 → 维护窗口评估 VACUUM FULL
对于按日期分区的行情表,删除整个月或整年的历史数据时,逐行 DELETE 通常不是最佳方案。
例如:
daily_quote_2024
daily_quote_2025
daily_quote_2026
如果确定整个2024年分区都不再需要,可以在严格确认后移除对应分区,而不是逐行删除其中所有数据。
这种方式通常能减少:
- 大量逐行删除
- WAL
- 表膨胀
- VACUUM 压力
但分区操作属于高风险 DDL,必须先确认:
- 分区边界
- 外键依赖
- 备份或归档
- 复制状态
- 锁影响
你的 TimescaleDB 时序行情数据也更适合使用基于 chunk 的保留和清理机制,而不是对海量旧行情反复执行逐行 DELETE。
| 操作 | 删除内容 | 可带条件 | 保留表结构 | 典型用途 |
|---|---|---|---|---|
DELETE |
行 | 是 | 是 | 删除指定业务数据 |
TRUNCATE |
全部行 | 否 | 是 | 快速清空整张表 |
DROP TABLE |
表和全部数据 | 否 | 否 | 删除整个表对象 |
DELETE FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01';
特点:
- 可以使用
WHERE - 可以使用
RETURNING - 执行行级删除触发器
- 会产生死元组
- 适合选择性删除
- 不会重置 Identity 序列
TRUNCATE TABLE staging.security_import;
它快速清空整张表,不扫描和逐行删除。
保留 Identity 序列:
TRUNCATE TABLE staging.security_import
CONTINUE IDENTITY;
重置 Identity 序列:
TRUNCATE TABLE staging.security_import
RESTART IDENTITY;
PostgreSQL 中 TRUNCATE 可以放在事务里,并在事务回滚时恢复:
BEGIN;
TRUNCATE TABLE staging.security_import;
ROLLBACK;
但它会取得 ACCESS EXCLUSIVE 锁,阻塞该表上的其他并发操作;也不会触发 ON DELETE 触发器,而是触发专门的 ON TRUNCATE 触发器。PostgreSQL 18 TRUNCATE
TRUNCATE TABLE market.security CASCADE;
这可能同时清空所有通过外键引用该表的关联表。
风险非常高,通常不应在不检查依赖关系的情况下使用。
更安全的默认行为是:
TRUNCATE TABLE market.security RESTRICT;
RESTRICT 是默认值,存在未一同清空的外键引用表时拒绝执行。
DROP TABLE market.daily_quote;
它会删除:
- 全部数据
- 表结构
- 索引
- 约束
- 触发器
- 表权限
如果只是清空临时导入数据,不应使用 DROP TABLE,而应使用:
TRUNCATE TABLE staging.security_import;
下面的删除:
DELETE FROM market.daily_quote
WHERE security_id = 100
AND trade_date < DATE '2020-01-01';
可以利用主键:
PRIMARY KEY (security_id, trade_date)
如果经常只按日期清理:
DELETE FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01';
可以考虑:
CREATE INDEX idx_daily_quote_trade_date
ON market.daily_quote (trade_date);
但如果每次需要删除表中绝大多数数据,即使有索引,顺序扫描也可能比索引扫描更合理。
应通过执行计划判断:
EXPLAIN
DELETE FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01';
特别注意:
EXPLAIN ANALYZE DELETE ...
会真正执行删除。
安全分析可以对等价查询执行:
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01';
或者把删除放进事务,确认后回滚;但如果存在会产生外部副作用的触发器,仍应格外谨慎。
DELETE 会锁定实际删除的行。其他事务同时更新或删除这些行时,可能:
- 等待
- 超时
- 发生死锁
- 在等待后发现记录已被修改
降低风险的方法:
- 让过滤字段有合适索引
- 尽量按主键删除
- 缩短事务时间
- 避免事务中等待用户输入
- 多张表按固定顺序删除
- 大量数据分批删除
- 设置合理的
lock_timeout - 监控长事务和复制延迟
不要在事务开始后先处理大量与数据库无关的任务,再长时间不提交。
表上可能存在:
BEFORE DELETE
AFTER DELETE
触发器可以用于:
- 审计
- 阻止删除
- 保存历史记录
- 同步关联状态
如果 BEFORE DELETE 触发器跳过某些行,最终显示的:
DELETE count
可能少于最初条件匹配的数量。
RETURNING 返回的是实际被删除的行,而不是所有最初匹配的候选行。
重要审计不应只依赖应用程序“先查询再删除”,因为其他程序可能绕过应用直接执行 SQL。可以考虑数据库触发器或集中审计机制。
参数化删除:
import psycopg
sql = """
DELETE FROM market.security
WHERE exchange_code = %s
AND code = %s
RETURNING security_id, code, name
"""
with psycopg.connect(
"postgresql://postgres:password@localhost:5432/stock"
) as connection:
with connection.cursor() as cursor:
cursor.execute(
sql,
("SSE", "600519"),
)
deleted = cursor.fetchone()
if deleted is None:
print("没有找到需要删除的数据")
else:
print("已删除:", deleted)
不要字符串拼接:
## 不推荐
sql = f"DELETE FROM market.security WHERE code = '{code}'"
应始终使用参数:
cursor.execute(
"DELETE FROM market.security WHERE code = %s",
(code,),
)
单元素元组必须保留逗号:
(code,)
try:
with connection.transaction():
with connection.cursor() as cursor:
cursor.execute(
"""
DELETE FROM market.daily_quote
WHERE security_id = %s
AND trade_date < %s
RETURNING trade_date
""",
(100, "2020-01-01"),
)
deleted_rows = cursor.fetchall()
print(f"删除了 {len(deleted_rows)} 行")
except psycopg.Error:
raise
高风险业务可以先检查数量:
if delete_count > 10_000:
raise ValueError("删除数量超过安全阈值")
但预览和删除仍应在同一事务和合理的并发控制下完成。
from sqlalchemy import delete
statement = (
delete(Security)
.where(
Security.exchange_code == "SSE",
Security.code == "600519",
)
.returning(
Security.security_id,
Security.code,
Security.name,
)
)
result = session.execute(statement)
deleted = result.one_or_none()
session.commit()
异常时回滚:
try:
result = session.execute(statement)
session.commit()
except Exception:
session.rollback()
raise
软删除应使用 update():
from datetime import datetime, timezone
from sqlalchemy import update
statement = (
update(Security)
.where(
Security.security_id == 100,
Security.deleted_at.is_(None),
)
.values(
is_active=False,
deleted_at=datetime.now(timezone.utc),
)
)
session.execute(statement)
session.commit()
DELETE FROM market.security;
删除所有行。
WHERE code LIKE '60%'
可能匹配大量股票。
WHERE exchange_code = 'SSE'
OR code = '600519'
它会删除所有上交所股票,以及任何交易所代码为 600519 的股票。
WHERE deleted_at = NULL
应使用:
WHERE deleted_at IS NULL
父表一行删除,可能连带删除大量子表数据。
普通删除产生的空间通常需要由 VACUUM 回收并复用。
退市属于状态变化,应保留历史记录。
可能造成大量 WAL、锁等待和复制延迟,应评估分批或分区清理。
它会实际执行删除。
事务进入失败状态后,后续 SQL 不能正常执行,必须回滚。
执行删除前检查:
- 是否写了
WHERE? - 是否可以按主键或唯一键删除?
- 同条件
SELECT返回哪些数据? count(*)是多少?- 是否可能受到并发变化影响?
- 是否存在外键?
- 外键是否使用
CASCADE? - 是否有删除触发器?
- 应该物理删除还是软删除?
- 是否需要归档或审计?
- 是否有可靠备份?
- 删除条件是否有索引?
- 删除量是否需要分批?
- 是否会产生大量 WAL 和复制延迟?
- 删除后是否需要评估
VACUUM (ANALYZE)? - 是否在显式事务中?
- 执行后检查
DELETE count了吗?
“删”的完整决策过程是:
确认业务目的
↓
状态变化能否代替物理删除
├── 可以 → UPDATE 软删除
└── 不可以 → 继续
↓
预览匹配记录
↓
统计删除数量
↓
检查外键、级联和触发器
↓
是否需要归档
↓
小批量 DELETE / 大批量分批或分区清理
↓
检查 RETURNING 或 DELETE count
↓
COMMIT 或 ROLLBACK
↓
评估 VACUUM 与运行状态
核心原则:
删除之前先证明“这些数据确实应该被删除”,删除过程中保证“范围不会扩大”,删除之后确认“结果符合预期并且可以追溯”。