大家好,我是大华。
做后端时间久了,我们很容易忽略一个很常见的问题:很多数据库字段默认值都是 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 COM #后端 #Java #数据库MENT '邮箱', `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 前缀!很容易背锅》