Skip to main content
Septvean's Documents
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

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。删除语句中应主动加括号,不要依赖记忆判断优先级。


四、最危险的写法:没有 WHERE

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 隔离级别下,预览查询和真正删除之间可能有并发变化。关键业务还应考虑:

  • 根据主键删除
  • 锁定目标行
  • 使用更严格的事务隔离级别
  • 在业务低峰期执行

六、使用 RETURNING 返回被删数据

删除后可以直接返回被删除的行:

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 的 OLDNEW

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;

七、根据另一张表删除:USING

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

因为别名会隐藏目标表原来的名称。


八、使用子查询删除

1. IN

DELETE FROM market.daily_quote
WHERE security_id IN (
    SELECT security_id
    FROM market.security
    WHERE exchange_code = 'SSE'
);

2. EXISTS

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 更直接地表达:

只要存在满足条件的关联记录,就删除当前行。


3. NOT 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
);

如果存在外键,这类孤立记录正常情况下不应该出现。


谨慎使用 NOT IN

不推荐:

DELETE FROM market.daily_quote
WHERE security_id NOT IN (
    SELECT security_id
    FROM market.security_import
);

如果子查询返回结果中含有 NULLNOT 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
);

九、NULL 条件

错误写法:

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 子表外键改成默认值

适合 CASCADE

完全依附于父记录的中间表:

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)
);

删除股票时,股票与题材的关联关系可以自动删除。

不适合随意 CASCADE

历史数据通常不应自动删除:

  • 行情
  • 财务报表
  • 订单
  • 交易记录
  • 审计日志

删除主表前,先检查外键关系:

SELECT count(*)
FROM market.daily_quote
WHERE security_id = 100;

不要为了让删除成功而随意修改为 ON DELETE CASCADE


十一、物理删除与软删除

1. 物理删除

DELETE FROM market.security
WHERE security_id = 100;

记录从业务表中移除,适合:

  • 临时数据
  • 错误导入的数据
  • 可重新生成的缓存
  • 明确不再需要的数据
  • 法规要求必须清除的数据

2. 软删除

软删除不真正执行 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;

3. A股退市不等于删除

一只股票退市后,历史数据仍然有研究价值。因此更适合:

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;

因为这可能影响历史行情、财务数据和题材历史。


4. 软删除的代价

软删除并非只有优点,它会带来:

  • 所有查询都要过滤 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;

这是一条完整语句:

  • 删除成功但归档失败:整条语句失败,删除回滚。
  • 删除和归档都成功:一起提交。

如果需要完整审计,还应记录:

  • 删除时间
  • 操作用户
  • 删除原因
  • 请求编号
  • 原始记录

十三、DELETE 没有 LIMIT

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

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


十七、DELETETRUNCATEDROP 区别

操作 删除内容 可带条件 保留表结构 典型用途
DELETE 删除指定业务数据
TRUNCATE 全部行 快速清空整张表
DROP TABLE 表和全部数据 删除整个表对象

1. DELETE

DELETE FROM market.daily_quote
WHERE trade_date < DATE '2020-01-01';

特点:

  • 可以使用 WHERE
  • 可以使用 RETURNING
  • 执行行级删除触发器
  • 会产生死元组
  • 适合选择性删除
  • 不会重置 Identity 序列

2. TRUNCATE

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


3. TRUNCATE ... CASCADE

TRUNCATE TABLE market.security CASCADE;

这可能同时清空所有通过外键引用该表的关联表。

风险非常高,通常不应在不检查依赖关系的情况下使用。

更安全的默认行为是:

TRUNCATE TABLE market.security RESTRICT;

RESTRICT 是默认值,存在未一同清空的外键引用表时拒绝执行。


4. DROP TABLE

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。可以考虑数据库触发器或集中审计机制。


二十一、Python psycopg 3 删除

参数化删除:

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("删除数量超过安全阈值")

但预览和删除仍应在同一事务和合理的并发控制下完成。


二十二、SQLAlchemy 2.0 删除

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()

二十三、常见删除错误

1. 漏写 WHERE

DELETE FROM market.security;

删除所有行。

2. 条件范围过宽

WHERE code LIKE '60%'

可能匹配大量股票。

3. 把 AND 写成 OR

WHERE exchange_code = 'SSE'
   OR code = '600519'

它会删除所有上交所股票,以及任何交易所代码为 600519 的股票。

4. 使用 = NULL

WHERE deleted_at = NULL

应使用:

WHERE deleted_at IS NULL

5. 忽略外键级联

父表一行删除,可能连带删除大量子表数据。

6. 认为 DELETE 会立即缩小磁盘文件

普通删除产生的空间通常需要由 VACUUM 回收并复用。

7. 用 DELETE 表示股票退市

退市属于状态变化,应保留历史记录。

8. 大表一次删除全部历史记录

可能造成大量 WAL、锁等待和复制延迟,应评估分批或分区清理。

9. 使用 EXPLAIN ANALYZE DELETE

它会实际执行删除。

10. 应用捕获异常后没有回滚

事务进入失败状态后,后续 SQL 不能正常执行,必须回滚。


二十四、安全删除检查清单

执行删除前检查:

  • 是否写了 WHERE
  • 是否可以按主键或唯一键删除?
  • 同条件 SELECT 返回哪些数据?
  • count(*) 是多少?
  • 是否可能受到并发变化影响?
  • 是否存在外键?
  • 外键是否使用 CASCADE
  • 是否有删除触发器?
  • 应该物理删除还是软删除?
  • 是否需要归档或审计?
  • 是否有可靠备份?
  • 删除条件是否有索引?
  • 删除量是否需要分批?
  • 是否会产生大量 WAL 和复制延迟?
  • 删除后是否需要评估 VACUUM (ANALYZE)
  • 是否在显式事务中?
  • 执行后检查 DELETE count 了吗?

“删”的完整决策过程是:

确认业务目的
状态变化能否代替物理删除
   ├── 可以 → UPDATE 软删除
   └── 不可以 → 继续
预览匹配记录
统计删除数量
检查外键、级联和触发器
是否需要归档
小批量 DELETE / 大批量分批或分区清理
检查 RETURNING 或 DELETE count
COMMIT 或 ROLLBACK
评估 VACUUM 与运行状态

核心原则:

删除之前先证明“这些数据确实应该被删除”,删除过程中保证“范围不会扩大”,删除之后确认“结果符合预期并且可以追溯”。