このチュートリアルでは、Azure Database for PostgreSQL とエラスティック クラスターを使用して、水平スケールアウトのメリットを得るマルチテナント アプリケーションを設計する方法について説明します。
- 前提条件
- psql ユーティリティを使用してスキーマを作成する
- ノード全体でテーブルをシャード化する
- サンプル データを取り込む
- テナント データをクエリする
- テナント間でデータを共有する
- テナントごとにスキーマをカスタマイズする
前提条件
次のいずれかの方法でエラスティック クラスターを作成します。
psql ユーティリティを使用してスキーマを作成する
psql を使用してエラスティック クラスターに接続したら、エラスティック クラスターを構成できます。 このチュートリアルでは、企業が広告キャンペーンを追跡できるアプリケーション プラットフォームを作成する手順について説明します。
Note
クラスター全体にデータを分散する場合、一意のデータ制約はそれぞれの分散「シャード(分割領域)」に限定されます。 マルチテナントの例では、テナントごとにアプリケーション データの一意性が適用されます (会社 ID など)。 このため、主キー制約と外部キー制約の分散テーブル定義には、常に会社 ID 列が含まれます。
マルチテナント企業情報を保持するテーブルと、キャンペーン用の別のテーブルを作成します。 psql コンソールで、次のコマンドを実行します。
CREATE TABLE companies (
id bigserial PRIMARY KEY,
name text NOT NULL,
image_url text,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
CREATE TABLE campaigns (
id bigserial,
company_id bigint REFERENCES companies (id),
name text NOT NULL,
cost_model text NOT NULL,
state text NOT NULL,
monthly_budget bigint,
blocked_site_urls text[],
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
PRIMARY KEY (company_id, id)
);
各キャンペーンでは、広告の掲載は有料になります。 次のコードを使用して、psql に広告テーブルを追加します。
CREATE TABLE ads (
id bigserial,
company_id bigint,
campaign_id bigint,
name text NOT NULL,
image_url text,
target_url text,
impressions_count bigint DEFAULT 0,
clicks_count bigint DEFAULT 0,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
PRIMARY KEY (company_id, id),
FOREIGN KEY (company_id, campaign_id)
REFERENCES campaigns (company_id, id)
);
最後に、広告の選択とインプレッションに基づいて統計情報を追跡します。
CREATE TABLE clicks (
id bigserial,
company_id bigint,
ad_id bigint,
clicked_at timestamp without time zone NOT NULL,
site_url text NOT NULL,
cost_per_click_usd numeric(20,10),
user_ip inet NOT NULL,
user_data jsonb NOT NULL,
PRIMARY KEY (company_id, id),
FOREIGN KEY (company_id, ad_id)
REFERENCES ads (company_id, id)
);
CREATE TABLE impressions (
id bigserial,
company_id bigint,
ad_id bigint,
seen_at timestamp without time zone NOT NULL,
site_url text NOT NULL,
cost_per_impression_usd numeric(20,10),
user_ip inet NOT NULL,
user_data jsonb NOT NULL,
PRIMARY KEY (company_id, id),
FOREIGN KEY (company_id, ad_id)
REFERENCES ads (company_id, id)
);
次を実行して、psql から新しく作成されたテーブルを表示できるようになりました。
\dt
ノード全体でテーブルをシャード化する
ここまでは、標準の Postgres テーブルを作成しましたが、最終的にはエラスティック クラスター全体に分散テーブルを作成する必要があります。 エラスティック クラスター内の分散テーブルは、ディストリビューション列で定義された値に基づいて、異なるノードにデータを格納します。 この列は、基になるワーカー ノード間の行の配置を決定するために使用されます。
分散列をcompany_idするように設定します。これはマルチテナント識別子として機能します。 psql で、次の関数を実行します。
SELECT create_distributed_table('companies', 'id');
SELECT create_distributed_table('campaigns', 'company_id');
SELECT create_distributed_table('ads', 'company_id');
SELECT create_distributed_table('clicks', 'company_id');
SELECT create_distributed_table('impressions', 'company_id');
Note
Azure Database for PostgreSQL パフォーマンス機能を使用してエラスティック クラスターを利用するには、テーブルを分散する必要があります。 テーブルとスキーマを配布しない限り、クラスター ノードは分散クエリや操作に参加しません。
サンプル データを取り込む
psql の外部では、通常のコマンド ラインでサンプル データ セットをダウンロードします。
for dataset in companies campaigns ads clicks impressions geo_ips; do
curl -O https://raw.githubusercontent.com/Azure-Samples/azure-postgresql-elastic-clusters/main/multi-tenant/${dataset}.csv
done
psql 内に戻って、データを一括読み込みします。 psql は必ず、データ ファイルをダウンロードしたところと同じディレクトリで実行してください。
SET client_encoding TO 'UTF8';
\copy companies from 'companies.csv' with csv
\copy campaigns from 'campaigns.csv' with csv
\copy ads from 'ads.csv' with csv
\copy clicks from 'clicks.csv' with csv
\copy impressions from 'impressions.csv' with csv
これで、分散テーブル内のデータがエラスティック クラスター ワーカー ノードに分散されるようになりました。
テナント データをクエリする
アプリケーションが特定の会社のデータを要求すると、データベースは適切なワーカー ノードでクエリを効率的に実行できるようになりました。 たとえば、次のクエリ (company_id = 5) では、広告とインプレッションを絞り込みます。 結果を確認するには、psql で実行してみてください。
SELECT a.campaign_id,
RANK() OVER (
PARTITION BY a.campaign_id
ORDER BY a.campaign_id, count(*) DESC
), count(*) AS n_impressions, a.id
FROM ads AS a
JOIN impressions AS i
ON i.company_id = a.company_id
AND i.ad_id = a.id
WHERE a.company_id = 5
GROUP BY a.campaign_id, a.id
ORDER BY a.campaign_id, n_impressions DESC;
テナント間でデータを共有する
これまでは、 company_idによってクラスター全体にすべてのテーブルを分散しました。 ただし、一部の種類のデータは、すべてのテナントに自然に "属している" ので、すべてのテナントディストリビューションと共に配置できます。 たとえば、広告プラットフォーム内のすべての企業は、IP アドレスの詳細に基づいて対象ユーザーの地理的情報を取得したい場合があります。
この地理的 IP 情報を保持する参照テーブルを作成します。 次のコマンドを psql で実行します。
CREATE TABLE geo_ips (
addrs cidr NOT NULL PRIMARY KEY,
latlon point NOT NULL
CHECK (-90 <= latlon[0] AND latlon[0] <= 90 AND
-180 <= latlon[1] AND latlon[1] <= 180)
);
CREATE INDEX ON geo_ips USING gist (addrs inet_ops);
次に、 geo_ips を "参照テーブル" として識別します。 クラスターでこのテーブルを管理するには、クラスター化されたすべてのワーカー ノードに同期テーブルを格納します。
SELECT create_reference_table('geo_ips');
次に、サンプル データを使用して参照テーブルを読み込みます。 データセット ファイルをダウンロードしたディレクトリから、このコマンドを必ず実行してください。
\copy geo_ips from 'geo_ips.csv' with csv
選択したテーブルを geo_ips に結合する SQL ステートメントが、すべてのノードで効率的になりました。 広告 290 で選択したすべての IP の場所を検索するには、この参加に注目してください。 psql でクエリを実行してみてください。
SELECT c.id, clicked_at, latlon
FROM geo_ips, clicks c
WHERE addrs >> c.user_ip
AND c.company_id = 5
AND c.ad_id = 290;
テナントごとのスキーマをカスタマイズする
場合によっては、個々のテナントが、他のテナントに必要のない特別な情報を格納する必要がある場合があります。 ただし、すべてのテナントは、同じデータベース スキーマを持つ共通の定義を共有します。 追加のデータはどこに配置できますか?
1 つの解決策は、PostgreSQL の JSONB のような柔軟な列型を使用することです。 スキーマには、clicks に user_data という JSONB フィールドがあります。 ある会社 (会社 5 など) は、この列を使用して、ユーザーがモバイル デバイス上にあるかどうかに関する情報を追跡できます。
こちらは、会社5がモバイルまたは従来の訪問者のうち、どちらをより多く選んでいるかを調べるために使用できるクエリです。
SELECT
user_data->>'is_mobile' AS is_mobile,
count(*) AS count
FROM clicks
WHERE company_id = 5
GROUP BY user_data->>'is_mobile'
ORDER BY count DESC;
PostgreSQL には、データの特定の部分またはサブセットのインデックスを作成できる強力な機能が含まれています。 部分インデックスを作成することで、会社 5 のクエリをさらに最適化できます。
CREATE INDEX click_user_data_is_mobile
ON clicks ((user_data->>'is_mobile'))
WHERE company_id = 5;
さらに、JSONB 列を含むクエリ全体のパフォーマンスを向上させるもう 1 つの方法は、JSONB 列内のすべての基になるキーと値に 対して GIN インデックス を作成することです。
CREATE INDEX click_user_data
ON clicks USING gin (user_data);
-- this speeds up queries like, "which clicks have the is_mobile key present in user_data?"
SELECT id
FROM clicks
WHERE user_data ? 'is_mobile'
AND company_id = 5;