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 UPDATE

PostgreSQL 使用 UPDATE 修改已有数据:

UPDATE 表名
SET 字段 = 新值
WHERE 条件;

一条完整的更新语句需要明确:

  1. 修改哪张表;
  2. 修改哪些字段;
  3. 新值如何计算;
  4. 修改哪些行。

官方完整语法见 PostgreSQL 18 UPDATE


一、基础更新

修改一只股票的名称:

UPDATE market.security
SET name = '贵州茅台股份'
WHERE security_id = 100;

执行成功后通常显示:

UPDATE 1

表示有一行被更新。

如果显示:

UPDATE 0

表示没有记录满足条件,不属于 SQL 语法错误。


使用业务唯一键更新

UPDATE market.security
SET name = '贵州茅台股份'
WHERE exchange_code = 'SSE'
  AND code = '600519';

只有当下面的唯一约束存在时,才能保证最多更新一行:

UNIQUE (exchange_code, code)

如果条件不唯一,一条 UPDATE 可以修改多行。


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

UPDATE market.security
SET is_active = false;

它会把整张表的所有股票都改为停用状态。

UPDATE 没有 WHERE 是完全合法的 SQL,PostgreSQL 不会自动询问是否确认。PostgreSQL 更新数据教程

更新前,应先执行相同条件的查询:

SELECT
    security_id,
    exchange_code,
    code,
    name,
    is_active
FROM market.security
WHERE exchange_code = 'SSE'
  AND code = '600519';

再确认数量:

SELECT count(*)
FROM market.security
WHERE exchange_code = 'SSE'
  AND code = '600519';

最后才执行更新。


三、同时修改多个字段

UPDATE market.security
SET
    name       = '贵州茅台股份',
    industry   = '白酒',
    is_st      = false,
    updated_at = now()
WHERE exchange_code = 'SSE'
  AND code = '600519';

没有出现在 SET 中的字段保持原值。

也可以使用行赋值语法:

UPDATE market.security
SET (
    name,
    industry,
    is_st,
    updated_at
) = (
    '贵州茅台股份',
    '白酒',
    false,
    now()
)
WHERE exchange_code = 'SSE'
  AND code = '600519';

普通多字段写法更常见;行赋值适合字段较多或字段来自子查询的场景。


四、使用旧值计算新值

SET 右侧可以引用当前行修改前的值。

数值增加

UPDATE account
SET balance = balance + 1000
WHERE account_id = 1;

数值减少

UPDATE account
SET balance = balance - 500
WHERE account_id = 1;

百分比调整

UPDATE product
SET price = round(price * 1.10, 2)
WHERE category = 'computer';

布尔值反转

UPDATE market.security
SET is_active = NOT is_active
WHERE security_id = 100;

这种数据库内计算通常比“程序先查值,再计算,再写回”更安全,因为整个表达式在一次原子更新中完成。


多字段计算使用的是旧值

假设原数据:

a = 10
b = 20

执行:

UPDATE example
SET
    a = b,
    b = a
WHERE id = 1;

结果为:

a = 20
b = 10

右侧的 ab 都取修改前的值,不是从左到右逐个执行后的中间值。


五、NULL 对更新计算的影响

如果:

amount = NULL

执行:

UPDATE account
SET amount = amount + 100;

结果仍然是:

NULL

因为大多数表达式只要包含 NULL,结果也是 NULL

如果业务上希望把空值当作零:

UPDATE account
SET amount = coalesce(amount, 0) + 100;

但是必须先确认业务含义:

  • NULL 表示未知;
  • 0 表示已经明确为零。

不要为了方便计算,就无条件把未知值当成零。


六、设置为 NULL 或默认值

设置为 NULL

UPDATE market.security
SET industry = NULL
WHERE security_id = 100;

前提是该字段允许 NULL

如果字段定义为:

industry text NOT NULL

更新会失败。


恢复为默认值

假设字段定义:

is_active boolean NOT NULL DEFAULT true

可以执行:

UPDATE market.security
SET is_active = DEFAULT
WHERE security_id = 100;

结果恢复为默认值:

true

如果字段没有定义默认值:

SET 字段 = DEFAULT

通常会尝试设置为 NULL,如果字段有 NOT NULL 约束,则更新失败。


Identity 字段

Identity 字段也可以设置为 DEFAULT

UPDATE example
SET id = DEFAULT
WHERE id = 100;

这会从对应序列生成一个新值。

但主键应该保持稳定,业务中通常不应修改主键。


七、条件更新

1. 比较条件

UPDATE market.security
SET is_active = false
WHERE delisted_on < CURRENT_DATE;

2. 范围条件

UPDATE market.daily_quote
SET updated_at = now()
WHERE trade_date >= DATE '2026-01-01'
  AND trade_date <  DATE '2027-01-01';

3. 字符串条件

UPDATE market.security
SET is_st = true
WHERE name LIKE 'ST%';

如果还需要匹配 *ST

UPDATE market.security
SET is_st = true
WHERE name LIKE 'ST%'
   OR name LIKE '*ST%';

为了避免逻辑歧义,建议加括号:

UPDATE market.security
SET is_st = true
WHERE (
    name LIKE 'ST%'
    OR name LIKE '*ST%'
);

4. 空值条件

UPDATE market.security
SET industry = '待分类'
WHERE industry IS NULL;

错误写法:

WHERE industry = NULL

NULL 比较必须使用:

IS NULL
IS NOT NULL
IS DISTINCT FROM
IS NOT DISTINCT FROM

八、避免没有变化的无效更新

下面的语句即使名称已经相同,也算匹配并更新:

UPDATE market.security
SET
    name       = '贵州茅台',
    updated_at = now()
WHERE security_id = 100;

PostgreSQL 返回的 UPDATE count 包括“满足条件但值没有实质变化”的记录。

为了避免无意义的行版本、WAL 和 updated_at 变化,可以写:

UPDATE market.security
SET
    name       = '贵州茅台',
    updated_at = now()
WHERE security_id = 100
  AND name IS DISTINCT FROM '贵州茅台';

多字段判断:

UPDATE market.security
SET
    name       = '贵州茅台',
    industry   = '白酒',
    is_st      = false,
    updated_at = now()
WHERE security_id = 100
  AND (
      name,
      industry,
      is_st
  ) IS DISTINCT FROM (
      '贵州茅台',
      '白酒',
      false
  );

IS DISTINCT FROM 能正确处理 NULL,比普通的 <> 更适合变更检测。


九、使用 CASE 条件计算

CASE 可以根据不同条件设置不同值。

UPDATE market.security
SET board = CASE
    WHEN code LIKE '60%' THEN '沪市主板'
    WHEN code LIKE '00%' THEN '深市主板'
    WHEN code LIKE '30%' THEN '创业板'
    WHEN code LIKE '68%' THEN '科创板'
    ELSE board
END;

这里:

ELSE board

表示不满足条件时保持原值。

如果省略 ELSE

CASE
    WHEN ...
    THEN ...
END

不满足条件的结果会变成 NULL,可能误清空原数据。


根据数值区间修改状态

UPDATE task
SET priority = CASE
    WHEN score >= 90 THEN 'high'
    WHEN score >= 60 THEN 'normal'
    ELSE 'low'
END;

十、updated_at 不会自动变化

字段定义:

updated_at timestamptz NOT NULL DEFAULT now()

只表示插入时默认使用当前时间。

普通更新不会自动修改它:

UPDATE market.security
SET name = '新名称'
WHERE security_id = 100;

应主动设置:

UPDATE market.security
SET
    name       = '新名称',
    updated_at = now()
WHERE security_id = 100;

或者使用统一触发器:

CREATE OR REPLACE FUNCTION market.set_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    NEW.updated_at := clock_timestamp();
    RETURN NEW;
END;
$$;
CREATE TRIGGER trg_security_updated_at
BEFORE UPDATE ON market.security
FOR EACH ROW
EXECUTE FUNCTION market.set_updated_at();

使用触发器后,所有来源的更新都会自动维护时间,包括:

  • Python
  • SQLAlchemy
  • SQL脚本
  • 管理工具
  • 其他服务

十一、使用 RETURNING 获取更新结果

更新后返回新数据:

UPDATE market.security
SET
    name       = '贵州茅台股份',
    updated_at = now()
WHERE security_id = 100
RETURNING
    security_id,
    code,
    name,
    updated_at;

返回整行:

UPDATE market.security
SET is_active = false
WHERE security_id = 100
RETURNING *;

生产代码通常只返回需要的字段,避免大量更新时返回过多数据。

RETURNING 能在同一条 SQL 中取得最终值,不需要再执行一次查询。PostgreSQL RETURNING


十二、PostgreSQL 18 返回修改前后数据

PostgreSQL 18 可以直接引用:

  • OLD:修改前的数据
  • NEW:修改后的数据
UPDATE market.security
SET
    name       = '贵州茅台股份',
    updated_at = now()
WHERE security_id = 100
RETURNING
    OLD.name AS old_name,
    NEW.name AS new_name,
    OLD.updated_at AS old_updated_at,
    NEW.updated_at AS new_updated_at;

还可以计算变化量:

UPDATE market.daily_quote
SET close_price = close_price * 1.10
WHERE security_id = 100
  AND trade_date = DATE '2026-09-14'
RETURNING
    OLD.close_price,
    NEW.close_price,
    NEW.close_price - OLD.close_price AS change_amount;

如果表上存在 BEFORE UPDATE 触发器,RETURNING 可以看到触发器处理后的最终新值。


十三、根据另一张表更新:UPDATE ... FROM

假设暂存表中有最新股票资料:

staging.security_import

可以关联更新正式表:

UPDATE market.security AS s
SET
    name       = i.name,
    industry   = i.industry,
    is_st      = i.is_st,
    updated_at = now()
FROM staging.security_import AS i
WHERE s.exchange_code = i.exchange_code
  AND s.code = i.code;

执行逻辑:

security 与 security_import 关联
找到业务键相同的记录
用导入表数据更新正式表

FROM 中的表只是提供数据,实际被修改的只有:

market.security

只更新有变化的数据

UPDATE market.security AS s
SET
    name       = i.name,
    industry   = i.industry,
    is_st      = i.is_st,
    updated_at = now()
FROM staging.security_import AS i
WHERE s.exchange_code = i.exchange_code
  AND s.code = i.code
  AND (
      s.name,
      s.industry,
      s.is_st
  ) IS DISTINCT FROM (
      i.name,
      i.industry,
      i.is_st
  );

必须保证来源表唯一

如果导入表中存在两条:

SSE + 600519 + 贵州茅台
SSE + 600519 + 贵州茅台股份

那么同一目标行会匹配两条来源记录。PostgreSQL 只会选择其中一条用于更新,但具体选择哪条并不可靠。

因此 UPDATE ... FROM 必须保证:

一个目标行最多匹配一个来源行

可以在暂存表建立唯一约束,或者先去重:

WITH source AS (
    SELECT DISTINCT ON (exchange_code, code)
        exchange_code,
        code,
        name,
        industry,
        is_st
    FROM staging.security_import
    ORDER BY
        exchange_code,
        code,
        source_updated_at DESC
)
UPDATE market.security AS s
SET
    name       = source.name,
    industry   = source.industry,
    is_st      = source.is_st,
    updated_at = now()
FROM source
WHERE s.exchange_code = source.exchange_code
  AND s.code = source.code;

官方文档明确指出,如果一个目标行连接到多条来源记录,最终使用哪一条来源并不可预测。


十四、使用子查询更新

UPDATE market.security AS s
SET industry = (
    SELECT i.industry
    FROM staging.security_import AS i
    WHERE i.exchange_code = s.exchange_code
      AND i.code = s.code
)
WHERE EXISTS (
    SELECT 1
    FROM staging.security_import AS i
    WHERE i.exchange_code = s.exchange_code
      AND i.code = s.code
);

子查询必须:

  • 返回一行:使用该行的值;
  • 返回零行:结果为 NULL
  • 返回多行:报错。

这里增加 WHERE EXISTS,避免没有匹配来源时把 industry 更新为 NULL


同时从子查询更新多个字段

UPDATE market.security AS s
SET (
    name,
    industry,
    is_st,
    updated_at
) = (
    SELECT
        i.name,
        i.industry,
        i.is_st,
        now()
    FROM staging.security_import AS i
    WHERE i.exchange_code = s.exchange_code
      AND i.code = s.code
)
WHERE EXISTS (
    SELECT 1
    FROM staging.security_import AS i
    WHERE i.exchange_code = s.exchange_code
      AND i.code = s.code
);

十五、使用 VALUES 批量更新不同值

如果少量记录需要分别修改成不同值:

UPDATE market.security AS s
SET
    name       = v.name,
    industry   = v.industry,
    updated_at = now()
FROM (
    VALUES
        ('SSE',  '600519', '贵州茅台', '白酒'),
        ('SZSE', '000001', '平安银行', '银行'),
        ('SSE',  '601318', '中国平安', '保险')
) AS v (
    exchange_code,
    code,
    name,
    industry
)
WHERE s.exchange_code = v.exchange_code
  AND s.code = v.code;

这比逐条执行三次 UPDATE 更紧凑,也减少数据库往返次数。

同样要保证 VALUES 中的业务键不重复。


十六、更新 JSONB

假设:

extra jsonb NOT NULL DEFAULT '{}'::jsonb

原数据:

{
  "province": "贵州",
  "source": "同花顺"
}

1. 更新指定路径

UPDATE market.security
SET
    extra = jsonb_set(
        extra,
        '{province}',
        to_jsonb('贵州省'::text),
        true
    ),
    updated_at = now()
WHERE security_id = 100;

最后一个 true 表示字段不存在时创建。

2. 合并顶层字段

UPDATE market.security
SET extra = extra || '{
    "province": "贵州省",
    "company_type": "国企"
}'::jsonb
WHERE security_id = 100;

相同键会被右侧新值覆盖。

3. 删除 JSON 字段

UPDATE market.security
SET extra = extra - 'source'
WHERE security_id = 100;

稳定、常用、需要排序或关联的字段,仍应使用普通列,不应全部塞入 jsonb


十七、更新与约束

UPDATE 后的数据必须继续满足所有约束:

  • NOT NULL
  • CHECK
  • UNIQUE
  • 主键
  • 外键
  • 排除约束

例如:

UPDATE market.security
SET code = '001'
WHERE security_id = 100;

如果存在:

CHECK (code ~ '^[0-9]{6}$')

更新失败。

如果更新多行,其中一行违反约束,整条 UPDATE 默认都会失败,不会只更新剩下的合法记录。


唯一约束冲突

UPDATE market.security
SET
    exchange_code = 'SSE',
    code = '600519'
WHERE security_id = 200;

如果另一个证券已经使用:

SSE + 600519

就会违反唯一约束。

业务主键和业务唯一键通常不应随意修改。


十八、UPDATE 和 UPSERT 的区别

UPDATE 只修改已经存在的数据:

UPDATE market.security
SET name = '贵州茅台'
WHERE exchange_code = 'SSE'
  AND code = '600519';

如果记录不存在:

UPDATE 0

不会自动插入。

如果需要:

存在 → 更新
不存在 → 插入

应使用:

INSERT ... ON CONFLICT ... DO UPDATE

如果需要根据来源数据混合执行插入、更新和删除,可以考虑 PostgreSQL 的 MERGE


十九、事务中的安全更新

重要更新建议放入显式事务:

BEGIN;

SET LOCAL lock_timeout = '3s';
SET LOCAL statement_timeout = '30s';

SELECT *
FROM market.security
WHERE exchange_code = 'SSE'
  AND code = '600519';

UPDATE market.security
SET
    name       = '贵州茅台股份',
    updated_at = now()
WHERE exchange_code = 'SSE'
  AND code = '600519'
RETURNING
    OLD.name,
    NEW.name;

COMMIT;

发现问题:

ROLLBACK;

第一次执行大范围更新时,可以先用 ROLLBACK 演练一次。


二十、并发下的丢失更新

假设原值:

version = 5
name = 贵州茅台

两个程序同时读取到版本5:

程序A:准备改名
程序B:准备改行业

程序B先保存,随后程序A用旧数据覆盖整行,就可能丢失程序B的修改。


方案一:直接在数据库内计算

计数、余额增减应直接写:

UPDATE account
SET balance = balance + 100
WHERE account_id = 1;

不要:

  1. Python 读取 balance
  2. Python 计算;
  3. Python 把绝对值写回。

单条 UPDATE 会锁定目标行,并基于数据库中的行版本完成更新。


方案二:乐观锁

增加版本字段:

ALTER TABLE market.security
ADD COLUMN version integer NOT NULL DEFAULT 1;

读取数据时得到:

security_id = 100
version = 5

更新时同时检查版本:

UPDATE market.security
SET
    name       = '贵州茅台股份',
    version    = version + 1,
    updated_at = now()
WHERE security_id = 100
  AND version = 5
RETURNING version;

结果:

  • UPDATE 1:更新成功,新版本为6。
  • UPDATE 0:数据已经被其他事务修改,需要重新读取并决定是否重试。

乐观锁适合:

  • 冲突不频繁
  • Web API
  • 编辑表单
  • 不希望长时间持锁

方案三:悲观锁

BEGIN;

SELECT *
FROM market.security
WHERE security_id = 100
FOR UPDATE;

-- 执行业务判断

UPDATE market.security
SET
    name       = '贵州茅台股份',
    updated_at = now()
WHERE security_id = 100;

COMMIT;

FOR UPDATE 会锁定选中的行,其他事务对同一行的修改通常需要等待。

不希望等待时:

FOR UPDATE NOWAIT

任务队列并发消费时:

FOR UPDATE SKIP LOCKED

悲观锁适合:

  • 冲突概率高
  • 多步业务计算
  • 库存扣减
  • 账户结算
  • 同一数据不能同时处理

事务必须尽量短。不要锁住记录后等待网络请求或用户输入。PostgreSQL 显式锁


二十一、死锁与重试

两个事务如果以不同顺序修改相同记录,可能形成死锁:

事务A:先锁股票1,再锁股票2
事务B:先锁股票2,再锁股票1

降低死锁的方法:

  • 按固定主键顺序更新;
  • 缩短事务;
  • 不在事务中执行耗时外部操作;
  • 一次锁定所需记录;
  • 捕获死锁错误并重试整个事务。

常见需要重试的 SQLSTATE:

40001  serialization_failure
40P01  deadlock_detected

重试的单位应是整个事务,而不是只重新执行最后一条 SQL。


二十二、PostgreSQL 没有 UPDATE ... LIMIT

下面的写法不支持:

UPDATE market.daily_quote
SET verified = true
WHERE verified = false
LIMIT 5000;

假设表中存在:

verified boolean NOT NULL DEFAULT false

可以使用 CTE 分批更新:

WITH update_batch AS (
    SELECT
        security_id,
        trade_date
    FROM market.daily_quote
    WHERE verified = false
    ORDER BY trade_date, security_id
    FOR UPDATE SKIP LOCKED
    LIMIT 5000
)
UPDATE market.daily_quote AS q
SET
    verified   = true,
    updated_at = now()
FROM update_batch AS b
WHERE q.security_id = b.security_id
  AND q.trade_date = b.trade_date
RETURNING q.security_id, q.trade_date;

重复执行,直到:

UPDATE 0

使用稳定主键分批,比依赖内部 ctid 更容易理解和维护。

如果使用 SKIP LOCKED,最后应进行一次不带 SKIP LOCKED 或批量限制的检查,避免遗漏此前被其他事务锁住的记录。


二十三、为什么大量更新要分批

PostgreSQL 使用 MVCC。普通 UPDATE 通常会:

  1. 创建新的行版本;
  2. 将旧行版本逐渐变成死元组;
  3. 写入 WAL;
  4. 更新相关索引;
  5. 锁定被修改的行。

一次修改数千万行可能造成:

  • 表和索引膨胀
  • 大量 WAL
  • 主从复制延迟
  • 锁等待增加
  • 事务持续时间过长
  • 回滚时间很长
  • autovacuum 压力增大

PostgreSQL 官方也建议,在大范围更新造成明显性能影响时考虑分批处理。

优化方向:

  • 只更新真正发生变化的行;
  • 过滤条件建立合适索引;
  • 控制单批事务大小;
  • 避免同时修改不必要的索引字段;
  • 在低峰期执行;
  • 监控 WAL、锁、复制延迟和 autovacuum;
  • 大规模处理后评估 VACUUM (ANALYZE)

二十四、分区字段更新

对原生分区表更新分区键时,记录可能不再属于原分区。

例如按照 trade_date 分区:

UPDATE market.daily_quote
SET trade_date = DATE '2027-01-01'
WHERE security_id = 100
  AND trade_date = DATE '2026-12-31';

如果目标日期属于另一个已有分区,PostgreSQL 会将记录移动到目标分区。内部效果类似:

从旧分区 DELETE
向新分区 INSERT

如果没有能够接收新值的分区,更新失败。

分区键变更还可能在并发操作下触发序列化错误,应用应准备重试事务。通常应尽量避免频繁修改时间分区键。


二十五、查看执行计划

安全查看计划:

EXPLAIN
UPDATE market.security
SET is_active = false
WHERE exchange_code = 'SSE'
  AND code = '600519';

特别注意:

EXPLAIN ANALYZE UPDATE ...

会真正执行更新。

如果只想分析筛选性能,可以对等价查询执行:

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM market.security
WHERE exchange_code = 'SSE'
  AND code = '600519';

重点检查:

  • 是否使用合适索引;
  • 预估行数与实际行数是否接近;
  • 是否扫描了过多数据;
  • 更新范围是否符合预期。

二十六、Python psycopg 3 更新

参数化更新:

import psycopg


sql = """
UPDATE market.security
SET
    name       = %s,
    industry   = %s,
    updated_at = now()
WHERE exchange_code = %s
  AND code = %s
  AND (
      name,
      industry
  ) IS DISTINCT FROM (
      %s,
      %s
  )
RETURNING
    OLD.name,
    NEW.name,
    NEW.updated_at
"""


params = (
    "贵州茅台",
    "白酒",
    "SSE",
    "600519",
    "贵州茅台",
    "白酒",
)


with psycopg.connect(
    "postgresql://postgres:password@localhost:5432/stock"
) as connection:
    with connection.cursor() as cursor:
        cursor.execute(sql, params)
        result = cursor.fetchone()

        if result is None:
            print("记录不存在,或者数据没有变化")
        else:
            print("更新结果:", result)

不要拼接 SQL:

## 不推荐
sql = f"""
UPDATE market.security
SET name = '{name}'
WHERE code = '{code}'
"""

应始终传递参数。


乐观锁示例

sql = """
UPDATE market.security
SET
    name       = %s,
    version    = version + 1,
    updated_at = now()
WHERE security_id = %s
  AND version = %s
RETURNING version
"""


with connection.cursor() as cursor:
    cursor.execute(
        sql,
        ("贵州茅台股份", 100, 5),
    )

    result = cursor.fetchone()

    if result is None:
        raise RuntimeError("数据已被其他事务修改")

    new_version = result[0]

二十七、SQLAlchemy 2.0 更新

from sqlalchemy import update


statement = (
    update(Security)
    .where(
        Security.exchange_code == "SSE",
        Security.code == "600519",
    )
    .values(
        name="贵州茅台",
        industry="白酒",
    )
    .returning(
        Security.security_id,
        Security.code,
        Security.name,
    )
)

result = session.execute(statement)
updated = result.one_or_none()

session.commit()

异常时必须回滚:

try:
    result = session.execute(statement)
    session.commit()
except Exception:
    session.rollback()
    raise

乐观锁:

statement = (
    update(Security)
    .where(
        Security.security_id == security_id,
        Security.version == expected_version,
    )
    .values(
        name=new_name,
        version=Security.version + 1,
    )
    .returning(Security.version)
)

new_version = session.execute(statement).scalar_one_or_none()

if new_version is None:
    session.rollback()
    raise RuntimeError("数据已被其他事务修改")

session.commit()

二十八、常见更新错误

1. 漏写 WHERE

UPDATE market.security
SET is_active = false;

修改全表。

2. 条件不唯一

以为只改一行,实际更新几百行。

3. 使用 = NULL

WHERE industry = NULL

应使用:

WHERE industry IS NULL

4. CASE 忘记 ELSE

不满足条件的字段可能被设置为 NULL

5. UPDATE ... FROM 来源重复

一个目标行匹配多条来源,结果不确定。

6. 无变化也反复更新

产生不必要的行版本、WAL 和表膨胀。

7. 忘记更新 updated_at

DEFAULT now() 不会在更新时自动执行。

8. 读取后在程序中计算绝对值

并发下可能覆盖其他事务的修改。

9. 随意修改主键

会影响外键、缓存、日志以及外部系统。

10. 一次更新海量数据

可能造成长事务、复制延迟、锁竞争和膨胀。

11. 使用 EXPLAIN ANALYZE UPDATE

会真实修改数据。

12. 捕获异常后没有回滚

事务失败后应执行 ROLLBACK,不能继续当作正常事务使用。


二十九、安全更新检查清单

执行更新前确认:

  • 是否写了 WHERE
  • 条件能匹配多少行?
  • 能否使用主键或唯一键?
  • 同条件 SELECT 返回什么?
  • 新值与旧值是否真的不同?
  • 是否需要使用 IS DISTINCT FROM
  • 是否可能产生 NULL
  • 是否满足检查、唯一和外键约束?
  • 是否需要更新 updated_at
  • 是否需要 RETURNING OLD/NEW
  • UPDATE ... FROM 来源是否唯一?
  • 是否存在并发覆盖风险?
  • 是否需要乐观锁或 FOR UPDATE
  • 数据量是否需要分批?
  • 条件字段是否有合适索引?
  • 是否会修改分区键?
  • 是否在事务中执行?
  • 更新后是否检查 UPDATE count
  • 出错后是否正确回滚?

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

确定目标数据
用 SELECT 预览
统计影响行数
计算新值
检查约束和并发风险
避免无变化更新
UPDATE ... RETURNING OLD/NEW
检查实际修改结果
COMMIT 或 ROLLBACK

核心原则:

更新不是简单覆盖旧值,而是在正确范围、事务保护和并发控制下,把数据从一个合法状态转换到另一个合法状态。