【OCI クラウド移行ガイド】 Amazon RDS for PostgreSQLから OCI Autonomous Database へ移行してみた

# OCIクラウド移行ガイドとは
オンプレミスやAWSなど、複数のプラットフォームからOracle Cloud Infrastructureへの移行プロジェクトに取り組んでいるクラウドエンジニア（@araidon,@kazunishi,@yama6,@tktk2712,@ritokuna,@nomu_kyou）による、OCI移行手順をまとめたシリーズ記事です。
各回、サンプルワークロードから対象サービスを取り上げ、移行手順をガイドいたします。
まとめ記事は以下になります。

https://qiita.com/yama6/items/b197c0fe3ec75eb02637

# 移行するサービス：RDS for PostgreSQL

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/6464dee5-e8f6-ba6c-8bb6-e2199287bf3b.png)

今回、移行対象とするのはAmazon RDS for PostgreSQLです。
Autonomous Databaseで利用可能なDBMS_CLOUD_MIGRATIONパッケージを用いて、Amazon RDS for PostgreSQLをAutonomous Databaseに移行する手順を解説します。

[DBMS_CLOUD_MIGRATIONパッケージ](https://docs.public.oneportal.content.oci.oraclecloud.com/ja-jp/iaas/autonomous-database-serverless/doc/migrate-postgresql-to-oraclesql.html)


DBMS_CLOUD_MIGRATIONパッケージを使用すると、PostgreSQLからOracle SQLへのSQLコードの変換が容易になります。
このパッケージからはいくつかのプロシージャが提供されています。今回は、その中でもDBMS_CLOUD_MIGRATION.MIGRATE_FILEプロシージャを使用します。DBMS_CLOUD_MIGRATION.MIGRATE_FILEプロシージャは、オブジェクト・ストレージのPostgreSQLファイル内のSQL文を変換し、Oracle SQLを含む新しいファイルを生成します。

:::note info
一言で言うと、RDS for PostgreSQLでDumpしたPostgreSQLの.sqlファイルをOracleの.sqlファイルに変換してくれるという代物です。
:::

[DBMS_CLOUD_MIGRATION.MIGRATE_FILEプロシージャ](https://docs.public.oneportal.content.oci.oraclecloud.com/ja-jp/iaas/autonomous-database-serverless/doc/migrate-postgresql-to-oraclesql.html#GUID-1A4497B4-2A9B-4576-803F-149498EEDB35)

このプロシージャにはDBエンジン差異による一部制約があるため、生成されたファイルに記載されたSQL文を実行し、DBMS_CLOUD_MIGRATION.MIGRATE_FILEプロシージャで変換されたSQL文の整合性を検証します。

[Oracle以外のSQLからOracle SQLへの移行および変換の制限](https://docs.public.oneportal.content.oci.oraclecloud.com/ja-jp/iaas/autonomous-database-serverless/doc/migrate-postgresql-to-oraclesql.html#GUID-9C183556-8FC0-4B08-9994-65CFAA08048E)

# 検証データ

移行対象とするサンプルデータとして、PostgreSQL Tutorialが提供するDVDレンタルデータベースを使用します。

https://neon.tech/postgresql/postgresql-getting-started/postgresql-sample-database

このデータベースには下記オブジェクトが含まれています。
|No|オブジェクト |数量
|---|---|---
|1|sequences|13
|2|tables|15
|3|trigger|15
|4|views|7
|5|functions|9
|6|domain|1
|-|SUM|60

# 検証 Summary
検証手順が長くなったため、先に検証結果を記載します。
|No|オブジェクト |全数量|改修数量|結果|コメント
|---|---|---|---|---|---
|1|sequences|13|0|🙆‍♂️|---
|2|tables|15|2|⚠️|address.phone,film.fulltext 一部改修
|3|trigger|15|1|⚠️|film_fulltext_trigger 書き直し
|4|views|7|2|⚠️|customer_list,nicer_but_slower_film_list 一部改修
|5|functions|9|9|❌|GET_CUSTOMER_BALANCE,REWARDS_REPORT 未対応
|6|domain|1|1|❌|domainに直接対応するオブジェクトがないため、OracleDBのtypeで実現

全60オブジェクトのうち、75%にあたる45オブジェクトについては変換されたSQL文のまま実行することができましたが、残り25%のオブジェクトについては改修もしくは1から書き直しが必要となりました。
functionsについては9本中、9本が書き直しとなりました。
うち、の2つについてはサンプルデータ起因でPostgreSQL側でもうまく動作確認ができなかったため改修検証の対象外としました。

# 前提条件
## DBバージョン
RDS for PostgreSQL：14.11
Autonomous Database：19c

## SQLの実行について
### 実行方法
検証目的のため、Oracle SQLに変換された.sqlファイルから、該当部分のSQL文を逐一抜き出して実行しています。本番の移行方法で採択されなさそうな回りくどい方法となっている点、SQLの実行順がDumpfileの記載順となっていない点をご容赦ください。

### オブジェクトの作成順
オブジェクト作成の実行順は下記の通りです。
SEQUENCE → TYPE(domain) → TABLE → INDEX → TRIGGER → VIEW → FUNCTION

### スキーマ名の変更　"public"→"postgres"
サンプルデータのスキーマ名となっている"public"がプロシージャで変換されません。"public"はOracleの予約語のため、そのまま実行するとエラーが発生します。そのため、Oracle Databaseで"postgres"というユーザーを作成した上で、Dumpfileでpublicスキーマを指定している部分はpostgresに置換して実行しています。

# 移行手順
1. EC2作成
1. RDS for PostgreSQL作成
1. サンプルデータのインポート
1. サンプルデータのエクスポート
1. Object Storageの作成
1. Autonomous Databaseの作成
1. 資格証明の作成
1. DBMS_CLOUD_MIGRATION.MIGRATE_FILEプロシージャの実行
1. 変換されたSQL文の実行
    1. SEQUENCE
    1. TABLE
    1. INDEX
    1. TRIGGER
    1. VIEW
    1. TYPE
    1. FUNCTION
1. データロード
1. 動作検証

# 1. EC2作成
RDS操作用のインスタンスとして、Amazon Linux2023でEC2を作成します。
作成方法は下記の記事等を参考にしてください。

https://qiita.com/seikida/items/8a68ceabfd1342f636bd

EC2へはCloudShellから接続し、postgresql15をインストールしておきます。

```rb
$ sudo dnf install postgresql15
```

# 2. RDS for PostgreSQL作成

下記手順に概ね沿い、RDS for PostgreSQLを作成します。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/CHAP_GettingStarted.CreatingConnecting.PostgreSQL.html

EC2との接続オプションを選ぶと、自動的にセキュリティグループを設定してくれて便利ですね。
下記コマンドでEC2からRDSへ接続できれば成功です。
```rb
$ psql --host=test-postgres.xxxxxx.ap-northeast-1.rds.amazonaws.com --port=5432 --dbname=postgres --username=postgres
```

# 3. サンプルデータのインポート

下記サイトよりサンプルデータをダウンロードします。
今回の検証では、DVD レンタル データベースを使用します。
https://neon.tech/postgresql/postgresql-getting-started/postgresql-sample-database

一度ローカルPCにデータをダウンロードし、cloudShellにアップロードします。
scpコマンドを使用し、CloudShellからEC2へファイルを転送します。

```rb
$ scp -i posgre-test.pem dvdrental.zip ec2-user@XXX.XX.XXX.XXX:/home/ec2-user/
dvdrental.zip 100%  538KB  66.7MB/s   00:00
```
RDSへ接続し、データのインポート前にdvdrentalという名前のDatabaseを作ります。
```rb
$ psql --host=test-postgres.xxxxxx.ap-northeast-1.rds.amazonaws.com --port=5432 --dbname=postgres --username=postgres
postgres=>create database dvdrental;
```
exitコマンドでEC2に戻り、pg_restoreコマンドでzipファイルからDBを作成します。
```rb
$ pg_restore -U postgres -h test-postgres.xxxxxx.ap-northeast-1.rds.amazonaws.com -d dvdrental -Ft dvdrental.tar -v
```
再度RDSへ接続し、インポートデータを確認します。
```rb
$ psql --host=test-postgres.xxxxxx.ap-northeast-1.rds.amazonaws.com --port=5432 --dbname=postgres --username=postgres
postgres=> \c dvdrental
dvdrental=> dvdrental=#
dvdrental-> \dt
             List of relations
 Schema |     Name      | Type  |  Owner   
--------+---------------+-------+----------
 public | actor         | table | postgres
 public | address       | table | postgres
 public | category      | table | postgres
 public | city          | table | postgres
 public | country       | table | postgres
 public | customer      | table | postgres
 public | film          | table | postgres
 public | film_actor    | table | postgres
 public | film_category | table | postgres
 public | inventory     | table | postgres
 public | language      | table | postgres
 public | payment       | table | postgres
 public | rental        | table | postgres
 public | staff         | table | postgres
 public | store         | table | postgres
(15 rows)
```

# 4. サンプルデータのエクスポート

EC2上からpg_dumpコマンドで、メタデータをエクスポートします。

```rb
$ pg_dump -U postgres -h test-postgres.xxxxxx.ap-northeast-1.rds.amazonaws.com -s -E 'UTF8' -d dvdrental -f dvdrental_metadata.sql
```
データ本体は、のちにSQL Loaderでインポートします。
RDSに接続し、テーブル単位でcsvファイルに出力します。

```rb
dvdrental-> \copy ACTOR to ACTOR.csv with csv delimiter ',' force quote * null as '' header;
COPY 200
dvdrental-> \copy ADDRESS       to ADDRESS.csv      with csv delimiter ',' force quote * null as '' header;
COPY 603
dvdrental-> \copy CATEGORY      to CATEGORY.csv     with csv delimiter ',' force quote * null as '' header;
COPY 16
dvdrental-> \copy CITY          to CITY.csv         with csv delimiter ',' force quote * null as '' header;
COPY 600
dvdrental-> \copy COUNTRY       to COUNTRY.csv      with csv delimiter ',' force quote * null as '' header;
COPY 109
dvdrental-> \copy CUSTOMER      to CUSTOMER.csv     with csv delimiter ',' force quote * null as '' header;
COPY 599
dvdrental-> \copy FILM          to FILM.csv         with csv delimiter ',' force quote * null as '' header;
COPY 1000
dvdrental-> \copy FILM_ACTOR    to FILM_ACTOR.csv   with csv delimiter ',' force quote * null as '' header;
COPY 5462
dvdrental-> \copy FILM_CATEGORY to FILM_CATEGORY.csv with csv delimiter ',' force quote * null as '' header;
COPY 1000
dvdrental-> \copy INVENTORY     to INVENTORY.csv    with csv delimiter ',' force quote * null as '' header;
COPY 4581
dvdrental-> \copy LANGUAGE      to LANGUAGE.csv     with csv delimiter ',' force quote * null as '' header;
COPY 6
dvdrental-> \copy PAYMENT       to PAYMENT.csv      with csv delimiter ',' force quote * null as '' header;
COPY 14596
dvdrental-> \copy RENTAL        to RENTAL.csv       with csv delimiter ',' force quote * null as '' header;
COPY 16044
dvdrental-> \copy STAFF         to STAFF.csv        with csv delimiter ',' force quote * null as '' header;
COPY 2
dvdrental-> \copy STORE         to STORE.csv        with csv delimiter ',' force quote * null as '' header;
COPY 2
```
上記実行すると、EC2上にファイルが生成されます。
先ほどご紹介したscpコマンド等を使い、何らかの手段でこれらのエクスポートファイルをローカルPCに保存します。

# 5. Object Storageの作成
下記手順に沿い、Object Storageを作成します。

https://docs.oracle.com/ja-jp/iaas/Content/Object/Tasks/managingbuckets_topic-To_create_a_bucket.htm

4でエクスポートしたデータ群をアップロードします。


アップロード後、オブジェクト一覧右のボタンを押下し、「オブジェクト詳細」の表示を押下してこれをコピーします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/2aa83cde-f982-8b05-70d2-f80be12ca228.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/89095bf4-6dbe-522d-6e2c-74887a222b22.png)

のちのプロシージャ実行にて、オブジェクトのURIが必要となります。

# 6. Autonomous Databaseの作成

下記手順に従ってAutonomous Data Warehouseを作成します。

https://oracle-japan.github.io/ocitutorials/adb/adb101-provisioning/#2-adb%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86

ワークロード・タイプには、トランザクション・ワークロードを対象とするAutonomous Transaction Processingを選択します。

データベース・バージョンは19cを選択しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/386b8e0f-3b3b-12aa-4bd5-2b6358c969b1.png)

ネットワーク・アクセスは、「すべての場所からのセキュア・アクセス」を選択ました。

Autonomous Databaseの作成 ボタンを押下し、プロビジョニングの完了を確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/0c8986fb-b9ae-2ba5-4501-f6d9c8633d90.png)

Autonomous Databaseの作成作業は以上です。

## Autonomous Databaseへの接続
Autonomous Databaseの詳細 画面から「データベース接続」を選択し、クライアント資格証明(ウォレット)をダウンロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/63c5e02b-a455-a70d-321c-e2bbf1aba154.png)

ウォレット保存先ディレクトリとして"/network/admin"を作成した上で、これをcloudshellにアップロードして展開します。
```rb
$ mkdir -p ~/network/admin
```
TNS_ADMIN 環境変数にウォレット保存先のパスを設定します。
```
$ export TNS_ADMIN=~/network/admin
```
viコマンドなどを使用し、sqlnet.oraにこの環境変数を設定します。
```rb
WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY=$TNS_ADMIN)))
```
環境変数”ORACLE_HOME”を設定します。
```rb
$  export ORACLE_HOME=~
```
sqlplusから接続します。
```sql
$ sqlplus admin/<password>@adb4psql_tp
SQL*Plus: Release 19.0.0.0.0 - Production on Fri Dec 27 12:58:04 2024
Version 19.10.0.0.0

Copyright (c) 1982, 2021, Oracle.  All rights reserved.

Last Successful login time: Fri Dec 27 2024 07:28:03 +00:00

Connected to:
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.25.0.1.0

SQL> 
```

# 7. 資格証明の作成
5で作成したObject Storageへ、6で作成したAutonomous Databaseから操作を行うために権限を付与します。今回は、Object Storageへの操作権限を持つユーザーの認証トークンを使用して権限を付与しました。
リソース・プリンシパル等でも設定が可能です。詳しくは下記をご参照ください。

https://docs.oracle.com/cd//E83857_01/paas/autonomous-database/serverless/adbsb/resource-principal.html#GUID-E283804C-F266-4DFB-A9CF-B098A21E496A

## 認証トークンの取得
OCIコンソール画面右上のユーザーアイコンを押下し、「ユーザー設定」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/51a0af54-aec1-c5a5-9bb8-ca52953cafee.png)

画面遷移後、画面左下「リソース」のナビゲーションペインから「認証トークン」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/f8f0824d-ee9b-64a8-7709-76ee8f706c4a.png)

トークンの作成ボタンを押下し、説明を記述後、「トークンの作成」ボタンを押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/592c9c88-aff8-bcc6-d880-f27fdafbd1a6.png)

遷移後の画面で表示される認証トークンの文字列をコピーします。

## 認証トークンの設定
ユーザーの認証トークンをAutonomous Databaseに設定します。
権限付与には、DBMS_CLOUDパッケージのCREATE_CREDENTIALプロシージャを使用します。

```sql
SQL> BEGIN DBMS_CLOUD.CREATE_CREDENTIAL( 
 credential_name => 'OCI_NATIVE_CRED', 
 username => '<ユーザー名>', 
 password => '<認証トークンの文字列>'
 );
 END;
/
PL/SQL procedure successfully completed.
```
これでAutonomous DatabaseにObject Storageへの操作権限が付与されました。
6で作成したバケットのURIを指定して、下記SQL文を実行した結果、アップロードしたファイル名が表示されれば権限付与は成功です。

```sql
SQL> SELECT * FROM DBMS_CLOUD.LIST_OBJECTS(
NULL, 
'https://objectstorage.ap-tokyo-1.oraclecloud.com/n/<NameSpace名>/b/<Bucket名>/o/'
);
/
```

オブジェクトストレージへの DBMS_CLOUD アクセスのトラブルシューティングには下記の記事をご参照ください。

https://blogs.oracle.com/datawarehousing/post/troubleshooting-dbmscloud-access-to-object-storage

# 8. DBMS_CLOUD_MIGRATION.MIGRATE_FILEプロシージャの実行

本題である、PostgreSQLからOracleへSQLを変換するMIGRATE_FILEプロシージャを実行します。
実行前のObject Storageは、先ほどアップロードした"dvdrental_metadata.sql"のみ格納されている状態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/28306dd5-6d50-eb49-9d82-7fde4cdf0930.png)

6で取得したURIをlocation_uriに指定し、下記のようにプロシージャを実行します。
```sql
SQL> BEGIN DBMS_CLOUD_MIGRATION.MIGRATE_FILE ( 
 credential_name => 'OCI_NATIVE_CRED', 
 location_uri => 'https://objectstorage.ap-tokyo-1.oraclecloud.com/n/<NameSpace名>/b/<Bucket名>/o/dvdrental_metadata.sql', 
 source_db => 'POSTGRES'
 ); 
 END;
 /
 PL/SQL procedure successfully completed.
```
実行後、"dvdrental_metadata_oracle.sql"というファイルが追加されていることがわかります。
これがMIGRATE_FILEプロシージャによりOracle SQLへ変換された.sqlファイルとなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/a2752056-b50f-8825-9fd2-7d5f0c57492c.png)

このファイルに記載されたSQL文を実行し、Autonomous Database上でデータベースを構築します。


# 9. 変換されたSQL文の実行

出力されたSQL文から、下記順でオブジェクトを作成していきます。
SEQUENCE → TYPE(domain) → TABLE → INDEX → TRIGGER → VIEW → FUNCTION

冒頭のサマリ通り、一部オブジェクトについては改修または書き直しが必要となったため、該当オブジェクトについては注釈を示します。ここまでの手順はdvdrentalのデータベースを使用すれば共通のアウトプットとなるので、そちらと見比べながら実行してください。

## 9-1. Database Actionsの開始
以降のSQL文はDatabase Actionsを利用してブラウザから実行していきます。
Autonomous Databaseの詳細画面から、「データベース・アクション」を押下してください。
DBにログインされていれば、別タブで「Oracle Database Actions」が立ち上がります。
ログインされていない場合は、Autonomous Database作成時に設定したパスワードを利用し、ユーザー名：adminで認証します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325706/754a7e50-a240-272d-363c-012dac6d213a.png)

SQLの画面からSQL文を実行していきます。

## 9-2. ユーザ作成

```sql
CREATE USER postgres IDENTIFIED BY <パスワード>;
ALTER USER postgres quota unlimited on DATA;
```
postgresというユーザを作成します。
また、postgresに対するリソースの制限を解除します。

## 9-3. シーケンスの作成
シーケンスを作成します。
"dvdrental_metadata_oracle.sql"におけるユーザ名を"public"に置換し、実行します。
```sql
CREATE SEQUENCE postgres.customer_customer_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE 
    NOMAXVALUE 
    CACHE 2;
```
残り12シーケンスについても同様に実行します。

<details><summary>シーケンス作成</summary>

```sql
CREATE SEQUENCE postgres.actor_actor_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE 
    NOMAXVALUE 
    CACHE 2;

CREATE SEQUENCE postgres.category_category_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE 
    NOMAXVALUE 
    CACHE 2;

CREATE SEQUENCE postgres.film_film_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE
    NOMAXVALUE
    CACHE 2;

CREATE SEQUENCE postgres.address_address_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE
    NOMAXVALUE
    CACHE 2;

CREATE SEQUENCE postgres.city_city_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE
    NOMAXVALUE
    CACHE 2;

CREATE SEQUENCE postgres.country_country_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE 
    NOMAXVALUE 
    CACHE 2;

CREATE SEQUENCE postgres.inventory_inventory_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE 
    NOMAXVALUE 
    CACHE 2;


CREATE SEQUENCE postgres.language_language_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE 
    NOMAXVALUE 
    CACHE 2;

CREATE SEQUENCE postgres.payment_payment_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE 
    NOMAXVALUE 
    CACHE 2;

CREATE SEQUENCE postgres.rental_rental_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE 
    NOMAXVALUE 
    CACHE 2;

CREATE SEQUENCE postgres.staff_staff_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE 
    NOMAXVALUE 
    CACHE 2;
    
CREATE SEQUENCE postgres.store_store_id_seq
    START WITH 1
    INCREMENT BY 1
    NOMINVALUE 
    NOMAXVALUE 
    CACHE 2;
```
</details>

実行後、下記SQL文を実行し、シーケンスが13個確認できれば成功です。

```sql
select object_name,object_type,status from dba_objects where owner='POSTGRES' and object_type='SEQUENCE' order by 1;
```

## 9-4. タイプの作成

タイプ（PostgreSQLで言うところのドメイン）を作成します。
```sql
CREATE or REPLACE TYPE postgres."year" AS OBJECT (year_type NUMBER(10));
```


## 9-5. テーブルの作成
テーブルを作成します。
"dvdrental_metadata_oracle.sql"におけるユーザ名を"public"に置換し、実行します。
下記テーブルについては、のちに実データをロードする際にエラーが発生します。
これは実データでNULL値が入っているためです。ALTER文で部分的にNULL値を許可します。

```sql
CREATE TABLE postgres.address (
    address_id NUMBER(10) DEFAULT postgres.address_address_id_seq.nextval NOT NULL,
    address VARCHAR2(50) NOT NULL,
    address2 VARCHAR2(50),
    district VARCHAR2(20) NOT NULL,
    city_id NUMBER(5) NOT NULL,
    postal_code VARCHAR2(10),
    phone VARCHAR2(20) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
);
alter table postgres.address modify phone NULL;

CREATE TABLE postgres.film (
    film_id NUMBER(10) DEFAULT postgres.film_film_id_seq.nextval NOT NULL,
    title VARCHAR2(255) NOT NULL,
    description VARCHAR2(4000),
    release_year postgres."year",
    language_id NUMBER(5) NOT NULL,
    rental_duration NUMBER(5) DEFAULT 3 NOT NULL,
    rental_rate NUMBER(4,2) DEFAULT 4.99 NOT NULL,
    length NUMBER(5),
    replacement_cost NUMBER(5,2) DEFAULT 19.99 NOT NULL,
    rating VARCHAR2(4000) CHECK (rating IN ('G','PG','PG-13','R','NC-17')) ,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL,
    special_features CLOB,
    fulltext CLOB NOT NULL
);
alter table postgres.film modify fulltext NULL;

```
残りの13テーブルについても同様にCRETAE文を実行します。

<details><summary>テーブル作成</summary>

```sql
CREATE TABLE postgres.customer (
    customer_id NUMBER(10) DEFAULT postgres.customer_customer_id_seq.nextval NOT NULL,
    store_id NUMBER(5) NOT NULL,
    first_name VARCHAR2(45) NOT NULL,
    last_name VARCHAR2(45) NOT NULL,
    email VARCHAR2(50),
    address_id NUMBER(5) NOT NULL,
    activebool CHAR(1) DEFAULT 1 NOT NULL,
    create_date date DEFAULT CAST((CAST(SYSDATE AS date)) AS date) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP,
    active NUMBER(10)
);

CREATE TABLE postgres.actor (
    actor_id NUMBER(10) DEFAULT postgres.actor_actor_id_seq.nextval NOT NULL,
    first_name VARCHAR2(45) NOT NULL,
    last_name VARCHAR2(45) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
);

CREATE TABLE postgres.category (
    category_id NUMBER(10) DEFAULT postgres.category_category_id_seq.nextval NOT NULL,
    name VARCHAR2(25) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
); 

CREATE TABLE postgres.film_actor (
    actor_id NUMBER(5) NOT NULL,
    film_id NUMBER(5) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
); 
CREATE TABLE postgres.film_category (
    film_id NUMBER(5) NOT NULL,
    category_id NUMBER(5) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
);
CREATE TABLE postgres.city (
    city_id NUMBER(10) DEFAULT postgres.city_city_id_seq.nextval NOT NULL,
    city VARCHAR2(50) NOT NULL,
    country_id NUMBER(5) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
);
CREATE TABLE postgres.country (
    country_id NUMBER(10) DEFAULT postgres.country_country_id_seq.nextval NOT NULL,
    country VARCHAR2(50) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
);
CREATE TABLE postgres.inventory (
    inventory_id NUMBER(10) DEFAULT postgres.inventory_inventory_id_seq.nextval NOT NULL,
    film_id NUMBER(5) NOT NULL,
    store_id NUMBER(5) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
);
CREATE TABLE postgres.language (
    language_id NUMBER(10) DEFAULT postgres.language_language_id_seq.nextval NOT NULL,
    name character(20) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
); 
CREATE TABLE postgres.payment (
    payment_id NUMBER(10) DEFAULT postgres.payment_payment_id_seq.nextval NOT NULL,
    customer_id NUMBER(5) NOT NULL,
    staff_id NUMBER(5) NOT NULL,
    rental_id NUMBER(10) NOT NULL,
    amount NUMBER(5,2) NOT NULL,
    payment_date timestamp NOT NULL
);
CREATE TABLE postgres.rental (
    rental_id NUMBER(10) DEFAULT postgres.rental_rental_id_seq.nextval NOT NULL,
    rental_date timestamp NOT NULL,
    inventory_id NUMBER(10) NOT NULL,
    customer_id NUMBER(5) NOT NULL,
    return_date timestamp,
    staff_id NUMBER(5) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
);
CREATE TABLE postgres.store (
    store_id NUMBER(10) DEFAULT postgres.store_store_id_seq.nextval NOT NULL,
    manager_staff_id NUMBER(5) NOT NULL,
    address_id NUMBER(5) NOT NULL,
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL
);
CREATE TABLE postgres.staff (
    staff_id NUMBER(10) DEFAULT postgres.staff_staff_id_seq.nextval NOT NULL,
    first_name VARCHAR2(45) NOT NULL,
    last_name VARCHAR2(45) NOT NULL,
    address_id NUMBER(5) NOT NULL,
    email VARCHAR2(50),
    store_id NUMBER(5) NOT NULL,
    active CHAR(1) DEFAULT 1 NOT NULL,
    username VARCHAR2(16) NOT NULL,
    password VARCHAR2(40),
    last_update timestamp DEFAULT SYSTIMESTAMP NOT NULL,
    picture BLOB
);
```
</details>

実行後、下記SQL文を実行し、テーブルが15個確認できれば成功です。

```sql
select object_name,object_type,status from dba_objects where owner='POSTGRES' and object_type='TABLE' order by 1;
```

## 9-6. キーの作成
主キー、外部キーを作成します。

<details><summary>主キー作成</summary>
ALTER TABLE  postgres.actor
    ADD CONSTRAINT actor_pkey PRIMARY KEY (actor_id);

ALTER TABLE  postgres.address
    ADD CONSTRAINT address_pkey PRIMARY KEY (address_id);

ALTER TABLE  postgres.category
    ADD CONSTRAINT category_pkey PRIMARY KEY (category_id);

ALTER TABLE  postgres.city
    ADD CONSTRAINT city_pkey PRIMARY KEY (city_id);

ALTER TABLE  postgres.country
    ADD CONSTRAINT country_pkey PRIMARY KEY (country_id);

ALTER TABLE  postgres.customer
    ADD CONSTRAINT customer_pkey PRIMARY KEY (customer_id);
ALTER TABLE  postgres.film_actor
    ADD CONSTRAINT film_actor_pkey PRIMARY KEY (actor_id, film_id);

ALTER TABLE  postgres.film_category
    ADD CONSTRAINT film_category_pkey PRIMARY KEY (film_id, category_id);

ALTER TABLE  postgres.film
    ADD CONSTRAINT film_pkey PRIMARY KEY (film_id);

ALTER TABLE  postgres.inventory
    ADD CONSTRAINT inventory_pkey PRIMARY KEY (inventory_id);

ALTER TABLE  postgres.language
    ADD CONSTRAINT language_pkey PRIMARY KEY (language_id);

ALTER TABLE  postgres.payment
    ADD CONSTRAINT payment_pkey PRIMARY KEY (payment_id);

ALTER TABLE  postgres.rental
    ADD CONSTRAINT rental_pkey PRIMARY KEY (rental_id);

ALTER TABLE  postgres.staff
    ADD CONSTRAINT staff_pkey PRIMARY KEY (staff_id);

ALTER TABLE  postgres.store
    ADD CONSTRAINT store_pkey PRIMARY KEY (store_id);

</details>

実行後、15個の主キーが確認できれば成功です。
```sql
select constraint_name,constraint_type,table_name,status from dba_constraints where owner='POSTGRES' and constraint_type='P' order by 1;
```

<details><summary>外部キーの作成</summary>

```sql
ALTER TABLE  postgres.customer
    ADD CONSTRAINT customer_address_id_fkey FOREIGN KEY (address_id) REFERENCES postgres.address(address_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.film_actor
    ADD CONSTRAINT film_actor_actor_id_fkey FOREIGN KEY (actor_id) REFERENCES postgres.actor(actor_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.film_actor
    ADD CONSTRAINT film_actor_film_id_fkey FOREIGN KEY (film_id) REFERENCES postgres.film(film_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.film_category
    ADD CONSTRAINT film_category_category_id_fkey FOREIGN KEY (category_id) REFERENCES postgres.category(category_id)  ON DELETE CASCADE;
ALTER TABLE  postgres.film_category
    ADD CONSTRAINT film_category_film_id_fkey FOREIGN KEY (film_id) REFERENCES postgres.film(film_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.film
    ADD CONSTRAINT film_language_id_fkey FOREIGN KEY (language_id) REFERENCES postgres.language(language_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.address
    ADD CONSTRAINT fk_address_city FOREIGN KEY (city_id) REFERENCES postgres.city(city_id);

ALTER TABLE  postgres.city
    ADD CONSTRAINT fk_city FOREIGN KEY (country_id) REFERENCES postgres.country(country_id);
    
ALTER TABLE  postgres.inventory
    ADD CONSTRAINT inventory_film_id_fkey FOREIGN KEY (film_id) REFERENCES postgres.film(film_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.payment
    ADD CONSTRAINT payment_customer_id_fkey FOREIGN KEY (customer_id) REFERENCES postgres.customer(customer_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.payment
    ADD CONSTRAINT payment_rental_id_fkey FOREIGN KEY (rental_id) REFERENCES postgres.rental(rental_id)  ON DELETE SET NULL;

ALTER TABLE  postgres.payment
    ADD CONSTRAINT payment_staff_id_fkey FOREIGN KEY (staff_id) REFERENCES postgres.staff(staff_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.rental
    ADD CONSTRAINT rental_customer_id_fkey FOREIGN KEY (customer_id) REFERENCES postgres.customer(customer_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.rental
    ADD CONSTRAINT rental_inventory_id_fkey FOREIGN KEY (inventory_id) REFERENCES postgres.inventory(inventory_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.rental
    ADD CONSTRAINT rental_staff_id_key FOREIGN KEY (staff_id) REFERENCES postgres.staff(staff_id);

ALTER TABLE  postgres.staff
    ADD CONSTRAINT staff_address_id_fkey FOREIGN KEY (address_id) REFERENCES postgres.address(address_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.store
    ADD CONSTRAINT store_address_id_fkey FOREIGN KEY (address_id) REFERENCES postgres.address(address_id)  ON DELETE CASCADE;

ALTER TABLE  postgres.store
    ADD CONSTRAINT store_manager_staff_id_fkey FOREIGN KEY (manager_staff_id) REFERENCES postgres.staff(staff_id)  ON DELETE CASCADE;
```
</details>

実行後、18個の外部キーが確認できれば成功です。
```sql
select constraint_name,constraint_type,table_name,status from dba_constraints where owner='POSTGRES' and  constraint_type='R' ORDER by 1;
```

## 9-7. インデックスの作成
インデックスを作成します。
<details><summary>インデックス作成</summary>

```sql

CREATE INDEX postgres.idx_actor_last_name ON  postgres.actor  (last_name);

CREATE INDEX postgres.idx_fk_address_id ON  postgres.customer  (address_id);

CREATE INDEX postgres.idx_fk_city_id ON  postgres.address  (city_id);

CREATE INDEX postgres.idx_fk_country_id ON  postgres.city  (country_id);

CREATE INDEX postgres.idx_fk_customer_id ON  postgres.payment  (customer_id);

CREATE INDEX postgres.idx_fk_film_id ON  postgres.film_actor  (film_id);

CREATE INDEX postgres.idx_fk_inventory_id ON  postgres.rental  (inventory_id);

CREATE INDEX postgres.idx_fk_language_id ON  postgres.film  (language_id);


CREATE INDEX postgres.idx_fk_rental_id ON  postgres.payment  (rental_id);

CREATE INDEX postgres.idx_fk_staff_id ON  postgres.payment  (staff_id);

CREATE INDEX postgres.idx_fk_store_id ON  postgres.customer  (store_id);
CREATE INDEX postgres.idx_last_name ON  postgres.customer  (last_name);
CREATE INDEX postgres.idx_store_id_film_id ON  postgres.inventory  (store_id, film_id);

CREATE INDEX postgres.idx_title ON  postgres.film  (title);

CREATE UNIQUE INDEX postgres.idx_unq_manager_staff_id ON  postgres.store  (manager_staff_id);

CREATE UNIQUE INDEX postgres.idx_unq_rental_rental_date_inventory_id_customer_id ON  postgres.rental  (rental_date, inventory_id, customer_id);

```
</details>

実行後、16個のインデックスが確認できれば成功です。
```sql
select index_name,index_type,table_name,status from dba_indexes where owner='POSTGRES' and index_name like 'IDX_%' order by 1;
```

## 9-8. トリガーの作成
トリガーを作成します。
<details><summary>トリガー作成</summary>

```sql
CREATE OR REPLACE TRIGGER postgres.last_updated BEFORE UPDATE ON postgres.actor FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_1 BEFORE UPDATE ON postgres.address FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;   
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_2 BEFORE UPDATE ON  postgres.category FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_3 BEFORE UPDATE ON  postgres.city FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_4 BEFORE UPDATE ON  postgres.country FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_5 BEFORE UPDATE ON  postgres.customer FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;
CREATE OR REPLACE TRIGGER postgres.last_updated_6 BEFORE UPDATE ON  postgres.film FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_7 BEFORE UPDATE ON  postgres.film_actor FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_8 BEFORE UPDATE ON  postgres.film_category FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_9 BEFORE UPDATE ON  postgres.inventory FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_10 BEFORE UPDATE ON  postgres.language FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_11 BEFORE UPDATE ON  postgres.rental FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_12 BEFORE UPDATE ON  postgres.staff FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.last_updated_13 BEFORE UPDATE ON  postgres.store FOR EACH ROW
BEGIN
    :NEW.last_update := SYSTIMESTAMP;
END;

CREATE OR REPLACE TRIGGER postgres.film_fulltext_trigger
BEFORE INSERT OR UPDATE ON postgres.film
FOR EACH ROW
BEGIN
    :NEW.fulltext := :NEW.title || ' ' || :NEW.description;
END;
```

</details>

実行後、15個のトリガーが確認できれば成功です。
```sql
select object_name,object_type,status from dba_objects where owner='POSTGRES' and object_type='TRIGGER' order by 1; 
```

## 9-9. ビューの作成
ビューを作成します。
<details><summary>ビュー作成</summary>

```sql
CREATE OR REPLACE TYPE postgres.group_concat_aggr AS OBJECT (
    aggregated_text CLOB,
    STATIC FUNCTION ODCIAggregateInitialize(sctx IN OUT group_concat_aggr) RETURN NUMBER,
    MEMBER FUNCTION ODCIAggregateIterate(self IN OUT group_concat_aggr, value IN CLOB) RETURN NUMBER,
    MEMBER FUNCTION ODCIAggregateMerge(self IN OUT group_concat_aggr, ctx2 IN group_concat_aggr) RETURN NUMBER,
    MEMBER FUNCTION ODCIAggregateTerminate(self IN group_concat_aggr, returnValue OUT CLOB, flags IN NUMBER) RETURN NUMBER
);
CREATE OR REPLACE FUNCTION postgres.group_concat (input CLOB) RETURN CLOB AGGREGATE USING group_concat_aggr;

CREATE OR REPLACE TYPE BODY postgres.group_concat_aggr AS
    STATIC FUNCTION ODCIAggregateInitialize(sctx IN OUT group_concat_aggr) RETURN NUMBER IS
    BEGIN
        sctx := group_concat_aggr(NULL);
        RETURN ODCIConst.Success;
    END;
    MEMBER FUNCTION ODCIAggregateIterate(self IN OUT group_concat_aggr, value IN CLOB) RETURN NUMBER IS
    BEGIN
        IF self.aggregated_text IS NULL THEN
            self.aggregated_text := value;
        ELSE
            self.aggregated_text := self.aggregated_text || ', ' || value;
        END IF;
        RETURN ODCIConst.Success;
    END;
    MEMBER FUNCTION ODCIAggregateMerge(self IN OUT group_concat_aggr, ctx2 IN group_concat_aggr) RETURN NUMBER IS
    BEGIN
        IF ctx2.aggregated_text IS NOT NULL THEN
            IF self.aggregated_text IS NULL THEN
                self.aggregated_text := ctx2.aggregated_text;
            ELSE
                self.aggregated_text := self.aggregated_text || ', ' || ctx2.aggregated_text;
            END IF;
        END IF;
        RETURN ODCIConst.Success;
    END;
    MEMBER FUNCTION ODCIAggregateTerminate(self IN group_concat_aggr, returnValue OUT CLOB, flags IN NUMBER) RETURN NUMBER IS
    BEGIN
        returnValue := self.aggregated_text;
        RETURN ODCIConst.Success;
    END;
END;

CREATE VIEW postgres.actor_info AS
 SELECT a.actor_id,
    a.first_name,
    a.last_name,
    postgres.group_concat(DISTINCT ((CAST((c.name) AS VARCHAR2(4000)) || CAST(': ' AS VARCHAR2(4000))) || ( SELECT postgres.group_concat(CAST((f.title) AS VARCHAR2(4000))) AS group_concat
           FROM ((postgres.film f
             JOIN postgres.film_category fc_1 ON ((f.film_id = fc_1.film_id)))
             JOIN postgres.film_actor fa_1 ON ((f.film_id = fa_1.film_id)))
          WHERE ((fc_1.category_id = c.category_id) AND (fa_1.actor_id = a.actor_id))
          GROUP BY fa_1.actor_id))) AS film_info
   FROM (((postgres.actor a
     LEFT JOIN postgres.film_actor fa ON ((a.actor_id = fa.actor_id)))
     LEFT JOIN postgres.film_category fc ON ((fa.film_id = fc.film_id)))
     LEFT JOIN postgres.category c ON ((fc.category_id = c.category_id)))
  GROUP BY a.actor_id, a.first_name, a.last_name;

CREATE VIEW postgres.customer_list AS
 SELECT cu.customer_id AS id,
    ((CAST((cu.first_name) AS VARCHAR2(4000)) || CAST(' ' AS VARCHAR2(4000))) || CAST((cu.last_name) AS VARCHAR2(4000))) AS name,
    a.address,
    a.postal_code AS "zip code",
    a.phone,
    city.city,
    country.country,
        CASE
            WHEN cu.activebool= 't' THEN CAST('active' AS VARCHAR2(4000))
            ELSE CAST('' AS VARCHAR2(4000))
        END AS notes,
    cu.store_id AS sid
   FROM (((postgres.customer cu
     JOIN postgres.address a ON ((cu.address_id = a.address_id)))
     JOIN postgres.city ON ((a.city_id = city.city_id)))
     JOIN postgres.country ON ((city.country_id = country.country_id)));


CREATE VIEW postgres.film_list AS
 SELECT film.film_id AS fid,
    film.title,
    film.description,
    category.name AS category,
    film.rental_rate AS price,
    film.length,
    film.rating,
    postgres.group_concat(((CAST((actor.first_name) AS VARCHAR2(4000)) || CAST(' ' AS VARCHAR2(4000))) || CAST((actor.last_name) AS VARCHAR2(4000)))) AS actors
   FROM ((((postgres.category
     LEFT JOIN postgres.film_category ON ((category.category_id = film_category.category_id)))
     LEFT JOIN postgres.film ON ((film_category.film_id = film.film_id)))
     JOIN postgres.film_actor ON ((film.film_id = film_actor.film_id)))
     JOIN postgres.actor ON ((film_actor.actor_id = actor.actor_id)))
  GROUP BY film.film_id, film.title, film.description, category.name, film.rental_rate, film.length, film.rating;


CREATE VIEW postgres.nicer_but_slower_film_list AS
 SELECT film.film_id AS fid,
    film.title,
    film.description,
    category.name AS category,
    film.rental_rate AS price,
    film.length,
    film.rating,
    postgres.group_concat((((upper(SUBSTR(CAST((actor.first_name) AS VARCHAR2(4000)), 1, 1)) || lower(SUBSTR(CAST((actor.first_name) AS VARCHAR2(4000)), 2))) || upper(SUBSTR(CAST((actor.last_name) AS VARCHAR2(4000)), 1, 1))) || lower(SUBSTR(CAST((actor.last_name) AS VARCHAR2(4000)), 2)))) AS actors
   FROM ((((postgres.category
     LEFT JOIN postgres.film_category ON ((category.category_id = film_category.category_id)))
     LEFT JOIN postgres.film ON ((film_category.film_id = film.film_id)))
     JOIN postgres.film_actor ON ((film.film_id = film_actor.film_id)))
     JOIN postgres.actor ON ((film_actor.actor_id = actor.actor_id)))
  GROUP BY film.film_id, film.title, film.description, category.name, film.rental_rate, film.length, film.rating;

CREATE VIEW postgres.sales_by_film_category AS
 SELECT c.name AS category,
    sum(p.amount) AS total_sales
   FROM (((((postgres.payment p
     JOIN postgres.rental r ON ((p.rental_id = r.rental_id)))
     JOIN postgres.inventory i ON ((r.inventory_id = i.inventory_id)))
     JOIN postgres.film f ON ((i.film_id = f.film_id)))
     JOIN postgres.film_category fc ON ((f.film_id = fc.film_id)))
     JOIN postgres.category c ON ((fc.category_id = c.category_id)))
  GROUP BY c.name
  ORDER BY (sum(p.amount)) DESC;

CREATE VIEW postgres.sales_by_store AS
 SELECT ((CAST((c.city) AS VARCHAR2(4000)) || CAST(',' AS VARCHAR2(4000))) || CAST((cy.country) AS VARCHAR2(4000))) AS store,
    ((CAST((m.first_name) AS VARCHAR2(4000)) || CAST(' ' AS VARCHAR2(4000))) || CAST((m.last_name) AS VARCHAR2(4000))) AS manager,
    sum(p.amount) AS total_sales
   FROM (((((((postgres.payment p
     JOIN postgres.rental r ON ((p.rental_id = r.rental_id)))
     JOIN postgres.inventory i ON ((r.inventory_id = i.inventory_id)))
     JOIN postgres.store s ON ((i.store_id = s.store_id)))
     JOIN postgres.address a ON ((s.address_id = a.address_id)))
     JOIN postgres.city c ON ((a.city_id = c.city_id)))
     JOIN postgres.country cy ON ((c.country_id = cy.country_id)))
     JOIN postgres.staff m ON ((s.manager_staff_id = m.staff_id)))
  GROUP BY cy.country, c.city, s.store_id, m.first_name, m.last_name
  ORDER BY cy.country, c.city;

CREATE VIEW postgres.staff_list AS
 SELECT s.staff_id AS id,
    ((CAST((s.first_name) AS VARCHAR2(4000)) || CAST(' ' AS VARCHAR2(4000))) || CAST((s.last_name) AS VARCHAR2(4000))) AS name,
    a.address,
    a.postal_code AS "zip code",
    a.phone,
    city.city,
    country.country,
    s.store_id AS sid
   FROM (((postgres.staff s
     JOIN postgres.address a ON ((s.address_id = a.address_id)))
     JOIN postgres.city ON ((a.city_id = city.city_id)))
     JOIN postgres.country ON ((city.country_id = country.country_id)));
```

</details>

実行後、7個のビューが確認できれば成功です。
```sql
select object_name,object_type,status from dba_objects where owner='POSTGRES' and object_type='VIEW' order by 1 ;
```

## 9-10. ファンクションの作成
ファンクションを作成します。

<details><summary>ファンクション作成</summary>

```sql
CREATE OR REPLACE TYPE postgres.group_concat_aggr AS OBJECT (
    aggregated_text CLOB,
    STATIC FUNCTION ODCIAggregateInitialize(sctx IN OUT group_concat_aggr) RETURN NUMBER,
    MEMBER FUNCTION ODCIAggregateIterate(self IN OUT group_concat_aggr, value IN CLOB) RETURN NUMBER,
    MEMBER FUNCTION ODCIAggregateMerge(self IN OUT group_concat_aggr, ctx2 IN group_concat_aggr) RETURN NUMBER,
    MEMBER FUNCTION ODCIAggregateTerminate(self IN group_concat_aggr, returnValue OUT CLOB, flags IN NUMBER) RETURN NUMBER
);

CREATE OR REPLACE TYPE BODY postgres.group_concat_aggr AS
    STATIC FUNCTION ODCIAggregateInitialize(sctx IN OUT group_concat_aggr) RETURN NUMBER IS
    BEGIN
        sctx := group_concat_aggr(NULL);
        RETURN ODCIConst.Success;
    END;
    MEMBER FUNCTION ODCIAggregateIterate(self IN OUT group_concat_aggr, value IN CLOB) RETURN NUMBER IS
    BEGIN
        IF self.aggregated_text IS NULL THEN
            self.aggregated_text := value;
        ELSE
            self.aggregated_text := self.aggregated_text || ', ' || value;
        END IF;
        RETURN ODCIConst.Success;
    END;
    MEMBER FUNCTION ODCIAggregateMerge(self IN OUT group_concat_aggr, ctx2 IN group_concat_aggr) RETURN NUMBER IS
    BEGIN
        IF   ctx2.aggregated_text IS NOT NULL THEN
            IF self.aggregated_text IS NULL THEN
             self.aggregated_text := ctx2.aggregated_text;
            ELSE
                self.aggregated_text := self.aggregated_text || ', ' || ctx2.aggregated_text;
            END IF;
        END IF;
        RETURN ODCIConst.Success;
    END;
    MEMBER FUNCTION ODCIAggregateTerminate(self IN group_concat_aggr, returnValue OUT CLOB, flags IN NUMBER) RETURN NUMBER IS
    BEGIN
        returnValue := self.aggregated_text;
        RETURN  ODCIConst.Success;
    END;
END;

CREATE OR REPLACE FUNCTION postgres.group_concat (input CLOB) RETURN CLOB AGGREGATE USING group_concat_aggr;

CREATE OR REPLACE FUNCTION postgres.inventory_in_stock (p_inventory_id NUMBER) RETURN CHAR IS
    v_rentals NUMBER(10) := 0;
    v_out     NUMBER(10) := 0;
BEGIN
    SELECT COUNT(*) INTO v_rentals
    FROM postgres.rental
    WHERE inventory_id = p_inventory_id;

    IF v_rentals = 0 THEN
        RETURN '1';
    END IF;

    SELECT COUNT(rental.rental_id) INTO v_out
    FROM postgres.inventory inventory
    LEFT JOIN postgres.rental rental ON inventory.inventory_id = rental.inventory_id
    WHERE inventory.inventory_id = p_inventory_id
      AND rental.return_date IS NULL;

    IF v_out > 0 THEN
        RETURN '0';
    ELSE
        RETURN '1';
    END IF;
END;

CREATE OR REPLACE FUNCTION postgres.film_in_stock(p_film_id NUMBER, p_store_id NUMBER, p_film_count OUT NUMBER) return sys_refcursor is 
    l_ret_var sys_refcursor;
 BEGIN
     OPEN l_ret_var FOR SELECT inventory_id
     FROM inventory
     WHERE film_id = p_film_id
     AND store_id = p_store_id
     AND inventory_in_stock(inventory_id) = 1;
dbms_sql.return_result(l_ret_var);
END; 

CREATE OR REPLACE FUNCTION postgres.film_not_in_stock(p_film_id NUMBER, p_store_id NUMBER, p_film_count OUT NUMBER) return sys_refcursor is 
    l_ret_var sys_refcursor;
 BEGIN
    OPEN l_ret_var FOR SELECT inventory_id
    FROM inventory
    WHERE film_id = p_film_id
    AND store_id = p_store_id
    AND NOT inventory_in_stock(inventory_id) = 0;
dbms_sql.return_result(l_ret_var);
END;

CREATE OR REPLACE FUNCTION postgres.inventory_held_by_customer(p_inventory_id NUMBER) RETURN NUMBER IS
    v_customer_id NUMBER(10);
BEGIN
    
    SELECT customer_id
    INTO v_customer_id
    FROM postgres.rental
    WHERE return_date IS NULL
      AND inventory_id = p_inventory_id;


    RETURN v_customer_id;

EXCEPTION
    -- Handle the case where no rows are found (e.g., inventory is not held by any customer)
    WHEN NO_DATA_FOUND THEN
        RETURN NULL;
END;

CREATE OR REPLACE FUNCTION postgres.last_day(f_param1 TIMESTAMP) RETURN DATE IS
    l_ret_var DATE;  
BEGIN
    l_ret_var := LAST_DAY(f_param1);

    RETURN l_ret_var;
END;

CREATE OR REPLACE  FUNCTION postgres.rewards_report (
    min_monthly_purchases NUMBER,
    min_dollar_amount_purchased NUMBER
) RETURN SYS_REFCURSOR IS
    l_ret_var SYS_REFCURSOR;
    last_month_start DATE;
    last_month_end DATE;
BEGIN

    IF min_monthly_purchases <= 0 THEN
        RAISE_APPLICATION_ERROR(-20001, 'Minimum monthly purchases parameter must be greater than 0');
    END IF;

    IF min_dollar_amount_purchased <= 0 THEN
        RAISE_APPLICATION_ERROR(-20002, 'Minimum monthly dollar amount purchased parameter must be greater than $0.00');
    END IF;

    last_month_start := TRUNC(ADD_MONTHS(SYSDATE, -3), 'MM');
    last_month_end := LAST_DAY(last_month_start);

    OPEN l_ret_var FOR
        SELECT c.*
        FROM customer c
        WHERE c.customer_id IN (
            SELECT p.customer_id
            FROM payment p
            WHERE p.payment_date BETWEEN last_month_start AND last_month_end
            GROUP BY p.customer_id
            HAVING SUM(p.amount) > min_dollar_amount_purchased
               AND COUNT(p.customer_id) > min_monthly_purchases
        );

    RETURN l_ret_var;

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RAISE_APPLICATION_ERROR(-20003, 'No customers found matching the criteria.');
    WHEN OTHERS THEN
        RAISE_APPLICATION_ERROR(-20004, 'Unexpected error occurred: ' || SQLERRM);
END rewards_report;

CREATE OR REPLACE FUNCTION postgres.get_customer_balance (p_customer_id NUMBER, p_effective_date DATE) RETURN NUMBER IS

    v_rentfees NUMBER(5, 2);
    v_overfees NUMBER(10);
    v_payments NUMBER(5, 2);
    v_replacement_cost NUMBER(5, 2);
    v_rental_duration_interval INTERVAL DAY TO SECOND;
BEGIN
    SELECT NVL(SUM(film.rental_rate), 0) INTO v_rentfees
    FROM film
    JOIN inventory ON film.film_id = inventory.film_id
    JOIN rental ON inventory.inventory_id = rental.inventory_id
    WHERE rental.rental_date <= p_effective_date
      AND rental.customer_id = p_customer_id;

    SELECT NVL(SUM(CASE
        WHEN (rental.return_date - rental.rental_date) > (film.rental_duration * INTERVAL '1' DAY) THEN
            EXTRACT(DAY FROM (rental.return_date - rental.rental_date) - (film.rental_duration * INTERVAL '1' DAY))
        ELSE 0 END), 0) INTO v_overfees
    FROM rental
    JOIN inventory ON rental.inventory_id = inventory.inventory_id
    JOIN film ON inventory.film_id = film.film_id
    WHERE rental.rental_date <= p_effective_date
      AND rental.customer_id = p_customer_id;

    SELECT NVL(SUM(CASE
        WHEN (rental.return_date - rental.rental_date) > (film.rental_duration * 2 * INTERVAL '1' DAY) THEN
            film.replacement_cost
        ELSE 0 END), 0) INTO v_replacement_cost
    FROM rental
    JOIN inventory ON rental.inventory_id = inventory.inventory_id
    JOIN film ON inventory.film_id = film.film_id
    WHERE rental.rental_date <= p_effective_date
      AND rental.customer_id = p_customer_id;

    SELECT NVL(SUM(payment.amount), 0) INTO v_payments
    FROM payment
    WHERE payment.payment_date <= p_effective_date
      AND payment.customer_id = p_customer_id;

    RETURN v_rentfees + v_overfees + v_replacement_cost - v_payments;
END;
```

</details>

実行後、8個のファンクションが確認できれば成功です。
```sql
select object_name,object_type,status from dba_objects where owner='POSTGRES' and object_type='FUNCTION' order by 1;
```

## 9-11. 主な変更点

|No |オブジェクト|オブジェクト名|変更点
|---|---|---|---
|1|TYPE|postgres."year"|「year 」 →「"year" 」へ変更
|2|TABLE|address.phone|実データにNULLあるので、NULL許可。
|3|TABLE|film.fulltext|同上
|4|VIEW|customer_list|「cu.activebool=1」→「cu.activebool= 't‘」へ変更
|5|VIEW|nicer_but_slower_film_list|「“substring”」 →「SUBSTR」へ変更
|6|TRIGGER|CREATE TRIGGER film_fulltext_trigger|tsvector_update_triggerが変換されていないため、書き直し
|7|FUNCTION|inventory_in_stock|「CREATE TABLE」→「SELECT」へ変更
|8|FUNCTION|inventory_held_by_customer|「CREATE TABLE」→「SELECT」へ変更
|9|FUNCTION|get_customer_balance |「CAST(‘1 day’ AS interval DAY(5) TO SECOND) 」→「INTERVAL ‘1’ DAY」へ変更
|10|FUNCTION|postgres.film_in_stock|「inventory_in_stock(inventory_id) 」→「inventory_in_stock(inventory_id) = 1’へ変更
|11|FUNCTION|postgres.film_not_in_stock|「inventory_in_stock(inventory_id) 」→「inventory_in_stock(inventory_id) = 0’へ変更
|12|FUNCTION|postgres.group_concat |書き直し

# 10. データロード
SQL*Loaderを使用してデータをロードします。

## 10-1. SQL*Loaderの設定
SQL*Loaderの設定
```rb
$ wget https://download.oracle.com/otn_software/linux/instantclient/214000/instantclient-tools-linux.x64-21.4.0.0.0dbru.zip
$ wget https://download.oracle.com/otn_software/linux/instantclient/2111000/instantclient-basic-linux.x64-21.11.0.0.0dbru.zip
$ unzip instantclient-tools-linux.x64-21.4.0.0.0dbru.zip
$ unzip instantclient-basic-linux.x64-21.11.0.0.0dbru.zip
$ cd /home/<OCIユーザ名>/network/admin
$ mv Wallet_adb4psql.zip ../instantclient_21_11/network/admin
$ cd ../instantclient_21_11/network/admin
$ unzip Wallet_adb4psql.zip
```
viで編集
```
WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="/home/<OCIユーザ名>/network/admin/instantclient_21_11/network/admin")))
SSL_SERVER_DN_MATCH=yes
```
パスの設定
```rb
$ export TNS_ADMIN=/home/<OCIユーザ名>/network/admin/instantclient_21_11/network/admin
$ export PATH=$PATH:/home/<OCIユーザ名>/network/admin/instantclient_21_4/
```

## 10-2. ロードファイルの準備

viコマンド等を使用し、ロード用のファイルを作成します。

actor.ctl
```sql
LOAD DATA
INFILE 'ACTOR.csv'
INTO TABLE postgres.ACTOR
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
TRAILING NULLCOLS
(
  ACTOR_ID "TO_NUMBER(:ACTOR_ID, '99999')",
  FIRST_NAME,
  LAST_NAME,
  LAST_UPDATE TIMESTAMP "YYYY-MM-DD HH24:MI:SS.FF6"
)
```




## 10-3. データロードの実行

ロード実行前に、外部制約キーが設定されているテーブルはエラーが出るため、一度外部制約を無効化します。
```sql
ALTER TABLE POSTGRES.ADDRESS DISABLE CONSTRAINT FK_ADDRESS_CITY;
ALTER TABLE POSTGRES.CITY DISABLE CONSTRAINT FK_CITY;
ALTER TABLE POSTGRES.CUSTOMER DISABLE CONSTRAINT CUSTOMER_ADDRESS_ID_FKEY;
ALTER TABLE POSTGRES.FILM_CATEGORY DISABLE CONSTRAINT FILM_CATEGORY_FILM_ID_FKEY;
ALTER TABLE POSTGRES.FILM_ACTOR DISABLE CONSTRAINT FILM_ACTOR_FILM_ID_FKEY;
ALTER TABLE POSTGRES.FILM DISABLE CONSTRAINT FILM_LANGUAGE_ID_FKEY;
ALTER TABLE POSTGRES.INVENTORY DISABLE CONSTRAINT INVENTORY_FILM_ID_FKEY;
ALTER TABLE POSTGRES.PAYMENT DISABLE CONSTRAINT PAYMENT_STAFF_ID_FKEY;
ALTER TABLE POSTGRES.PAYMENT DISABLE CONSTRAINT PAYMENT_RENTAL_ID_FKEY;
ALTER TABLE POSTGRES.RENTAL DISABLE CONSTRAINT RENTAL_STAFF_ID_KEY;
ALTER TABLE POSTGRES.STAFF DISABLE CONSTRAINT STAFF_ADDRESS_ID_FKEY;
ALTER TABLE POSTGRES.STORE DISABLE CONSTRAINT STORE_MANAGER_STAFF_ID_FKEY;
```
順次、SQL*Loaderでデータをロードします。
actor.ctlを実行すると下記のような実行結果になります。
```sql
$ sqlldr admin/<パスワード>@adb4psql_high control=actor.ctl

SQL*Loader: Release 19.0.0.0.0 - Production on Sun Dec 29 05:22:37 2024
Version 19.10.0.0.0

Copyright (c) 1982, 2021, Oracle and/or its affiliates.  All rights reserved.

Path used:      Conventional
Commit point reached - logical record count 201

Table POSTGRES.ACTOR:
  200 Rows successfully loaded.

Check the log file:
  actor.log
for more information about the load.
```
他のロード文は下記の通りとなります。

```jsp
$ sqlldr admin/<パスワード>@adb4psql_high control=address.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=category.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=city.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=country.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=customer.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=film_category.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=film_actor.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=film.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=inventory.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=language.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=payment.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=rental.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=staff.ctl
$ sqlldr admin/<パスワード>@adb4psql_high control=store.ctl
```
ロードが成功したら、外部制約キーを有効化します。
```sql
ALTER TABLE POSTGRES.ADDRESS ENABLE CONSTRAINT FK_ADDRESS_CITY;
ALTER TABLE POSTGRES.CITY ENABLE CONSTRAINT FK_CITY;
ALTER TABLE POSTGRES.CUSTOMER ENABLE CONSTRAINT CUSTOMER_ADDRESS_ID_FKEY;
ALTER TABLE POSTGRES.FILM_CATEGORY ENABLE CONSTRAINT FILM_CATEGORY_FILM_ID_FKEY;
ALTER TABLE POSTGRES.FILM_ACTOR ENABLE CONSTRAINT FILM_ACTOR_FILM_ID_FKEY;
ALTER TABLE POSTGRES.FILM ENABLE CONSTRAINT FILM_LANGUAGE_ID_FKEY;
ALTER TABLE POSTGRES.INVENTORY ENABLE CONSTRAINT INVENTORY_FILM_ID_FKEY;
ALTER TABLE POSTGRES.PAYMENT ENABLE CONSTRAINT PAYMENT_STAFF_ID_FKEY;
ALTER TABLE POSTGRES.PAYMENT ENABLE CONSTRAINT PAYMENT_RENTAL_ID_FKEY;
ALTER TABLE POSTGRES.RENTAL ENABLE CONSTRAINT RENTAL_STAFF_ID_KEY;
ALTER TABLE POSTGRES.STAFF ENABLE CONSTRAINT STAFF_ADDRESS_ID_FKEY;
ALTER TABLE POSTGRES.STORE ENABLE CONSTRAINT STORE_MANAGER_STAFF_ID_FKEY;
```

# 11. 動作検証

行数をカウントするSQL文を実行し、RDS for PostgreSQLとAutonomous Databaseで行数がマッチするか検証します。ACTOR TABLEの例を下記に示します。

Oracle
```sql
SQL> select count(*) from postgres.ACTOR;
COUNT(*)
----------
       200
```

PostgreSQL
```sql
dvdrental=>  select count(*) from public.ACTOR;
 count 
-------
   200
(1 row)

```

# まとめ

RDS for PostgreSQLからAutonomous Databaseへの移行検証は以上になります。
DBエンジンを変換する移行方法を試してみました。
DBMS_CLOUD_MIGRATION.MIGRATE_FILEプロシージャを使用することで、DumpファイルからOracle用のSQL文を自動変換してくれることは便利です。ただ、100%変換した上で自動実行してくれる代物ではないため、現時点では一部手動で改修する前提が必要です。
変換までしきれずとも、自動試行まで実施した上で、変換のエラー部分を特定してくれるとさらに便利だと思いました。（よりよい方法をご存じのOraclemanがいらっしゃれば教えてください・・・）
とはいえ、このような変換ファイルなく、1からSQL文を書いて実行するのは大変工数がかかる作業ですので、その一助にはなりえる移行ツールであることは間違いなさそうです。