PostgreSQL『more than one row returned by a subquery used as an expression』の原因と対処
- 作成日 2025.09.26
- 更新日 2025.10.06
- その他
単一の値を返す前提(スカラ副問い合わせ)の場所で、実際には2行以上返ってしまったときに発生する。=(等価比較)の右側、SELECT句の式、INSERT/UPDATEの値式、CHECK制約や生成列の式などが対象。要件別の書き換え(IN/EXISTS/JOIN/集約/上位1件の決定規則/配列化)と、スキーマ側の恒久対策(ユニーク制約)をセットで整理した。
目次
- 1. エラーの意味とSQLSTATE
- 2. 発生条件の早見表
- 3. まず特定:どの副問い合わせが“複数行”なのかを見抜く
- 4. 最小再現(NG→エラー)
- 5. 最短復旧チートシート(要件別の代表解)
- 6. IN/EXISTS/JOIN への書き換え(複数行を許可する比較)
- 7. 「1件だけ選ぶ」規則を決める(ORDER BY / DISTINCT ON / 窓関数)
- 8. 集約で“1値”に畳む(代表値/連結/集計)
- 9. INSERT/UPDATE での多行問題:FROM/JOIN で書き換える
- 10. 相関副問い合わせの“1対多”をスキーマで根治(UNIQUE)
- 11. 配列にして = ANY を使う(式のまま複数値を許容)
- 12. 0行/複数行を事前ガード(検出・例外化・フォールバック)
- 13. 書き換えの基本指針(誤用→正解)
- 14. 通し例:NG→4通りのOK書き換え
- 15. 再発防止チェックリスト
エラーの意味とSQLSTATE
ERROR: more than one row returned by a subquery used as an expression
-- SQLSTATE 21000(cardinality violation)・「式の中に置いた副問い合わせは“ちょうど1行”であること」というルールに違反
・0行はNULLとみなされる(多くのケースで許容)が、2行以上はエラー
発生条件の早見表
□ = (SELECT ...) / <> (SELECT ...) / col = (SELECT col FROM ...) で複数行が返る
□ SELECT 句の中に (SELECT ...) があり、1行に決めきれない
□ INSERT/UPDATE の値式に (SELECT ...) を使い、複数行が返る
□ CHECK/生成列などの式でも同様に複数行
□ 相関副問い合わせの結合キーがユニークでない(子が1対多)まず特定:どの副問い合わせが“複数行”なのかを見抜く
-- 失敗したSQLの“副問い合わせ部分”だけを単独で実行し、件数を確認
SELECT COUNT(*) FROM ( <問題のサブクエリ> ) s;
-- psql なら直前エラーの詳報
\errverbose
\set VERBOSITY verbose
-- 典型:相関キーでの重複を発見
SELECT key, COUNT(*) c
FROM child
GROUP BY key HAVING COUNT(*) > 1;最小再現(NG→エラー)
DROP TABLE IF EXISTS users, orders;
CREATE TABLE users (id int PRIMARY KEY, name text);
CREATE TABLE orders (id int PRIMARY KEY, user_id int, amount int);
INSERT INTO users VALUES (1,'alice'), (2,'bob');
INSERT INTO orders VALUES (10,1,100), (11,1,200), (12,2,300);
-- ユーザー毎の最新注文金額を“式のサブクエリ”で取得しようとして…
SELECT
u.id,
(SELECT amount FROM orders o WHERE o.user_id = u.id) AS any_amount
FROM users u;
-- エラー:alice(1)の行で2行返るため最短復旧チートシート(要件別の代表解)
・「どれか1件でよい」→ ORDER BY ... LIMIT 1 / DISTINCT ON / ROW_NUMBER()=1
・「重複があるが、代表値を決めたい」→ MIN/MAX/AVG/STRING_AGG/JSON_AGG
・「複数可」→ IN / EXISTS / JOIN(1対多として展開)
・「全部まとめて1値」→ ARRAY(SELECT ...) / jsonb_agg(SELECT ...)
・「本来は1件のはず」→ キーに UNIQUE 制約を付けてデータ不整合を排除IN/EXISTS/JOIN への書き換え(複数行を許可する比較)
-- NG:単一値比較
SELECT * FROM users u
WHERE u.id = (SELECT user_id FROM orders WHERE amount >= 200);
-- OK:複数行を前提に
SELECT * FROM users u
WHERE u.id IN (SELECT user_id FROM orders WHERE amount >= 200);
-- EXISTS 版(関連行の存在を問う)
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount >= 200);
-- JOIN で明示的に1対多を展開
SELECT DISTINCT u.*
FROM users u JOIN orders o ON o.user_id = u.id
WHERE o.amount >= 200;「1件だけ選ぶ」規則を決める(ORDER BY / DISTINCT ON / 窓関数)
-- 1) ORDER BY ... LIMIT 1(最新/最小/最大などのルールを明示)
SELECT u.id,
(SELECT o.amount
FROM orders o
WHERE o.user_id = u.id
ORDER BY o.id DESC -- 例:新しい順
LIMIT 1) AS latest_amount
FROM users u;
-- 2) DISTINCT ON(グループ内で優先順位を決めて1行)
SELECT DISTINCT ON (o.user_id) o.user_id, o.amount
FROM orders o
ORDER BY o.user_id, o.id DESC;
-- 3) 窓関数 ROW_NUMBER(後続で rn=1 を選ぶ)
WITH ranked AS (
SELECT o.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id DESC) rn
FROM orders o
)
SELECT u.id,
(SELECT amount FROM ranked r WHERE r.user_id=u.id AND r.rn=1) AS latest_amount
FROM users u;・LIMIT 1 は必ず ORDER BY とセットで決定性を持たせる
集約で“1値”に畳む(代表値/連結/集計)
-- 代表値を決める
SELECT u.id,
(SELECT MAX(o.amount) FROM orders o WHERE o.user_id=u.id) AS max_amount
FROM users u;
-- 文字列に連結(API応答向け)
SELECT u.id,
(SELECT STRING_AGG(o.amount::text, ',' ORDER BY o.id)
FROM orders o WHERE o.user_id=u.id) AS amounts
FROM users u;
-- JSON/配列で受ける
SELECT u.id,
(SELECT jsonb_agg(o.amount ORDER BY o.id)
FROM orders o WHERE o.user_id=u.id) AS amounts_json
FROM users u;INSERT/UPDATE での多行問題:FROM/JOIN で書き換える
-- NG:SET col = (SELECT ...) が複数行
UPDATE users u
SET name = (SELECT 'VIP' FROM orders o WHERE o.user_id=u.id AND o.amount>=200);
-- OK:FROM で結合し、代表行か集約にする
UPDATE users u
SET name = 'VIP'
FROM (
SELECT DISTINCT user_id FROM orders WHERE amount>=200
) o
WHERE u.id = o.user_id;
-- 代表値を使う(例:最新金額で更新)
UPDATE users u
SET name = 'VIP ' || latest.amount
FROM (
SELECT DISTINCT ON (user_id) user_id, amount
FROM orders ORDER BY user_id, id DESC
) latest
WHERE u.id = latest.user_id;相関副問い合わせの“1対多”をスキーマで根治(UNIQUE)
-- 「user→profile は常に1:1のはず」ならスキーマで保証
CREATE TABLE profiles(user_id int PRIMARY KEY, nick text);
-- あるいは
CREATE UNIQUE INDEX ux_profile_user ON profiles(user_id);
-- これにより (SELECT nick FROM profiles WHERE user_id=u.id) は1行に保たれる・ビジネス上 1:1 ならアプリやSQLで“期待”するのではなく制約で担保
配列にして = ANY を使う(式のまま複数値を許容)
-- 任意一致:= ANY(ARRAY(SELECT ...)) で“式のまま”複数値に拡張
SELECT * FROM users
WHERE id = ANY( ARRAY(SELECT user_id FROM orders WHERE amount>=200) );・IN と同趣旨。式の位置を変えたくない場合のテクニック
0行/複数行を事前ガード(検出・例外化・フォールバック)
-- 1) 2件以上ならNULLに落とす(あるいはデフォルト)
SELECT CASE
WHEN (SELECT COUNT(*) FROM orders o WHERE o.user_id=u.id) = 1
THEN (SELECT amount FROM orders o WHERE o.user_id=u.id)
END AS only_amount
FROM users u;
-- 2) テスト/監視用:LIMIT 2 で「2件目があるか」を探知
SELECT u.id,
(SELECT COUNT(*) FROM (SELECT 1 FROM orders o WHERE o.user_id=u.id LIMIT 2) s) AS c2
FROM users u
WHERE /* c2 = 2 の行が“要修正データ” */;書き換えの基本指針(誤用→正解)
× col = (SELECT x FROM t WHERE ...) -- 複数行の可能性
○ col IN (SELECT x FROM t WHERE ...) -- 複数行を許容
○ EXISTS (SELECT 1 FROM t WHERE ...) -- 存在判定
○ col = (SELECT ... ORDER BY ... LIMIT 1) -- 代表1件を決める
○ (SELECT MAX(x) FROM t WHERE ...) -- 集約で1値化
○ ARRAY(SELECT ...) / jsonb_agg(...) -- まとめて1値(配列/JSON)通し例:NG→4通りのOK書き換え
-- 目的:ユーザーごとに「最新注文金額」を1値で取得
-- NG(エラー)
SELECT u.id,
(SELECT amount FROM orders o WHERE o.user_id=u.id) AS latest_amount
FROM users u;
-- OK1:ORDER BY + LIMIT 1
SELECT u.id,
(SELECT o.amount FROM orders o WHERE o.user_id=u.id ORDER BY o.id DESC LIMIT 1) AS latest_amount
FROM users u;
-- OK2:DISTINCT ON で事前に最新行を作ってJOIN
WITH latest AS (
SELECT DISTINCT ON (user_id) user_id, amount
FROM orders ORDER BY user_id, id DESC
)
SELECT u.id, l.amount
FROM users u LEFT JOIN latest l ON l.user_id=u.id;
-- OK3:ROW_NUMBER でランク付け
WITH ranked AS (
SELECT o.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id DESC) rn
FROM orders o
)
SELECT u.id, r.amount
FROM users u LEFT JOIN ranked r ON r.user_id=u.id AND r.rn=1;
-- OK4:MAX で代表値(“最新=id最大”が妥当な定義なら)
SELECT u.id,
(SELECT MAX(o.amount) FROM orders o WHERE o.user_id=u.id) AS latest_amount
FROM users u;再発防止チェックリスト
□ 副問い合わせが“必ず1行”である保証があるか(データ/制約で担保)
□ 1件の定義(最新/最大/最小/優先度)を明文化し ORDER BY / 集約で表現
□ 複数行が正なら IN/EXISTS/JOIN を用いる
□ まとめて返すなら ARRAY/jsonb_agg/STRING_AGG で1値化
□ UPDATE/DELETE は FROM/JOIN で多行を自然に扱う形に
□ 相関キーがユニークになるよう UNIQUE 制約/インデックスを整備-
前の記事
PostgreSQL『cast from type to integer is not allowed』の原因と対処 2025.09.25
-
次の記事
PostgreSQL『ERROR: could not access file … No such file or directory』の原因と対処 2025.09.28
コメントを書く