PostgreSQL 本格入門

■ 日本PostgreSQLユーザ会
※ The PostgreSQL Global Development Group の和訳
■ PostgresQL の整備
Docker コンテナの作成
Claude3 が出力(https://claude.ai/chat/9c8e2631-4466-4423-a5d1-628f6821730a)
version: '3.8'
services:
db:
image: postgres:15
container_name: postgres_test
environment:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
▽ 作ったDBに入り込むコマンド(psql)
docker exec -it postgres_test psql -U testuser -d testdb

■ 言語の整理
- SQL: DBを操作するための言語。
- DBEngine: DBを操作するエンジン。SQLを解釈してデータを実際に操作する人。システム。
- DBMS:DB管理システム。 サーバ型やアプリケーションに組み込むタイプなど。インタフェース。
- RDB: リレーショナル・データベース。
SQLiteなどは、サーバ型でない典型例。
主なやり取りはここ。(https://claude.ai/chat/e54c727c-14cd-4a37-a535-9a92c75eeb5a)
■ その他
▽ マスタとは?

■ SQLのキーワード(自分用)
■ MySQL と PostgreSQL のコマンド対応表
PostgreSQLのほうが短いコマンドになっている分、ショートカットっぽい。
(学習コストは高め)

■ Row Level Security の検証
所属する組織が合致する人間のみデータを取得・更新・削除できるようにする。
▽ テーブルの作成
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title TEXT,
content TEXT,
org_codes TEXT[] -- 組織コードの配列
);
▽ テストデータの挿入
INSERT INTO documents (title, content, org_codes)
VALUES
('Document 1', 'Content 1', ARRAY['ORG1']),
('Document 2', 'Content 2', ARRAY['ORG2']),
('Document 3', 'Content 3', ARRAY['ORG3']),
('Document 4', 'Content 4', ARRAY['ORG1', 'ORG2']),
('Document 5', 'Content 5', ARRAY['ORG1', 'ORG3']),
('Document 6', 'Content 6', ARRAY['ORG2', 'ORG3']),
('Document 7', 'Content 7', ARRAY['ORG1', 'ORG2', 'ORG3']);
▽ RLSを有効化しポリシーを貼る。
-- RLSの有効化
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Policy の作成
CREATE POLICY documents_org_policy ON documents
FOR ALL
USING (
-- ユーザーの組織コードが文書の組織コードをすべて満たしている場合にアクセス可能
string_to_array(current_setting('app.current_org_codes'), ',') @> org_codes
);
-- 作成した Policy の確認
select * from pg_policies where tablename = 'documents';
■ policy を And条件でつなぎたい時
デフォルトでは、ORでつながってしまうので注意
-- AS RESTRICTIVE をつける
CREATE POLICY documents_org_policy ON documents AS RESTRICTIVE
FOR ALL
USING (
string_to_array(current_setting('app.current_org_codes'), ',') @> org_codes
);
■ 注意事項
スーパーユーザはBypassされてしまう。そのため、新規ユーザを作成する必要あり。
スーパーユーザ、およびBYPASSRLS属性のあるロールは、テーブルへのアクセス時に、常に行セキュリティシステムを無視します。 テーブルの所有者も通常は行セキュリティを無視しますが、ALTER TABLE ... FORCE ROW LEVEL SECURITYにより、テーブルの所有者も行セキュリティの対象となることができます。

■ RLSでの index 検証
- 複数のポリシーを設定時にインデックスが正しく働くかの検証
- 配列と文字列(セッション変数が配列でテーブル側が文字列)比較ポリシーの検証
▽ テーブルの準備
-- 検証用テーブルの作成
CREATE TABLE saas_users (
id SERIAL PRIMARY KEY,
tenant varchar,
name TEXT,
email TEXT,
org varchar,
date TEXT
);
-- データの大量投入(テナント10種、組織10種の計10万件)
\copy saas_users FROM 'sample_data.csv' with (FORMAT csv, HEADER);
-- rls 有効化 (テーブル所有者を含める)
ALTER TABLE saas_users enable row level security;
ALTER TABLE saas_users force row level security;
▼ 設定した index のパターン
Explain analyze 実行時に利用されたIndexが出現するので、とりあえず全部貼った。
複合がない場合は2つのインデックスがANDで利用されていることが確認できる。
-- index: 単一バージョン
CREATE INDEX idx_org ON saas_users (org);
CREATE INDEX idx_tenant ON saas_users (tenant);
-- index: 複合バージョン
CREATE INDEX idx_tenant_org ON saas_users (tenant, org);
CREATE INDEX idx_org_tenant ON saas_users (org, tenant); -- 最も利用された
データにもよりけりだが、基本的には複合のほうが早いと思う。
※ 必要に応じて検証してください。
▼ Policyのパターン
下記のような話をWEBで見たので、これらを意識した検証を作成
- 配列と文字列比較の場合、ポリシーの記述でインデックスが 有効/無効 が変わる
- string_to_array が text型を返すのでインデックスが効かない
-- tenant policy
CREATE POLICY tenant_isolation_policy ON saas_users
USING (
(tenant = current_setting('app.current_tenant_id'::text))
);
-- 1. orgの型がvarchar, text 型の場合
CREATE POLICY org_access_policy ON saas_users AS RESTRICTIVE
USING (
org = ANY(string_to_array(current_setting('app.allowed_orgs'), ','))
);
-- 2. orgの型がbpchar 型の場合
CREATE POLICY org_access_policy ON saas_users AS RESTRICTIVE
USING (
org = ANY(cast(string_to_array(current_setting('app.allowed_orgs'), ',') as bpchar[]))
);
-- 3. サブクエリ (Claude3の初期案)
CREATE POLICY org_access_policy ON saas_users AS RESTRICTIVE
USING (
EXISTS (
SELECT 1
FROM unnest(string_to_array(current_setting('app.allowed_orgs'), ',')) allowed_org
WHERE saas_users.org = allowed_org
)
);
結果として、bpchar型とvarchar型による差異はなし。サブクエリは遅いので不採用。
記事は過去バージョンなのでそれが理由かな。結論、varchar-キャストなしのANYで良い。
▽検証用のメモ
-- セッション変数
SET app.current_tenant_id = 'current_tenant';
SET app.allowed_orgs = 'org1,org3';
-- bypass権限のないユーザで実行
EXPLAIN ANALYZE select * from saas_users;
-- べき等性担保
DROP POLICY IF EXISTS org_access_policy ON saas_users;
DROP POLICY IF EXISTS tenant_isolation_policy ON saas_users;
DROP INDEX IF EXISTS idx_org;
DROP INDEX IF EXISTS idx_tenant;
DROP INDEX IF EXISTS idx_tenant_org;
DROP INDEX IF EXISTS idx_org_tenant;
DROP TABLE IF EXISTS saas_users;

■ 比較演算子検証
要件: セッションユーザの権限にない組織コードが少なくとも1つ以上存在する場合に、
セレクトできてはいけない。
検証事項: @>
or <@
どっちが正しいか?
-- **************************************
-- 0. 事前準備
-- **************************************
\echo '■ 0. テスト準備開始'
-- テスト用のテーブルを作成
CREATE TABLE test_table (
id serial PRIMARY KEY,
title text,
content text,
org_codes text[]
);
-- 'テーブルにデータを挿入';
INSERT INTO test_table (title, content, org_codes) VALUES
('パターン0', '単一組織', ARRAY['org_0']),
('パターン1', '単一組織', ARRAY['org_1']),
('パターン2', '単一組織', ARRAY['org_2']),
('パターン3', '複数組織(2)', ARRAY['org_0', 'org_1']),
('パターン4', '複数組織(2)', ARRAY['org_0', 'org_2']),
('パターン5', '複数組織(2)', ARRAY['org_1', 'org_2']),
('パターン6', '複数組織(3)', ARRAY['org_0', 'org_1', 'org_2']);
-- RLSを有効化
ALTER TABLE test_table ENABLE ROW LEVEL SECURITY;
-- ポリシーを設定
-- Policy: レコードが複数の組織を持つテーブル (単一ポリシーのため、RESTRICTIVEは除去)
CREATE POLICY test_policy on test_table
FOR ALL
USING (
string_to_array(current_setting('app.session_org_code'), ',') @> org_codes
);
-- テスト用のロールを作成
CREATE ROLE test_user;
GRANT SELECT ON test_table TO test_user;
\echo ''
-- **************************************
-- 1. テスト開始
-- **************************************
\echo '■ 1. テスト開始'
-- スーパーユーザーとして全データを確認
\echo '⇒ [DB 確認]: スーパーユーザでのテーブルフルスキャン';
SELECT * FROM test_table;
-- test_userロールに切り替え
\echo '⇒ [ロール切り替え]: セッション変数未定義のため error';
SET ROLE test_user;
SELECT * FROM test_table; -- error になる
\echo ''
-- case 1: 単一権限ユーザ
\echo '⇒ [case1: 単一権限] org_0 を持つ場合';
SET app.session_org_code = 'org_0';
SELECT * FROM test_table;
-- case 2: 複数権限ユーザ
\echo '⇒ [case2: 複数権限] org_0,org_1 を持つ場合';
SET app.session_org_code = 'org_0,org_1';
SELECT * FROM test_table;
-- case3: 全権限ユーザ
\echo '⇒ [case3: 全権限] org_0,org_1,org_2 を持つ場合';
SET app.session_org_code = 'org_0,org_1,org_2';
SELECT * FROM test_table;
\echo ''
-- **************************************
-- 2. 事後処理
-- **************************************
\echo '■ 2. 事後処理'
RESET ROLE; -- スーパーユーザに戻す
DROP TABLE test_table;
DROP ROLE test_user;
(https://claude.ai/chat/3bb6234f-9d6e-498b-b88e-fcaa211603a1)
■ 実行コマンド
cat test-compare.sql | docker exec -i postgres_test psql -U testuser -d testdb

■ SuperUserであることの確認
-- PostgreSQL ユーザのテーブルを閲覧
select * from pg_user where usename = 'username'
-- or current_setting関数を使用
select current_setting('is_superuser');
superuserの場合、RLSは機能しない。そのため、権限確認用のカラムで確認する。(計算させる)

めも:
せっと・せっしょん・ユーザ周り等に関しての質疑(https://claude.ai/chat/04794f54-5003-4ecf-bcde-3303374b3054)

■ CSVによる大量データ投入の方法
- Python による CSVデータ作成
- psql コマンドによる コピー
(https://claude.ai/chat/ad931a5b-d27b-4e49-b1be-7b9aaf0f9297)
▽ 環境用意
version: '3.8'
services:
python:
build:
context: .
dockerfile: Dockerfile.python
volumes:
- ./src:/app/src
- ./output:/app/output
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "./src/main.py"]
faker==19.3.0
pandas==2.1.0
numpy==1.24.3
▽ DB投入用CSV作成スクリプト
from faker import Faker
import pandas as pd
import random
fake = Faker()
print("CSV生成を開始します...")
# tenant と org の候補を事前に用意
tenants = [f'Company {i}' for i in range(10)] # Company 0-9
orgs = [f'org{i}' for i in range(10)] # org0-9
data = []
for i in range(100000):
data.append({
'id': i,
'tenant': random.choice(tenants), # ランダムに選択
'name': fake.name(),
'email': fake.email(),
'org': random.choice(orgs), # ランダムに選択
'date': fake.date()
})
if i % 100 == 0:
print(f"{i}件のデータを生成しました")
df = pd.DataFrame(data)
output_file = '/app/output/sample_data.csv'
df.to_csv(output_file, index=False)
print(f"CSV生成が完了しました: {output_file}")
▽ 投入用コマンド
\copy target_table FROM 'sample_data.csv' with (FORMAT csv, HEADER);
上記のコマンドを利用することで、ファイルパーミッションを気にせずDBにコピーできる
※ csv ファイルはコンテナが参照できる必要あり