PostgreSQL UPDATE
PostgreSQL 使用 UPDATE 修改已有数据:
UPDATE 表名
SET 字段 = 新值
WHERE 条件;
一条完整的更新语句需要明确:
- 修改哪张表;
- 修改哪些字段;
- 新值如何计算;
- 修改哪些行。
官方完整语法见 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 可以修改多行。
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
右侧的 a 和 b 都取修改前的值,不是从左到右逐个执行后的中间值。
如果:
amount = NULL
执行:
UPDATE account
SET amount = amount + 100;
结果仍然是:
NULL
因为大多数表达式只要包含 NULL,结果也是 NULL。
如果业务上希望把空值当作零:
UPDATE account
SET amount = coalesce(amount, 0) + 100;
但是必须先确认业务含义:
NULL表示未知;0表示已经明确为零。
不要为了方便计算,就无条件把未知值当成零。
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 字段也可以设置为 DEFAULT:
UPDATE example
SET id = DEFAULT
WHERE id = 100;
这会从对应序列生成一个新值。
但主键应该保持稳定,业务中通常不应修改主键。
UPDATE market.security
SET is_active = false
WHERE delisted_on < CURRENT_DATE;
UPDATE market.daily_quote
SET updated_at = now()
WHERE trade_date >= DATE '2026-01-01'
AND trade_date < DATE '2027-01-01';
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%'
);
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 可以根据不同条件设置不同值。
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 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脚本
- 管理工具
- 其他服务
更新后返回新数据:
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 可以直接引用:
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 可以看到触发器处理后的最终新值。
假设暂存表中有最新股票资料:
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
);
如果少量记录需要分别修改成不同值:
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 中的业务键不重复。
假设:
extra jsonb NOT NULL DEFAULT '{}'::jsonb
原数据:
{
"province": "贵州",
"source": "同花顺"
}
UPDATE market.security
SET
extra = jsonb_set(
extra,
'{province}',
to_jsonb('贵州省'::text),
true
),
updated_at = now()
WHERE security_id = 100;
最后一个 true 表示字段不存在时创建。
UPDATE market.security
SET extra = extra || '{
"province": "贵州省",
"company_type": "国企"
}'::jsonb
WHERE security_id = 100;
相同键会被右侧新值覆盖。
UPDATE market.security
SET extra = extra - 'source'
WHERE security_id = 100;
稳定、常用、需要排序或关联的字段,仍应使用普通列,不应全部塞入 jsonb。
UPDATE 后的数据必须继续满足所有约束:
NOT NULLCHECKUNIQUE- 主键
- 外键
- 排除约束
例如:
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 只修改已经存在的数据:
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;
不要:
- Python 读取
balance; - Python 计算;
- 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。
下面的写法不支持:
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 通常会:
- 创建新的行版本;
- 将旧行版本逐渐变成死元组;
- 写入 WAL;
- 更新相关索引;
- 锁定被修改的行。
一次修改数千万行可能造成:
- 表和索引膨胀
- 大量 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';
重点检查:
- 是否使用合适索引;
- 预估行数与实际行数是否接近;
- 是否扫描了过多数据;
- 更新范围是否符合预期。
参数化更新:
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]
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()
UPDATE market.security
SET is_active = false;
修改全表。
以为只改一行,实际更新几百行。
WHERE industry = NULL
应使用:
WHERE industry IS NULL
不满足条件的字段可能被设置为 NULL。
一个目标行匹配多条来源,结果不确定。
产生不必要的行版本、WAL 和表膨胀。
DEFAULT now() 不会在更新时自动执行。
并发下可能覆盖其他事务的修改。
会影响外键、缓存、日志以及外部系统。
可能造成长事务、复制延迟、锁竞争和膨胀。
会真实修改数据。
事务失败后应执行 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
核心原则:
更新不是简单覆盖旧值,而是在正确范围、事务保护和并发控制下,把数据从一个合法状态转换到另一个合法状态。