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" 열은 Oracle에 없는 text[](이)라서 같은 값을 담은 CLOB(으)로 만들었어요.
- orders의 "delivery" 열은 Oracle에 없는 interval(이)라서 같은 값을 담은 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개 값은 infinity처럼 Oracle이(가) 저장할 수 없는 날짜라서 NULL로 두었어요. Oracle은(는) 1년부터 9999년까지만 저장하고 infinity는 없어요. 이 날짜가 중요하면 원본에서 바꾸세요.
파일은 이 컴퓨터에서 변환되고 업로드되지 않아요.
다른 샘플:
PostgreSQL에서 Oracle로 바뀌는 점
각 행은 변환기가 적용하는 규칙이며, 규칙이 데이터를 바꿀 때 보고서에 쓰는 줄이 함께 나와요.
boolean
0과 1의 CHECK가 붙은 NUMBER(1)
Oracle 21c에는 boolean 열 타입이 없어요. 1과 0은 모든 버전에서 true와 false를 유지해요.
varchar(n), char(n)
VARCHAR2(n CHAR), 문자 수로 셈
길이는 PostgreSQL처럼 문자 수로 세요.
text
모든 값이 4000바이트(키에서는 750자)에 들어가면 VARCHAR2(4000 CHAR), 아니면 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
PostgreSQL의 텍스트를 담은 VARCHAR2(100 CHAR)
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
PostgreSQL의 텍스트를 담은 VARCHAR2 또는 CLOB
Oracle에는 같은 타입이 없어서 각 값은 PostgreSQL이 쓴 텍스트를 유지해요.
보고서 줄: type_replaced
json, jsonb
CLOB
Oracle의 JSON 함수는 모든 버전에서 텍스트에서 JSON을 읽고, CLOB은 어떤 크기의 값이든 담아요.
timestamp
TIMESTAMP
timestamp without time zone은 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에는 infinity가 없어요. 이 값은 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
레이블 CHECK가 붙은 VARCHAR2
체크 제약이 있는 텍스트는 같은 값을 유지해요.
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
음수가 아닌지 확인하는 CHECK가 붙은 NUMBER(10)
NUMBER는 모든 값을 담아요.
serial, identity
GENERATED BY DEFAULT ON NULL AS IDENTITY, 가장 큰 id 다음부터 이어짐
복사된 id는 그대로 불러오고, 카운터는 가장 큰 id와 시퀀스의 다음 값 중 더 큰 값 다음부터 이어져요.
a second serial or identity column in a table
일반 정수
Oracle은 테이블마다 identity 열 하나만 번호를 매겨요. 다른 열은 카운터 없이 값을 유지해요.
보고서 줄: 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
하나의 스키마, 테이블 이름 앞에 스키마 이름
스크립트는 실행하는 사용자의 스키마에 불러오므로, 다른 스키마의 테이블은 이름 앞에 스키마 이름이 붙어요. 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 | 0과 1의 CHECK가 붙은 NUMBER(1)Oracle 21c에는 boolean 열 타입이 없어요. 1과 0은 모든 버전에서 true와 false를 유지해요. | |
| varchar(n), char(n) | VARCHAR2(n CHAR), 문자 수로 셈길이는 PostgreSQL처럼 문자 수로 세요. | |
| text | 모든 값이 4000바이트(키에서는 750자)에 들어가면 VARCHAR2(4000 CHAR), 아니면 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 | PostgreSQL의 텍스트를 담은 VARCHAR2(100 CHAR)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 | PostgreSQL의 텍스트를 담은 VARCHAR2 또는 CLOBOracle에는 같은 타입이 없어서 각 값은 PostgreSQL이 쓴 텍스트를 유지해요. | type_replaced |
| json, jsonb | CLOBOracle의 JSON 함수는 모든 버전에서 텍스트에서 JSON을 읽고, CLOB은 어떤 크기의 값이든 담아요. | |
| timestamp | TIMESTAMPtimestamp without time zone은 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에는 infinity가 없어요. 이 값은 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 | 레이블 CHECK가 붙은 VARCHAR2체크 제약이 있는 텍스트는 같은 값을 유지해요. | |
| 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 | 음수가 아닌지 확인하는 CHECK가 붙은 NUMBER(10)NUMBER는 모든 값을 담아요. | |
| serial, identity | GENERATED BY DEFAULT ON NULL AS IDENTITY, 가장 큰 id 다음부터 이어짐복사된 id는 그대로 불러오고, 카운터는 가장 큰 id와 시퀀스의 다음 값 중 더 큰 값 다음부터 이어져요. | |
| a second serial or identity column in a table | 일반 정수Oracle은 테이블마다 identity 열 하나만 번호를 매겨요. 다른 열은 카운터 없이 값을 유지해요. | counter_dropped |
| sequences not owned by a column | 도달한 값부터 시작하는 CREATE SEQUENCEOracle 시퀀스도 같은 방식으로 작동해요. | |
| 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 | 하나의 스키마, 테이블 이름 앞에 스키마 이름스크립트는 실행하는 사용자의 스키마에 불러오므로, 다른 스키마의 테이블은 이름 앞에 스키마 이름이 붙어요. 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 덤프 방법
변환기는 덤프에 담긴 내용을 읽어요. 다음 옵션을 쓰면 더 많은 데이터가 덤프에 남아요:
- pg_dump의 기본값인 --format=plain은 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과 identity 값은 유지되나요?
네. 각 테이블의 serial 또는 identity 열은 identity 열이 되고, 모든 행은 id를 유지하며, 다음 id는 가장 큰 id 다음부터 이어져요. 열에 속하지 않은 시퀀스는 도달한 값에서 Oracle 시퀀스가 돼요.
배열, enum 등 PostgreSQL 타입은 어떻게 되나요?
배열은 JSON 배열, boolean은 NUMBER(1), 시간대가 있는 timestamp는 TIMESTAMP WITH TIME ZONE, 텍스트는 값이 들어가면 VARCHAR2, 아니면 CLOB이 돼요. 빈 문자열은 Oracle이(가) 저장하는 방식대로 NULL이 되고, 보고서에 개수가 나와요.
함수, 트리거, 뷰는 어떻게 되나요?
데이터베이스마다 자체 언어로 작성하므로 변환하지 않고 보고서에 이름으로 나열해요.
스크립트는 어떤 Oracle 버전에서 불러올 수 있나요?
Oracle 19c 이상이에요. 모든 테스트 덤프를 21c와 23ai에서 불러오며, 문제가 생기면 첫 오류에서 멈춰요.