PostgreSQL | SQLite+MySQL経験者向け、30日で習得するPostgreSQL:高度SQL - Day13 配列型

SQL PostgreSQL
スポンサーリンク

Day13 後半のゴール

「“配列を条件に使う・展開する・更新する”を一通りこなせるようにする」

前半で「配列を持つ」「要素を取り出す」「長さを数える」ところまでは来ました。
後半では、実務でよく使う3つの軸を押さえます。

配列の中身で絞り込む(タグ検索など)。
配列を行に展開して集計する。
配列に要素を足す・消すといった更新をする。

ここまでできると、「ARRAYをちゃんと使っている」と胸を張って言えるレベルになります。


配列の中身で絞り込む

「このタグを持っているユーザー」を探す

前半の users.tags TEXT[] をそのまま使います。
やりたいことは、「タグ ‘premium’ を持っているユーザーを探したい」です。

最初に覚えるべき演算子は = ANY(...)@> の2つです。

= ANY(tags) は、「tags のどれか1つと等しいか?」を意味します。

SELECT
  id,
  email,
  tags
FROM users
WHERE 'premium' = ANY(tags);
SQL

このクエリは、「tags の中に ‘premium’ が1つでもあればマッチ」します。
イメージとしては「配列に ‘premium’ が含まれているか?」です。

もうひとつの書き方が @> です。

SELECT
  id,
  email,
  tags
FROM users
WHERE tags @> ARRAY['premium'];
SQL

tags @> ARRAY['premium'] は、「tags が ARRAY[‘premium’] を“含んでいるか”」を意味します。
配列同士の「包含」チェックです。

どちらも結果は同じですが、@> は「複数タグをまとめて条件にしたい」ときに強くなります。
例えば、「’premium’ と ‘beta_user’ の両方を持っているユーザー」を探したいときは、こう書けます。

SELECT
  id,
  email,
  tags
FROM users
WHERE tags @> ARRAY['premium', 'beta_user'];
SQL

「配列の中身で絞り込む」場面では、
「1つだけなら = ANY、複数まとめてなら @>」という感覚を持っておくと選びやすくなります。


配列を行に展開して集計する

「タグごとのユーザー数」を数える

次は、「タグごとに何人のユーザーがいるか」を数えてみます。
これは「配列をバラしてから集計する」典型パターンです。

unnest を使うと、配列を「1要素=1行」に展開できます。

SELECT
  email,
  unnest(tags) AS tag
FROM users;
SQL

ある行で tags = {"premium","beta_user"} なら、結果は2行になります。

1行目:tag = 'premium'
2行目:tag = 'beta_user'

これをCTEにして、タグごとに集計します。

WITH user_tags AS (
  SELECT
    id    AS user_id,
    unnest(tags) AS tag
  FROM users
)
SELECT
  tag,
  COUNT(DISTINCT user_id) AS user_count
FROM user_tags
GROUP BY tag
ORDER BY user_count DESC;
SQL

流れを整理するとこうです。

user_tags で「ユーザー×タグ」の行を作る。
tag ごとに COUNT(DISTINCT user_id) で人数を数える。

結果は例えばこうなります。

tag          | user_count
-------------+-----------
premium      | 120
beta_user    | 40
free         | 300
dark_mode... | 25

「配列を展開してから GROUP BY する」というパターンは、
JSONの jsonb_array_elements と同じノリで、「配列の中身を集計対象にする」ための基本形です。


配列に要素を追加する

「既存のタグに ‘dark_mode_enabled’ を足す」

配列を更新するときは、「配列同士の演算」を使います。
一番よく使うのが、連結演算子 || です。

例えば、「premiumユーザー全員に ‘dark_mode_enabled’ タグを追加したい」とします。

UPDATE users
SET tags = tags || ARRAY['dark_mode_enabled']
WHERE 'premium' = ANY(tags);
SQL

ここでのポイントはこうです。

tags || ARRAY['dark_mode_enabled']
既存の配列の末尾に、新しい要素をくっつける。

ただし、このままだと「すでに ‘dark_mode_enabled’ を持っているユーザー」にも重複して追加される可能性があります。
重複を避けたいなら、条件を工夫します。

UPDATE users
SET tags = tags || ARRAY['dark_mode_enabled']
WHERE 'premium' = ANY(tags)
  AND NOT ('dark_mode_enabled' = ANY(tags));
SQL

「premiumを持っていて、かつまだdark_mode_enabledを持っていないユーザー」にだけ追加する、という形です。

配列更新の基本は、「新しい配列を作って入れ直す」イメージです。
「配列の中身を直接書き換える」というより、「元の配列+追加分で新しい配列を作る」と考えると理解しやすくなります。


配列から要素を削除する

「特定のタグだけ取り除く」

逆に、「特定のタグを配列から取り除きたい」こともあります。
このときは、array_remove を使います。

例えば、「全ユーザーから ‘beta_user’ タグを外したい」とします。

UPDATE users
SET tags = array_remove(tags, 'beta_user')
WHERE 'beta_user' = ANY(tags);
SQL

array_remove(tags, 'beta_user') は、「tags の中から ‘beta_user’ を全部取り除いた新しい配列」を返します。

結果として、

{"premium","beta_user"}{"premium"}
{"beta_user"}{}

のように変わります。

ここでも、「配列を直接いじる」のではなく、「関数で加工した新しい配列をセットし直す」という発想です。


配列とセキュリティ・権限のイメージ

「“権限のリスト”をARRAYで持つときの注意」

セキュリティ寄りの話も少しだけ触れておきます。
例えば、「ユーザーが持つ権限を text[] で持つ」設計はよくあります。

roles TEXT[] に、{"admin","editor"} のような配列を入れるイメージです。

このとき、「admin権限を持つユーザー」を探すクエリはこうなります。

SELECT
  id,
  email
FROM users
WHERE 'admin' = ANY(roles);
SQL

ここで大事なのは、「クエリがシンプルだからこそ、権限の意味付けをアプリ側でちゃんと管理する」ことです。

「rolesに ‘admin’ が入っていたら何でもできる」ような設計にすると、
誤ってrolesを更新したときの影響が大きくなります。

現場では、

「rolesは“アプリ側のロール名”だけを入れる」
「ロール名ごとの具体的な権限は、別テーブルやアプリの設定で管理する」

といった分離をすることが多いです。

ARRAY自体はただの「文字列のリスト」なので、
「何を入れるか」「どう解釈するか」のルールをチームで決めておくことが、セキュリティ的にはとても重要になります。


ARRAYを使うときの思考パターン

「“リストとして扱いたい”のか、“正規化したい”のかを意識する」

最後に、ARRAYを使うかどうか迷ったときの考え方をまとめます。

「タグのように、“リストとしてまとめて扱いたい”」「JOINを増やしたくない」「件数もそこまで多くない」
→ ARRAYで持って、= ANY@>unnest でクエリするのは十分アリ。

「1つ1つの要素に対して、別の属性を持たせたい」「要素ごとに更新・削除したい」「要素数がかなり多くなりそう」
→ 素直に別テーブル(user_tags など)に正規化した方が、長期的には安全で柔軟。

PostgreSQLはARRAYもJSONも強いので、「全部正規化」「全部JSON」の二択ではなく、
「ここはARRAYで軽く」「ここはJSONで柔軟に」「ここは正規化して堅く」というグラデーションで設計できます。


Day13 後半のまとめ

ARRAYの実戦的な使い方は、「配列の中身で絞り込む('premium' = ANY(tags)tags @> ARRAY['premium','beta_user'])」「配列を unnest(tags) で行に展開してからタグごとのユーザー数などを集計する」「tags || ARRAY['new_tag'] で要素を追加し、array_remove(tags,'beta_user') で要素を削除する」という3本柱で押さえると整理しやすい。
配列更新は「元の配列を関数や演算子で加工して、新しい配列としてセットし直す」イメージで考えると理解しやすく、権限リストのようなセキュリティに関わる情報をARRAYで持つ場合は、「rolesに何を入れるか」「どう解釈するか」のルールをアプリ側・チーム側で明確にしておくことが重要になる。
最終的には、「単純なリストで済むならARRAY」「構造が複雑ならJSON」「要素ごとにしっかり管理したいなら別テーブル」という選択肢の中から、要件に合わせて選べるようになることが、Day13 後半の着地点になる。

タイトルとURLをコピーしました