🇯🇵 日本語 | 🇺🇸 English | 🇪🇸 Español | 🇵🇹 Português | 🇹🇭 ไทย | 🇨🇳 中文

【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初学者百分之百会掉进这个坑里,那就是写出WHERE phone_number = NULL这样的语句。虽然看起来似乎能正常工作,但执行这个查询后,一条数据也返回不了。

-- 【错误!】这个查询不会按预期工作
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`(数据为空)的记录,应该使用`IS NULL`,而不是= 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”的区别

初学者容易将“空字符串('')”和“数字0”与`NULL`混为一谈。在SQL中,它们被明确区分为不同的东西。

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表已经为你准备好了。

亲手试试,看看= NULLIS NULL的结果有何不同,以及为什么COUNT(*)COUNT(列名)的结果会不一样吧!

<!DOCTYPE html>
<html lang="zh-CN">
<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 = '数据库准备中...';
      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`的概念,是熟练运用SQL的必经之路。一开始可能会有些困惑,但只要你意识到“`NULL`不是一个值,而是一种状态”,就应该能逐渐习惯。这项技能对于正确处理数据缺失、开发bug少的健壮应用程序至关重要。你的SQL水平又上了一个新台阶!