PostgreSQL INSERT
PostgreSQL 使用 INSERT 向表中增加数据。它不仅能插入一条数据,还支持:
- 单行和多行插入
- 默认值与自动主键
- 返回插入结果
- 查询结果写入
- 冲突忽略
- UPSERT:存在则更新,不存在则插入
- 事务批量写入
COPY高性能导入
官方完整语法见 PostgreSQL 18 INSERT。
CREATE SCHEMA IF NOT EXISTS market;
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,
industry text,
is_st boolean NOT NULL DEFAULT false,
is_active boolean NOT NULL DEFAULT true,
extra jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT uq_security_exchange_code
UNIQUE (exchange_code, code),
CONSTRAINT ck_security_code
CHECK (code ~ '^[0-9]{6}$'),
CONSTRAINT ck_security_name
CHECK (btrim(name) <> '')
);
日线行情表:
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(),
updated_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),
CONSTRAINT ck_daily_quote_price
CHECK (
open_price >= 0
AND high_price >= open_price
AND high_price >= close_price
AND low_price <= open_price
AND low_price <= close_price
),
CONSTRAINT ck_daily_quote_volume
CHECK (volume >= 0),
CONSTRAINT ck_daily_quote_amount
CHECK (amount >= 0)
);
INSERT INTO 表名 (字段1, 字段2, 字段3)
VALUES (值1, 值2, 值3);
示例:
INSERT INTO market.security (
exchange_code,
code,
name,
industry
)
VALUES (
'SSE',
'600519',
'贵州茅台',
'白酒'
);
执行成功后,psql 通常显示:
INSERT 0 1
最后的 1 表示插入了一行。
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
);
对应关系是:
| 字段 | 值 |
|---|---|
exchange_code |
'SSE' |
code |
'600519' |
name |
'贵州茅台' |
字段顺序可以调整,但值也必须同步调整:
INSERT INTO market.security (
name,
code,
exchange_code
)
VALUES (
'贵州茅台',
'600519',
'SSE'
);
PostgreSQL 允许省略字段名:
INSERT INTO market.security
VALUES (
DEFAULT,
'SSE',
'600519',
'贵州茅台',
'白酒',
false,
true,
'{}'::jsonb,
now(),
now()
);
不推荐这种写法,因为它依赖表中字段的物理顺序。
一旦表结构发生变化,例如新增字段或调整字段顺序,这条 SQL 就可能:
- 插入失败
- 数据错位
- 难以阅读
- 难以维护
推荐始终写成:
INSERT INTO market.security (
exchange_code,
code,
name,
industry
)
VALUES (
'SSE',
'600519',
'贵州茅台',
'白酒'
);
PostgreSQL 官方教程同样将显式列出字段名视为良好实践。插入数据教程
这三个概念非常容易混淆。
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SZSE',
'000001',
'平安银行'
);
没有提供的字段会使用:
- 字段定义的默认值;
- 如果没有默认值,则使用
NULL。
因此本次插入中:
industry → NULL
is_st → false
is_active → true
extra → {}
created_at → now()
updated_at → now()
INSERT INTO market.security (
exchange_code,
code,
name,
is_st,
created_at
)
VALUES (
'SZSE',
'000001',
'平安银行',
DEFAULT,
DEFAULT
);
等价于让数据库使用字段默认值:
is_st → false
created_at → now()
多行插入时,部分记录可以单独使用 DEFAULT:
INSERT INTO market.security (
exchange_code,
code,
name,
industry
)
VALUES
('SSE', '600519', '贵州茅台', '白酒'),
('SZSE', '000001', '平安银行', DEFAULT);
INSERT INTO market.security (
exchange_code,
code,
name,
industry
)
VALUES (
'SZSE',
'000001',
'平安银行',
NULL
);
这里明确表示行业数据未知。
需要特别注意:
INSERT INTO market.security (
exchange_code,
code,
name,
is_active
)
VALUES (
'SZSE',
'000001',
'平安银行',
NULL
);
不会使用 is_active 的默认值,而是试图插入 NULL,最终违反:
is_active boolean NOT NULL
总结:
| 写法 | 实际行为 |
|---|---|
| 省略字段 | 使用默认值;没有默认值则为 NULL |
DEFAULT |
强制使用字段默认值 |
NULL |
明确插入空值,不会使用默认值 |
假设有一张表:
CREATE TABLE system_task (
task_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
可以写:
INSERT INTO system_task DEFAULT VALUES;
数据库会自动生成整行数据。
字符串必须使用单引号:
'贵州茅台'
'SSE'
'600519'
双引号表示数据库对象名称:
"name"
因此下面是错误思路:
VALUES ("贵州茅台")
股票代码必须作为字符串插入:
'000001'
不能写成数字:
000001
否则会变成整数 1,丢失前导零。
推荐使用明确的日期字面量:
DATE '2026-09-14'
例如:
INSERT INTO market.daily_quote (
security_id,
trade_date,
open_price,
high_price,
low_price,
close_price,
volume,
amount
)
VALUES (
1,
DATE '2026-09-14',
1500.0000,
1520.0000,
1490.0000,
1510.0000,
2586321,
3895200000.00
);
也可以让 PostgreSQL进行类型转换:
'2026-09-14'
但是明确写成 DATE 更容易理解和排错。
TIMESTAMPTZ '2026-09-14 09:30:00+08'
或者使用函数:
now()
current_timestamp
TRUE
FALSE
NULL
推荐:
INSERT INTO market.security (
exchange_code,
code,
name,
is_st
)
VALUES (
'SSE',
'600519',
'贵州茅台',
FALSE
);
INSERT INTO market.security (
exchange_code,
code,
name,
extra
)
VALUES (
'SSE',
'600519',
'贵州茅台',
'{
"province": "贵州",
"concepts": ["白酒", "国企", "消费"]
}'::jsonb
);
也可以使用函数构造:
INSERT INTO market.security (
exchange_code,
code,
name,
extra
)
VALUES (
'SSE',
'600519',
'贵州茅台',
jsonb_build_object(
'province', '贵州',
'concepts', jsonb_build_array('白酒', '国企', '消费')
)
);
INSERT INTO market.security (
exchange_code,
code,
name,
industry
)
VALUES
('SSE', '600519', '贵州茅台', '白酒'),
('SZSE', '000001', '平安银行', '银行'),
('SSE', '601318', '中国平安', '保险');
相比逐条执行:
INSERT ...;
INSERT ...;
INSERT ...;
多行 VALUES 通常具有以下优势:
- 减少客户端与数据库通信次数
- 减少 SQL 解析开销
- 事务控制更简单
- 批量写入速度更快
一条多行 INSERT 是原子操作。如果其中一行违反约束,默认情况下整条语句都会失败。
例如第二行代码不合法:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES
('SSE', '600519', '贵州茅台'),
('SZSE', '001', '错误代码'),
('SSE', '601318', '中国平安');
由于 '001' 违反股票代码检查约束,三行都不会插入。
VALUES 中不只能写固定值,还可以使用表达式和函数:
INSERT INTO market.daily_quote (
security_id,
trade_date,
open_price,
high_price,
low_price,
close_price,
volume,
amount
)
VALUES (
1,
CURRENT_DATE,
round(1500.12345::numeric, 4),
1520.00,
1490.00,
1510.00,
1000000 * 2,
1510.00 * 2000000
);
还可以使用:
now()
uuidv7()
coalesce(...)
nullif(...)
jsonb_build_object(...)
表中定义:
security_id bigint
GENERATED ALWAYS AS IDENTITY
正常插入时不要提供 security_id:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
);
数据库自动生成:
security_id = 1
如果显式插入:
INSERT INTO market.security (
security_id,
exchange_code,
code,
name
)
VALUES (
100,
'SSE',
'600519',
'贵州茅台'
);
由于字段是 GENERATED ALWAYS,会报错。
特殊的数据迁移场景可以使用:
INSERT INTO market.security (
security_id,
exchange_code,
code,
name
)
OVERRIDING SYSTEM VALUE
VALUES (
100,
'SSE',
'600519',
'贵州茅台'
);
日常业务不建议手动覆盖自增主键。
序列出现跳号是正常现象。即使插入失败或事务回滚,已经获取的序列值也不一定会被再次使用。
插入数据后,经常需要获得:
- 自动生成的主键
- 默认时间
- 触发器修改后的值
- 生成列
- 完整的新记录
可以使用:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
)
RETURNING security_id;
返回:
security_id
-----------
1
返回多个字段:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
)
RETURNING
security_id,
exchange_code,
code,
name,
created_at;
返回整行:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
)
RETURNING *;
生产代码通常只返回需要的字段,不建议无条件使用 RETURNING *。
RETURNING 可以直接取得数据库最终生成的结果,不需要再执行一次 SELECT。PostgreSQL RETURNING
PostgreSQL 18 的 RETURNING 可以引用:
OLD:修改前的行NEW:修改后的行
普通插入不存在旧记录,因此 OLD 字段为 NULL:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
)
RETURNING
OLD.name AS old_name,
NEW.name AS new_name;
结果类似:
old_name | new_name
---------+---------
NULL | 贵州茅台
它在后面的 UPSERT 中更有价值,因为可以同时观察冲突前后的数据。
可以把一个查询的结果插入目标表:
INSERT INTO market.security (
exchange_code,
code,
name,
industry
)
SELECT
exchange_code,
code,
name,
industry
FROM staging.security_import
WHERE code ~ '^[0-9]{6}$';
执行逻辑:
- 先执行
SELECT - 得到零行、一行或多行
- 把查询结果插入目标表
目标字段与查询结果必须:
- 数量一致
- 顺序对应
- 数据类型兼容
不推荐:
INSERT INTO market.security
SELECT *
FROM staging.security_import;
推荐显式写出两边字段:
INSERT INTO market.security (
exchange_code,
code,
name,
industry
)
SELECT
exchange_code,
code,
name,
industry
FROM staging.security_import;
这样源表增加或调整字段时,不容易导致数据错位。
INSERT INTO market.security (
exchange_code,
code,
name,
industry
)
SELECT
upper(btrim(exchange_code)),
btrim(code),
btrim(name),
nullif(btrim(industry), '')
FROM staging.security_import
WHERE btrim(code) ~ '^[0-9]{6}$';
这里完成了:
- 去除首尾空格
- 交易所代码转大写
- 空字符串转换为
NULL - 过滤非法股票代码
表中存在唯一约束:
UNIQUE (exchange_code, code)
第一次执行:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
);
能够成功。
再次插入相同的交易所和代码:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台股份'
);
会报唯一约束冲突:
duplicate key value violates unique constraint
解决方案不是删除唯一约束,而是明确业务希望:
- 重复时直接报错;
- 重复时忽略;
- 重复时更新。
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
)
ON CONFLICT (exchange_code, code)
DO NOTHING;
含义:
不存在 → 插入
已存在 → 什么都不做
如果希望忽略所有可以处理的唯一冲突:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
)
ON CONFLICT DO NOTHING;
推荐明确写出冲突目标:
ON CONFLICT (exchange_code, code)
可读性更好,也更容易判断究竟在处理哪一种重复。
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
)
ON CONFLICT (exchange_code, code)
DO NOTHING
RETURNING security_id;
如果成功插入,会返回 security_id。
如果发生冲突并被忽略,不会返回已有记录。也就是说,不能依赖这条语句在冲突时获得旧行。
UPSERT 表示:
UPDATE + INSERT
标准写法:
INSERT INTO market.security AS s (
exchange_code,
code,
name,
industry,
is_st
)
VALUES (
'SSE',
'600519',
'贵州茅台',
'白酒',
false
)
ON CONFLICT (exchange_code, code)
DO UPDATE SET
name = EXCLUDED.name,
industry = EXCLUDED.industry,
is_st = EXCLUDED.is_st,
updated_at = now();
其中:
s:数据库中已经存在的目标行EXCLUDED:本次原本准备插入的新数据DO UPDATE:发生唯一冲突后执行更新
逻辑等价于:
如果 SSE + 600519 不存在:
插入新股票
否则:
使用新数据更新名称、行业和ST状态
PostgreSQL 保证 ON CONFLICT DO UPDATE 在并发下实现原子性的“插入或更新”结果。相比“先 SELECT 判断,再决定 INSERT 或 UPDATE”,它能避免典型的并发竞态。PostgreSQL INSERT 冲突处理
假设数据库原有:
name = 贵州茅台
industry = 食品饮料
准备插入:
name = 贵州茅台股份
industry = 白酒
那么在冲突更新阶段:
s.name
表示:
贵州茅台
而:
EXCLUDED.name
表示:
贵州茅台股份
下面的 UPSERT 即使数据完全相同,也会执行一次 UPDATE:
ON CONFLICT (exchange_code, code)
DO UPDATE SET
name = EXCLUDED.name,
updated_at = now();
这可能产生:
- 新的行版本
- 更多 WAL
- 更多表膨胀
- 不必要的索引和触发器处理
updated_at被无意义刷新
可以增加条件:
INSERT INTO market.security AS s (
exchange_code,
code,
name,
industry,
is_st
)
VALUES (
'SSE',
'600519',
'贵州茅台',
'白酒',
false
)
ON CONFLICT (exchange_code, code)
DO UPDATE SET
name = EXCLUDED.name,
industry = EXCLUDED.industry,
is_st = EXCLUDED.is_st,
updated_at = now()
WHERE (
s.name,
s.industry,
s.is_st
) IS DISTINCT FROM (
EXCLUDED.name,
EXCLUDED.industry,
EXCLUDED.is_st
);
使用 IS DISTINCT FROM 而不是普通的 <>,是因为它可以正确比较 NULL。
行情采集可能反复获取同一个交易日的数据,因此适合使用:
INSERT INTO market.daily_quote AS q (
security_id,
trade_date,
open_price,
high_price,
low_price,
close_price,
volume,
amount
)
VALUES (
1,
DATE '2026-09-14',
1500.0000,
1520.0000,
1490.0000,
1510.0000,
2586321,
3895200000.00
)
ON CONFLICT (security_id, trade_date)
DO UPDATE SET
open_price = EXCLUDED.open_price,
high_price = EXCLUDED.high_price,
low_price = EXCLUDED.low_price,
close_price = EXCLUDED.close_price,
volume = EXCLUDED.volume,
amount = EXCLUDED.amount,
updated_at = now()
WHERE (
q.open_price,
q.high_price,
q.low_price,
q.close_price,
q.volume,
q.amount
) IS DISTINCT FROM (
EXCLUDED.open_price,
EXCLUDED.high_price,
EXCLUDED.low_price,
EXCLUDED.close_price,
EXCLUDED.volume,
EXCLUDED.amount
);
这里的冲突目标:
(security_id, trade_date)
必须有对应的:
- 主键
- 唯一约束
- 或符合条件的唯一索引
本例已经存在:
PRIMARY KEY (security_id, trade_date)
PostgreSQL 18 可以同时返回更新前后的值:
INSERT INTO market.security AS s (
exchange_code,
code,
name,
industry
)
VALUES (
'SSE',
'600519',
'贵州茅台',
'白酒'
)
ON CONFLICT (exchange_code, code)
DO UPDATE SET
name = EXCLUDED.name,
industry = EXCLUDED.industry,
updated_at = now()
RETURNING
OLD.name AS old_name,
NEW.name AS new_name,
OLD.industry AS old_industry,
NEW.industry AS new_industry;
还可以判断本次是插入还是更新:
RETURNING
OLD.security_id IS NULL AS inserted,
NEW.security_id,
NEW.code,
NEW.name;
含义:
inserted = true → 新插入
inserted = false → 冲突后更新
ON CONFLICT 主要处理唯一约束或唯一索引冲突。
它不能把下面所有错误都自动忽略:
NOT NULL违规CHECK违规- 外键违规
- 数据类型错误
- 字符串超长
- 非法日期
- 数字越界
例如:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'519',
'错误代码'
)
ON CONFLICT DO NOTHING;
仍然会因为代码检查约束失败而报错:
CHECK (code ~ '^[0-9]{6}$')
DO NOTHING 不是“忽略所有异常”。
下面一条语句中包含两个相同的业务键:
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES
('SSE', '600519', '贵州茅台'),
('SSE', '600519', '贵州茅台股份')
ON CONFLICT (exchange_code, code)
DO UPDATE SET
name = EXCLUDED.name;
PostgreSQL 不允许同一条 INSERT ... ON CONFLICT DO UPDATE 多次影响同一目标行,可能产生 cardinality violation。
因此批量导入前应先按照唯一键去重。
例如暂存表中保留最新记录:
WITH ranked AS (
SELECT
exchange_code,
code,
name,
updated_at,
row_number() OVER (
PARTITION BY exchange_code, code
ORDER BY updated_at DESC
) AS row_num
FROM staging.security_import
)
INSERT INTO market.security AS s (
exchange_code,
code,
name
)
SELECT
exchange_code,
code,
name
FROM ranked
WHERE row_num = 1
ON CONFLICT (exchange_code, code)
DO UPDATE SET
name = EXCLUDED.name,
updated_at = now();
一条多行插入中,只要有一行失败,整条语句通常都会回滚。
BEGIN;
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
);
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SZSE',
'000001',
'平安银行'
);
COMMIT;
如果中间发生错误:
ROLLBACK;
事务保证:
全部成功 → COMMIT
任意一步失败 → ROLLBACK
如果希望部分步骤失败后还能继续:
BEGIN;
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
'贵州茅台'
);
SAVEPOINT before_second_insert;
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'错误代码',
'错误记录'
);
ROLLBACK TO SAVEPOINT before_second_insert;
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SZSE',
'000001',
'平安银行'
);
COMMIT;
在显式事务中,一条 SQL 报错后,事务通常会进入失败状态。必须执行:
ROLLBACK;
或者:
ROLLBACK TO SAVEPOINT ...
不能假设捕获应用程序异常后事务会自动恢复。
| 数据规模 | 推荐方式 |
|---|---|
| 单条业务数据 | 单行 INSERT |
| 几十到几千条 | 多行 INSERT或驱动批处理 |
| 大型 CSV、几十万行以上 | COPY |
| 需要清洗、去重、UPSERT | 暂存表 + COPY + INSERT ... SELECT |
PostgreSQL 官方建议,大批量加载数据时优先考虑 COPY,它通常比逐行 INSERT 更高效。批量填充数据库
服务器端读取文件:
COPY staging.security_import (
exchange_code,
code,
name,
industry
)
FROM '/data/security.csv'
WITH (
FORMAT csv,
HEADER true,
ENCODING 'UTF8'
);
在 psql 中读取客户端本地文件:
\copy staging.security_import (
exchange_code,
code,
name,
industry
)
FROM './security.csv'
WITH (
FORMAT csv,
HEADER true,
ENCODING 'UTF8'
);
区别:
COPY:文件必须位于数据库服务器能够访问的位置。\copy:文件位于运行psql的客户端。\copy是psql命令,不是标准 SQL。
完整选项见 PostgreSQL 18 COPY。
CSV / 接口数据
↓
COPY 到暂存表
↓
检查非法数据
↓
清洗和去重
↓
INSERT ... SELECT
↓
ON CONFLICT 更新正式表
暂存表:
CREATE TEMP TABLE security_import (
exchange_code text,
code text,
name text,
industry text
);
导入后检查:
SELECT *
FROM security_import
WHERE btrim(code) !~ '^[0-9]{6}$'
OR nullif(btrim(name), '') IS NULL;
清洗写入正式表:
INSERT INTO market.security AS s (
exchange_code,
code,
name,
industry
)
SELECT
upper(btrim(exchange_code)),
btrim(code),
btrim(name),
nullif(btrim(industry), '')
FROM security_import
WHERE btrim(code) ~ '^[0-9]{6}$'
AND nullif(btrim(name), '') IS NOT NULL
ON CONFLICT (exchange_code, code)
DO UPDATE SET
name = EXCLUDED.name,
industry = EXCLUDED.industry,
updated_at = now();
不要通过字符串拼接 SQL:
## 错误示范
sql = f"""
INSERT INTO market.security (exchange_code, code, name)
VALUES ('{exchange}', '{code}', '{name}')
"""
这会带来:
- SQL 注入
- 引号转义错误
NULL处理错误- 类型转换问题
应使用参数化查询:
import psycopg
sql = """
INSERT INTO market.security (
exchange_code,
code,
name,
industry
)
VALUES (%s, %s, %s, %s)
RETURNING security_id, created_at
"""
with psycopg.connect(
"postgresql://postgres:password@localhost:5432/stock"
) as connection:
with connection.cursor() as cursor:
cursor.execute(
sql,
("SSE", "600519", "贵州茅台", "白酒"),
)
result = cursor.fetchone()
print(result)
参数中:
None
会被转换为 SQL 的:
NULL
不要自己加引号:
## 错误
("'SSE'", "'600519'")
应该直接传原始值:
("SSE", "600519")
基础插入:
from sqlalchemy import insert
statement = (
insert(Security)
.values(
exchange_code="SSE",
code="600519",
name="贵州茅台",
industry="白酒",
)
.returning(
Security.security_id,
Security.created_at,
)
)
result = session.execute(statement)
security_id, created_at = result.one()
session.commit()
PostgreSQL UPSERT 需要使用 PostgreSQL 方言的 insert:
from sqlalchemy.dialects.postgresql import insert
statement = insert(Security).values(
exchange_code="SSE",
code="600519",
name="贵州茅台",
industry="白酒",
)
statement = statement.on_conflict_do_update(
index_elements=[
Security.exchange_code,
Security.code,
],
set_={
"name": statement.excluded.name,
"industry": statement.excluded.industry,
},
)
session.execute(statement)
session.commit()
异常时必须回滚:
try:
session.execute(statement)
session.commit()
except Exception:
session.rollback()
raise
不要只捕获异常然后继续使用已经失败的事务。
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519'
);
INSERT INTO market.daily_quote (
security_id,
trade_date,
open_price,
high_price,
low_price,
close_price,
volume,
amount
)
VALUES (
1,
'不是日期',
10,
11,
9,
10.5,
1000,
10500
);
INSERT INTO market.security (
exchange_code,
code,
name
)
VALUES (
'SSE',
'600519',
NULL
);
重复插入:
SSE + 600519
code = '6005'
volume = -100
high_price < low_price
security_id = 999999
但主表中不存在这只证券。
code varchar(6)
却插入:
600519.SH
参数化查询应让数据库驱动负责类型转换。
- 始终明确写出目标字段。
- 股票代码使用字符串,不使用整数。
- 让数据库生成 Identity 主键。
- 使用
RETURNING获取主键和默认值。 - 使用参数化查询,禁止字符串拼接 SQL。
- 多行数据尽量批量写入。
- 大型文件使用
COPY。 - 批量导入先进入暂存表,再清洗和写入正式表。
- 使用唯一约束定义什么叫“重复”。
- 重复时明确选择报错、忽略或 UPSERT。
- 使用
IS DISTINCT FROM避免无意义更新。 - 不要用
ON CONFLICT代替全部异常处理。 - 多个相关插入放在同一个事务中。
- 数据库异常后及时回滚事务。
- 插入前先想清楚字段中
NULL、零和默认值的区别。
“增”的完整判断模型是:
准备数据
↓
类型是否正确
↓
必填字段是否完整
↓
约束是否满足
↓
业务唯一键是否冲突
├── 不冲突 → INSERT
├── 冲突忽略 → DO NOTHING
└── 冲突更新 → DO UPDATE
↓
RETURNING 获取结果
↓
COMMIT 提交事务
核心原则:
INSERT不只是把数据写进去,而是让数据在类型、约束、唯一性和事务保护下,正确地进入数据库。