Last Updated: 2024-01-20
データベースの移行は、日常的に行うオペレーションではありません。しかしYugabyteDBを適用するアプリケーションが新規開発ではなく、既存アプリケーションの移行である場合には必須のプロセスです。
YugabyteDBは、Oracle、PostgreSQL、MySQLのインターフェースを持つ各データベースから、YugabyteDBへと移行するオープンソースの移行ツールVoyagerを提供しています。
このハンズオンでは、YugabyteDB Voyagerを使用して、既存のPostgreSQLデータベースからスキーマとデータを移行して、移行ステップや移行の考慮点について学びます。
このハンズオンでは、ローカルのPostgreSQLとYugabyteDB Managedのクラスタを使用し、YugabyteDB Voyagerでサンプルのデータセットを移行します。
以下の内容を実施します:
YugabyteDB Voyagerは、既存データベースを稼働したまま実行するオンライン・マイグレーションと、既存データベースからアプリケーション・ワークロードを停止して行うオフライン・マイグレーションをサポートしています。(注:2024年4月時点では、オンライン・マイグレーションのサポート対象はOracleおよびPostgreSQLデータベースです。)
ここでは、オフライン・マイグレーションのステップを確認します。
Voyagerのダウンロードとインストール、またVoyagerのコマンドが移行元と移行先のデータベースに接続し必要な操作を行うためのユーザー権限の設定を行います。
移行元データベースから、スキーマとデータをエクスポートします。YugabyteDBで未サポートの機能などを分析レポートで確認し、必要に応じてスキーマの修正を行います。
移行先データベースに、スキーマとデータをインポートします。データ投入後に、インデックスやトリガーを作成します。
全てのデータの移行が完了したことを確認し、ログ等をクリーンアップします。
YugabyteDB Voyagerは、Linux、MacOS、Dockerで実行することができます。このハンズオンでは、Docker版のVoyagerを使用します。Dockerがインストールされていない場合は、 こちらから環境にあったDockerをインストールしてください。
docker pull yugabytedb/yb-voyager
wget -O ./yb-voyager https://raw.githubusercontent.com/yugabyte/yb-voyager/main/docker/yb-voyager-docker && chmod +x ./yb-voyager && sudo mv yb-voyager /usr/local/bin/yb-voyager
yb-voyager version
下のような応答が返れば、Voyagerが実行可能になっています。
以上で、YugabyteDB Voyagerのインストールは完了です。
ここではVoyagerの移行元として使用する、PostgreSQLの サンプル・データベースを準備します。使用可能なPostgreSQLがない場合は、PostgreSQLのDockerイメージをインストールしてください。
docker pull postgres
docker run --name postgres-test -e POSTGRES_PASSWORD=password \
-p 5432:5432 -d postgres
docker exec -it postgres-test psql -U postgres
postgres=#
CREATE DATABASE dvdrental;
postgres=#
\q
docker cp <zipを解凍したフォルダ> postgres-test:/tmp
docker exec -it postgres-test sh -c "pg_restore -U postgres --host localhost --port 5432 --dbname dvdrental --format=d /tmp/dvdrental"
docker exec -it postgres-test psql -U postgres -d dvdrental
dvdrental=#
\dt
dvdrental=#
select * from store;
以上で、サンプル・データベースの準備は完了です。
ここではVoyagerの移行先データベースとして、YugabyteDB Managedのシングルリージョン・クラスタ (3ノードのクラスタ) を作成します。
以上で、クラスタの作成は完了です。
YugabyteDBは、セッション・レベル または クラスタ・レベルでの設定によって機能を調整することができます。基本的に、全てのユーザーにとってメリットのある機能 (例:pushdown) はデフォルトで有効化されますが、テクニカルプレビューとして提供されている新機能などは、設定を変更することで期待する挙動やパフォーマンスを得ることができます。
セッション・レベルの設定変更であればSETコマンドで実施できますが、クラスタ・レベルでの設定変更を行う場合、YugabyteDB Managedではサポートへの依頼が必要です。
ここではサポートへの依頼方法の確認も兼ねて、いくつかのフラグをクラスタ・レベルで変更します。
ISSUE TYPE (問題の種類) | Feature Request |
SUBJECT (タイトル) | フラグの設定 |
DESCRIPTION (内容) | 以下のフラグを設定してください。–yb_bnl_batch_size=1024–ysql_sequence_cache_method=server–ysql_enable_packed_row=true–ysql_enable_packed_row_for_colocated_table=true |
CLUSTER (対象クラスタ名) | (前の手順で作成したクラスタの名前) |
SEVIRITY (重要度) | Normal |
Batched Nested Loop (yb_bnl_batch_size) は、YugabyteDB 2.21以降でデフォルト有効となる予定です。Sequence Cashe (ysql_sequence_cache_method) は、デフォルトでsessionに設定されています。
SHOW yb_bnl_batch_size;
CREATE DATABASE dvdrental WITH COLOCATION=true;
\c dvdrental
以上で、移行先となるYugabyteDB Managedのクラスタ準備は完了です。
YugabyteDB Voyagerを使用して、移行元データベースであるPostgreSQLからスキーマをエクスポートします。
オプション | 説明 | 設定例 |
-e, –export-dir | エクスポートしたスキーマ・ファイルを出力するディレクトリ(事前に作成しておく) | ./export |
–source-db-host | 移行元DBのホスト | localhost |
–source-db-name | 移行元DBの名前 | dvdrebtal |
–source-db-port | 移行元DBのポート | 5432 |
–source-db-schema | 移行元DBのスキーマ | default |
–source-db-type | 移行元DBの種類 | postgresql |
–source-db-user | 移行元DBのユーザー | postgres |
–source-db-password | 移行元DBのユーザー・パスワード | (なし) |
yb-voyager export schema --export-dir ./export \
--source-db-type postgresql \
--source-db-host localhost \
--source-db-user postgres \
--source-db-name dvdrental \
--source-db-schema public
YugabyteDBはPostgreSQL互換のデータベースですが、YugabyteDB Voyagerを使用してエクスポートしたスキーマが、そのままYugabyteDBにインポートできるわけではありません。非サポート機能のチェックや、データをどのように分散配置するかを考慮した調整が必要です。
Voyagerでは、スキーマを分析してレポートを出力することで、非サポート機能のチェックを支援します。
yb-voyager analyze-schema --export-dir ./export --output-format txt
film_fulltext_index
を作成する行を削除します。(下の例では、13行目をコメントアウトしています。)yb-voyager analyze-schema --export-dir ./export --output-format txt
従来のシングルノード・データベースからYugabyteDBに移行すると、データは単一ディスクではなく、分散されたノードに配置されることになります。どのようにデータを分散するか、しないかを予め設計しておくことで、移行後のクエリのパフォーマンスをより改善することができます。
データの適切な分散を検討するには、テーブルのデータ量 (既存と今後の増加見込み) 、実行されるクエリパターンを把握することが重要です。
検討するポイントは、以下のような点があります。(参考: ドキュメント)
ここでは、 サンプル・データベースのER図を確認し、想定するクエリパターンを元に分散SQLに移行するためのデータモデリングを行います。まずは最小限の修正を行ってからスキーマとデータを移行し、移行先でスキーマの変更を含むデータモデルの変更を行います。
テーブル名 | 行数 | データ増加 | 更新頻度 | 参照頻度 | 移行案 |
actor | 200 | 中 | 低 | 高 | コロケーション |
address | 603 | 大 | 中 | 低 | JSONB型で統合 |
category | 16 | 小 | 低 | 中 | enumで統合 |
city | 600 | 小 | 低 | 低 | JSONB型で統合 |
country | 109 | 小 | 低 | 低 | JSONB型で統合 |
customer | 599 | 大 | 中 | 中 | 分散 |
film | 1000 | 中 | 低 | 高 | コロケーション |
film_actor | 5462 | 中 | 低 | 高 | コロケーション |
film_category | 1000 | 中 | 低 | 中 | enumで統合 |
inventory | 4581 | 大 | 低 | 高 | 分散 |
language | 6 | 小 | 低 | 中 | enumで統合 |
payment | 14596 | 大 | 低 | 低 | 分散 |
rental | 16044 | 大 | 低 | 高 | 分散 |
staff | 2 | 中 | 中 | 低 | コロケーション |
store | 2 | 中 | 低 | 低 | コロケーション |
/schema/tables/table.sql
を開いてください。table.sql
には、15の CREATE TABLE
ステートメントがありますが、NOT NULL
制約があるだけでプライマリキーは設定されていません。CREATE TABLE
ステートメントの後に、ALTER TABLE
ステートメントがあり(160行目前後から)、そこでプライマリキーが設定されていることを確認してください。/schema/tables/INDEX_table.sql を開いて、
移行先で不要となるインデックスを作成しないよう、19, 22, 25行目をコメントアウトしてください。INDEX_table.sql にある、language_id のインデックスは不要になるため、37行目をコメントアウトしてください。
CREATE TABLE public.customer...
のDDLステートメントを探して、ステートメントの最後に WITH (COLOCATION = false) SPLIT INTO 1 TABLETS
を追加してください。CREATE TABLE
のDDLステートメントを探して、ステートメントの最後に WITH (COLOCATION = false) SPLIT INTO 3 TABLETS
と追加します。以上で、移行前のスキーマ編集は完了です。
YugabyteDB Voyagerを使用して、移行元データベースであるPostgreSQLからデータをエクスポートします。
オプション | 説明 | 設定例 |
-e, –export-dir | エクスポートしたスキーマ・ファイルを出力するディレクトリ(事前に作成しておく) | ./export |
–source-db-host | 移行元DBのホスト | localhost |
–source-db-name | 移行元DBの名前 | dvdrental |
–source-db-port | 移行元DBのポート | 5432 |
–source-db-schema | 移行元DBのスキーマ | default |
–source-db-type | 移行元DBの種類 | postgresql |
–source-db-user | 移行元DBのユーザー | postgres |
–source-db-password | 移行元DBのユーザー・パスワード | (なし) |
yb-voyager export data --export-dir ./export \
--source-db-type postgresql \
--source-db-host localhost \
--source-db-port 5432 \
--source-db-user postgres \
--source-db-name dvdrental \
--source-db-schema public
オプション | 説明 | 設定例 |
-e, –export-dir | エクスポートしたスキーマ・ファイルを出力するディレクトリ(事前に作成しておく) | ./export |
–target-db-host | 移行先DBのホスト | ap-northeast-1.<uuid>.aws.ybdb.i |
–target-db-name | 移行先DBの名前 | dvdrental |
–target-db-port | 移行先DBのポート | 5433 |
–target-db-user | 移行先DBのユーザー | admin |
–target-db-password | 移行先DBのユーザー・パスワード | password |
yb-voyager import schema --export-dir ./export \
--target-db-host ap-northeast-1.<uuid>.aws.ybdb.io\
--target-db-port 5433 \
--target-db-user admin \
--target-db-name dvdrental
オプション | 説明 | 設定例 | |
-e, –export-dir | エクスポートしたスキーマ・ファイルを出力するディレクトリ(事前に作成しておく) | ./export | |
–target-db-host | 移行先DBのホスト | ap-northeast-1.<uuid>.aws.ybdb.i | |
–target-db-name | 移行先DBの名前 | dvdrental | |
–target-db-port | 移行先DBのポート | 5433 | |
–target-db-user | 移行先DBのユーザー | admin | |
–target-db-password | 移行先DBのユーザー・パスワード | password | |
–parallel-jobs | インポートJOBの並列度 | 6 | |
–table-list | データをインポートする対象テーブルのリスト。指定しなければ全てのテーブルが対象となる。 | ||
–exclude-table-list | データのインポートから除外するテーブルのリスト。 | ||
–start-clean | インポート済みのデータを削除し、最初からインポートを行う。 |
yb-voyager import data --export-dir ./export \
--target-db-host ap-northeast-1.<uuid>.aws.ybdb.io\
--target-db-port 5433 \
--target-db-user admin \
--target-db-name dvdrental \
--parallel-jobs 6
オプション | 説明 | 設定例 | |
-e, –export-dir | エクスポートしたスキーマ・ファイルを出力するディレクトリ(事前に作成しておく) | ./export | |
–target-db-host | 移行先DBのホスト | ap-northeast-1.<uuid>.aws.ybdb.i | |
–target-db-name | 移行先DBの名前 | dvdrental | |
–target-db-port | 移行先DBのポート | 5433 | |
–target-db-user | 移行先DBのユーザー | admin | |
–target-db-password | 移行先DBのユーザー・パスワード | password | |
–refresh-mviews | マテリアライズドビューにデータを投入するためのフラグ。 | ||
–post-import-data | データインポート後にインデックスを作成するためのフラグ。 |
yb-voyager import schema --export-dir ./export \
--target-db-host ap-northeast-1.<uuid>.aws.ybdb.io\
--target-db-port 5433 \
--target-db-user admin \
--target-db-name dvdrental \
--refresh-mviews
--post-import-data
移行前に確認したテーブルの特性だけでなく、クエリパターンに基づいてテーブルの統合 (非正規化) を検討します。
テーブル名 | クエリパターン (ユースケース) | 備考 | |
actor | filmに出演しているactorをactor_idで検索actorのlast_nameでfilmを検索 | film_actor, filmと結合 | |
address | customer, store, staffの住所をaddress_idで取得、更新 | 他テーブルで特定されたaddress_idでポイント検索 | |
category | category_idでfilmを検索 | film_category, filmと結合 | |
city | addressの一部として取得 | address, countryと結合 | |
country | addressの一部として取得 | address, cityと結合 | |
customer | 貸出時にcustomer_idで検索支払時にcustomer_idで検索登録情報変更時にlast_nameで検索店舗からのDM発送時にstore_idで検索 | ||
film | title検索category, release_year, ratingでの検索film_id, release_year, ratingでの検索language_idでのフィルタリング | category, film_category,actor, film_actorと結合 | |
film_actor | 特定filmに出演しているactorをリスト特定actorの出演filmをリスト | film_idでポイント検索 (actorと結合)actor_idでポイント検索 (filmと結合) | |
film_category | 特定categoryのfilmをリスト特定filmのcategoryをリスト | category_idでポイント検索 (filmと結合)film_idでポイント検索 (category)と結合 | |
inventory | 貸出/返却時にinventory_idで検索 | ||
language | film検索の一部として参照 | filmと結合 | |
payment | 支払時にrental_idを指定して更新支払状況の確認時にrental_idで検索スタッフごとの支払処理確認時にstaff_idで検索 | customer_idでポイント検索rental_idでポイント検索staff_idでポイント検索 | |
rental | 貸出時にinventry_id, customer_id, staff_id, rental_dateを指定して更新返却時にinventory_idとrental_dateからrental_idを特定し、return_dateを更新 | ||
staff | paymentやrentalの担当staff確認時に、staff_idで検索 | staff_idでポイント検索 | |
store | 貸出可能filmの在庫検索時に、store_idで店舗情報を参照 | inventory, filmと結合 |
CREATE TABLE public.new_payment(
customer_id smallint NOT NULL,
staff_id smallint NOT NULL,
rental_id integer NOT NULL,
amount numeric(5,2) NOT NULL,
payment_date timestamp without time zone NOT NULL,
CONSTRAINT new_payment_pkey
PRIMARY KEY (rental_id Hash, payment_date DESC),
CONSTRAINT new_payment_customer_id_fkey
FOREIGN KEY (customer_id)
REFERENCES public.customer(customer_id)
ON UPDATE CASCADE ON DELETE RESTRICT,
CONSTRAINT new_payment_rental_id_fkey
FOREIGN KEY (rental_id)
REFERENCES public.rental(rental_id)
ON UPDATE CASCADE ON DELETE SET NULL,
CONSTRAINT new_payment_staff_id_fkey
FOREIGN KEY (staff_id)
REFERENCES public.staff(staff_id)
ON UPDATE CASCADE ON DELETE RESTRICT
) WITH (COLOCATION = false) SPLIT INTO 3 TABLETS;
INSERT INTO public.new_payment
SELECT customer_id,staff_id,rental_id,amount,payment_date
FROM public.payment;
CREATE INDEX new_idx_fk_customer_id ON public.new_payment USING lsm (customer_id);
CREATE INDEX new_idx_fk_staff_id ON public.new_payment USING lsm (staff_id);
CREATE UNIQUE INDEX idx_unq_rental_inventory_id_rental_date ON public.rental USING lsm (inventory_id Hash, rental_date DESC);
CREATE INDEX idx_rental_customer_id_return_date ON public.rental USING lsm (customer_id Hash, return_date DESC);
CREATE TYPE lang AS ENUM (
'English',
'Italian',
'Japanese',
'Mandarin',
'French',
'German'
);
ALTER TABLE public.film ADD COLUMN language lang;
UPDATE public.film SET language='English' where language_id=1;
UPDATE public.film SET language='Italian' where language_id=2;
UPDATE public.film SET language='Japanese' where language_id=3;
UPDATE public.film SET language='Mandarin' where language_id=4;
UPDATE public.film SET language='French' where language_id=5;
UPDATE public.film SET language='German' where language_id=6;
CREATE TYPE ctgy AS ENUM (
'Action',
'Animation',
'Children',
'Classics',
'Comedy',
'Documentary',
'Drama',
'Family',
'Foreign',
'Games',
'Horror',
'Music',
'New',
'Sci-Fi',
'Sports',
'Travel'
);
ALTER TABLE public.film ADD COLUMN category ctgy;
UPDATE public.film SET category='Action'
Where film_id IN (
SELECT film_id from film_category where category_id=1
);
UPDATE public.film SET category='Animation'
Where film_id IN (
SELECT film_id from film_category where category_id=2
);
UPDATE public.film SET category='Children'
Where film_id IN (
SELECT film_id from film_category where category_id=3
);
UPDATE public.film SET category='Classics'
Where film_id IN (
SELECT film_id from film_category where category_id=4
);
UPDATE public.film SET category='Comedy'
Where film_id IN (
SELECT film_id from film_category where category_id=5
);
UPDATE public.film SET category='Documentary'
Where film_id IN (
SELECT film_id from film_category where category_id=6
);
UPDATE public.film SET category='Drama'
Where film_id IN (
SELECT film_id from film_category where category_id=7
);
UPDATE public.film SET category='Family'
Where film_id IN (
SELECT film_id from film_category where category_id=8
);
UPDATE public.film SET category='Foreign'
Where film_id IN (
SELECT film_id from film_category where category_id=9
);
UPDATE public.film SET category='Games'
Where film_id IN (
SELECT film_id from film_category where category_id=10
);
UPDATE public.film SET category='Horror'
Where film_id IN (
SELECT film_id from film_category where category_id=11
);
UPDATE public.film SET category='Music'
Where film_id IN (
SELECT film_id from film_category where category_id=12
);
UPDATE public.film SET category='New'
Where film_id IN (
SELECT film_id from film_category where category_id=13
);
UPDATE public.film SET category='Sci-Fi'
Where film_id IN (
SELECT film_id from film_category where category_id=14
);
UPDATE public.film SET category='Sports'
Where film_id IN (
SELECT film_id from film_category where category_id=15
);
UPDATE public.film SET category='Travel'
Where film_id IN (
SELECT film_id from film_category where category_id=16
);
ALTER TABLE public.customer ADD COLUMN address jsonb;
ALTER TABLE public.store ADD COLUMN address jsonb;
ALTER TABLE public.staff ADD COLUMN address jsonb;
do
$$
declare
addr_count integer;
addr_store integer[]=(select array_agg(address_id) from public.store);
addr_staff integer[]=(select array_agg(address_id) from public.staff);
addr jsonb;
begin
SELECT count(*) INTO addr_count FROM address;
for counter in 1..addr_count loop
SELECT jsonb_build_object(
'address_id', a.address_id,
'address', a.address,
'address2', a.address2,
'district', a.district,
'city_id', a.city_id,
'city', ci.city,
'country', co.country,
'postal_code', a.postal_code,
'phone', a.phone
) INTO addr
from address a
join city ci ON ci.city_id=a.city_id
join country co ON co.country_id=ci.country_id
where a.address_id=counter;
IF counter=ANY(addr_store) then
UPDATE public.store SET address=addr
where address_id=counter;
ELSIF counter=ANY(addr_staff) then
UPDATE public.staff SET address=addr
where address_id=counter;
ELSE
UPDATE public.customer SET address=addr
where address_id=counter;
END IF;
end loop;
end;
$$;
YugabyteDB Voyagerを使用したマイグレーションを、以下のコマンドで完了します。
yb-voyager end migration --export-dir ./export \
--backup-log-files=true \
--backup-data-files=true \
--backup-schema-files=true \
--save-migration-reports=true \
--backup-dir ./backup
前のステップでは、分散SQLに適したデータモデルにするために、3つの変更を行いました。
以上の変更によって、dvdrentalデータベースのテーブルスキーマは以下のようになっています。 こちらにあるPostgreSQLチュートリアルのER図と比較すると、テーブルの数が15から9に減り、シンプルになっていることがわかります。
各テーブルのクエリが、どのように変化したかを確認しましょう。
explain (analyze, dist, costs off)
をつけて実行計画を確認してください。–-original query for the existing data model
SELECT f.title, f.description FROM film f
JOIN language l ON f.language_id=l.language_id
JOIN film_category fc ON f.film_id=fc.film_id
JOIN category c ON fc.category_id=c.category_id
WHERE c.name='Action'
AND f.release_year > 2000
AND f.rating IN ('G','PG')
AND l.name='English';
--updated query for the new data model
select title, description from film
where category='Action'
AND release_year > 2000
AND rating IN ('G','PG')
AND language='English';
explain (analyze, dist, costs off)
をつけて実行計画を確認してください。–-original query for the existing data model
SELECT r.rental_id, r.rental_date, r.return_date,
p.payment_date, p.amount from payment p
JOIN rental r ON r.rental_id=p.rental_id
WHERE r.rental_id=1395
ORDER BY p.payment_date DESC;
--updated query for the new data model
SELECT r.rental_id, r.rental_date, r.return_date,
p.payment_date, p.amount from new_payment p
JOIN rental r ON r.rental_id=p.rental_id
WHERE r.rental_id=1395
ORDER BY p.payment_date DESC;
explain (analyze, dist, costs off)
をつけて実行計画を確認してください。–-original query for the existing data model
INSERT INTO payment
(customer_id, staff_id, rental_id, amount,payment_date)
VALUES (43,1,123,50.0,now());
--updated query for the new data model
INSERT INTO new_payment
(customer_id, staff_id, rental_id, amount,payment_date)
VALUES (43,1,123,50.0,now());
explain (analyze, dist, costs off)
をつけて実行計画を確認してください。–-original query for the existing data model
SELECT co.country, ci.city,
a.address, a.district, a.postal_code,
c.first_name, c.last_name from customer c
JOIN address a ON c.address_id=a.address_id
JOIN city ci ON ci.city_id=a.city_id
JOIN country co ON co.country_id=ci.country_id
JOIN store s ON s.store_id=c.store_id
WHERE s.store_id=1;
--updated query for the new data model
SELECT first_name,last_name,address from customer
WHERE store_id=1;
filmテーブルとcustomer (address) テーブルの変更については、実行計画がよりシンプルになりクエリ実行が効率的になっていることが確認できるはずです。
一方で、paymentテーブルについてはselectの実行計画には大きな変化がみられません。不要なインデックスが削除されたことで、新規レコードを追加するinsertではクエリ実行がより効率的になっていることが確認できます。
以上で、データモデルを変更した効果の確認は完了です。
YugabyteDB VoyagerによるPostgreSQLからYugabyteDBへの移行ハンズオンが完了しました!
データモデルやクエリパターンに合わせたデータの分散方法を設計することで、スケールやパフォーマンスの目標に合わせたデータの配置になることが、実行計画から確認できました。
以下のハンズオンも実施してみてください。