【SQL NULL入門】IS NULL / IS NOT NULLを制してデータ抽出をマスターする
SQLを学び始めると、必ず出会う不思議な存在、それが`NULL`です。「電話番号が未登録のユーザー」「備考欄に何も書かれていない商品」など、データが存在しない状態を表すために使われます。しかし、この`NULL`の扱いは非常に特殊で、多くの初学者が「なぜか思った通りにデータが取得できない…」という壁にぶつかります。
その最大の原因は、`NULL`を「0」や「空の文字列('')」と同じように、=(イコール)で比較しようとしてしまうことです。実は、SQLの世界では`NULL`は他のどんな値とも等しくなく、`NULL`自身とすら等しくないという、哲学的なルールが存在します。
この記事では、この`NULL`の正体と、それを正しく扱うための専用の演算子`IS NULL`と`IS NOT NULL`について、コピペで動くコードを使いながら徹底的に解説します。`NULL`を制する者はSQLを制す、と言っても過言ではありません。この機会に、データ存在チェックのスキルを確実にマスターしましょう!
準備:NULLを含む会員データを用意しよう
`NULL`の挙動を実際に確認するために、まずはサンプルデータを用意します。今回は、シンプルな会員情報を管理する`members`テーブルを作成します。電話番号や最終ログイン日など、意図的に`NULL`(データが空の状態)を含むレコードを登録しておくのがポイントです。
-- もしmembersテーブルが存在すれば削除(繰り返し試せるように)
DROP TABLE IF EXISTS members;
-- 新しいmembersテーブルを作成
CREATE TABLE members (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
phone_number TEXT, -- NULLを許可
last_login DATE, -- NULLを許可
points INTEGER DEFAULT 0
);
-- 初期データを挿入
INSERT INTO members (id, name, email, phone_number, last_login, points) VALUES
(1, '山田 太郎', 'yamada@example.com', '090-1111-2222', '2025-06-25', 150),
(2, '鈴木 花子', 'suzuki@example.com', NULL, '2025-07-01', 50),
(3, '佐藤 次郎', 'sato@example.com', '080-3333-4444', NULL, 200),
(4, '伊藤 さくら', 'ito@example.com', NULL, NULL, 0),
(5, '渡辺 健太', 'watanabe@example.com', '', '2025-06-28', 300); -- 電話番号は空文字
このデータには、電話番号が`NULL`のユーザー(ID 2, 4)や、最終ログインが`NULL`のユーザー(ID 3, 4)が含まれています。また、ID 5の渡辺さんの電話番号は`NULL`ではなく**空文字**(`''`)である点にも注目してください。この違いが後ほど重要になります。
最大の落とし穴:なぜ `= NULL` では検索できないのか?
SQL初心者が100%ハマると言っても過言ではないのが、WHERE phone_number = NULLという書き方です。一見、正しく動作しそうですが、このクエリを実行しても、1件もデータは返ってきません。
-- 【間違い!】このクエリは意図通りに動作しません
SELECT * FROM members WHERE phone_number = NULL;
なぜでしょうか? それは、`NULL`が「値がない、不明である」という特殊な状態を表すからです。`NULL`は値ではないため、どんな値と比較しても(`NULL`自身との比較でさえも)、その結果は`TRUE`でも`FALSE`でもなく、`UNKNOWN`(不明)という第三の状態になります。WHERE句は条件が`TRUE`になるレコードだけを抽出するため、結果が`UNKNOWN`になるレコードは決して選ばれないのです。
アンケート用紙で「好きな食べ物」という質問があったとして、「未記入」の人を探したいときに、「『未記入』と書いた人を探す」という方法では見つからないのと同じです。「未記入である」という状態そのものを探す必要があるのです。
【基本】`IS NULL` と `IS NOT NULL` の正しい使い方
SQLには、この`NULL`という特殊な状態を判定するための専用の演算子が用意されています。それが`IS NULL`と`IS NOT NULL`です。
`IS NULL`:値がNULLのレコードを抽出する
ある列の値が`NULL`(データが空)であるレコードを探したい場合は、= NULLではなく、IS NULLを使いましょう。
シナリオ:「電話番号を登録していない(`phone_number`が`NULL`の)会員を探したい」
SELECT * FROM members WHERE phone_number IS NULL;
実行結果:
id | name | email | phone_number | last_login | points
---|-----------|-------------------|--------------|------------|-------
2 | 鈴木 花子 | suzuki@example.com| NULL | 2025-07-01 | 50
4 | 伊藤 さくら | ito@example.com | NULL | NULL | 0
今度は意図通り、IDが2と4のユーザーが正しく抽出されました。ID 5の渡辺さんは、電話番号が「空文字」であり`NULL`ではないため、結果に含まれない点に注意してください。
`IS NOT NULL`:値がNULLでないレコードを抽出する
逆に、値が空ではない、つまり何かしらのデータが入っているレコードを探したい場合は、IS NOT NULLを使います。
シナリオ:「最終ログインの記録がある(`last_login`が`NULL`ではない)会員を探したい」
SELECT * FROM members WHERE last_login IS NOT NULL;
実行結果:
id | name | email | phone_number | last_login | points
---|-----------|---------------------|----------------|------------|-------
1 | 山田 太郎 | yamada@example.com | 090-1111-2222 | 2025-06-25 | 150
2 | 鈴木 花子 | suzuki@example.com | NULL | 2025-07-01 | 50
5 | 渡辺 健太 | watanabe@example.com| | 2025-06-28 | 300
このように、`IS NOT NULL`を使えば、データが欠損していないレコードだけを効率的に選び出すことができます。
応用:`NULL`と「空文字」「0」の違いを理解する
初心者が`NULL`と同じように扱ってしまいがちなのが、「空文字('')」と「数値の0」です。これらはSQLでは明確に異なるものとして扱われます。
- `NULL`: 「不明」「存在しない」という状態。
- 空文字(
''): 長さが0の文字列という「値」。 0: 「ゼロ」という数値の「値」。
ID 5の渡辺さんの電話番号は、`NULL`ではなく空文字です。したがって、以下のクエリではID 5のユーザーだけがヒットします。
SELECT * FROM members WHERE phone_number = '';
同様に、ID 4の伊藤さんのポイントは`0`ですが、これは`NULL`とは異なります。`COUNT`のような集計関数を使うと、この違いがさらに明確になります。
-- 全会員数、電話番号が登録されている会員数、ポイントの合計
SELECT COUNT(*), COUNT(phone_number), SUM(points) FROM members;
実行結果:
COUNT(*) | COUNT(phone_number) | SUM(points)
--------|---------------------|------------
5 | 3 | 650
COUNT(*)は全レコードを数えるので5ですが、COUNT(phone_number)は`NULL`を無視するため3(ID 1, 3, 5)となります。同様に、SUM(points)も`NULL`のレコードは計算から除外されます。この挙動はデータ分析において非常に重要なので、ぜひ覚えておきましょう。
【体験コーナー】ブラウザでSQLを動かし、NULLの挙動を確かめよう!
さあ、知識をスキルに変える時間です! 以下のHTMLコードを丸ごとコピーして、sql_null_test.htmlのようなファイル名で保存し、ブラウザで開いてみてください。あなた専用のSQL実行環境が立ち上がり、この記事で使っているmembersテーブルが用意されています。
= NULLとIS NULLで結果がどう違うのか、COUNT(*)とCOUNT(列名)でなぜ結果が変わるのか、実際に手を動かしてその目で確かめてみましょう!
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SQL NULL オンライン実行環境</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.7; color: #333; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
h1 { color: #34495e; }
textarea { width: 100%; height: 180px; font-family: "SF Mono", "Consolas", monospace; font-size: 16px; padding: 12px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; margin-bottom: 1rem; }
button { background-color: #34495e; color: white; border: none; padding: 12px 22px; font-size: 16px; border-radius: 6px; cursor: pointer; transition: background-color 0.2s; }
button:hover { background-color: #2c3e50; }
button:disabled { background-color: #bdc3c7; cursor: not-allowed; }
#result-container { margin-top: 2rem; border: 1px solid #ddd; padding: 1rem; border-radius: 6px; background: #fdfdfd; min-height: 50px; }
#error-message { color: #e74c3c; font-weight: bold; }
table { border-collapse: collapse; width: 100%; margin-top: 1rem; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
th { background-color: #f2f2f2; }
tr:nth-child(even) { background-color: #f9f9f9; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js"></script>
</head>
<body>
<h1>SQLを試してみよう!</h1>
<p>下のテキストエリアにSQL文を入力して「実行」ボタンを押してください。色々な条件を試してみましょう!</p>
<textarea id="sql-input">-- まだ一度もログインしたことがない(last_loginがNULLの)会員を探す
SELECT * FROM members WHERE last_login IS NULL;
</textarea>
<button id="execute-btn">実行</button>
<div id="result-container">
<p id="error-message"></p>
<div id="result-output"></div>
</div>
<script>
const sqlInput = document.getElementById('sql-input');
const executeBtn = document.getElementById('execute-btn');
const errorMsg = document.getElementById('error-message');
const resultOutput = document.getElementById('result-output');
let db;
async function initDb() {
executeBtn.disabled = true;
executeBtn.textContent = 'DB準備中...';
try {
const SQL = await initSqlJs({
locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/${file}`
});
db = new SQL.Database();
const setupSql = `
DROP TABLE IF EXISTS members;
CREATE TABLE members (id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, phone_number TEXT, last_login DATE, points INTEGER DEFAULT 0);
INSERT INTO members (id, name, email, phone_number, last_login, points) VALUES
(1, '山田 太郎', 'yamada@example.com', '090-1111-2222', '2025-06-25', 150),
(2, '鈴木 花子', 'suzuki@example.com', NULL, '2025-07-01', 50),
(3, '佐藤 次郎', 'sato@example.com', '080-3333-4444', NULL, 200),
(4, '伊藤 さくら', 'ito@example.com', NULL, NULL, 0),
(5, '渡辺 健太', 'watanabe@example.com', '', '2025-06-28', 300);
`;
db.run(setupSql);
executeBtn.disabled = false;
executeBtn.textContent = '実行';
resultOutput.innerHTML = '<p>準備完了!自由にSQLを試してみてください。</p>';
} catch (err) {
errorMsg.textContent = 'データベースの初期化に失敗しました: ' + err.message;
console.error(err);
}
}
function executeSql() {
if (!db) return;
const sql = sqlInput.value;
errorMsg.textContent = '';
resultOutput.innerHTML = '';
try {
const results = db.exec(sql);
if (results.length === 0) {
resultOutput.innerHTML = '<p>クエリは成功しましたが、結果セットは返されませんでした。</p>';
return;
}
results.forEach(result => {
const table = document.createElement('table');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
const headerRow = document.createElement('tr');
result.columns.forEach(colName => {
const th = document.createElement('th');
th.textContent = colName;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
result.values.forEach(row => {
const bodyRow = document.createElement('tr');
row.forEach(cellValue => {
const td = document.createElement('td');
td.textContent = cellValue === null ? 'NULL' : cellValue;
bodyRow.appendChild(td);
});
tbody.appendChild(bodyRow);
});
table.appendChild(thead);
table.appendChild(tbody);
resultOutput.appendChild(table);
});
} catch (err) {
errorMsg.textContent = 'SQLエラー: ' + err.message;
console.error(err);
}
}
executeBtn.addEventListener('click', executeSql);
initDb();
</script>
</body>
</html>
まとめ
今回は、SQLにおける`NULL`の正しい扱い方について、その概念と具体的な判定方法を学びました。
- `NULL`の正体: `NULL`は「値がない」「不明」という特殊な状態であり、「0」や「空文字」とは全くの別物。
- 比較のルール: `NULL`との比較(
=,<>)は常に`UNKNOWN`になるため、WHERE句で意図した結果が得られない。 - 正しい判定方法: `NULL`であるかを判定するには
IS NULLを、`NULL`でないことを判定するにはIS NOT NULLを使うのが唯一の正しい方法。 - 集計関数との関係:
COUNT(列名)やSUM(),AVG()などの集計関数は、計算の対象から`NULL`のレコードを自動的に除外する。
`NULL`の概念を正しく理解することは、SQLを使いこなす上で避けては通れない道です。最初は少し戸惑うかもしれませんが、「`NULL`は値ではない、状態なのだ」ということを意識すれば、徐々に慣れていくはずです。このスキルは、データの欠損を正しくハンドリングし、バグの少ない堅牢なアプリケーションを開発するために不可欠です。あなたのSQLレベルが、また一段階上がりましたね!