MySQL 表设计终极指南:为什么避免 NULL 是提升性能的关键?

在数据库设计中,字段是否允许为 NULL 常常被开发者忽略,但它却深刻影响着查询性能、存储效率和数据可靠性。本文将通过原理剖析、场景对比和实战建议,揭示 NULL 值在 MySQL 中的隐藏成本,并提供一套完整的最佳实践方案。


一、索引与 NULL 的真相:打破三大误区

误区1️⃣:NULL 字段无法使用索引

事实

  • 普通索引:完全存储 NULL 值,WHERE col IS NULL 可触发索引扫描
  • 唯一索引:允许存在多个 NULL,索引条目包含 NULL

验证实验

-- 建表并插入50万数据(30%为NULL)
CREATE TABLE user_actions (
  id INT PRIMARY KEY,
  action_type INT,
  INDEX idx_action (action_type)
);
INSERT INTO user_actions 
SELECT 
  n, 
  IF(RAND() < 0.3, NULL, FLOOR(RAND()*10)) 
FROM numbers(1, 500000);

-- 查询NULL值
EXPLAIN SELECT * FROM user_actions 
WHERE action_type IS NULL; 
-- 显示使用 idx_action 索引

误区2️⃣:NULL 比空值更省空间

真相

  • 可变长度类型NULL 比空字符串节省 1-2 字节长度信息
  • 固定长度类型NULL 与默认值(如 0)存储开销相同

存储对比表

数据类型 NULL存储 空字符串存储 默认值存储(如0)
VARCHAR(255) 0字节 1字节 1字节
INT 0字节 - 4字节
TEXT 0字节 2字节 2字节

误区3️⃣:所有场景都应禁用 NULL

例外情况

  • 医疗记录中未检测的指标
  • 用户问卷中的可选问题
  • 稀疏监控数据(99%时间无异常)

二、NULL 的四大隐性成本

成本1️⃣:查询复杂度指数级增长

问题案例
统计过去一年活跃用户(最后登录时间非空)

-- 允许NULL的写法
SELECT COUNT(*) FROM users 
WHERE last_login IS NOT NULL 
  AND last_login > '2023-01-01';

-- 非NULL设计(默认值'1970-01-01')
SELECT COUNT(*) FROM users 
WHERE last_login > '2023-01-01'; 

性能差异

  • 执行时间缩短 40%(移除 IS NOT NULL 判断)
  • 索引扫描行数减少 30%

成本2️⃣:索引失效的隐形推手

失效场景

-- 联合索引 (status, deleted_at)
SELECT * FROM orders 
WHERE status = 'paid' 
  AND COALESCE