大家好,我是大华。
做后端时间久了,我们很容易忽略一个很常见的问题:很多数据库字段默认值都是 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,往往就是问题的开始。