The Silent Saboteur NULL SQLクエリへの影響を理解する
Lukas Schneider
DevOps Engineer · Leapcell

はじめに
リレーショナルデータベースの世界では、データの整合性と正確なクエリが最も重要です。開発者やデータアナリストは、意味のある洞察を引き出すために、正確なSQLステートメントを作成するために無数の時間を費やしています。しかし、一見単純なクエリの表面下には、単純に見えながらも非常に複雑な概念、すなわちNULLが潜んでいます。しばしば誤解され、頻繁に見過ごされるNULLは、COUNT()、JOIN、WHERE句の期待される動作を静かに妨害し、集計の間違い、JOINでのデータ損失、そして捉えどころのないレコードにつながる可能性があります。この記事では、NULLのニュアンスを掘り下げ、この特別なマーカーが単純に見えるクエリを複雑なパズルに変える方法を説明し、その影響を明らかにするための実践的な例を提供します。
NULLの特別な性質
複雑さを解き明かす前に、NULLについて明確な理解を確立しましょう。
- NULL: SQLでは、NULLは欠落している、または不明な情報のマーカーです。NULLは値ではないことを理解することが重要です。ゼロではなく、空の文字列ではなく、偽でもありません。それは値の不在です。この区別は、NULLが演算子や関数とどのように相互作用するかに影響するため、基本的です。
この基本的な理解をもって、NULLが一般的なSQL操作をどのように複雑にするかを探りましょう。
COUNT()とNULLの奇妙な性質
COUNT()集計関数は、列内の行または非NULL値の数を数えるために使用されます。NULLの存在は、その結果を大きく変える可能性があります。
COUNT(*): これは、どの列にNULL値が含まれる行も含め、結果セット内のすべての行を数えます。COUNT(column_name): これは、指定されたcolumn_name内の非NULL値のみを数えます。COUNT(DISTINCT column_name): これは、指定されたcolumn_name内のユニークな非NULL値の数を数えます。
productsテーブルを考えてみましょう。
CREATE TABLE products ( product_id INT PRIMARY KEY, product_name VARCHAR(100), price DECIMAL(10, 2), category_id INT ); INSERT INTO products (product_id, product_name, price, category_id) VALUES (1, 'Laptop', 1200.00, 101), (2, 'Keyboard', 75.00, 102), (3, 'Mouse', 25.00, 102), (4, 'Monitor', 300.00, NULL), (5, 'Webcam', 50.00, 103), (6, 'Speaker', NULL, 103), (7, NULL, 10.00, 104); -- 名前が不明な商品と仮定
COUNT()の動作を見てみましょう。
-- すべての行をカウント SELECT COUNT(*) FROM products; -- 期待される出力: 7 -- 非NULLの価格をカウント SELECT COUNT(price) FROM products; -- 期待される出力: 6 (product_id 6はNULLの価格を持っています) -- 非NULLのcategory_idをカウント SELECT COUNT(category_id) FROM products; -- 期待される出力: 6 (product_id 4はNULLのcategory_idを持っています) -- ユニークなcategory_idをカウント (NULLはカウントされません) SELECT COUNT(DISTINCT category_id) FROM products; -- 期待される出力: 3 (101, 102, 103. NULLは除外されます) -- 非NULLのproduct_nameをカウント SELECT COUNT(product_name) FROM products; -- 期待される出力: 6 (product_id 7はNULLのproduct_nameを持っています)
これらの例は、COUNT(column_name)が明示的にNULLを無視すること明確に示しています。これは、その列が存在するすべての行をカウントすると期待している場合、混乱の原因となる可能性があります。
JOIN操作と捉えどころのないNULL
JOIN句は、関連する列に基づいて2つ以上のテーブルの行を結合します。これらの関連列にNULLが含まれている場合、動作は直感的でない可能性があります。
- NULLはNULLに等しいは FALSE: SQLでは、
NULL = NULLは不明と評価され、比較操作ではFALSEとして扱われます。これは、JOIN列にNULLを持つ行が、標準の等価性 (=) を使用してJOIN列の別のNULLと一致することは決してないことを意味します。
categoriesテーブルを考えてみましょう。
CREATE TABLE categories ( category_id INT PRIMARY KEY, category_name VARCHAR(100) ); INSERT INTO categories (category_id, category_name) VALUES (101, 'Electronics'), (102, 'Peripherals'), (103, 'Accessories'), (999, 'Uncategorized'); -- 未使用のカテゴリ
ここで、productsとcategoriesを結合してみましょう。
-- INNER JOIN: product.category_idがcategory.category_idと一致する行のみ SELECT p.product_name, c.category_name FROM products p INNER JOIN categories c ON p.category_id = c.category_id;
期待される出力:
| product_name | category_name |
|---|---|
| Laptop | Electronics |
| Keyboard | Peripherals |
| Mouse | Peripherals |
| Webcam | Accessories |
| Speaker | Accessories |
'Monitor' (category_idがNULL) と product_nameがNULLの商品 (category_idが104、categoriesに存在しない) が完全に欠落していることに注意してください。これは、product.category_id = category.category_idがNULLに対して不明/偽と評価されるためです。
'left'テーブル (products) の行を、'right'テーブル (categories) に一致するものがない場合でも含めるには、LEFT JOINを使用します。
-- LEFT JOIN: カテゴリの一致がない場合でも、productsのすべての行を含めます SELECT p.product_name, c.category_name FROM products p LEFT JOIN categories c ON p.category_id = c.category_id;
期待される出力:
| product_name | category_name |
|---|---|
| Laptop | Electronics |
| Keyboard | Peripherals |
| Mouse | Peripherals |
| Monitor | NULL |
| Webcam | Accessories |
| Speaker | Accessories |
| NULL |
ここでは、'Monitor'が表示されますが、category_nameはNULLで、一致が見つからなかったことを示しています。product_id 7の商品も、category_id 104がcategoriesテーブルに存在しないため、NULLのcategory_nameとともに表示されます。product_id 7のcategory_idもNULLであった場合、NULLのcategory_nameとともに表示されるでしょう。
NULLの存在に基づいて明示的に結合したい場合は、IS NULLまたはIS NOT NULLを使用して明示的に処理する必要があります。たとえば、これは通常、結合のための良い習慣ではありませんが、概念を説明しています。
-- これはNULLを明示的に一致させようとしますが、標準の等価性ではできません SELECT p.product_name, c.category_name FROM products p LEFT JOIN categories c ON (p.category_id = c.category_id) OR (p.category_id IS NULL AND c.category_id IS NULL); -- これは、productsのNULLのcategory_idをcategoriesのNULLのcategory_idに一致させる可能性があり、 -- categoriesにそのような行が存在すると仮定していますが、例にはありません。 -- ほとんどのリレーショナル設計では、category_idは外部キーであり、categoryテーブルではNULLではありません。
JOINの重要な点は、JOIN条件 NULLは標準の等価性のもとでは一致を妨げるということです。
WHERE句と謎めいたNULL
WHERE句は、指定された条件に基づいて行をフィルタリングします。WHERE句のNULLは、標準の比較演算子 (=, !=, <, >) がNULLと比較されると不明と評価されるため、予期しないフィルタリングにつながる可能性があります。
column_name = NULLは FALSE (または不明): この条件は決して真になりません。つまり、WHERE my_column = NULLは、my_columnにNULLが含まれる行でさえ、決して行を選択しません。column_name != NULLは FALSE (または不明): 同様に、これも決して真になりません。IS NULLとIS NOT NULL: これらは、NULLの存在または不在をチェックするための正しい演算子です。
productsテーブルをクエリしてみましょう。
-- NULL価格の商品を見つけようとします (これは何も返しません) SELECT product_name, price FROM products WHERE price = NULL; -- 期待される出力: (空のセット) -- NULL価格の商品を見つける正しい方法 SELECT product_name, price FROM products WHERE price IS NULL; -- 期待される出力: -- | product_name | price | -- | Speaker | NULL | -- NULLでない価格の商品を見つけようとします (これは何も返しません) SELECT product_name, price FROM products WHERE price != NULL; -- 期待される出力: (空のセット) -- NULLでない価格の商品を見つける正しい方法 SELECT product_name, price FROM products WHERE price IS NOT NULL; -- 期待される出力: -- | product_name | price | -- | Laptop | 1200.00| -- | Keyboard | 75.00 | -- | Mouse | 25.00 | -- | Monitor | 300.00 | -- | Webcam | 50.00 | -- | NULL | 10.00 |
さらに、NULL値はWHERE句論理演算子を複雑にする可能性があります。
-- 価格が75.00 または category_idがNULLの商品を見つけます SELECT product_name, price, category_id FROM products WHERE price = 75.00 OR category_id IS NULL; -- 期待される出力: -- | product_name | price | category_id | -- | Keyboard | 75.00 | 102 | -- | Monitor | 300.00| NULL | -- 価格が75.00 かつ category_idがNULLの商品を見つけます (これは何も返しません) SELECT product_name, price, category_id FROM products WHERE price = 75.00 AND category_id IS NULL; -- 期待される出力: (空のセット)
NULL (特に一方が不明と評価される場合) とのANDおよびORの動作は、3値論理に従っており、そのルールをしっかりと把握せずに予測するのは非常に複雑になる可能性があります。
NULLを処理するためのベストプラクティス
NULLによって導入される複雑さを軽減するために、これらのプラクティスを検討してください。
-
IS NULLおよびIS NOT NULLを使用する:WHERE句でNULLの存在または不在をチェックする際は、常にこれらの演算子を使用してください。 -
COUNT()のバリエーションを理解する: すべての行をカウントしたいのか (COUNT(*))、それとも列の非NULL値のみをカウントしたいのか (COUNT(column_name)) を明確にしてください。 -
JOINでのNULLの処理: 標準の等価性比較ではNULLは一致しないことに注意してください。JOIN条件でNULLを処理する必要がある場合は、
COALESCEまたは明示的なIS NULLチェックを使用する必要があるかもしれませんが、これは設計上の問題を示すことがよくあります。 -
COALESCEとIFNULL(またはISNULL): これらの関数を使用して、NULL値にデフォルト値を代入し、比較および集計を予測可能にします。-- 集計のためにNULL価格を0に置き換えます SELECT product_name, COALESCE(price, 0) AS actual_price FROM products; -- category_idが102またはNULLである (NULLを「不明なカテゴリ」として扱う) 商品をカウントします SELECT COUNT(*) FROM products WHERE COALESCE(category_id, -1) = 102 OR COALESCE(category_id, -1) = -1; -
データベース設計: 可能であればNULLを最小限に抑えます。列に常に値が必要な場合は、
NOT NULLと宣言します。値が実際に欠落している場合は、その影響を理解してください。
結論
NULLは単なる「値なし」以上のものであり、SQLクエリの実行に浸透する基本的な概念であり、慎重な検討が必要です。COUNT()、JOIN、WHERE句でのそのユニークな動作は、単純な操作を微妙なバグや不正確な結果の原因に変える可能性があります。IS NULLやCOALESCEのような適切なSQL構文を使用することで、NULLの複雑さをマスターし、データベースクエリの正確性と信頼性を確保できます。NULLとの調和のとれた相互作用の鍵は、その存在を認識し、そのルールを尊重することにあります。

