{
    "componentChunkName": "component---src-templates-post-js",
    "path": "/postgresqlwoshi-tutamarutitenantodetawojian-zhi-su/",
    "result": {"data":{"ghostPost":{"id":"Ghost__Post__610e30223986b000013a5503","title":"PostgreSQLを使ったマルチテナントデータを見直す","slug":"postgresqlwoshi-tutamarutitenantodetawojian-zhi-su","featured":false,"feature_image":"https://ghost.tech.anti-pattern.co.jp/content/images/2021/08/29791364_1246754985456610_5906763196705800192_n-9.jpg","excerpt":"こんにちは、Anti-Patternの塚本です。\n\n突然ですが、みなさんマルチテナントアーキテクチャってご存知ですか？\n\n複数の企業データを扱うシステムで、リソースや運用コストなどを最大限に抑えて管理することなのですが、これを検討するのは大変です。\n\nこのブログは、あるマルチテナントデータの見直しについて、書きたいと思います。\n\nでは、どんな方法があるのでしょう？\n\n1.複数データベース（単一インスタンス）\n\nテナント毎に物理的にインスタンスを分けたデータベース管理\n\n2.単一データベース＋複数スキーマ\n\n物理的に一つのインスタンスで同じデータベースを、テナント毎にスキーマを分けて管理\n\n3.単一データベース＋単一スキーマ\n\n物理的に一つのインスタンスで全テナントを、一つのデータベースで管理する\n\nそれぞれ、メリット・デメリットはありそうです。安全性を考えると、1.が良さそうですが、お金かかりそうですよね。また、システムの規模、テナント数とかにも左右されそうです。\n\n私が携わったシステムでは、運用から数年でこんな症状が出始めました。\n\n * DB全体の容量増加\n * 運用コストの増加\n","custom_excerpt":null,"visibility":"public","created_at_pretty":"07 August, 2021","published_at_pretty":"11 November, 2020","updated_at_pretty":"07 August, 2021","created_at":"2021-08-07T16:02:58.000+09:00","published_at":"2020-11-11T16:07:00.000+09:00","updated_at":"2021-08-07T16:07:50.000+09:00","meta_title":null,"meta_description":null,"og_description":null,"og_image":null,"og_title":null,"twitter_description":null,"twitter_image":null,"twitter_title":null,"authors":[{"name":"takeshi tsukamoto","slug":"zhong","bio":null,"profile_image":null,"twitter":null,"facebook":null,"website":null}],"primary_author":{"name":"takeshi tsukamoto","slug":"zhong","bio":null,"profile_image":null,"twitter":null,"facebook":null,"website":null},"primary_tag":null,"tags":[],"plaintext":"こんにちは、Anti-Patternの塚本です。\n\n突然ですが、みなさんマルチテナントアーキテクチャってご存知ですか？\n\n複数の企業データを扱うシステムで、リソースや運用コストなどを最大限に抑えて管理することなのですが、これを検討するのは大変です。\n\nこのブログは、あるマルチテナントデータの見直しについて、書きたいと思います。\n\nでは、どんな方法があるのでしょう？\n\n1.複数データベース（単一インスタンス）\n\nテナント毎に物理的にインスタンスを分けたデータベース管理\n\n2.単一データベース＋複数スキーマ\n\n物理的に一つのインスタンスで同じデータベースを、テナント毎にスキーマを分けて管理\n\n3.単一データベース＋単一スキーマ\n\n物理的に一つのインスタンスで全テナントを、一つのデータベースで管理する\n\nそれぞれ、メリット・デメリットはありそうです。安全性を考えると、1.が良さそうですが、お金かかりそうですよね。また、システムの規模、テナント数とかにも左右されそうです。\n\n私が携わったシステムでは、運用から数年でこんな症状が出始めました。\n\n * DB全体の容量増加\n * 運用コストの増加\n * 性能劣化\n\nそのため、マルチテナントデータの見直しに着手しました。\n\n私が行ったのは、“単一データベース＋複数スキーマ”から\n\n“単一データベース＋単一スキーマ”への変更です。\n\nセキュリティの担保\n複数スキーマの各テナントデータを、単一スキーマに統合するためにデータを寄せる必要があります。そのために、PostgreSQLの行セキュリティポリシー(ROW\nLEVEL SECURITY)を利用しました。\n\n> https://www.postgresql.jp/docs/10/ddl-rowsecurity.html\n\n\nCREATE POLICY sample_table_policy_view ON sample_table\n  FOR ALL\n  USING (tenant_id = current_tenant_id())\n  WITH CHECK (tenant_id = current_tenant_id());\nALTER TABLE sample_table ENABLE ROW LEVEL SECURITY;\n\n\nsample_tableには、tenant_id(テナントID)を設定しています。tenant_idはDBのroleと一致します。\n\nそのため、テナントが接続した際にコンテキストのユーザ名（CURRENT_USER）からtenant_idをcurrent_tenant_id()で取得しできる仕組みです。\n\nUSINGでデータ参照のできる範囲、WITH CHECKでデータ参照、データ更新の範囲が抑止されています。\n\nテナント毎のスキーマ定義で実現していたセキュリティは、これで担保することができました。\n\n大量データの対応\n複数スキーマを統合したことにより、データ量が肥大化したテーブルが幾つかできてしまいました。トランザクションデータが格納されるテーブルのため、更にデータが増加する可能性が高く、Indexだけは対応できません。そのため、テーブルのパーティショニングを利用しました。\n\n> https://www.postgresql.jp/document/10/html/ddl-partitioning.html\n\n\nCREATE TABLE parent_table (\n id bigserial,\n tenant_id integer DEFAULT current_tenant_id() NOT NULL,\n partition_id SMALLINT NOT NULL DEFAULT partition_id(current_tenant_id())\n) PARTITION BY LIST (partition_id);\n\n\nPARTITION BY LISTで分割キーを指定します。\n\nCREATE POLICY partition_parent_table_view ON parent_table\n  FOR ALL\n  USING (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()))\n  WITH CHECK (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()));\nALTER TABLE parent_table ENABLE ROW LEVEL SECURITY;\n\n\nまずは、親テーブル（継承元）に対してRLSを設定します。\n\n-- 分割キー(partition_id)の値(0)格納するテーブル\nCREATE TABLE partition_0 PARTITION OF parent_table FOR VALUES IN (0);\nCREATE POLICY partition_parent_table_view ON partition_0\n  FOR ALL\n  USING (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()))\n  WITH CHECK (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()));\nALTER TABLE partition_0 ENABLE ROW LEVEL SECURITY;\n-- 分割キー(partition_id)の値(1)格納するテーブル\nCREATE TABLE partition_1 PARTITION OF parent_table FOR VALUES IN (1);\nCREATE POLICY partition_parent_table_view ON partition_1\n  FOR ALL\n  USING (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()))\n  WITH CHECK (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()));\nALTER TABLE partition_1 ENABLE ROW LEVEL SECURITY;\n-- 分割キー(partition_id)の値(2)格納するテーブル\nCREATE TABLE partition_2 PARTITION OF parent_table FOR VALUES IN (2);\nCREATE POLICY partition_parent_table_view ON partition_2\n  FOR ALL\n  USING (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()))\n  WITH CHECK (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()));\nALTER TABLE partition_2 ENABLE ROW LEVEL SECURITY;\n\n\n分割キー(partition_id)の値が0の値を格納する、子テーブル（partition_0）を作成しRLSを設定します。分割キーは、partition_id(current_tenant_id())で取得します。同様に分割キー(partition_id)の値が1、2\nの値を格納する子テーブルをそれぞれ作成します。\n\nこの設定で、tenant_idから計算されたパーティションテーブルへデータが格納されます。そして、行セキュリティポリシーに従ったデータ参照、更新が可能となりました。\n\nそしてチューニング\nデータを一つのスキーマに寄せた後が大変でした。\n\n実行計画を確認すると、ビックリする程、高コストだったり、Indexが効かない、設定しても逆にコストがかかったりと。\n\n多段に ネストするクエリの実行計画・・・\n\n一部を除き、何とかなるところまでチューニングを繰り返しました。\n\nさらっと書いてますが、大変でした！\n\n大変でしたが、収穫も大きく。\n\n後輩が実行計画を読んで、クエリをチューニングし、Index設定をしている姿はちょっと感動しました。（初めての挑戦です）\n\nそれと、このブログに書いた内容は、PostgreSQL有識者の永安氏に大変お世話になってます。ありがとうございました！！\n\n> http://pgsqldeepdive.blogspot.com/\n\nhttps://www.slideshare.net/uptimejp/postgresql-6872500","html":"<!--kg-card-begin: markdown--><p>こんにちは、Anti-Patternの塚本です。</p>\n<p>突然ですが、みなさんマルチテナントアーキテクチャってご存知ですか？</p>\n<p>複数の企業データを扱うシステムで、リソースや運用コストなどを最大限に抑えて管理することなのですが、これを検討するのは大変です。</p>\n<p>このブログは、あるマルチテナントデータの見直しについて、書きたいと思います。</p>\n<p>では、どんな方法があるのでしょう？</p>\n<p>1.複数データベース（単一インスタンス）</p>\n<p>テナント毎に物理的にインスタンスを分けたデータベース管理</p>\n<p>2.単一データベース＋複数スキーマ</p>\n<p>物理的に一つのインスタンスで同じデータベースを、テナント毎にスキーマを分けて管理</p>\n<p>3.単一データベース＋単一スキーマ</p>\n<p>物理的に一つのインスタンスで全テナントを、一つのデータベースで管理する</p>\n<p>それぞれ、メリット・デメリットはありそうです。安全性を考えると、1.が良さそうですが、お金かかりそうですよね。また、システムの規模、テナント数とかにも左右されそうです。</p>\n<p>私が携わったシステムでは、運用から数年でこんな症状が出始めました。</p>\n<ul>\n<li>DB全体の容量増加</li>\n<li>運用コストの増加</li>\n<li>性能劣化</li>\n</ul>\n<p>そのため、マルチテナントデータの見直しに着手しました。</p>\n<p>私が行ったのは、“単一データベース＋複数スキーマ”から</p>\n<p>“単一データベース＋単一スキーマ”への変更です。</p>\n<h3 id=\"%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E3%81%AE%E6%8B%85%E4%BF%9D\">セキュリティの担保</h3>\n<p>複数スキーマの各テナントデータを、単一スキーマに統合するためにデータを寄せる必要があります。そのために、PostgreSQLの行セキュリティポリシー(ROW LEVEL SECURITY)を利用しました。</p>\n<blockquote>\n<p><a href=\"https://www.postgresql.jp/docs/10/ddl-rowsecurity.html\">https://www.postgresql.jp/docs/10/ddl-rowsecurity.html</a></p>\n</blockquote>\n<pre><code>CREATE POLICY sample_table_policy_view ON sample_table\n  FOR ALL\n  USING (tenant_id = current_tenant_id())\n  WITH CHECK (tenant_id = current_tenant_id());\nALTER TABLE sample_table ENABLE ROW LEVEL SECURITY;\n</code></pre>\n<p>sample_tableには、tenant_id(テナントID)を設定しています。tenant_idはDBのroleと一致します。</p>\n<p>そのため、テナントが接続した際にコンテキストのユーザ名（CURRENT_USER）からtenant_idをcurrent_tenant_id()で取得しできる仕組みです。</p>\n<p>USINGでデータ参照のできる範囲、WITH CHECKでデータ参照、データ更新の範囲が抑止されています。</p>\n<p>テナント毎のスキーマ定義で実現していたセキュリティは、これで担保することができました。</p>\n<h3 id=\"%E5%A4%A7%E9%87%8F%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%AF%BE%E5%BF%9C\">大量データの対応</h3>\n<p>複数スキーマを統合したことにより、データ量が肥大化したテーブルが幾つかできてしまいました。トランザクションデータが格納されるテーブルのため、更にデータが増加する可能性が高く、Indexだけは対応できません。そのため、テーブルのパーティショニングを利用しました。</p>\n<blockquote>\n<p><a href=\"https://www.postgresql.jp/document/10/html/ddl-partitioning.html\">https://www.postgresql.jp/document/10/html/ddl-partitioning.html</a></p>\n</blockquote>\n<pre><code>CREATE TABLE parent_table (\n id bigserial,\n tenant_id integer DEFAULT current_tenant_id() NOT NULL,\n partition_id SMALLINT NOT NULL DEFAULT partition_id(current_tenant_id())\n) PARTITION BY LIST (partition_id);\n</code></pre>\n<p>PARTITION BY LISTで分割キーを指定します。</p>\n<pre><code>CREATE POLICY partition_parent_table_view ON parent_table\n  FOR ALL\n  USING (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()))\n  WITH CHECK (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()));\nALTER TABLE parent_table ENABLE ROW LEVEL SECURITY;\n</code></pre>\n<p>まずは、親テーブル（継承元）に対してRLSを設定します。</p>\n<pre><code>-- 分割キー(partition_id)の値(0)格納するテーブル\nCREATE TABLE partition_0 PARTITION OF parent_table FOR VALUES IN (0);\nCREATE POLICY partition_parent_table_view ON partition_0\n  FOR ALL\n  USING (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()))\n  WITH CHECK (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()));\nALTER TABLE partition_0 ENABLE ROW LEVEL SECURITY;\n-- 分割キー(partition_id)の値(1)格納するテーブル\nCREATE TABLE partition_1 PARTITION OF parent_table FOR VALUES IN (1);\nCREATE POLICY partition_parent_table_view ON partition_1\n  FOR ALL\n  USING (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()))\n  WITH CHECK (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()));\nALTER TABLE partition_1 ENABLE ROW LEVEL SECURITY;\n-- 分割キー(partition_id)の値(2)格納するテーブル\nCREATE TABLE partition_2 PARTITION OF parent_table FOR VALUES IN (2);\nCREATE POLICY partition_parent_table_view ON partition_2\n  FOR ALL\n  USING (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()))\n  WITH CHECK (tenant_id = current_tenant_id() AND\n         partition_id = partition_id(current_tenant_id()));\nALTER TABLE partition_2 ENABLE ROW LEVEL SECURITY;\n</code></pre>\n<p>分割キー(partition_id)の値が0の値を格納する、子テーブル（partition_0）を作成しRLSを設定します。分割キーは、partition_id(current_tenant_id())で取得します。同様に分割キー(partition_id)の値が1、2 の値を格納する子テーブルをそれぞれ作成します。</p>\n<p>この設定で、tenant_idから計算されたパーティションテーブルへデータが格納されます。そして、行セキュリティポリシーに従ったデータ参照、更新が可能となりました。</p>\n<h3 id=\"%E3%81%9D%E3%81%97%E3%81%A6%E3%83%81%E3%83%A5%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0\">そしてチューニング</h3>\n<p>データを一つのスキーマに寄せた後が大変でした。</p>\n<p>実行計画を確認すると、ビックリする程、高コストだったり、Indexが効かない、設定しても逆にコストがかかったりと。</p>\n<p>多段に ネストするクエリの実行計画・・・</p>\n<p>一部を除き、何とかなるところまでチューニングを繰り返しました。</p>\n<p>さらっと書いてますが、大変でした！</p>\n<p>大変でしたが、収穫も大きく。</p>\n<p>後輩が実行計画を読んで、クエリをチューニングし、Index設定をしている姿はちょっと感動しました。（初めての挑戦です）</p>\n<p>それと、このブログに書いた内容は、PostgreSQL有識者の永安氏に大変お世話になってます。ありがとうございました！！</p>\n<blockquote>\n<p><a href=\"http://pgsqldeepdive.blogspot.com/\">http://pgsqldeepdive.blogspot.com/</a></p>\n<p><a href=\"https://www.slideshare.net/uptimejp/postgresql-6872500\">https://www.slideshare.net/uptimejp/postgresql-6872500</a></p>\n</blockquote>\n<!--kg-card-end: markdown-->","url":"https://ghost.tech.anti-pattern.co.jp/postgresqlwoshi-tutamarutitenantodetawojian-zhi-su/","canonical_url":null,"uuid":"e8b68513-afc5-48d4-be9e-173264be745f","page":null,"codeinjection_foot":null,"codeinjection_head":null,"codeinjection_styles":null,"comment_id":"610e30223986b000013a5503","reading_time":3}},"pageContext":{"slug":"postgresqlwoshi-tutamarutitenantodetawojian-zhi-su"}},
    "staticQueryHashes": ["176528973","2358152166","2561578252","2731221146","4145280475"]}