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 LATERAL
  • array_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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?