「COALESCEを使ったのに、空文字が残ってる…」 「NULLと空文字、両方をデフォルト値に置換したい…」 「” と NULL の違いって実際どう処理すべき?」
PostgreSQLでデータ処理をしていると、必ずぶつかる問題があります。それはCOALESCEはNULLしか処理しないという事実。
実は、空文字(”)とNULLは全く別物。COALESCEだけでは空文字を処理できません。でも大丈夫!NULLIFと組み合わせれば、完璧に処理できるんです。
この記事では、COALESCEの基本から、空文字も含めた処理方法、実践的なユースケースまで、すべてお伝えします!
COALESCEの基本:NULLを処理する魔法の関数

🎯 COALESCEとは何か
基本構文と動作
COALESCE(値1, 値2, 値3, ...)
-- 最初のNULLでない値を返す
動作の仕組み:
- 左から順番に評価
- 最初に見つかった非NULL値を返す
- すべてNULLなら、NULLを返す
基本的な使用例
-- 例1:NULLをデフォルト値に置換
SELECT COALESCE(phone, 'なし') AS phone_number
FROM users;
-- 例2:複数の候補から選択
SELECT COALESCE(mobile, home_phone, office_phone, '連絡先なし') AS contact
FROM contacts;
-- 例3:計算での使用
SELECT
product_name,
price * COALESCE(discount_rate, 1.0) AS final_price
FROM products;
⚠️ COALESCEの限界:空文字は素通り
問題の実例
-- テストデータ
CREATE TABLE test_data (
id INT,
name TEXT
);
INSERT INTO test_data VALUES
(1, 'Alice'),
(2, NULL), -- NULL
(3, ''), -- 空文字
(4, ' '); -- スペースのみ
-- COALESCEを使った結果
SELECT
id,
name,
COALESCE(name, 'デフォルト') AS coalesce_result
FROM test_data;
-- 結果:
-- id | name | coalesce_result
-- 1 | Alice | Alice
-- 2 | NULL | デフォルト ← NULLは処理される
-- 3 | | ← 空文字は処理されない!
-- 4 | | ← スペースも処理されない!
なぜ空文字が処理されないのか:
- 空文字(”)は値として存在する
- NULLは「値がない」状態
- PostgreSQLはこれらを明確に区別
空文字も処理する最強の組み合わせ:NULLIF + COALESCE
🔧 NULLIFの魔法:空文字をNULLに変換
NULLIF関数の基本
NULLIF(値1, 値2)
-- 値1と値2が等しい場合、NULLを返す
-- 異なる場合、値1を返す
空文字をNULLに変換
-- 基本パターン
SELECT NULLIF('', ''); -- 結果:NULL
SELECT NULLIF('hello', ''); -- 結果:'hello'
-- スペースも含めて処理
SELECT NULLIF(TRIM(' '), ''); -- 結果:NULL
🎨 完璧な組み合わせパターン
パターン1:基本的な空文字処理
-- NULLIF + COALESCE の黄金パターン
SELECT
COALESCE(NULLIF(name, ''), 'デフォルト名') AS clean_name
FROM users;
-- 処理の流れ:
-- 1. NULLIF(name, '') で空文字をNULLに変換
-- 2. COALESCEでNULLをデフォルト値に置換
パターン2:スペースも含めた処理
-- TRIM + NULLIF + COALESCE の完全パターン
SELECT
COALESCE(NULLIF(TRIM(name), ''), 'デフォルト名') AS clean_name
FROM users;
-- より詳細な例
SELECT
id,
name AS original,
COALESCE(
NULLIF(TRIM(name), ''),
'ゲストユーザー'
) AS processed_name
FROM (
VALUES
(1, 'Alice'),
(2, NULL),
(3, ''),
(4, ' '),
(5, ' Bob ')
) AS t(id, name);
-- 結果:
-- id | original | processed_name
-- 1 | Alice | Alice
-- 2 | NULL | ゲストユーザー
-- 3 | | ゲストユーザー
-- 4 | | ゲストユーザー
-- 5 | Bob | Bob
📊 実践的な活用例
住所データのクリーニング
-- 複数カラムの処理
SELECT
user_id,
COALESCE(NULLIF(address1, ''), '住所未登録') AS address_line1,
COALESCE(NULLIF(address2, ''), '') AS address_line2,
COALESCE(NULLIF(city, ''), '市区町村未登録') AS city,
COALESCE(NULLIF(TRIM(postal_code), ''), '〒000-0000') AS postal_code
FROM user_addresses;
連絡先の優先順位処理
-- 複数の連絡先から有効なものを選択
SELECT
user_id,
COALESCE(
NULLIF(TRIM(mobile), ''),
NULLIF(TRIM(home_phone), ''),
NULLIF(TRIM(office_phone), ''),
NULLIF(TRIM(email), ''),
'連絡先なし'
) AS primary_contact
FROM contacts;
より高度な処理パターン

🚀 CASE文を使った柔軟な処理
条件別の処理
-- より細かい制御が必要な場合
SELECT
id,
name,
CASE
WHEN name IS NULL THEN 'NULLです'
WHEN TRIM(name) = '' THEN '空文字です'
WHEN LENGTH(TRIM(name)) < 2 THEN '短すぎます'
ELSE name
END AS validated_name
FROM users;
複雑な変換ロジック
-- ビジネスルールに基づく処理
WITH processed_data AS (
SELECT
id,
name,
CASE
WHEN name IS NULL OR TRIM(name) = '' THEN
CASE
WHEN user_type = 'guest' THEN 'ゲスト' || id::TEXT
WHEN user_type = 'member' THEN 'メンバー' || id::TEXT
ELSE '不明なユーザー'
END
ELSE TRIM(name)
END AS display_name
FROM users
)
SELECT * FROM processed_data;
🔄 UPDATE文での一括クリーニング
データベース全体のクリーニング
-- 空文字をNULLに統一する
UPDATE users
SET
name = NULLIF(TRIM(name), ''),
email = NULLIF(TRIM(email), ''),
phone = NULLIF(TRIM(phone), '')
WHERE
TRIM(name) = '' OR
TRIM(email) = '' OR
TRIM(phone) = '';
-- または、空文字をデフォルト値に置換
UPDATE users
SET
name = COALESCE(NULLIF(TRIM(name), ''), 'ゲストユーザー'),
email = COALESCE(NULLIF(TRIM(email), ''), 'no-email@example.com')
WHERE
name IS NULL OR TRIM(name) = '';
パフォーマンスを考慮した実装
⚡ インデックスへの影響
問題:関数を使うとインデックスが効かない
-- インデックスが効かないパターン
CREATE INDEX idx_users_name ON users(name);
-- この検索はインデックスを使えない
SELECT * FROM users
WHERE COALESCE(NULLIF(TRIM(name), ''), 'default') = 'Alice';
解決策:関数インデックスの作成
-- 関数インデックスを作成
CREATE INDEX idx_users_clean_name
ON users (COALESCE(NULLIF(TRIM(name), ''), 'default'));
-- または、生成列を使用(PostgreSQL 12+)
ALTER TABLE users
ADD COLUMN clean_name TEXT
GENERATED ALWAYS AS (
COALESCE(NULLIF(TRIM(name), ''), 'ゲストユーザー')
) STORED;
CREATE INDEX idx_users_clean_name ON users(clean_name);
📈 大量データ処理の最適化
バッチ処理での効率化
-- 一時テーブルを使った処理
CREATE TEMP TABLE cleaned_users AS
SELECT
id,
COALESCE(NULLIF(TRIM(name), ''), 'デフォルト') AS name,
COALESCE(NULLIF(TRIM(email), ''), 'no-email') AS email
FROM users;
-- インデックスを作成
CREATE INDEX idx_temp_name ON cleaned_users(name);
-- 高速な結合や集計が可能
SELECT
c.name,
COUNT(*) as user_count
FROM cleaned_users c
GROUP BY c.name;
JSON/JSONBデータでの空文字処理
🗂️ JSONデータ内の空文字対策
-- JSONBデータの処理例
CREATE TABLE json_data (
id SERIAL PRIMARY KEY,
data JSONB
);
INSERT INTO json_data (data) VALUES
('{"name": "Alice", "email": "alice@example.com"}'),
('{"name": "", "email": "bob@example.com"}'),
('{"name": null, "email": ""}'),
('{"name": " ", "email": null}');
-- JSON内の空文字とNULLを処理
SELECT
id,
COALESCE(
NULLIF(TRIM(data->>'name'), ''),
'デフォルト名'
) AS name,
COALESCE(
NULLIF(TRIM(data->>'email'), ''),
'no-email@example.com'
) AS email
FROM json_data;
-- JSONBの更新
UPDATE json_data
SET data = jsonb_build_object(
'name', COALESCE(NULLIF(TRIM(data->>'name'), ''), 'デフォルト名'),
'email', COALESCE(NULLIF(TRIM(data->>'email'), ''), 'no-email@example.com')
);
実用的な関数の作成
🛠️ カスタム関数で処理を標準化
空文字処理用の汎用関数
-- 空文字とNULLを処理する関数
CREATE OR REPLACE FUNCTION clean_text(
input_text TEXT,
default_value TEXT DEFAULT ''
) RETURNS TEXT AS $$
BEGIN
RETURN COALESCE(NULLIF(TRIM(input_text), ''), default_value);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- 使用例
SELECT clean_text(name, 'ゲスト') AS clean_name FROM users;
SELECT clean_text(email) AS clean_email FROM users; -- デフォルトは空文字
複数カラム処理用の関数
-- 複数の値から最初の有効な値を返す
CREATE OR REPLACE FUNCTION first_non_empty(VARIADIC texts TEXT[])
RETURNS TEXT AS $$
DECLARE
txt TEXT;
BEGIN
FOREACH txt IN ARRAY texts
LOOP
IF txt IS NOT NULL AND TRIM(txt) != '' THEN
RETURN TRIM(txt);
END IF;
END LOOP;
RETURN NULL;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- 使用例
SELECT first_non_empty(mobile, home_phone, office_phone, 'なし') AS contact
FROM contacts;
よくある間違いと注意点
⚠️ 間違いやすいパターン
間違い1:空文字の比較
-- ❌ 間違い:これではNULLしか処理できない
SELECT COALESCE(name, 'デフォルト') FROM users;
-- ✅ 正解:空文字も処理
SELECT COALESCE(NULLIF(name, ''), 'デフォルト') FROM users;
間違い2:TRIMの位置
-- ❌ 間違い:TRIMが後
SELECT TRIM(COALESCE(name, 'デフォルト')) FROM users;
-- ✅ 正解:TRIMが先
SELECT COALESCE(NULLIF(TRIM(name), ''), 'デフォルト') FROM users;
間違い3:データ型の不一致
-- ❌ 間違い:型が異なる
SELECT COALESCE(user_id, 'なし'); -- user_idがINTの場合エラー
-- ✅ 正解:型を統一
SELECT COALESCE(user_id::TEXT, 'なし');
ベストプラクティス

✅ 推奨される実装パターン
1. データ投入時の正規化
-- トリガーで入力時に正規化
CREATE OR REPLACE FUNCTION normalize_empty_strings()
RETURNS TRIGGER AS $$
BEGIN
NEW.name = NULLIF(TRIM(NEW.name), '');
NEW.email = NULLIF(TRIM(NEW.email), '');
NEW.phone = NULLIF(TRIM(NEW.phone), '');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER normalize_before_insert
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION normalize_empty_strings();
2. ビューでの統一処理
-- クリーンなデータを提供するビュー
CREATE VIEW v_users_clean AS
SELECT
id,
COALESCE(NULLIF(TRIM(name), ''), 'ゲストユーザー') AS name,
COALESCE(NULLIF(TRIM(email), ''), 'no-email') AS email,
COALESCE(NULLIF(TRIM(phone), ''), 'なし') AS phone
FROM users;
よくある質問と回答
Q:COALESCEとNVLの違いは?
A: NVLはOracle固有の関数で、PostgreSQLではCOALESCEを使用します。COALESCEは複数の引数を取れる点で、より柔軟です。
Q:大量のカラムを処理する効率的な方法は?
A: 動的SQLを使用して、一括処理することができます:
DO $$
DECLARE
col_name TEXT;
BEGIN
FOR col_name IN
SELECT column_name FROM information_schema.columns
WHERE table_name = 'users' AND data_type = 'text'
LOOP
EXECUTE format('UPDATE users SET %I = NULLIF(TRIM(%I), '''')', col_name, col_name);
END LOOP;
END $$;
Q:パフォーマンスへの影響は?
A: NULLIF + TRIM + COALESCEの組み合わせは、単純なCOALESCEより2-3倍遅くなります。大量データの場合は、事前に正規化するか、マテリアライズドビューの使用を検討してください。
Q:”とNULLどちらを使うべき?
A: データベース設計ではNULLを推奨します。空文字は「空の値」、NULLは「値がない」という意味的な違いがあり、NULLの方が適切に扱えます。
まとめ:空文字とNULLを完璧に処理しよう!
PostgreSQLでの空文字処理は、NULLIF + COALESCEの組み合わせが鍵です。
重要ポイント:
- COALESCEだけでは空文字は処理できない
- NULLIF(TRIM(column), ”)で空文字をNULLに変換
- その後COALESCEでデフォルト値に置換
- パフォーマンスが重要なら関数インデックスを作成
- データ投入時に正規化するのがベスト
黄金パターンを覚えよう:
COALESCE(NULLIF(TRIM(column), ''), 'デフォルト値')
この記事の手法を使えば、NULLも空文字も、もう怖くありません!
クリーンなデータで、快適なSQL生活を! 🚀💫
コメント