共计 3196 个字符,预计需要花费 8 分钟才能阅读完成。
最近刚入职新公司,发现数据库设计有点小问题,数据库字段很多没有 NOT NULL,对于强迫症早期患者来说,几乎难以忍受,因而有了这篇文章。
基于目前大部分的开发现状来说,咱们都会把字段全副设置成 NOT NULL
并且给默认值的模式。
通常,对于默认值个别这样设置:
- 整形,咱们个别应用 0 作为默认值。
- 字符串,默认空字符串
- 工夫,能够默认
1970-01-01 08:00:01
,或者默认0000-00-00 00:00:00
,然而连贯参数要增加zeroDateTimeBehavior=convertToNull
,倡议的话还是不要用这种默认的工夫格局比拟好
然而,思考下起因,为什么要设置成 NOT NULL?
来自高性能 Mysql 中有这样一段话:
尽量避免 NULL
很多表都蕴含可为 NULL(空值)的列,即便应用程序并不需要保留 NULL 也是如此,这是因为可为 NULL 是列的默认属性。通常状况下最好指定列为 NOT NULL,除非真的须要存储 NULL 值。
如果查问中蕴含可为 NULL 的列,对 MySql 来说更难优化,因为可为 NULL 的列使得索引、索引统计和值比拟都更简单。可为 NULL 的列会应用更多的存储空间,在 MySql 里也须要非凡解决。当可为 NULL 的列被索引时,每个索引记录须要一个额定的字节,在 MyISAM 里甚至还可能导致固定大小的索引(例如只有一个整数列的索引)变成可变大小的索引。
通常把可为 NULL 的列改为 NOT NULL 带来的性能晋升比拟小,所以(调优时)没有必要首先在现有 schema 中查找并批改掉这种状况,除非确定这会导致问题。然而,如果打算在列上建索引,就应该尽量避免设计成可为 NULL 的列。
当然也有例外,例如值得一提的是,InnoDB 应用独自的位(bit)存储 NULL 值,所以对于稠密数据有很好的空间效率。但这一点不适用于 MyISAM。
书中的形容说了几个次要问题,我这里暂且抛开 MyISAM 的问题不谈,这里我针对 InnoDB 作为考量条件。
- 如果不设置 NOT NULL 的话,NULL 是列的默认值,如果不是自身需要的话,尽量就不要应用 NULL
- 应用 NULL 带来更多的问题,比方索引、索引统计、值计算更加简单,如果应用索引,就要防止列设置成 NULL
- 如果是索引列,会带来的存储空间的问题,须要额定的非凡解决,还会导致更多的存储空间占用
- 对于稠密数据又更好的空间效率,稠密数据指的是 很多值为 NULL,只有少数行的列有非 NULL 值 的状况
默认值
对于 MySql 而言,如果不被动设置为 NOT NULL 的话,那么插入数据的时候默认值就是 NULL。
NULL 和 NOT NULL 应用的空值代表的含意是不一样,NULL 能够认为这一列的值是未知的,空值则能够认为咱们晓得这个值,只不过他是空的而已。
举个例子,一张表中的某一条 name
字段是 NULL,咱们能够认为 不晓得名字是什么 ,反之如果是空字符串则能够认为 咱们晓得没有名字,他就是一个空值。
而对于大多数程序的状况而言,没有什么非凡须要非要字段要 NULL 的吧,NULL 值反而会对程序造成比方空指针的问题。
对于现状大部分应用 MyBatis
的状况来说,我倡议应用默认生成的 insertSelective
办法或者纯手动写插入方法,能够防止新增 NOT NULL 字段导致的默认值不失效或者插入报错的问题。
值计算
聚合函数不精确
对于 NULL 值的列,应用聚合函数的时候会疏忽 NULL 值。
当初咱们有一张表,name
字段默认是 NULL,此时对 name
进行 count
得出的后果是 1,这个是谬误的。
count(*)
是对表中的行数进行统计,count(name)
则是对表中非 NULL 的列进行统计。
= 生效
对于 NULL 值的列,是不能应用 =
表达式进行判断的,上面对 name
的查问是不成立的,必须应用is NULL
。
与其余值运算
NULL 和其余任何值进行运算都是 NULL,包含表达式的值也是 NULL。
user
表第二条记录 age
是 NULL,所以 +1
之后还是 NULL,name
是 NULL,进行 concat
运算之后后果还是 NULL。
能够再看下上面的例子,任何和 NULL 进行运算的话得出的后果都会是 NULL,设想下你设计的某个字段如果是 NULL 还不小心进行各种运算,最初得出的后果。。。
distinct、group by、order by
对于 distinct
和group by
来说,所有的 NULL 值都会被视为相等,对于 order by
来说升序 NULL 会排在最前
其余问题
表中只有一条有名字的记录,此时查问名字 !=a
预期的后果应该是想查出来残余的两条记录,会发现与预期后果不匹配。
索引问题
为了验证 NULL 字段对索引的影响,别离对 name
和age
增加索引。
对于网上很多说如果 NULL 那么不能应用索引的说法,这个形容其实并不精确,依据援用官网文档 [3] 里形容,应用 is NULL 和范畴查问都是能够和失常一样应用索引的,理论验证的后果如同也是这样,看以下例子。
而后接着咱们往数据库中持续插入一些数据进行测试,当 NULL 列值变多之后发现索引生效了。
咱们晓得,一个查问 SQL 执行大略是这样的流程:
首先连接器负责连贯到指定的数据库上,接着看看查问缓存中是否有这条语句,如果有就间接返回后果。
如果缓存没有命中的话,就须要分析器来对 SQL 语句进行语法和词法剖析,判断 SQL 语句是否非法。
当初来到优化器,就会抉择应用什么索引比拟正当,SQL 语句具体怎么执行的计划就确定下来了。
最初执行器负责执行语句、有无权限进行查问,返回执行后果。
从下面的简略测试后果其实能够看到,索引列存在 NULL 就会存在书中所说的导致优化器在做索引抉择的时候更简单,更加难以优化。
存储空间
数据库中的一行记录在最终磁盘文件中也是以行的形式来存储的,对于 InnoDB 来说,有 4 种行存储格局:REDUNDANT
、COMPACT
、DYNAMIC
和 COMPRESSED
。
InnoDB 的默认行存储格局是COMPACT
,存储格局如下所示,虚线局部代表可能不肯定会存在。
变长字段长度列表:有多个字段则以逆序存储,咱们只有一个字段所有不思考那么多,存储格局是 16 进制,如果没有变长字段就不须要这一部分了。
NULL 值列表:用来存储咱们记录中值为 NULL 的状况,如果存在多个 NULL 值那么也是逆序存储,并且必须是 8bit 的整数倍,如果不够 8bit,则高位补 0。1 代表是 NULL,0 代表不是 NULL。如果都是 NOT NULL 那么这个就存在了。
ROW_ID:一行记录的惟一标记,没有指定主键的时候主动生成的 ROW_ID 作为主键。
TRX_ID:事务 ID。
ROLL_PRT:回滚指针。
最初就是每列的值。
为了阐明分明这个存储格局的问题,我弄张表来测试,这张表只有 c1
字段是 NOT NULL,其余都是能够为 NULL 的。
可变字段长度列表 :c1
和c3
字段值长度别离为 1 和 2,所以长度转换为 16 进制是0x01 0x02
,逆序之后就是0x02 0x01
。
NULL 值列表 :因为存在容许为 NULL 的列,所以c2,c3,c4
别离为 010,逆序之后还是一样,同时高位补 0 满 8 位,后果是00000010
。
其余字段咱们临时不论他,最初第一条记录的后果就是,当然这里咱们就不思考编码之后的后果了。
这样就是一个残缺的数据行数据的格局,反之,如果咱们把所有字段都设置为 NOT NULL,并且插入一条数据 a,bb,ccc,dddd
的话,存储格局应该这样:
尽管咱们发现 NULL 自身并不会占用存储空间,然而如果存在 NULL 的话就会多占用一个字节的标记位的空间。
文章参考文档:
- https://dev.mysql.com/doc/ref…
- https://dev.mysql.com/doc/ref…
- https://dev.mysql.com/doc/ref…
- https://dev.mysql.com/doc/ref…
- https://www.cnblogs.com/zhouj…