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 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 基本语法

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 官方教程同样将显式列出字段名视为良好实践。插入数据教程


四、省略字段、DEFAULTNULL

这三个概念非常容易混淆。

1. 省略字段

INSERT INTO market.security (
    exchange_code,
    code,
    name
)
VALUES (
    'SZSE',
    '000001',
    '平安银行'
);

没有提供的字段会使用:

  1. 字段定义的默认值;
  2. 如果没有默认值,则使用 NULL

因此本次插入中:

industry   → NULL
is_st      → false
is_active  → true
extra      → {}
created_at → now()
updated_at → now()

2. 显式使用 DEFAULT

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

3. 显式插入 NULL

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 明确插入空值,不会使用默认值

4. 全部使用默认值

假设有一张表:

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;

数据库会自动生成整行数据。


五、数据类型的正确写法

1. 字符串

字符串必须使用单引号:

'贵州茅台'
'SSE'
'600519'

双引号表示数据库对象名称:

"name"

因此下面是错误思路:

VALUES ("贵州茅台")

2. 股票代码

股票代码必须作为字符串插入:

'000001'

不能写成数字:

000001

否则会变成整数 1,丢失前导零。


3. 日期

推荐使用明确的日期字面量:

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 更容易理解和排错。


4. 时间

TIMESTAMPTZ '2026-09-14 09:30:00+08'

或者使用函数:

now()
current_timestamp

5. 布尔值

TRUE
FALSE
NULL

推荐:

INSERT INTO market.security (
    exchange_code,
    code,
    name,
    is_st
)
VALUES (
    'SSE',
    '600519',
    '贵州茅台',
    FALSE
);

6. JSONB

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

八、自动主键与 Identity

表中定义:

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',
    '贵州茅台'
);

日常业务不建议手动覆盖自增主键。

序列出现跳号是正常现象。即使插入失败或事务回滚,已经获取的序列值也不一定会被再次使用。


九、使用 RETURNING 返回插入结果

插入数据后,经常需要获得:

  • 自动生成的主键
  • 默认时间
  • 触发器修改后的值
  • 生成列
  • 完整的新记录

可以使用:

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 可以直接取得数据库最终生成的结果,不需要再执行一次 SELECTPostgreSQL RETURNING


十、PostgreSQL 18 的 OLDNEW

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 ... SELECT

可以把一个查询的结果插入目标表:

INSERT INTO market.security (
    exchange_code,
    code,
    name,
    industry
)
SELECT
    exchange_code,
    code,
    name,
    industry
FROM staging.security_import
WHERE code ~ '^[0-9]{6}$';

执行逻辑:

  1. 先执行 SELECT
  2. 得到零行、一行或多行
  3. 把查询结果插入目标表

目标字段与查询结果必须:

  • 数量一致
  • 顺序对应
  • 数据类型兼容

不推荐:

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

解决方案不是删除唯一约束,而是明确业务希望:

  1. 重复时直接报错;
  2. 重复时忽略;
  3. 重复时更新。

十三、冲突时忽略:DO NOTHING

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)

可读性更好,也更容易判断究竟在处理哪一种重复。


DO NOTHINGRETURNING

INSERT INTO market.security (
    exchange_code,
    code,
    name
)
VALUES (
    'SSE',
    '600519',
    '贵州茅台'
)
ON CONFLICT (exchange_code, code)
DO NOTHING
RETURNING security_id;

如果成功插入,会返回 security_id

如果发生冲突并被忽略,不会返回已有记录。也就是说,不能依赖这条语句在冲突时获得旧行。


十四、存在则更新:UPSERT

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 判断,再决定 INSERTUPDATE”,它能避免典型的并发竞态。PostgreSQL INSERT 冲突处理


EXCLUDED 的含义

假设数据库原有:

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


十六、行情数据 UPSERT

行情采集可能反复获取同一个交易日的数据,因此适合使用:

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)

十七、UPSERT 返回新旧数据

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 不能处理所有错误

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 不是“忽略所有异常”。


十九、一批 UPSERT 数据内部不能重复

下面一条语句中包含两个相同的业务键:

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

二十、在事务中插入

1. 单条语句本身具有原子性

一条多行插入中,只要有一行失败,整条语句通常都会回滚。

2. 多条语句组成一个事务

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 还是 COPY

数据规模 推荐方式
单条业务数据 单行 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 的客户端。
  • \copypsql 命令,不是标准 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();

二十二、Python psycopg 3 插入

不要通过字符串拼接 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")

二十三、SQLAlchemy 2.0 插入

基础插入:

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

不要只捕获异常然后继续使用已经失败的事务。


二十四、常见插入错误

1. 字段和值数量不一致

INSERT INTO market.security (
    exchange_code,
    code,
    name
)
VALUES (
    'SSE',
    '600519'
);

2. 数据类型错误

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

3. 非空约束错误

INSERT INTO market.security (
    exchange_code,
    code,
    name
)
VALUES (
    'SSE',
    '600519',
    NULL
);

4. 唯一约束错误

重复插入:

SSE + 600519

5. 检查约束错误

code = '6005'
volume = -100
high_price < low_price

6. 外键错误

security_id = 999999

但主表中不存在这只证券。

7. 字符串超长

code varchar(6)

却插入:

600519.SH

8. 把数字当成字符串拼接

参数化查询应让数据库驱动负责类型转换。


二十五、“增”的推荐实践

  1. 始终明确写出目标字段。
  2. 股票代码使用字符串,不使用整数。
  3. 让数据库生成 Identity 主键。
  4. 使用 RETURNING 获取主键和默认值。
  5. 使用参数化查询,禁止字符串拼接 SQL。
  6. 多行数据尽量批量写入。
  7. 大型文件使用 COPY
  8. 批量导入先进入暂存表,再清洗和写入正式表。
  9. 使用唯一约束定义什么叫“重复”。
  10. 重复时明确选择报错、忽略或 UPSERT。
  11. 使用 IS DISTINCT FROM 避免无意义更新。
  12. 不要用 ON CONFLICT 代替全部异常处理。
  13. 多个相关插入放在同一个事务中。
  14. 数据库异常后及时回滚事务。
  15. 插入前先想清楚字段中 NULL、零和默认值的区别。

“增”的完整判断模型是:

准备数据
类型是否正确
必填字段是否完整
约束是否满足
业务唯一键是否冲突
   ├── 不冲突 → INSERT
   ├── 冲突忽略 → DO NOTHING
   └── 冲突更新 → DO UPDATE
RETURNING 获取结果
COMMIT 提交事务

核心原则:

INSERT 不只是把数据写进去,而是让数据在类型、约束、唯一性和事务保护下,正确地进入数据库。