- データベース
PostgreSQL配列型の使い方|unnest・ANY・array_agg早見表
PostgreSQLの配列型とは、1つの列に同じ型の値を複数まとめて持てる型で、int[] や text[] のように書きます。 値の作成は ARRAY[1,2,3] か '{1,2,3}'、検索は = ANY や @>、行への展開は unnest、行から配列への集約は array_agg が基本の4点セットです。
この記事では、配列の宣言・添字・スライス、演算子と関数(30種類以上)をPostgreSQL 16.15で実際に実行した結果つきで早見表にまとめました。さらに、10万件のテーブルでGINインデックスあり・なしを EXPLAIN ANALYZE で比較し、「= ANY(tags) ではGINが使われず @> なら使われる」という結果も載せています。Python(psycopg 3)から = ANY(%s) でリストを渡すコード、NULLと空配列の違いなど、つまずきやすい点も実際のエラー文で確認できます。
PostgreSQL配列の早見表
まずは「やりたいこと → 書き方」の早見表です。結果はすべてPostgreSQL 16.15で実行したものです。
作る・取り出す
| やりたいこと | 書き方 | 結果 |
|---|---|---|
| 列を配列型で定義する | tags text[] / scores int[] |
(DDL) |
| 配列を作る(コンストラクタ) | ARRAY[1,2,3] |
{1,2,3} |
| 配列を作る(リテラル) | '{1,2,3}'::int[] |
{1,2,3} |
| 空白・カンマを含む要素 | '{"a b","c,d",NULL}'::text[] |
{"a b","c,d",NULL} |
| サブクエリの結果を配列に | ARRAY(SELECT generate_series(1,5)) |
{1,2,3,4,5} |
| 1番目を取り出す(添字は1始まり) | (ARRAY[10,20,30])[1] |
10 |
| 範囲外の添字 | (ARRAY[10,20,30])[0] / [5] |
NULL(エラーにならない) |
| スライス(2〜4番目) | (ARRAY[10,20,30,40,50])[2:4] |
{20,30,40} |
| 先頭から2つ | [:2] |
{10,20} |
| 4番目以降 | [4:] |
{40,50} |
| 2次元配列の要素 | (ARRAY[[1,2],[3,4]])[2][1] |
3 |
| 次元の情報 | array_dims(ARRAY[[1,2],[3,4]]) |
[1:2][1:2] |
検索・比較する(演算子)
| やりたいこと | 書き方 | 結果 | GIN |
|---|---|---|---|
| どれか1つと等しい | 2 = ANY(ARRAY[1,2,3]) |
t |
使われない |
| どれとも等しくない | 5 <> ALL(ARRAY[1,2,3]) |
t |
使われない |
| 全部含む(包含) | ARRAY[1,2,3] @> ARRAY[2,3] |
t |
使われる |
| 全部含まれる | ARRAY[2] <@ ARRAY[1,2,3] |
t |
使われる |
| 1つでも共通がある(重なり) | ARRAY[1,2] && ARRAY[2,9] |
t |
使われる |
| 完全一致(順序も比較) | ARRAY[1,2] = ARRAY[2,1] |
f |
使われる |
| 連結 | ARRAY[1,2] || ARRAY[3,4] |
{1,2,3,4} |
- |
| 末尾に1つ追加 | ARRAY[1,2] || 3 |
{1,2,3} |
- |
| 先頭に1つ追加 | 0 || ARRAY[1,2] |
{0,1,2} |
- |
変更・調べる(関数)
| やりたいこと | 関数 | 結果 |
|---|---|---|
| 末尾に追加 | array_append(ARRAY[1,2],3) |
{1,2,3} |
| 先頭に追加 | array_prepend(0,ARRAY[1,2]) |
{0,1,2} |
| 値をすべて削除 | array_remove(ARRAY[1,2,1,3],1) |
{2,3} |
| 値を置き換え | array_replace(ARRAY[1,2,1],1,9) |
{9,2,9} |
| 最初の位置 | array_position(ARRAY['a','b','c','b'],'b') |
2 |
| すべての位置 | array_positions(ARRAY['a','b','c','b'],'b') |
{2,4} |
| 見つからないとき | array_position(ARRAY['a'],'z') |
NULL |
| 長さ(次元を指定) | array_length(ARRAY[1,2,3],1) |
3 |
| 全要素数 | cardinality(ARRAY[[1,2],[3,4]]) |
4 |
| 空配列の長さ | array_length('{}'::int[],1) / cardinality('{}'::int[]) |
NULL / 0 |
| 末尾からn個削る(PG14+) | trim_array(ARRAY[1,2,3,4],2) |
{1,2} |
| 同じ値で埋める | array_fill(0, ARRAY[3]) |
{0,0,0} |
| ランダムに抽出・並べ替え(PG16+) | array_sample(arr, 2) / array_shuffle(arr) |
実行ごとに変わる |
| 文字列 → 配列 | string_to_array('a,b,,c', ',') |
{a,b,"",c} |
| 空文字をNULLに | string_to_array('a,b,,c', ',', '') |
{a,b,NULL,c} |
| 配列 → 文字列 | array_to_string(ARRAY['a',NULL,'c'], '-') |
a-c(NULLは飛ばす) |
| NULLを別の文字に | array_to_string(ARRAY['a',NULL,'c'], '-', '*') |
a-*-c |
行と配列を行き来する
| やりたいこと | 書き方 | ポイント |
|---|---|---|
| 配列 → 行 | unnest(arr) |
多次元でも1次元に平らに展開 |
| 番号つきで行に | unnest(arr) WITH ORDINALITY AS t(val, idx) |
位置(1始まり)が取れる |
| 複数配列を横に並べて行に | unnest(arr1, arr2) |
短い側は NULL で埋まる |
| 行 → 配列 | array_agg(col) |
0行なら NULL |
| 並べ替えて集約 | array_agg(col ORDER BY col DESC) |
順序を保証したいときは必須 |
| 重複を除いて集約 | array_agg(DISTINCT col) |
結果は昇順 |
| NULLを除いて集約 | array_agg(col) FILTER (WHERE col IS NOT NULL) |
|
| 0行でも空配列 | COALESCE(array_agg(col), '{}') |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PostgreSQL配列型の使い方|unnest・ANY・array_agg早見表」
配列型の作り方と添字・スライス
配列は「ARRAY[...] で作る」「'{...}' で書く」の2通りがあり、添字は1から始まります。 ここからは早見表の中身を、実行結果とあわせて見ていきます。
テーブルに配列列を定義する
CREATE TABLE article (
id bigserial PRIMARY KEY,
title text,
tags text[] NOT NULL DEFAULT '{}' -- タグの配列。NULLではなく空配列を既定値にする
);
text[] の [] に数字を書いて text[3] としても、PostgreSQLは要素数を制限しません(公式ドキュメントでも「宣言したサイズや次元数は動作に影響しない」と説明されています)。長さを制限したい場合は CHECK (cardinality(tags) <= 10) のように制約で表します。
NOT NULL DEFAULT '{}' にしておくと、後で説明する「NULLと空配列の違い」によるつまずきを減らせます。
ARRAY[] と '{}' の違い
SELECT ARRAY[1,2,3] AS a, '{1,2,3}'::int[] AS b, '{"a b","c,d",NULL}'::text[] AS c;
a | b | c
---------+---------+--------------------
{1,2,3} | {1,2,3} | {"a b","c,d",NULL}
| 書き方 | 向いている場面 | 注意点 |
|---|---|---|
ARRAY[1,2,3] |
SQLの中で値や式から組み立てる | 要素は式なので ARRAY[now(), now()] も書ける |
'{1,2,3}'::int[] |
文字列で受け取った値をそのまま入れる | 空白・カンマ・波かっこを含む要素は "..." で囲む。NULL は大文字小文字問わず NULL 扱い |
添字・スライスの動き
SELECT (ARRAY[10,20,30])[1] AS first, (ARRAY[10,20,30])[0] AS zero, (ARRAY[10,20,30])[5] AS out_of;
SELECT (ARRAY[10,20,30,40,50])[2:4] AS s1, (ARRAY[10,20,30,40,50])[:2] AS s2, (ARRAY[10,20,30,40,50])[4:] AS s3;
first | zero | out_of
-------+--------+--------
10 | (null) | (null)
s1 | s2 | s3
------------+---------+---------
{20,30,40} | {10,20} | {40,50}
添字は1始まりで、範囲外はエラーではなく NULL が返ります。 プログラミング言語の感覚で [0] を書くと、エラーにならないまま NULL が返るので、結果が空になったときはまず添字を確認するとよいです。
なお、ARRAY[1,2,3][1] のようにかっこなしで添字を付けると構文エラーになります。
ERROR: syntax error at or near "["
LINE 1: SELECT ARRAY[1,2,3][1];
→ (ARRAY[1,2,3])[1] のように丸かっこで囲むと取り出せます。列名の場合は tags[1] とそのまま書けます。
配列を検索する:ANY・ALL・@>・<@・&&
「この値を含む行」を探すときは = ANY と @> のどちらでも書けますが、GINインデックスが使われるのは @> です。 意味の違いから整理します。
| 演算子 | 意味 | 左辺 | 右辺 | 例 |
|---|---|---|---|---|
x = ANY(arr) |
arr のどれかが x と等しい | 単一の値 | 配列 | 'tag42' = ANY(tags) |
x <> ALL(arr) |
arr のどれとも等しくない | 単一の値 | 配列 | 'draft' <> ALL(tags) |
a @> b |
a が b の要素をすべて含む | 配列 | 配列 | tags @> ARRAY['sql','pg'](AND検索) |
a <@ b |
a の要素がすべて b に含まれる | 配列 | 配列 | tags <@ ARRAY['sql','pg','go'](許可リスト内か) |
a && b |
1つでも共通の要素がある | 配列 | 配列 | tags && ARRAY['sql','pg'](OR検索) |
ANY は <、>、LIKE など他の比較演算子とも組み合わせられます。たとえば title LIKE ANY(ARRAY['pg%','sql%']) は「1つの値を複数のパターンと比べる」書き方で、OR を並べるより短く書けます。
順序と重複を無視して「同じ集合か」を比べる
SELECT ARRAY[1,2] = ARRAY[2,1] AS order_matters,
ARRAY[1,2] @> ARRAY[2,1] AND ARRAY[1,2] <@ ARRAY[2,1] AS same_set;
order_matters | same_set
---------------+----------
f | t
= は順序まで比べるので {1,2} と {2,1} は別物です。集合として同じかを見たいときは @> と <@ を両方満たすかで判定します。
配列を行に展開する:unnest
unnest は配列を1要素1行に展開する関数で、WITH ORDINALITY を付けると位置番号も一緒に取れます。
番号つきで展開する(WITH ORDINALITY)
SELECT * FROM unnest(ARRAY['x','y','z']) WITH ORDINALITY AS t(val, idx);
val | idx
-----+-----
x | 1
y | 2
z | 3
複数の配列を横に並べて展開する(多引数 unnest)
SELECT * FROM unnest(ARRAY[1,2,3], ARRAY['a','b']) AS t(n, s);
n | s
---+--------
1 | a
2 | b
3 | (null)
長さが違う場合は短い方が NULL で埋められます。 「商品IDの配列」と「数量の配列」をまとめて受け取り、行に戻してINSERTするときによく使う形です。
-- 2つの配列を受け取って一括INSERTする例
INSERT INTO order_line (item_id, qty)
SELECT * FROM unnest(ARRAY[101,102,103], ARRAY[2,1,5]);
テーブルの配列列を展開する(LATERAL)
CREATE TEMP TABLE task (id int, tags text[]);
INSERT INTO task VALUES (1,'{office,done}'),(2,'{}'),(3,NULL);
-- CROSS JOIN:空配列・NULLの行は消える
SELECT t.id, x.tag FROM task t CROSS JOIN LATERAL unnest(t.tags) AS x(tag) ORDER BY 1;
-- LEFT JOIN:すべての行が残る
SELECT t.id, x.tag FROM task t LEFT JOIN LATERAL unnest(t.tags) AS x(tag) ON true ORDER BY 1;
id | tag
----+--------
1 | office
1 | done
(2 rows)
id | tag
----+--------
1 | office
1 | done
2 | (null)
3 | (null)
(4 rows)
CROSS JOIN LATERAL unnest(...)(FROM task, unnest(tags) も同じ)では、空配列とNULLの行が結果から消えます。 タグなしのタスクも一覧に残したいときは LEFT JOIN LATERAL ... ON true を使います。
多次元配列を1次元にする
SELECT array_agg(u) AS flat FROM unnest(ARRAY[[1,2],[3,4]]) u;
flat
-----------
{1,2,3,4}
unnest は多次元配列も要素単位で平らに展開するので、array_agg と組み合わせると1次元配列に変換できます。
行を配列にまとめる:array_agg
array_agg は複数行を1つの配列に集約する集約関数で、ORDER BY・DISTINCT・FILTER を中に書けます。
CREATE TABLE item_tag (item_id int, tag text);
INSERT INTO item_tag VALUES (1,'red'),(1,'sale'),(1,'red'),(2,'blue'),(3,NULL);
SELECT item_id,
array_agg(tag) AS all_tags,
array_agg(DISTINCT tag) AS uniq,
array_agg(tag ORDER BY tag DESC) AS ordered,
array_agg(tag) FILTER (WHERE tag IS NOT NULL) AS no_null
FROM item_tag GROUP BY item_id ORDER BY item_id;
item_id | all_tags | uniq | ordered | no_null
---------+----------------+------------+----------------+----------------
1 | {red,red,sale} | {red,sale} | {sale,red,red} | {red,red,sale}
2 | {blue} | {blue} | {blue} | {blue}
3 | {NULL} | {NULL} | {NULL} | (null)
| 書き方 | 用途 | 覚えておくこと |
|---|---|---|
array_agg(col) |
そのまま集める | 並び順は保証されない。NULLも要素として入る |
array_agg(col ORDER BY col) |
順序を決めて集める | 順序が意味を持つなら必ず書く |
array_agg(DISTINCT col) |
重複を除く | DISTINCT と ORDER BY を併用するときは同じ式を並べる |
array_agg(col) FILTER (WHERE ...) |
条件に合う行だけ | 1行も合わなければ NULL |
COALESCE(array_agg(col), '{}') |
0行のとき空配列にする | アプリ側でNULLチェックが不要になる |
0行のときは NULL になる
SELECT array_agg(x) AS agg_empty, COALESCE(array_agg(x), '{}') AS agg_coalesce
FROM (SELECT 1 AS x WHERE false) s;
agg_empty | agg_coalesce
-----------+--------------
(null) | {}
配列を集約すると2次元配列になる
SELECT array_agg(a) AS two_d FROM (VALUES (ARRAY[1,2]),(ARRAY[3,4])) v(a);
two_d
---------------
{{1,2},{3,4}}
配列同士を array_agg すると、1次元を連結するのではなく2次元配列になります。 長さが揃っていないとエラーになるので(後述)、平らにつなげたいときは先に unnest してから集約します。
配列を更新する:追加・削除・置換
配列列の更新は array_append・array_remove・array_replace を UPDATE ... SET で使います。
CREATE TEMP TABLE task (id int, title text, tags text[] DEFAULT '{}');
INSERT INTO task VALUES (1,'書類整理','{office}'),(2,'備品発注','{office,buy}'),(3,'在庫確認',NULL);
UPDATE task SET tags = array_append(tags, 'done') WHERE id IN (1,3);
UPDATE task SET tags = array_remove(tags, 'office') WHERE id = 2;
SELECT * FROM task ORDER BY id;
id | title | tags
----+----------+---------------
1 | 書類整理 | {office,done}
2 | 備品発注 | {buy}
3 | 在庫確認 | {done}
NULL の配列に array_append すると、{done} という1要素の配列になります(NULLのままにはなりません)。重複を入れたくない場合は次のように条件を付けます。
-- まだ含まれていないときだけ追加する
UPDATE task SET tags = array_append(tags, 'done')
WHERE id = 1 AND NOT (tags @> ARRAY['done']);
GINインデックスあり・なしの速度比較(10万件)
配列列の検索を速くするには GIN インデックスを作り、条件は @>・<@・&&・= で書きます。'x' = ANY(tags) ではGINは使われません。
検証用データ
1〜5個のタグ(tag0〜tag499)を持つ記事を10万件作りました。
CREATE TABLE article (id bigserial PRIMARY KEY, title text, tags text[] NOT NULL DEFAULT '{}');
SELECT setseed(0.42);
INSERT INTO article (title, tags)
SELECT 'article ' || g,
ARRAY(SELECT 'tag' || (floor(random()*500))::int FROM generate_series(1, 1 + (g % 5)))
FROM generate_series(1, 100000) g;
ANALYZE article;
テーブルサイズは13MBでした。
インデックスなし
EXPLAIN (ANALYZE, COSTS OFF) SELECT id FROM article WHERE tags @> ARRAY['tag42'];
Seq Scan on article (actual time=0.036..47.421 rows=611 loops=1)
Filter: (tags @> '{tag42}'::text[])
Rows Removed by Filter: 99389
Planning Time: 0.466 ms
Execution Time: 47.479 ms
GINインデックスを作成
CREATE INDEX article_tags_gin ON article USING gin (tags);
ANALYZE article;
-- インデックスサイズ:896 kB
GINあり:@> の場合
Bitmap Heap Scan on article (actual time=0.251..0.758 rows=611 loops=1)
Recheck Cond: (tags @> '{tag42}'::text[])
Heap Blocks: exact=490
-> Bitmap Index Scan on article_tags_gin (actual time=0.085..0.085 rows=611 loops=1)
Index Cond: (tags @> '{tag42}'::text[])
Planning Time: 0.146 ms
Execution Time: 0.793 ms
GINあり:= ANY の場合
EXPLAIN (ANALYZE, COSTS OFF) SELECT id FROM article WHERE 'tag42' = ANY(tags);
Seq Scan on article (actual time=0.035..19.037 rows=611 loops=1)
Filter: ('tag42'::text = ANY (tags))
Rows Removed by Filter: 99389
Planning Time: 0.042 ms
Execution Time: 19.077 ms
結果のまとめ
| 条件 | インデックス | 実行計画 | 実行時間 |
|---|---|---|---|
tags @> ARRAY['tag42'] |
なし | Seq Scan | 47.479 ms |
'tag42' = ANY(tags) |
なし | Seq Scan | 44.616 ms |
tags @> ARRAY['tag42'] |
GIN | Bitmap Index Scan | 0.793 ms |
'tag42' = ANY(tags) |
GIN | Seq Scan(GIN未使用) | 19.077 ms |
tags && ARRAY['tag42','tag43'](OR) |
GIN | Bitmap Index Scan | 1.202 ms(1,216件) |
tags @> ARRAY['tag42','tag43'](AND) |
GIN | Bitmap Index Scan | 0.066 ms(3件) |
※ PostgreSQL 16.15、同一コンテナで1回ずつ計測。時間は環境で変わるので、比率の目安として見てください。
同じ611件を返す検索でも、@> に書き換えるだけでGINが使われ、約47msから約0.8msになりました。 = ANY(tags) は「1つの値を配列の各要素と比べる」という形のため、配列全体を対象にするGINの演算子クラス(array_ops)の対象外です。タグ検索は次のように書き換えるのがおすすめです。
| 直す前 | 直した後(GINが使える) |
|---|---|
WHERE 'sql' = ANY(tags) |
WHERE tags @> ARRAY['sql'] |
WHERE 'sql' = ANY(tags) AND 'pg' = ANY(tags) |
WHERE tags @> ARRAY['sql','pg'] |
WHERE 'sql' = ANY(tags) OR 'pg' = ANY(tags) |
WHERE tags && ARRAY['sql','pg'] |
一方、通常の列に対する id = ANY(配列) は B-tree インデックスが使われます。
Index Only Scan using article_pkey on article (actual time=0.014..0.017 rows=3 loops=1)
Index Cond: (id = ANY ('{10,20,30}'::bigint[]))
「配列列の中を探す」のか「普通の列を配列の値で絞る」のかで、使うべき書き方が変わる点を押さえておくと迷いません。インデックスの種類ごとの特徴や EXPLAIN ANALYZE の詳しい読み方は、それぞれの専用記事で扱っているため、ここでは配列に関係する部分だけに絞りました。
配列にするか、別テーブルにするかの判断基準
配列は「親と一緒に読み書きする、少数の値のリスト」に向き、要素ごとに属性や参照整合性が必要なら別テーブル(正規化)が向きます。
| 判断ポイント | 配列列が向く | 別テーブルが向く |
|---|---|---|
| 要素の数 | 数個〜数十個程度 | 上限なく増える |
| 要素の属性 | 値そのものだけ(タグ名など) | 付与日時・付与者など属性がある |
| 外部キー制約 | 不要 | 必要(配列要素には外部キーを張れない) |
| 更新の仕方 | 行ごとまとめて置き換えることが多い | 要素単位で頻繁に追加・削除する |
| 主な検索 | 「この値を含む行」 | 要素側からの集計・結合が中心 |
| 他のDBへの移行 | 考えなくてよい | 移植性を重視したい |
迷ったときは「要素を主語にした集計(タグごとの件数など)が多いか」で決めると判断しやすくなります。多いなら別テーブル、少ないなら配列で十分です。配列で保存していても、unnest で行に戻せば集計はできます。
-- 配列のままでもタグごとの件数は出せる
SELECT tag, count(*) FROM article, unnest(tags) AS tag
GROUP BY tag ORDER BY count(*) DESC LIMIT 3;
tag | count
--------+-------
tag371 | 690
tag83 | 676
tag247 | 668
キーと値の組や入れ子の構造を持たせたい場合は、配列より jsonb が向いています(jsonb の演算子は専用記事で整理しています)。
Python(psycopg 3)から配列を渡す
psycopg 3 では Python の list がそのまま PostgreSQL の配列に変換されるので、IN ではなく = ANY(%s) に渡します。
前提:Python 3.14、psycopg 3.3.6(pip install "psycopg[binary]")、上の article テーブルがある状態です。
import psycopg
DSN = "host=127.0.0.1 port=5432 dbname=t98 user=t98 password=t98pass"
with psycopg.connect(DSN) as conn:
# 1) Python の list はそのまま PostgreSQL の配列になる
ids = [10, 20, 30]
rows = conn.execute(
"SELECT id, title FROM article WHERE id = ANY(%s) ORDER BY id", (ids,)
).fetchall()
print("ANY(list):", rows)
# 2) 空リストでもエラーにならない(0件)
rows = conn.execute(
"SELECT count(*) FROM article WHERE id = ANY(%s)", ([],)
).fetchone()
print("ANY([]):", rows)
# 3) IN (%s) に list を渡すと構文エラー
try:
conn.execute("SELECT id FROM article WHERE id IN %s", (ids,))
except psycopg.errors.SyntaxError as e:
print("IN %s ->", type(e).__name__, str(e).splitlines()[0])
conn.rollback()
# 4) 配列列の読み書き(text[] は list[str] で返る)
conn.execute("CREATE TEMP TABLE memo (id int, tags text[])")
conn.execute("INSERT INTO memo VALUES (%s, %s)", (1, ["todo", "urgent"]))
tags = conn.execute("SELECT tags FROM memo WHERE id = 1").fetchone()[0]
print("read back:", tags, type(tags).__name__)
# 5) タグを含む行を GIN が使える @> で検索
n = conn.execute(
"SELECT count(*) FROM article WHERE tags @> %s::text[]", (["tag42"],)
).fetchone()[0]
print("tags @> ['tag42']:", n)
実行結果:
ANY(list): [(10, 'article 10'), (20, 'article 20'), (30, 'article 30')]
ANY([]): (0,)
IN %s -> SyntaxError syntax error at or near "$1"
read back: ['todo', 'urgent'] list
tags @> ['tag42']: 611
| 書き方 | 結果 | ポイント |
|---|---|---|
id = ANY(%s) に [10,20,30] |
動く | プレースホルダーは1つで済み、件数が変わってもSQLが同じ |
id = ANY(%s) に [] |
0件(エラーなし) | 空リストの特別扱いが不要 |
id IN %s に list |
SyntaxError |
psycopg 3 はサーバー側バインドのため IN に配列を展開しない |
tags @> %s::text[] |
動く | 型を明示するとGINが使える形で確実に比較できる |
SQLを文字列連結で組み立てずにプレースホルダーで値を渡すことで、値の型変換とエスケープをドライバーに任せられます。
よくあるつまずきと実際のエラー文
配列で戸惑いやすいのは「NULLを含む配列」「空配列とNULLの違い」「次元の不一致」の3つです。
NULLを含む配列と ANY / ALL
SELECT NULL = ANY(ARRAY[1,2]) AS null_any,
3 = ANY(ARRAY[1,NULL]) AS any_with_null,
3 <> ALL(ARRAY[1,NULL]) AS all_with_null,
1 = ANY(ARRAY[1,NULL]) AS found;
null_any | any_with_null | all_with_null | found
----------+---------------+---------------+-------
(null) | (null) | (null) | t
配列にNULLが含まれると、見つからない場合の結果が false ではなく NULL になります。 WHERE x <> ALL(arr) で「どれでもない行」を探すと、NULLを含む配列の行は1件も返りません。これは NOT IN とNULLの関係と同じ動きです。array_remove(arr, NULL) でNULLを除いてから比べると意図どおりになります。
SELECT ARRAY[1,NULL] @> ARRAY[NULL]::int[] AS contains_null,
array_position(ARRAY[1,NULL,3], NULL) AS pos_null,
array_remove(ARRAY[1,NULL,3], NULL) AS rm_null;
contains_null | pos_null | rm_null
---------------+----------+---------
f | 2 | {1,3}
@> ではNULL同士は等しいとみなされませんが、array_position と array_remove はNULLを探せます(IS NOT DISTINCT FROM で比較するため)。
空配列 '{}' と NULL の違い
SELECT '{}'::int[] IS NULL AS empty_is_null,
cardinality(NULL::int[]) AS card_null,
NULL::int[] || 1 AS null_append,
array_length('{}'::int[], 1) AS len_empty,
cardinality('{}'::int[]) AS card_empty;
SELECT 1 = ANY('{}'::int[]) AS any_empty, 1 <> ALL('{}'::int[]) AS all_empty;
empty_is_null | card_null | null_append | len_empty | card_empty
---------------+-----------+-------------+-----------+------------
f | (null) | {1} | (null) | 0
any_empty | all_empty
-----------+-----------
f | t
| 確認したいこと | 空配列 '{}' |
NULL |
|---|---|---|
IS NULL |
f |
t |
array_length(x, 1) |
NULL |
NULL |
cardinality(x) |
0 |
NULL |
1 = ANY(x) |
f |
NULL |
1 <> ALL(x) |
t |
NULL |
x || 1 |
{1} |
{1} |
array_length('{}', 1) は0ではなく NULL を返すので、「要素が空か」の判定は cardinality(tags) = 0 か tags = '{}' で書くのが確実です。列を NOT NULL DEFAULT '{}' にしておくと、NULLと空配列の2種類を気にせずに済みます。
次元・長さが揃っていない
SELECT ARRAY[[1,2],[3]];
ERROR: multidimensional arrays must have array expressions with matching dimensions
SELECT '{{1,2},{3}}'::int[];
ERROR: malformed array literal: "{{1,2},{3}}"
DETAIL: Multidimensional arrays must have sub-arrays with matching dimensions.
SELECT ARRAY[[1,2]] || ARRAY[3,4,5];
ERROR: cannot concatenate incompatible arrays
DETAIL: Arrays with differing dimensions are not compatible for concatenation.
SELECT array_agg(a) FROM (VALUES (ARRAY[1,2]),(ARRAY[3])) v(a);
ERROR: cannot accumulate arrays of different dimensionality
PostgreSQLの多次元配列は「長方形」である必要があり、行ごとに長さが違う配列は作れません。 長さの違うリストを持ちたい場合は、1次元配列を array_agg せずに別の行として持つか、jsonb を使います。
型や書式が合わない
SELECT ARRAY[1,2] @> ARRAY['a'];
ERROR: operator does not exist: integer[] @> text[]
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
SELECT 1 = ANY('1,2,3');
ERROR: malformed array literal: "1,2,3"
DETAIL: Array value must start with "{" or dimension information.
カンマ区切りの文字列を配列として比べたいときは、1 = ANY(string_to_array('1,2,3', ',')::int[]) のように string_to_array で変換してから使います。
つまずきチェックリスト
| 症状 | よくある原因 | 対処 |
|---|---|---|
添字で取り出すと NULL |
0始まりで数えている | 1始まりにする |
| GINを作ったのに Seq Scan | = ANY(tags) で書いている |
tags @> ARRAY[...] に書き換える |
<> ALL で行が返らない |
配列にNULLが含まれる | array_remove(arr, NULL) |
| 空配列の判定が効かない | array_length(...) = 0 と書いている |
cardinality(x) = 0 |
array_agg の順序が毎回違う |
ORDER BY を書いていない |
array_agg(x ORDER BY ...) |
unnest で行が減る |
空配列・NULLの行が CROSS JOIN で消える | LEFT JOIN LATERAL ... ON true |
Pythonから IN %s でエラー |
psycopg 3 はサーバー側バインド | = ANY(%s) に list を渡す |
よくある質問
Q. PostgreSQLの配列の添字は0から始まりますか?
A. 1から始まります。 tags[0] はエラーにならず NULL を返すので、結果が空になったら添字を確認してください。array_lower(arr, 1) で開始位置を確認できます(通常は1)。
Q. = ANY と IN はどちらを使えばよいですか?
A. 固定の値を書くなら IN (1,2,3) が読みやすく、アプリから可変個の値を渡すなら = ANY(%s) に配列を1つ渡す形が便利です。プレースホルダーが1つで済み、空リストでもエラーになりません。通常の列であれば、どちらもB-treeインデックスが使われます。
Q. 配列列にインデックスは張れますか?
A. GINインデックスが使えます(CREATE INDEX ... USING gin (tags))。検索条件は @>・<@・&&・= で書くとインデックスが使われます。10万件の検証では @> で約47msから約0.8msになりました。
Q. 配列の要素数を数えるには array_length と cardinality のどちらがよいですか?
A. cardinality がおすすめです。 空配列で 0 を返し、多次元配列でも全要素数を返します。array_length(arr, 1) は空配列で NULL を返し、次元の指定も必要です。
まとめ
- 配列は
ARRAY[...]か'{...}'で作り、添字は1始まり。範囲外はNULLが返る - 検索は
= ANY・@>・<@・&&の4つ。AND検索は@>、OR検索は&& - GINインデックスが使われるのは
@>などの配列演算子。'x' = ANY(tags)はtags @> ARRAY['x']に書き換える unnest WITH ORDINALITYで位置つき展開、空配列の行を残すならLEFT JOIN LATERALarray_aggはORDER BY・DISTINCT・FILTERを中に書け、0行ならNULL(COALESCEで空配列に)- Pythonからは
= ANY(%s)に list を渡す。空リストでもそのまま動く - NULLを含む配列・空配列・次元の不一致は、
array_remove・cardinality・NOT NULL DEFAULT '{}'で扱いやすくなる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PostgreSQL配列型の使い方|unnest・ANY・array_agg早見表」
株式会社RJCでは、PostgreSQLの配列型のようなデータベースの知識を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。