PostgreSQL『ERROR: operator does not exist』の原因と対処

PostgreSQL『ERROR: operator does not exist』の原因と対処

演算子(=、<、>、||、LIKE、ILIKE、@>、<@、~ など)に対して与えたオペランドの型の組み合わせに対応する定義が見つからないと発生する。メッセージには「operator does not exist: 左型 演算子 右型」と具体的な型が出る。型不一致・暗黙キャスト不能・配列/JSONB/範囲型の取り扱いミス・日付/時刻と整数の混在などを中心に、最短復旧と恒久対策をまとめた。

エラーの読み方(まずメッセージの型を確認)

ERROR:  operator does not exist: text = integer
HINT:   No operator matches the given name and argument type(s).
        You might need to add explicit type casts.

・「text = integer」のように“左右の型”が鍵
・HINT に “explicit type casts” が出たら CAST を検討
・pg_typeof() で式の実際の型を確認するのが近道

診断の基本フロー(最短復旧)

-- 1) 失敗した式(列・式・リテラル)の型を洗い出す
SELECT pg_typeof(col), pg_typeof($1), pg_typeof(data->>'age');

-- 2) 期待する型へ明示キャスト
SELECT ... WHERE col::int = $1::int;

-- 3) それでも不可なら演算子自体を見直す(LIKE→正規の型へ、@> は右もjsonb 等)

数値と文字列の比較(text = integer など)

・NG:文字列と数値を直接比較

SELECT '10' = 10;                       -- ERROR: operator does not exist: text = integer

・OK:どちらかに寄せる(設計的には列の型を揃えるのが本筋)

SELECT '10'::int = 10;                  -- true
SELECT CAST('10' AS int) = 10;

LIKE/ILIKE/正規表現は text 専用(int に対してはNG)

・NG

SELECT 123 LIKE '%23%';                 -- ERROR: operator does not exist: integer ~~ text

・OK:文字列にキャスト

SELECT 123::text LIKE '%23%';
SELECT col::text ILIKE 'abc%';
SELECT col::text ~ '^[0-9]+$';

日付・時刻と整数の四則演算(interval を使う)

・NG

SELECT now() + 1;                       -- ERROR: operator does not exist: timestamp with time zone + integer
SELECT '10:00'::time + 30;              -- ERROR: time + integer

・OK

SELECT now() + INTERVAL '1 day';
SELECT '10:00'::time + INTERVAL '30 minutes';

JSON/JSONB と比較・包含(jsonb は jsonb 同士)

・NG:jsonb と text の混在

SELECT data->'tags' @> '["pg"]';        -- ERROR: operator does not exist: jsonb @> text

・OK:右側も jsonb リテラルに

SELECT data->'tags' @> '["pg"]'::jsonb;

・数値比較は ->> で文字列→数値キャスト

SELECT (data->>'age')::int > 18;

配列(ANY/ALL, @>)の型を合わせる

・NG:左が text、右配列が integer[]

SELECT '3' = ANY(ARRAY[1,2,3]);         -- ERROR: operator does not exist: text = integer

・OK

SELECT 3 = ANY(ARRAY[1,2,3]);
SELECT '3'::int = ANY(ARRAY[1,2,3]);
SELECT 'a' = ANY(ARRAY['a','b']);       -- 型を text に統一

・配列包含 @> は「左も右も同じ配列型」

SELECT ARRAY[1,2,3] @> ARRAY[2];        -- true
-- 左が int[]、右が int だと不可。右も int[] にする。

範囲型(int4range/int8rangeなど)と要素型の不一致

・NG:bigint の値を int4range に照会

SELECT 10::bigint <@ int4range(1, 20);  -- ERROR: operator does not exist: bigint <@ int4range

・OK:範囲型に合わせてキャスト

SELECT 10 <@ int4range(1, 20);          -- 要素は int4
SELECT 10::bigint <@ int8range(1, 20);  -- 要素は int8

文字列結合(||)は text へ寄せる

・NG

SELECT 2025 || '年';                     -- ERROR: operator does not exist: integer || unknown

・OK

SELECT 2025::text || '年';
SELECT concat(2025, '年');               -- 関数で型を吸収

citext/拡張型と演算子の組合せ

・citext(大文字小文字を無視する文字列型)と text を混在させると不一致になる場合がある

-- 不一致時の対処例:どちらかに寄せる
SELECT 'Abc'::citext = 'abc'::citext;   -- true
SELECT ('Abc'::citext)::text = 'abc';   -- true

・citext の機能を使うなら拡張を有効化

CREATE EXTENSION IF NOT EXISTS citext;

演算子が“見つからない”ときの代替:関数を使う

・不一致が複雑なときは演算子を諦め、関数で明示的に変換

SELECT COALESCE(col::int, 0) > 0;
SELECT lower(col::text) = lower($1::text);

オペレータの検索と確認(\do, pg_operator)

-- psql:演算子一覧
\do =        -- 等価演算子
\do <@       -- 範囲/配列の包含系

-- カタログで型組合せを直接確認
SELECT oprname, oprleft::regtype, oprright::regtype
FROM pg_operator
WHERE oprname IN ('=', '@>', '<@', '||', '~~', '~~*')  -- ~~(LIKE), ~~* (ILIKE)
ORDER BY 1,2,3;

暗黙キャストに頼らない(曖昧化の回避)

・PostgreSQL は暗黙キャストが厳格。毎回 CAST を書くのが堅実
・アプリ層で型を決め、バインド変数の型も合わせる(text に数字を入れない)

ORM/バインド変数の型ミス(実戦パターン)

・NG:数値列に対し文字列型でプレースホルダを渡す

-- 例:WHERE id = $1 に '42'(text)を渡す → text = integer

・OK:プレースホルダの型を合わせる

-- JDBC
PreparedStatement ps = con.prepareStatement("... WHERE id = ?");
ps.setInt(1, 42);  // setString(1,"42") は避ける

よくある具体例と修正スニペット

-- 1) text と integer の比較
WHERE user_id::int = $1::int

-- 2) jsonb の包含
WHERE payload @> '{"role":"admin"}'::jsonb

-- 3) jsonb 数値比較
WHERE (payload->>'score')::numeric >= 80

-- 4) 配列検索
WHERE 3 = ANY (int_ids)              -- int_ids は integer[]

-- 5) 範囲型
WHERE 10 <@ int4range(min_id, max_id)

-- 6) LIKE/ILIKE/正規表現
WHERE col::text ILIKE 'pg%'
WHERE col::text ~ '^[0-9]{4}$'

再発防止チェックリスト

・列定義を正す(文字列で数値を持たない、JSON に数値は数値で保存)
・アプリのパラメータ型を厳格化(setInt/setLong 等)
・JSONB/配列/範囲型は「左右の型を必ず一致」させる運用ルール
・LIKE/ILIKE/正規表現は text にキャスト or citext を導入
・日時計算は interval を使う(+/- 整数は禁止)
・pg_typeof()/pg_operator で検証し、曖昧さを残さない

再現→解消の通し例(JSONB・配列・日付の複合)

-- 準備
DROP TABLE IF EXISTS t;
CREATE TABLE t(
  id    int PRIMARY KEY,
  tags  text[],
  meta  jsonb,
  at    timestamp
);

INSERT INTO t VALUES
(1, ARRAY['pg','db'], '{"age": "19", "role":"user"}', now()),
(2, ARRAY['pg','pro'], '{"age": 25,   "role":"admin"}', now());

-- NG:型不一致の例
SELECT * FROM t WHERE tags @> 'pg';          -- ERROR: text[] @> text
SELECT * FROM t WHERE meta @> '{"role":admin}';  -- ERROR: jsonb @> text(右がjsonbでない)
SELECT * FROM t WHERE at + 1 > now();        -- ERROR: timestamp + integer

-- 解消
SELECT * FROM t WHERE tags @> ARRAY['pg'];           -- OK
SELECT * FROM t WHERE meta @> '{"role":"admin"}'::jsonb;  -- OK
SELECT * FROM t WHERE at + INTERVAL '1 day' > now();      -- OK

-- JSON 数値比較(文字列と数値の混在に注意)
SELECT * FROM t WHERE (meta->>'age')::int >= 20;      -- OK(文字列化→数値化)