将 PostgreSQL 转换为 Oracle
拖入由 pg_dump 或 pg_dumpall 生成的纯文本转储(.sql 或 .sql.gz),即可下载包含相同表、键、索引和数据的 Oracle 脚本。Oracle 无法原样存储的每个值,以及它不支持的每个对象,都会在报告中列出。
已转换 18 行,2 处修正,无拒绝
shop_2024_postgres.sql 中的表PostgreSQL 18.6 转储 · 由 pg_dump 写出 · utf8mb4
customers写作 CUSTOMERS8 行1
8 列 ID NUMBER(10) not null NAME VARCHAR2(4000 CHAR) not null EMAIL VARCHAR2(750 CHAR) not null COUNTRY VARCHAR2(2 CHAR) not null IS_ACTIVE NUMBER(1) not null TAGS CLOB not null SIGNED_UP DATE CREATED_AT TIMESTAMP(6) WITH TIME ZONE not null orders写作 ORDERS10 行1
10 列 ID NUMBER(10) not null CUSTOMER_ID NUMBER(10) not null STATUS VARCHAR2(9 CHAR) not null TOTAL NUMBER(10,2) not null ITEMS NUMBER(5) not null ORDERED_AT TIMESTAMP(6) not null SHIPPED_AT TIMESTAMP(6) NOTE VARCHAR2(4000 CHAR) DETAILS CLOB DELIVERY VARCHAR2(100 CHAR)
未转换
- 视图 paid_orders
每种数据库都用自己的语言编写这些对象。请为 Oracle 重写它们。
-- Written by IntoSQL from a PostgreSQL dump. Load it with: NLS_LANG=.AL32UTF8 sqlplus -S -L <user>/<password>@<database> @<file>
WHENEVER SQLERROR EXIT FAILURE ROLLBACK;
SET DEFINE OFF;
SET SQLBLANKLINES ON;
SET FEEDBACK OFF;
SET SERVEROUTPUT ON;
CREATE OR REPLACE FUNCTION "INTOSQL_CLOB" (p SYS.ODCIVARCHAR2LIST) RETURN CLOB IS
r CLOB;
BEGIN
DBMS_LOB.CREATETEMPORARY(r, TRUE);
FOR i IN 1 .. p.COUNT LOOP
IF p(i) IS NOT NULL THEN
DBMS_LOB.WRITEAPPEND(r, LENGTH2(p(i)), p(i));
END IF;
END LOOP;
RETURN r;
END;
/
CREATE OR REPLACE FUNCTION "INTOSQL_BLOB" (p SYS.ODCIVARCHAR2LIST) RETURN BLOB IS
r BLOB;
piece RAW(2000);
BEGIN
DBMS_LOB.CREATETEMPORARY(r, TRUE);
FOR i IN 1 .. p.COUNT LOOP
piece := HEXTORAW(p(i));
DBMS_LOB.WRITEAPPEND(r, UTL_RAW.LENGTH(piece), piece);
END LOOP;
RETURN r;
END;
/
CREATE TABLE "CUSTOMERS" (
"ID" NUMBER(10) GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH 1001),
"NAME" VARCHAR2(4000 CHAR) NOT NULL,
"EMAIL" VARCHAR2(750 CHAR) NOT NULL,
"COUNTRY" VARCHAR2(2 CHAR) DEFAULT 'US' NOT NULL,
"IS_ACTIVE" NUMBER(1) DEFAULT 1 NOT NULL,
"TAGS" CLOB NOT NULL,
"SIGNED_UP" DATE,
"CREATED_AT" TIMESTAMP(6) WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT "CK_CUSTOMERS_IS_ACTIVE" CHECK ("IS_ACTIVE" IN (0, 1))
);
CREATE TABLE "ORDERS" (
"ID" NUMBER(10) GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH 5001),
"CUSTOMER_ID" NUMBER(10) NOT NULL,
"STATUS" VARCHAR2(9 CHAR) DEFAULT 'pending' NOT NULL,
"TOTAL" NUMBER(10,2) NOT NULL,
"ITEMS" NUMBER(5) DEFAULT 1 NOT NULL,
"ORDERED_AT" TIMESTAMP(6) NOT NULL,
"SHIPPED_AT" TIMESTAMP(6),
"NOTE" VARCHAR2(4000 CHAR),
"DETAILS" CLOB,
"DELIVERY" VARCHAR2(100 CHAR),
CONSTRAINT "CK_ORDERS_STATUS" CHECK ("STATUS" IN ('pending', 'paid', 'shipped', 'cancelled')),
CONSTRAINT "ORDERS_TOTAL_CHECK" CHECK ("TOTAL" >= (0))
);
BEGIN
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
1,
'Ana Martínez',
'ana@example.com',
'MX',
1,
'["vip"]',
DATE '2023-01-15',
TIMESTAMP '2023-01-15 10:24:00 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
2,
'Zoë Müller',
'zoe@example.de',
'DE',
1,
'[]',
DATE '2023-02-03',
TIMESTAMP '2023-02-03 08:11:42 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
3,
'José Peña',
'jose@example.es',
'ES',
0,
'["wholesale","late payer"]',
DATE '2023-03-21',
TIMESTAMP '2023-03-21 19:05:17 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
4,
'李雷',
'lilei@example.cn',
'CN',
1,
'[]',
DATE '2023-05-09',
TIMESTAMP '2023-05-09 02:40:00 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
5,
'Noah O''Brien',
'noah@example.ie',
'IE',
1,
'[]',
NULL,
TIMESTAMP '2023-06-30 14:00:00 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
6,
'Amélie Laurent',
'amelie@example.fr',
'FR',
1,
'["vip"]',
DATE '2024-01-02',
TIMESTAMP '2024-01-02 09:30:00 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
7,
'Ravi Patel',
'ravi@example.in',
'IN',
0,
'[]',
DATE '2024-02-29',
TIMESTAMP '2024-02-29 23:59:59 +00:00'
);
INSERT INTO "CUSTOMERS" (
"ID",
"NAME",
"EMAIL",
"COUNTRY",
"IS_ACTIVE",
"TAGS",
"SIGNED_UP",
"CREATED_AT"
) VALUES (
8,
'Sofia Rossi 🌻',
'sofia@example.it',
'IT',
1,
'[]',
DATE '2024-04-12',
TIMESTAMP '2024-04-12 12:12:12 +00:00'
);
COMMIT;
END;
/
BEGIN
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
1,
1,
'shipped',
129.90,
3,
TIMESTAMP '2024-03-01 10:00:00',
TIMESTAMP '2024-03-02 16:30:00',
NULL,
'{"gift": false}',
'1 day 06:30:00'
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
2,
2,
'paid',
54.00,
1,
TIMESTAMP '2024-03-02 11:15:00',
NULL,
'Gift wrap 🎁',
'{"gift": true, "wrap": "red"}',
NULL
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
3,
1,
'cancelled',
12.50,
1,
TIMESTAMP '2024-03-03 09:45:00',
NULL,
'Customer changed their mind',
NULL,
NULL
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
4,
4,
'shipped',
980.00,
12,
TIMESTAMP '2024-03-05 02:10:00',
TIMESTAMP '2024-03-07 08:00:00',
'请小心轻放',
'{"fragile": true}',
'2 days 05:50:00'
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
5,
5,
'pending',
45.75,
2,
TIMESTAMP '2024-03-08 17:20:00',
NULL,
'Leave at the door; ring twice',
NULL,
NULL
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
6,
6,
'shipped',
310.40,
5,
TIMESTAMP '2024-03-10 13:00:00',
TIMESTAMP '2024-03-11 09:00:00',
'Fragile: "glass"',
'{"fragile": true}',
'20:00:00'
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
7,
8,
'paid',
76.20,
2,
TIMESTAMP '2024-03-11 18:45:00',
NULL,
NULL,
NULL,
NULL
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
8,
2,
'shipped',
19.99,
1,
TIMESTAMP '2024-03-12 08:00:00',
NULL,
'Shipped date set to infinity by an old import',
NULL,
NULL
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
9,
3,
'shipped',
64.00,
2,
TIMESTAMP '2024-03-14 15:30:00',
TIMESTAMP '2024-03-15 10:00:00',
'Two parcels',
'{"parcels": [1, 2]}',
'18:30:00'
);
INSERT INTO "ORDERS" (
"ID",
"CUSTOMER_ID",
"STATUS",
"TOTAL",
"ITEMS",
"ORDERED_AT",
"SHIPPED_AT",
"NOTE",
"DETAILS",
"DELIVERY"
) VALUES (
10,
7,
'paid',
1499.00,
1,
TIMESTAMP '2024-03-15 21:05:00',
NULL,
'C:\Users\ravi\invoice.pdf',
NULL,
NULL
);
COMMIT;
END;
/
-- Keys, indexes, foreign keys, counters and triggers, after the rows.
ALTER TABLE "CUSTOMERS" ADD CONSTRAINT "UQ_CUSTOMERS_CUSTOMERS_EMAIL_KEY" UNIQUE ("EMAIL");
ALTER TABLE "CUSTOMERS" ADD CONSTRAINT "PK_CUSTOMERS" PRIMARY KEY ("ID");
ALTER TABLE "ORDERS" ADD CONSTRAINT "PK_ORDERS" PRIMARY KEY ("ID");
CREATE INDEX "IX_ORDERS_IDX_ORDERS_CUSTOMER" ON "ORDERS" ("CUSTOMER_ID");
CREATE INDEX "IX_ORDERS_IDX_ORDERS_STATUS_DATE" ON "ORDERS" ("STATUS", "ORDERED_AT");
ALTER TABLE "ORDERS" ADD CONSTRAINT "ORDERS_CUSTOMER_ID_FKEY" FOREIGN KEY ("CUSTOMER_ID") REFERENCES "CUSTOMERS" ("ID") ENABLE NOVALIDATE;
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE "ORDERS" ENABLE VALIDATE CONSTRAINT "ORDERS_CUSTOMER_ID_FKEY"';
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Foreign key ' || '"ORDERS_CUSTOMER_ID_FKEY"' || ' has rows without a parent: it stays unvalidated, and new rows are checked.');
END;
/
DECLARE
next_id NUMBER;
BEGIN
SELECT NVL(MAX("ID"), 0) + 1 INTO next_id FROM "CUSTOMERS";
IF next_id < 1001 THEN
next_id := 1001;
END IF;
EXECUTE IMMEDIATE 'ALTER TABLE "CUSTOMERS" MODIFY ("ID" GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH ' || next_id || '))';
END;
/
COMMENT ON TABLE "CUSTOMERS" IS 'People who bought at least once';
DECLARE
next_id NUMBER;
BEGIN
SELECT NVL(MAX("ID"), 0) + 1 INTO next_id FROM "ORDERS";
IF next_id < 5001 THEN
next_id := 5001;
END IF;
EXECUTE IMMEDIATE 'ALTER TABLE "ORDERS" MODIFY ("ID" GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH ' || next_id || '))';
END;
/
DROP FUNCTION "INTOSQL_CLOB";
DROP FUNCTION "INTOSQL_BLOB";
COMMIT;
EXIT;
修正
- customers 的 "tags" 是 text[],Oracle 没有该类型,因此为 CLOB,值保持不变。
- orders 的 "delivery" 是 interval,Oracle 没有该类型,因此为 VARCHAR2(100 CHAR),值保持不变。
需要检查
- 1 个视图、触发器、例程或事件未转换;报告逐一列出了它们。 请在 Oracle 中手动重新创建它们。
- customers 中 "tags" 的默认值无法为 Oracle 写出(a value its type can't hold),因此该列没有默认值。 如果新行需要,请手动添加默认值。
- orders 上的索引 idx_orders_details 使用了无法为 Oracle 写出的表达式(a gin index),因此未创建。 如果查询需要,请手动创建该索引。
- orders 的 "shipped_at" 中有 1 个值是 Oracle 无法存储的日期,例如 infinity,因此设为 NULL。 Oracle 存储 1 到 9999 年,不支持无穷值;如果这些日期很重要,请在源数据中替换它们。
文件在这台电脑上转换,永远不会上传。
其他示例:
从 PostgreSQL 到 Oracle 的变化
每一行是转换器应用的一条规则,以及该规则更改你的数据时在报告中写入的记录。
boolean
NUMBER(1),并带有限定 0 和 1 的 CHECK
Oracle 21c 没有布尔列类型;1 和 0 在每个版本上都能表示 true 和 false。
varchar(n), char(n)
VARCHAR2(n CHAR),按字符计数
长度像 PostgreSQL 一样按字符计数。
text
所有值都不超过 4000 字节时为 VARCHAR2(4000 CHAR)(键中为 750 个字符),否则为 CLOB
VARCHAR2 默认容纳 4000 字节,而 Oracle 无法为 CLOB 建立索引,因此值能容纳时文本保持为 VARCHAR2,否则为 CLOB。
报告记录: text_as_lob
empty strings and empty bytea values
NULL
Oracle 将空字符串存储为 NULL,因此原为 ' 的值读回时为 NULL。报告会按列统计它们。
报告记录: empty_string_null
citext
精确比较
除非另有指定,Oracle 会像 PostgreSQL 的文本一样精确比较文本;citext 列不再忽略大小写。
报告记录: case_insensitive_collation
numeric(p,s) past 38 digits; numeric without a precision
值不超过 38 位时为 NUMBER,否则为 VARCHAR2
Oracle 的 NUMBER 在任何小数位数下都可容纳 38 位数字。更宽的数字保存为文本,使每一位都保持精确。
报告记录: decimal_as_text
money
NUMBER(19,2)
Oracle 没有 money 类型。金额保存为数字,不带服务器区域设置添加的货币符号。
报告记录: type_replaced
interval
VARCHAR2(100 CHAR),存放 PostgreSQL 的文本
interval 混合了月与天和秒,Oracle 将它们存在两种不同的类型中;文本保留每个部分。
报告记录: type_replaced
arrays (integer[], text[]...)
存放 JSON 数组的 CLOB
Oracle 没有数组。JSON 数组保留每个元素和 NULL,JSON_TABLE 可以读取它。
报告记录: type_replaced
ranges, geometric types, tsvector, xml, hstore, PostGIS, composite and extension types
VARCHAR2 或 CLOB,存放 PostgreSQL 的文本
Oracle 没有对应的类型,因此每个值保留 PostgreSQL 写出的文本。
报告记录: type_replaced
json, jsonb
CLOB
Oracle 的 JSON 函数在每个版本上都能从文本读取 JSON,而 CLOB 能保存任意大小的值。
timestamp
TIMESTAMP
不带时区的 timestamp 是挂钟读数,与 Oracle 的 TIMESTAMP 相同。date 变为 DATE,它也包含一天中的时刻,保持为午夜。
timestamp with time zone
UTC 的 TIMESTAMP WITH TIME ZONE
每个值以 UTC 写入,无论转储写入的偏移量是多少,都是同一时刻。
time
从午夜起的 INTERVAL DAY TO SECOND
Oracle 没有表示一天中时刻的类型;从午夜起算的 interval 能容纳每个值,包括 24:00:00。
infinity, -infinity, BC dates and years past 9999
NULL
脚本写入 1 到 9999 年,而 Oracle 没有无穷值。这些值变为 NULL,报告会统计其数量。
报告记录: date_out_of_range
NaN and Infinity in numeric
NULL
Oracle 的 NUMBER 没有 NaN 或 Infinity,因此这些值变为 NULL,报告会统计其数量。
报告记录: invalid_value_null
NOT NULL columns with values that become NULL
去除 NOT NULL
值必须变为 NULL 的列无法保留 NOT NULL,否则导入会中止。
报告记录: not_null_relaxed
enum types
VARCHAR2,并带有限定其标签的 CHECK
带检查约束的文本保留相同的值。
enum columns holding values outside their labels
不加检查约束的 VARCHAR2
标签之外的值会使检查失败,因此不创建检查约束。
报告记录: enum_check_skipped
uuid, inet, cidr, macaddr
VARCHAR2
Oracle 没有这些类型;文本保留每个值。
bytea
BLOB
字节仍然是字节。
bit(n), bit varying
不超过 63 位时为 NUMBER,值为各位组成的数;更长则为文本数字
位串存储为其各位组成的数字。
oid, xid, cid
NUMBER(10),并带有不能为负数的 CHECK
NUMBER 可容纳每个值。
serial, identity
GENERATED BY DEFAULT ON NULL AS IDENTITY,从最大的 id 之后继续
复制过来的 id 按原样导入,计数器从最大的 id 或序列的下一个值之后继续,取两者中较大者。
a second serial or identity column in a table
普通整数
Oracle 每个表只有一个标识列计数;其他列保留其值,不带计数器。
报告记录: counter_dropped
sequences not owned by a column
CREATE SEQUENCE,从它已达到的值开始
Oracle 序列的工作方式相同。
generated columns
虚拟列
Oracle 只在读取时计算生成列;STORED 列变为值相同的虚拟列。
报告记录: stored_as_virtual
generated columns with functions Oracle lacks
带有这些值的普通列
无法为 Oracle 写出的公式会留下一个带有转储中值的普通列和一条报告记录。
报告记录: generated_not_translated
defaults with functions Oracle lacks, and defaults from a sequence
无默认值
无法为 Oracle 写出的默认值会被省略,并在报告中列出。
报告记录: default_not_translated
CHECK constraints
已转换,内联在 CREATE TABLE 中
内联的检查约束在每行导入时检验。
CHECK constraints with functions Oracle lacks
不创建
无法为 Oracle 写出的检查约束会被省略,并在报告中列出。
报告记录: check_skipped
CHECK ... NOT VALID
不创建
PostgreSQL 从未检查之前已有的行,因此它们可能违反该约束。
报告记录: check_not_valid
gin, gist, brin and hash indexes, partial and expression indexes, and indexes on CLOB columns
不创建
IntoSQL 为 Oracle 在列上写出 b-tree 索引,而 Oracle 无法为 CLOB 建立索引;其他索引请在导入后重新创建。
报告记录: index_skipped
foreign keys
在数据之后以 ENABLE NOVALIDATE 添加,然后验证,有孤立行时给出警告
外键立即检查新行,最后再验证;有孤立行时外键保持未验证状态并给出警告,而不是中止导入。
ON UPDATE CASCADE, SET NULL or SET DEFAULT; ON DELETE SET DEFAULT
去除
Oracle 的外键只在删除时执行操作,可用 CASCADE 或 SET NULL。
报告记录: foreign_key_action_unsupported
foreign keys to tables the dump doesn't create
不创建
外键无法引用不存在的表。
报告记录: foreign_key_skipped
names over 128 bytes
缩短
Oracle 12.2 及更高版本的名称最多 128 字节,多于 PostgreSQL 的 63 字节。
报告记录: name_shortened
partitioned tables
单个表,包含每个分区的行
行保持不变;如有需要,请在 Oracle 中重新对该表分区。
报告记录: partitions_dropped
schemas
单个 schema,每个表名前加上其 schema 名
脚本导入到运行它的用户的 schema 中,因此来自不同 schema 的表名前会加上各自的 schema 名;public 中的表保留原名。
报告记录: schemas_merged
views, functions, procedures, triggers, rules, policies, extensions and types
在报告中列出
它们的代码用 PostgreSQL 的语言编写,因此会列出来,由你重新创建。
报告记录: not_converted_objects
dumps without CREATE TABLE (--data-only)
根据值推断类型
只含数据的转储只有行,因此类型来自值。
报告记录: no_schema
expressions in INSERT values
NULL
不运行数据库的转换器无法对它们求值。
报告记录: expression_value
types IntoSQL doesn't know
CLOB
文本可以容纳任何值;请在导入后更改类型。
报告记录: unknown_type
text that isn't valid in its encoding
错误的字节变为替换字符
脚本为 UTF-8,使用 NLS_LANG=.AL32UTF8 导入。
报告记录: invalid_utf8_replaced
text decoded twice (ñ)
按原样复制
转储中存储的正是这些字符,因此按原样复制;报告会指出它们。
报告记录: mojibake_suspected
| 在 PostgreSQL 中 | 在 Oracle 中 | 报告记录 |
|---|---|---|
| boolean | NUMBER(1),并带有限定 0 和 1 的 CHECKOracle 21c 没有布尔列类型;1 和 0 在每个版本上都能表示 true 和 false。 | |
| varchar(n), char(n) | VARCHAR2(n CHAR),按字符计数长度像 PostgreSQL 一样按字符计数。 | |
| text | 所有值都不超过 4000 字节时为 VARCHAR2(4000 CHAR)(键中为 750 个字符),否则为 CLOBVARCHAR2 默认容纳 4000 字节,而 Oracle 无法为 CLOB 建立索引,因此值能容纳时文本保持为 VARCHAR2,否则为 CLOB。 | text_as_lob |
| empty strings and empty bytea values | NULLOracle 将空字符串存储为 NULL,因此原为 ' 的值读回时为 NULL。报告会按列统计它们。 | empty_string_null |
| citext | 精确比较除非另有指定,Oracle 会像 PostgreSQL 的文本一样精确比较文本;citext 列不再忽略大小写。 | case_insensitive_collation |
| numeric(p,s) past 38 digits; numeric without a precision | 值不超过 38 位时为 NUMBER,否则为 VARCHAR2Oracle 的 NUMBER 在任何小数位数下都可容纳 38 位数字。更宽的数字保存为文本,使每一位都保持精确。 | decimal_as_text |
| money | NUMBER(19,2)Oracle 没有 money 类型。金额保存为数字,不带服务器区域设置添加的货币符号。 | type_replaced |
| interval | VARCHAR2(100 CHAR),存放 PostgreSQL 的文本interval 混合了月与天和秒,Oracle 将它们存在两种不同的类型中;文本保留每个部分。 | type_replaced |
| arrays (integer[], text[]...) | 存放 JSON 数组的 CLOBOracle 没有数组。JSON 数组保留每个元素和 NULL,JSON_TABLE 可以读取它。 | type_replaced |
| ranges, geometric types, tsvector, xml, hstore, PostGIS, composite and extension types | VARCHAR2 或 CLOB,存放 PostgreSQL 的文本Oracle 没有对应的类型,因此每个值保留 PostgreSQL 写出的文本。 | type_replaced |
| json, jsonb | CLOBOracle 的 JSON 函数在每个版本上都能从文本读取 JSON,而 CLOB 能保存任意大小的值。 | |
| timestamp | TIMESTAMP不带时区的 timestamp 是挂钟读数,与 Oracle 的 TIMESTAMP 相同。date 变为 DATE,它也包含一天中的时刻,保持为午夜。 | |
| timestamp with time zone | UTC 的 TIMESTAMP WITH TIME ZONE每个值以 UTC 写入,无论转储写入的偏移量是多少,都是同一时刻。 | |
| time | 从午夜起的 INTERVAL DAY TO SECONDOracle 没有表示一天中时刻的类型;从午夜起算的 interval 能容纳每个值,包括 24:00:00。 | |
| infinity, -infinity, BC dates and years past 9999 | NULL脚本写入 1 到 9999 年,而 Oracle 没有无穷值。这些值变为 NULL,报告会统计其数量。 | date_out_of_range |
| NaN and Infinity in numeric | NULLOracle 的 NUMBER 没有 NaN 或 Infinity,因此这些值变为 NULL,报告会统计其数量。 | invalid_value_null |
| NOT NULL columns with values that become NULL | 去除 NOT NULL值必须变为 NULL 的列无法保留 NOT NULL,否则导入会中止。 | not_null_relaxed |
| enum types | VARCHAR2,并带有限定其标签的 CHECK带检查约束的文本保留相同的值。 | |
| enum columns holding values outside their labels | 不加检查约束的 VARCHAR2标签之外的值会使检查失败,因此不创建检查约束。 | enum_check_skipped |
| uuid, inet, cidr, macaddr | VARCHAR2Oracle 没有这些类型;文本保留每个值。 | |
| bytea | BLOB字节仍然是字节。 | |
| bit(n), bit varying | 不超过 63 位时为 NUMBER,值为各位组成的数;更长则为文本数字位串存储为其各位组成的数字。 | |
| oid, xid, cid | NUMBER(10),并带有不能为负数的 CHECKNUMBER 可容纳每个值。 | |
| serial, identity | GENERATED BY DEFAULT ON NULL AS IDENTITY,从最大的 id 之后继续复制过来的 id 按原样导入,计数器从最大的 id 或序列的下一个值之后继续,取两者中较大者。 | |
| a second serial or identity column in a table | 普通整数Oracle 每个表只有一个标识列计数;其他列保留其值,不带计数器。 | counter_dropped |
| sequences not owned by a column | CREATE SEQUENCE,从它已达到的值开始Oracle 序列的工作方式相同。 | |
| generated columns | 虚拟列Oracle 只在读取时计算生成列;STORED 列变为值相同的虚拟列。 | stored_as_virtual |
| generated columns with functions Oracle lacks | 带有这些值的普通列无法为 Oracle 写出的公式会留下一个带有转储中值的普通列和一条报告记录。 | generated_not_translated |
| defaults with functions Oracle lacks, and defaults from a sequence | 无默认值无法为 Oracle 写出的默认值会被省略,并在报告中列出。 | default_not_translated |
| CHECK constraints | 已转换,内联在 CREATE TABLE 中内联的检查约束在每行导入时检验。 | |
| CHECK constraints with functions Oracle lacks | 不创建无法为 Oracle 写出的检查约束会被省略,并在报告中列出。 | check_skipped |
| CHECK ... NOT VALID | 不创建PostgreSQL 从未检查之前已有的行,因此它们可能违反该约束。 | check_not_valid |
| gin, gist, brin and hash indexes, partial and expression indexes, and indexes on CLOB columns | 不创建IntoSQL 为 Oracle 在列上写出 b-tree 索引,而 Oracle 无法为 CLOB 建立索引;其他索引请在导入后重新创建。 | index_skipped |
| foreign keys | 在数据之后以 ENABLE NOVALIDATE 添加,然后验证,有孤立行时给出警告外键立即检查新行,最后再验证;有孤立行时外键保持未验证状态并给出警告,而不是中止导入。 | |
| ON UPDATE CASCADE, SET NULL or SET DEFAULT; ON DELETE SET DEFAULT | 去除Oracle 的外键只在删除时执行操作,可用 CASCADE 或 SET NULL。 | foreign_key_action_unsupported |
| foreign keys to tables the dump doesn't create | 不创建外键无法引用不存在的表。 | foreign_key_skipped |
| names over 128 bytes | 缩短Oracle 12.2 及更高版本的名称最多 128 字节,多于 PostgreSQL 的 63 字节。 | name_shortened |
| partitioned tables | 单个表,包含每个分区的行行保持不变;如有需要,请在 Oracle 中重新对该表分区。 | partitions_dropped |
| schemas | 单个 schema,每个表名前加上其 schema 名脚本导入到运行它的用户的 schema 中,因此来自不同 schema 的表名前会加上各自的 schema 名;public 中的表保留原名。 | schemas_merged |
| views, functions, procedures, triggers, rules, policies, extensions and types | 在报告中列出它们的代码用 PostgreSQL 的语言编写,因此会列出来,由你重新创建。 | not_converted_objects |
| dumps without CREATE TABLE (--data-only) | 根据值推断类型只含数据的转储只有行,因此类型来自值。 | no_schema |
| expressions in INSERT values | NULL不运行数据库的转换器无法对它们求值。 | expression_value |
| types IntoSQL doesn't know | CLOB文本可以容纳任何值;请在导入后更改类型。 | unknown_type |
| text that isn't valid in its encoding | 错误的字节变为替换字符脚本为 UTF-8,使用 NLS_LANG=.AL32UTF8 导入。 | invalid_utf8_replaced |
| text decoded twice (ñ) | 按原样复制转储中存储的正是这些字符,因此按原样复制;报告会指出它们。 | mojibake_suspected |
将脚本导入 Oracle
脚本先创建表、复制行,再添加键、索引和外键,因此导入速度快,并会在遇到第一个错误时停止。
NLS_LANG=.AL32UTF8 sqlplus -S -L user@//localhost/mydb @postgresql.oracle.sql外键以 ENABLE NOVALIDATE 添加,因此新行会立即检查,之后再验证。如果某个旧行指向不存在的行,脚本会说明是哪个约束,并让它保持未验证状态,而不是中途失败。
对于较大的转储,请在数据库所在的机器上运行 SQL*Plus。脚本每 100 行提交一次。运行脚本的用户需要 CREATE TABLE、CREATE SEQUENCE、CREATE TRIGGER 和 CREATE PROCEDURE 权限,以及其表空间上的配额。
如何转储 PostgreSQL 以获得最佳结果
转换器读取转储中的内容。以下选项能让转储保留更多数据:
- --format=plain 是 pg_dump 的默认格式,写出 SQL。custom 或 directory 归档(-Fc、-Fd)可用 pg_restore -f dump.sql 转为 SQL,无需数据库服务器。
- --no-owner --no-privileges 会省略角色和授权,转换器本来也会跳过它们,因此转储更小。
- pg_dumpall 将每个数据库转储到一个文件中;每个数据库的表名前会加上该数据库名,因此不会冲突。
哪些内容不会转换
视图、物化视图、函数、存储过程、触发器、规则、行安全策略、扩展和类型都是用 PostgreSQL 自己的语言编写的。它们会按名称在报告中列出,绝不会被悄悄丢弃,以便你为 Oracle 重写。
关于 PostgreSQL 转 Oracle 的问题
这里没有你要的答案?请通过联系页面给我们留言,会有专人回复。 联系页面
我的转储会被上传吗?
不会。转换器在你的浏览器中运行,分块从磁盘读取转储,因此转储不会离开你的电脑。桌面应用和命令行也在你的电脑上转换。
转储可以多大?
转换器从不把整个转储放进内存:它按范围读取文件两次,边读边写出脚本。几 GB 的转储可以在浏览器、桌面应用和命令行中转换。
它能读取 pg_dump -Fc 归档吗?
不能直接读取:custom 归档是二进制格式。pg_restore -f dump.sql dump.backup 会将其转为转换器读取的纯 SQL,无需连接服务器。转换器收到归档时会给出提示。
转储可以来自哪些 PostgreSQL 版本?
由 PostgreSQL 10 及更高版本的 pg_dump 和 pg_dumpall 写出的转储,行可以是 COPY 或 INSERT。每个测试转储都来自 PostgreSQL 13 和 18,并与其源数据逐个单元格比较。
serial 和标识列的值会保留吗?
会。每个表的 serial 或标识列变为标识列,每行保留其 id,下一个 id 从最大的 id 之后继续。不属于任何列的序列变为 Oracle 序列,从它们已达到的值开始。
数组、enum 和其他 PostgreSQL 类型会怎样处理?
数组变为 JSON 数组,布尔值变为 NUMBER(1),带时区的时间戳变为 TIMESTAMP WITH TIME ZONE,文本在值能容纳时为 VARCHAR2,否则为 CLOB。空字符串变为 NULL,因为 Oracle 就是这样存储它们的,报告会统计其数量。
函数、触发器和视图怎么处理?
它们会按名称在报告中列出,但不会转换,因为每种数据库都用自己的语言编写它们。
脚本可以导入哪些 Oracle 版本?
Oracle 19c 及更高版本。每个测试转储都能在 21c 和 23ai 上导入,如有问题会在第一个错误处停止。