PostgreSQL COPY
COPY 是 PostgreSQL 最快的数据导入 / 导出方式,直接在 数据库服务端 读写文件。
文件 <-> PostgreSQL 后端进程 <-> 表
性能远超 INSERT / ORM / 批量执行
COPY table_name
FROM '/absolute/path/file.csv'
WITH (FORMAT csv);
COPY table_name
TO '/absolute/path/file.csv'
WITH (FORMAT csv, HEADER true);
COPY articles (datetime, code, source, title, url)
FROM '/tmp/articles.csv'
WITH (FORMAT csv, HEADER true);
- 避免列顺序问题
- 可跳过自增 id
CSV 相关
WITH (
FORMAT csv,
HEADER true,
DELIMITER ',',
QUOTE '"',
ESCAPE '"',
NULL '',
ENCODING 'UTF8'
);
| 参数 | 说明 |
|---|---|
| FORMAT | 格式 |
| HEADER | 首行是列名 |
| DELIMITER | 字段分隔符 |
| QUOTE | 字段包裹符(默认为 “) |
| ESCAPE | 转义符(默认为 “) |
| NULL | 空值表示 |
| ENCODING | 文件编码 |
SET datestyle = 'ISO, YMD';
2025-01-01 10:00:00,,source,title,url
| 对比项 | COPY | \copy |
|---|---|---|
| 执行位置 | 数据库服务器 | 客户端(psql) |
| 文件位置 | 服务器磁盘 | 本地磁盘 |
| 权限 | 需要 superuser / pg_read_server_files | 普通用户即可 |
| 常用场景 | 生产服务器 | 本地导入 |
\copy articles (datetime, code, source, title, url)
FROM 'articles.csv'
WITH (FORMAT csv, HEADER true);
👉 优先用 \copy,少踩权限坑
id INTEGER GENERATED ALWAYS AS IDENTITY
CSV 包含 id -> 报错
COPY articles (datetime, code, source, title, url)
FROM '/path/articles.csv'
WITH (FORMAT csv, HEADER true);
COPY articles (id, datetime, code, source, title, url)
FROM '/path/articles.csv'
WITH (FORMAT csv, HEADER true)
OVERRIDING SYSTEM VALUE;
- OVERRIDING SYSTEM VALUE 可能不可用
导入后修复序列:
SELECT setval(
pg_get_serial_sequence('articles', 'id'),
(SELECT MAX(id) FROM articles)
);
SET synchronous_commit = off;
DROP INDEX idx_articles_title;
COPY ...
CREATE INDEX idx_articles_title ON articles (...);
CREATE UNLOGGED TABLE articles_tmp (...);
maintenance_work_mem = 1GB
work_mem = 64MB
BEGIN;
COPY articles FROM '/data/a.csv' CSV;
COMMIT;
-- 出错 -> 回滚
-- ROLLBACK;
- COPY 是事务性的
- 出错 -> 整个 COPY 回滚
could not open file
文件必须在 数据库服务器
must be superuser
用 \copy 或授予:
- 使用 COPY 会有权限问题
GRANT pg_read_server_files TO user;
GRANT pg_write_server_files TO user;
invalid byte sequence
转码:
iconv -f gbk -t utf8 a.csv > b.csv
- 明确列名
- 检查 delimiter / quote
COPY (
SELECT * FROM articles WHERE datetime >= '2025-01-01'
) TO '/data/export.csv' CSV HEADER;
CREATE TEMP TABLE tmp_articles (LIKE articles);
COPY tmp_articles FROM '/data/a.csv' CSV HEADER;
INSERT INTO articles
SELECT * FROM tmp_articles
ON CONFLICT (code, url) DO NOTHING;
COPY table_name FROM '/data/a.json' JSON;
- ✅ 本地导入用 \copy
- ✅ 永远显式写列名
- ✅ 不导入 ID(除非必要)
- ✅ 大批量导入前删除索引
- ✅ 导入后 ANALYZE
ANALYZE articles;
DateStyle 决定 PostgreSQL 如何解析和显示日期/时间字符串。
SHOW DateStyle;
常见输出:
ISO, MDY
含义:
- ISO:输出格式
- MDY:解析顺序(Month-Day-Year)
SET DateStyle = 'ISO, YMD';
或:
SET DateStyle TO 'ISO, YMD';
只影响 当前连接 / 当前事务。
BEGIN;
SET LOCAL DateStyle = 'ISO, YMD';
-- 在这里执行 COPY / INSERT
COMMIT;
- 事务结束自动恢复
- 非常适合一次性导入数据
ALTER DATABASE mydb
SET DateStyle = 'ISO, YMD';
新连接生效。
# postgresql.conf
DateStyle = 'ISO, YMD'
需要重启。
格式:
DateStyle = '输出格式, 输入解析顺序'
输出格式(第一部分)
| 值 | 示例 |
|---|---|
| ISO | 2025-12-10 10:54:37 |
| SQL | 12/10/2025 10:54:37 |
| Postgres | Wed Dec 10 10:54:37 2025 |
| German | 10.12.2025 10:54:37 |
输入解析顺序(第二部分)
| 值 | 含义 |
|---|---|
| MDY | 月-日-年 |
| DMY | 日-月-年 |
| YMD | 年-月-日 |
📌 只影响“有歧义”的日期字符串
❗️ 推荐:ISO + YMD
10-12-2025
12/10/2025
- MDY -> 12 月 10 日
- DMY -> 10 月 12 日
2025-12-10
2025-12-10 10:54:37
2025-12-10T10:54:37
ISO 8601 永远安全
场景:CSV 时间是 10/12/2025 10:54:37
正确写法:
BEGIN;
SET LOCAL DateStyle = 'ISO, DMY';
\copy articles FROM '/tmp/articles.csv' WITH (FORMAT csv);
COMMIT;
- 只影响当前导入
- 不污染其他会话
- ❌ 长期依赖 DateStyle
- ❌ 程序代码里隐式修改
- ❌ 多人共享数据库
- ❌ 自动化任务中
原因:不可预期 + 易出事故
用 to_timestamp() 明确格式
to_timestamp('10/12/2025 10:54:37', 'YYYY-MM-DD HH24:MI:SS')
或先导 TEXT 再转换(最稳)
DateStyle 是“最后手段”,不是长期方案
ISO + YMD 时间格式才是正解
只在“历史脏数据”导入时用 SET LOCAL
COPY 本身不定义时间格式
👉 时间格式由 PostgreSQL 的 DateStyle / 字段类型决定
也就是说:
- COPY 只是“搬数据”
- 解析时间的是 PostgreSQL 类型系统
✅ TIMESTAMP / TIMESTAMPTZ
YYYY-MM-DD HH:MI:SS
示例:
2025-12-10 10:54:37
2025-12-10 10:54:37.052
- 永远不会出错
- 不受 DateStyle 影响
- 所有 PostgreSQL 版本通用
表结构
datetime TIMESTAMP NOT NULL
CSV 示例
datetime
2025-12-10 10:54:37
2025-12-10 10:54:37.052
COPY
\copy articles (datetime)
FROM 'articles.csv'
WITH (FORMAT csv, HEADER true);
✅ 能成功(默认 DateStyle = ISO)
| 格式 | 示例 |
|---|---|
| ISO | 2025-12-10 10:54:37 |
| 带毫秒 | 2025-12-10 10:54:37.052 |
| T 分隔 | 2025-12-10T10:54:37 |
| UTC | 2025-12-10T10:54:37Z |
❌ 常见失败格式
| 格式 | 原因 |
|---|---|
| 2025/12/10 10:54:37 | DateStyle 不兼容 |
| 2025年12月10日 10:54:37 | 非标准 |
| 10-12-2025 10:54:37 | 月日歧义 |
| 20251210 | 非 timestamp |
报错通常是:
ERROR: invalid input syntax for type timestamp
把时间统一成:
YYYY-MM-DD HH:MI:SS
CREATE TEMP TABLE tmp_articles (
datetime TEXT,
...
);
\copy tmp_articles FROM 'articles.csv' WITH (FORMAT csv);
INSERT INTO articles (datetime, ...)
SELECT
to_timestamp(datetime, 'YYYY-MM-DD HH24:MI:SS'),
...
FROM tmp_articles;
SET DateStyle = 'ISO, MDY';
⚠️ 风险:
- 影响当前会话
- 不利于自动化
- 容易出事故
如果字段是:
datetime TIMESTAMPTZ
CSV 可写:
2025-12-10 10:54:37+08
2025-12-10T02:54:37Z
PostgreSQL 会:
- 自动转换为 UTC
- 内部统一存储
CSV:
datetime
\N
COPY:
\copy articles FROM 'articles.csv'
WITH (FORMAT csv, NULL '\N');
- 表字段用 TIMESTAMP 或 TIMESTAMPTZ
- CSV 时间统一成 ISO 8601
- 不在 COPY 时依赖 DateStyle
- 导入失败先导 TEXT 再转换
- 永远不要导入“中文时间”
COPY 不关心时间格式,PostgreSQL 类型才关心
👉 统一用 YYYY-MM-DD HH:MI:SS 永远不会踩坑
使用 Navicat 导出 CSV 文件时,字段数据都是使用 " " 括号括起来,而 PostgreSQL 的 CSV 文件则没有 " " 括号,梳理原因和正确用法。
COPY articles TO '/path/to/file.csv'
WITH (
FORMAT csv,
HEADER true,
DELIMITER ',',
QUOTE '"',
ESCAPE '"'
);
- DELIMITER -> 字段分隔符
- QUOTE -> 字段包裹符(默认为 “)
- ESCAPE -> 转义符(默认为 “)
注意:COPY TO 时只有在字段中出现 分隔符、换行符或引用符 时才会使用 QUOTE 包裹字段。
字段内容:hello, world
- CSV 默认规则:
- 字段中 没有分隔符、换行或引号 -> 不会加 " "
- 所以即使你设置 QUOTE ‘"’,输出也是:
hello,world
- 只有出现这些特殊字符,才会自动加引号:
字段内容:hello, "weixin"
输出: "hello, ""weixin"""
- QUOTE 并不是“每个字段都加引号”,而是“必要时自动加引号”
- 如果想每个字段都加引号,PostgreSQL 原生 COPY 没有直接参数,需要先处理数据或用客户端工具
COPY (
SELECT '"' || REPLACE(title, '"', '""') || '"' AS title,
'"' || REPLACE(content, '"', '""') || '"' AS content
FROM articles
) TO '/path/to/file.csv' WITH (FORMAT csv, HEADER true, DELIMITER ',');
- REPLACE(…, ‘”’, ‘””’) -> 转义引号
- 每个字段都会被包裹双引号
\copy (SELECT * FROM articles) TO 'articles.csv' CSV HEADER
- 由客户端 psql 生成 CSV
- 默认遵循 CSV 标准,遇到特殊字符自动加引号
- 可以结合 COPY TO PROGRAM ‘awk …’ 做自定义
- QUOTE ‘"’ 只在必要时加引号,字段没有特殊字符就不会显示
- 想强制加引号 -> 在 SELECT 里拼接 " " 或用客户端工具处理
- COPY TO CSV 本身严格遵守 CSV 标准,并不是“每个字段都加引号”
在 PostgreSQL COPY 中,双引号 " 不是分隔符(delimiter),而是 文本引用符(QUOTE)。
| 选项 | 含义 |
|---|---|
| DELIMITER | 字段分隔符(如 ,、\t、` |
| QUOTE | 字段包裹符(默认是 “) |
| ESCAPE | 转义符(默认也是 “) |
👉 双引号 " 不能当 delimiter 使用(CSV 规范不允许)
例如一行 CSV:
1,"hello,world","a ""quoted"" text"
解析规则:
- , -> delimiter
- “hello,world” -> 一个字段(即使内部有逗号)
- "” -> 表示一个 "
COPY articles
FROM STDIN
WITH (
FORMAT csv,
HEADER true
);
等价于:
DELIMITER ','
QUOTE '"'
ESCAPE '"'
COPY articles
FROM STDIN
WITH (
FORMAT csv,
DELIMITER ',',
QUOTE '"',
ESCAPE '"',
HEADER true
);
例如:
1|"hello world"|abc
正确写法:
COPY articles
FROM STDIN
WITH (
FORMAT text,
DELIMITER '|'
);
⚠️ 注意:
- FORMAT text 下 没有 QUOTE 语义
- " " 只是普通字符
COPY articles
FROM STDIN
WITH (DELIMITER '"');
问题:
- PostgreSQL 语法允许
- 但几乎 不可用
- 因为 " 本身用于标识字段边界
- 会导致字段无法正确解析
👉 强烈不建议
| 文件示例 | COPY 配置 |
|---|---|
| 1,abc,def | FORMAT csv |
| 1,“a,b”,c | FORMAT csv |
| `1 | abc |
| 1\tabc\tdef | FORMAT text, DELIMITER E’\t’ |
| 1;“abc”;“def” | FORMAT csv, DELIMITER ‘;’ |
问的是:
COPY delimiter 双引号 “
通常背后真实需求是:
👉 不用管,默认就支持
👉 不可能,CSV 规范必须用 QUOTE
👉 建议先转换文件(不推荐)
在 PostgreSQL COPY 中:
- ✅ " 是 QUOTE
- ❌ 不是 DELIMITER
- ✅ CSV 99% 情况下用默认配置
导出:
-- 登录数据库
-- psql postgresql://username:password@host/database
-- 显示日期格式
SHOW DateStyle;
-- DateStyle
-- -----------
-- ISO, MDY
-- (1 row)
-- 导出
BEGIN;
-- 日期格式: ISO, YMD
SET LOCAL DateStyle = 'ISO, YMD';
-- 包含列名
\copy articles TO '/tmp/articles.csv' WITH (FORMAT csv, HEADER true);
-- 表字段:id, datetime, code, source, title, url
-- 如果 id 是自增主键,并无实际的意义,可以忽略 id
\copy articles (datetime, code, source, title, url) TO '/tmp/articles.csv' WITH (FORMAT csv, HEADER true);
COMMIT;
导入:
-- 显示日期格式
SHOW DateStyle;
-- DateStyle
-- -----------
-- ISO, MDY
-- (1 row)
-- 导入
-- 检查文件内容,如果用 Navicat 导出的数据,日期格式默认是 ISO, DMY
BEGIN;
SET LOCAL DateStyle = 'ISO, DMY';
\copy articles FROM '/tmp/articles.csv' WITH (FORMAT csv, HEADER true);
COMMIT;
-- 重置序列
SELECT setval(
pg_get_serial_sequence('articles', 'id'),
(SELECT MAX(id) FROM articles)
);