大家好,我是大华。

做后端时间久了,我们很容易忽略一个很常见的问题:很多数据库字段默认值都是 NULL。 在写表结构的时候,大家往往图省事:“先允许 NULL 吧,有问题以后再说”。 但系统跑一段时间之后,往往就会出现各种琐事。

看个案例

假设我们有个用户表,记录用户的信息:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `status` tinyint(4) DEFAULT NULL COMMENT '状态',
  `created_at` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

看着没问题吧?很多新手甚至老手都这么设计,字段能 NULL 就 NULL,省事!

但后面写业务的时候,你就会发现各种奇奇怪怪的问题。

问题一:查询结果不一样

某天产品经理跑过来说:“帮我统计一下有多少用户没填邮箱,我们要做一波营销活动。”

你心想:这还不简单?邮箱为空的用户呗!于是你写了:

SELECT COUNT(*) FROM user WHERE email = NULL;

结果返回0,你懵了:明明有很多用户没填邮箱啊,怎么会是0?

改一下:

SELECT COUNT(*) FROM user WHERE email IS NULL;

这次对了。在SQL里,NULL 代表”未知”,它不等于任何值,甚至不等于它自己。所以判断是否为 NULL 不能用 = NULL,必须用 IS NULL

问题二:统计结果总是少那么几条

还是刚才那个需求,现在你想统计有邮箱的用户有多少:

SELECT COUNT(*) FROM user WHERE email != '';

等等,这样写对吗?不对! 因为 NULL 值不会被 != 条件包含进去。正确写法应该是:

SELECT COUNT(*) FROM user WHERE email IS NOT NULL AND email != '';

每次写这种条件都得记着排除NULL,似乎挺烦的。

问题三:函数计算错误

想统计一下用户平均年龄:

SELECT AVG(age) FROM user;

结果发现结果比实际小。为啥? 因为 AVG、SUM 这些聚合函数会忽略 NULL 值。如果你的表里很多用户的 age 是 NULL,那平均值就只计算有年龄的用户,结果当然不准。

更坑的是字符串拼接:

SELECT CONCAT(username, '的邮箱是', email) FROM user;

只要 email 是 NULL,整个结果就变成 NULL 了。想显示邮箱为空都不行。

问题四:程序里报错防不胜防

假设你用 Java 的 MyBatis 查询用户信息:

User user = userMapper.findById(1L);
String email = user.getEmail();
if (email.equals("")) {
    // 做点啥...
}

这里会直接空指针异常!因为 email 是 NULL,不能调用 equals 方法。 你可能会说:“我知道 NULL 会空指针,我加判断不就行了?”

可以,但你想想,每个字段取出来都可能 NULL,每个地方都得加判空,代码写出来全是 if else,看着不难受吗?

问题五:索引失效,查询变慢

NULL 值在索引中的处理方式比较特殊。虽然现在 MySQL 对 NULL 的索引优化做得比以前好了,但依然存在一些问题:

SELECT * FROM user WHERE email = 'xxx@qq.com';

这条语句如果 email 有很多 NULL 值,MySQL扫描索引时会跳过这些NULL吗?不一定。某些情况下,NULL 值会让索引的选择性变差,查询优化器可能选择全表扫描。

那应该怎么设计?

一个原则:字段尽量设置为 NOT NULL,并给定默认值

上面的表应该改成这样:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `nickname` varchar(50) NOT NULL DEFAULT '' COMMENT '昵称',
  `age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
  `phone` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
  `email` varchar(100) NOT NULL DEFAULT '' COMMENT '邮箱',
  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态 0-正常 1-禁用',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这样设计后:

1、查询没填邮箱的用户:WHERE email = '',简单直接。 2、统计平均年龄:不会漏掉任何用户(没填年龄的就是0岁,当然业务上可能需要区分未填和0岁,这种情况可以用-1表示未知)。 3、程序里取值:不用到处判空,反正不会是 NULL。 4、索引效率:没有 NULL 值,索引更紧凑,查询更快。

什么时候可以允许NULL?

当然,凡事无绝对。有些场景用 NULL 确实更合适:

1、业务上需要区分”空值”和”未知值”。比如婚姻状况,”未婚”是确定的状态,而NULL表示”未知”。 2、某些特殊字段,比如逻辑删除的时间戳,NULL表示未删除,有值表示删除时间。 3、第三方系统对接,必须保留NULL含义。

但这种字段应该是少数,大部分字段完全可以用空字符串或0代替NULL。

一句话总结:很多系统变复杂,并不是因为功能多,而是因为数据状态太多。而滥用 NULL,往往就是问题的开始。

往期推荐

《为什么 PUT 和 DELETE 请求在大厂中逐渐被弃用?》

《Java 双层 for 循环太慢?优化方案来了》

《几行代码就能实现的效果,为什么很多前端写不出来?》

《Java 开发千万别给布尔变量加 is 前缀!很容易背锅》

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com