行値式で複数カラムの組み合わせに対して一度に条件を指定する(PostgreSQL)
結論
- 行値式を使うと複数カラムの組み合わせに対して一度に条件を指定できる
- サブクエリとの組み合わせが便利
課題
テーブル定義とユースケース
以下のようなテーブルがあったとして、
$ cat ddl.sql drop table if exists test; drop index if exists test_a_b_index; create table test ( id integer primary key, a integer not null, b integer not null ); create index test_a_b_index on test (a, b);
次のような状況を考えてみる。
- アプリケーションでは id を入力として受け取り、それを元にレコードを検索する
- 該当レコードと a, b の値が一致するレコードを一斉に操作したい
サンプルデータ
- 括弧内は左から id, a, b
- a, b が (1, 1) となるレコードが 6 件ある
- id=1 で検索して前述の 6 件が抽出されるような SQL を考えてみる
$ head -n5 dml.sql && tail -n5 dml.sql insert into test values (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (50001, 1, 1), (50002, 1, 1), (50003, 1, 1), (50004, 1, 1), (50005, 1, 1);
解決策
いずれも PostgreSQL 15.3 での確認結果に基づく。
(1) 行値式を使わない場合
まずは行値式を使わない場合。ここではやりたいことを分かりやすく示すためにサブクエリを使ってるが、他にもやりようはありそうです(愚直にクエリ実行を 2 回に分ける、join で頑張る等)
select * from test where a = (select a from test where id = 1) and b = (select b from test where id = 1)
(2) 行値式を使う場合
こちらが今回の主題。in 句の評価式に注目。(a, b)
が行値式。
- サブクエリが返す列の数と順番は行値式と一致している必要がある
- in 句ではなく他の比較演算子、例えば
=
でも動く。ただしその場合はサブクエリが返す結果が 1 件である必要がある(結果が複数の場合はエラーになる) - a, b の複合インデックスもちゃんと効く
select * from test where (a, b) in ( select a, b from test where id = 1 )
in 句の値の指定はサブクエリじゃなくても OK。検索値が明確な場合は以下のように列挙すればよい
select * from test where (a, b) in ( (1, 1), (2, 2), (3, 3) )
ノート
ファイル一式
pg-row-constructor.zip - Google ドライブ
確認用の Docker コンテナ
起動
- POSTGRES_HOST_AUTH_METHOD に trust を指定している点に注意
- セキュリティが重要な場面では適切な認証を設定してください
$ docker run -d \ --name postgres-test \ -p 127.0.0.1:5432:5432 \ -e POSTGRES_HOST_AUTH_METHOD=trust \ -v data:/var/lib/postgresql/data \ postgres:15.3
停止・削除
$ docker stop postgres-test $ docker rm postgres-test
テーブル作成・データ投入
$ go run dml.go > dml.sql $ psql -h localhost -U postgres -f ddl.sql -f dml.sql
抽出結果
(1)
$ psql -h localhost -U postgres -f select-1.sql id | a | b -------+---+--- 1 | 1 | 1 50001 | 1 | 1 50002 | 1 | 1 50003 | 1 | 1 50004 | 1 | 1 50005 | 1 | 1 (6 rows)
(2)
$ psql -h localhost -U postgres -f select-2.sql id | a | b -------+---+--- 1 | 1 | 1 50001 | 1 | 1 50002 | 1 | 1 50003 | 1 | 1 50004 | 1 | 1 50005 | 1 | 1 (6 rows)
explain の結果
(1)
$ psql -h localhost -U postgres -f explain-1.sql QUERY PLAN -------------------------------------------------------------------------------------------- Index Scan using test_a_b_index on public.test (cost=16.90..24.92 rows=1 width=12) Output: test.id, test.a, test.b Index Cond: ((test.a = $0) AND (test.b = $1)) InitPlan 1 (returns $0) -> Index Scan using test_pkey on public.test test_1 (cost=0.29..8.31 rows=1 width=4) Output: test_1.a Index Cond: (test_1.id = 1) InitPlan 2 (returns $1) -> Index Scan using test_pkey on public.test test_2 (cost=0.29..8.31 rows=1 width=4) Output: test_2.b Index Cond: (test_2.id = 1) (11 rows)
(2)
$ psql -h localhost -U postgres -f explain-2.sql QUERY PLAN ------------------------------------------------------------------------------------------ Nested Loop (cost=0.58..16.63 rows=1 width=12) Output: test.id, test.a, test.b -> Index Scan using test_pkey on public.test test_1 (cost=0.29..8.31 rows=1 width=8) Output: test_1.id, test_1.a, test_1.b Index Cond: (test_1.id = 1) -> Index Scan using test_a_b_index on public.test (cost=0.29..8.31 rows=1 width=12) Output: test.id, test.a, test.b Index Cond: ((test.a = test_1.a) AND (test.b = test_1.b)) (8 rows)