Postgres: 空文字は NULL ではない。count(nullif(trim(col), '')) で「値あり」だけ数える
count(col) が飛ばすのは NULL だけで、'' や空白だけの文字列は「値あり」として数えられる。空文字を NULL に寄せてから数え、古い行も正規化しておく。
何が起きるか
書き込む側が「不明」を NULL ではなく '' で入れていると、集計がずれる。
count(country)が、空欄の行まで「入力済み」に数える。入力率のダッシュボードが実際より高く出る。GROUP BY countryで、NULL のグループと''のグループが別々にでき、「不明」が2つの行に分かれる。' '(空白だけ)が混ざるとさらに増える。
やり方
集計では空白を落としたうえで空文字を NULL に変えてから数える。
sql
-- 値が入っている行だけ数える
SELECT count(nullif(trim(country), '')) AS filled,
count(*) AS total
FROM users;
-- 「不明」を1つのグループにまとめる
SELECT coalesce(nullif(trim(country), ''), '(不明)') AS country,
count(*)
FROM users
GROUP BY 1;nullif(a, b)は a と b が等しければ NULL、違えば a を返す。trim()で前後の空白を落とすので、' 'も空文字として扱える。
根本対策(既存データと今後の書き込み)
クエリごとに nullif を書くのは漏れやすいので、データそのものを揃える。
sql
-- 既存の行を NULL に寄せる(大きなテーブルは範囲を区切って少しずつ)
UPDATE users SET country = NULL WHERE trim(country) = '';
-- 今後 '' が入らないようにする
ALTER TABLE users
ADD CONSTRAINT users_country_not_blank
CHECK (country IS NULL OR trim(country) <> '') NOT VALID;
ALTER TABLE users VALIDATE CONSTRAINT users_country_not_blank;制約を2段階で足す理由は Postgres: 大きなテーブルへの CHECK / 外部キーの追加は NOT VALID と VALIDATE の 2 段階に分ける を参照。
注意点
trim()が落とすのは既定では半角スペースだけ。タブや改行、全角スペースも空とみなすならbtrim(col, E' \t\r\n ')のように文字を指定するか、regexp_replaceを使う。- 「空文字」と「未回答(NULL)」を意味として区別しているテーブルでは、まとめてはいけない。先に仕様を確認する。
- Oracle は空文字を NULL として扱うが、Postgres・MySQL・SQLite は区別する。DB を移したときにこの違いで集計が変わることがある。
- NULL の扱いの別の落とし穴は Postgres / SQL: サブクエリの NOT IN は NULL が1つあると0件になる。NOT EXISTS を使う。