高质量数据库建模系列课程<10> PPT & 讲义 -- NULL之别传

浏览: 2940

时光荏苒,本课已经出了近半年,到今天(2016-6-13)为止,一共有1342名观众,非常感谢大家能有耐心听我的课。前一段时间家里出了一些变故,而且又一堆杂七杂八的事情,耽搁了发课,从本周开始,重新进入正常的轨道,在此对大家表示歉意。

Clipboard Image.png

课程地址:http://www.hellobi.com/course/54


此外,希望大家听完课以后去https://www.surveymonkey.com/r/CYQLCTD 填写一下调查问卷,我也好有的放矢,对于我教的不好的地方做调整和改善。




大家好, 非常对不起, 最近一个多月由于工作的原因以及家里出了一些事儿, 耽搁了发课,虽然是龟速,至少我们还在坚持,我曾经承诺过要把这课出完,我就一定会出完,从这周起我们就又重新回到轨道上来。

上一节课,我们谈到的NULL值约束,我也提到了,未来需要开单独的课来讲NULL值的设定。同时,我也听到一些同学的声音,觉得课讲的太理论化了, 和实践结合不够紧密,没什么意思,希望这节课能给这样的同学带来一点收获。

 

这节课的内容呢,就是单独讲NULL。先声明一下,有关NULL值的相关设计,数据库领域的几位大神争论了几十年也没个定论,为什么呢? 这个其实和我们设计数据库的道理是一样的, 就是这东西本来就没有绝对的对与错, 在设计数据库的时候我们要做很多取舍, 比如有的时候为了数据更干净, 我们会做很多规范化的设计, 但这样会带来很多编程实现以及性能上的麻烦; 同样的, 有的时候我们设置了很多NULL,可能数据内容的表述更为准确, 但可能容易引起一些程序上的BUG.

 

所以我在这里也只能是讲讲我个人的心得体会,主要的目的是让大家能够更深层次的了解NULL值,只有了解了以后才能减少犯错。至于实际工作中怎么用,还是取决于需求以及应用的场景,我这个不是金科玉律,只是作为参考。

 

首先,我还是要先来看NULL所代表的含义。我曾经说过,NULL是需要谨慎使用的,因为它可能会带来一些数据质量的问题,以及会有一些不太容易被发现的程序的BUG。这些问题引起的原因,其实很多时候是由于程序开发人员,甚至设计者对NULL值的理解是有问题的。或者说,他的理解可能没问题,但程序员编程序的时候呢,不自觉的忽略了。

 

首先对于NULL而言,假如数据类型是整型或者浮点型,那么NULL不代表0, 0实际上还是有含义的,比如我考试缺考,那么成绩是NULL;我考试得了0分,他们的含义是不同的,所以NULL不代表0这个还是容易理解的。

 

而假如数据类型是字符型,那么它也不是长度为0的字符串。这个说起来容易理解,但我经常看到有的人写程序的时候就忘了, 尤其是SQL的新学员,这种错误其实非常常见。这里,我举个例子看一下。

 

这例子非常简单,建一张表nulltst,两个字段,numb和ch,其中numb是整型,ch是varchar类型,插入5条数据。这里我们关注的是ch这个字段,第一行我们将一个数字‘0’作为字符插入,第二行插入一个字符‘A’,第三行插入一个空字符串,注意不是null哈,就是两个单引号,第四行我们插入一个字符B后面带个空格,第五行我们插入一个null值。然后呢,求一下ch这个字段的长度。我们看一下结果,我们可以看到, 前两行,长度都是1, 第三行空字符串,的长度是0,而对于null,求出的长度还是null。所以大家这里要注意,null的长度还是null而不是0,我经常遇到刚开始写SQL的小朋友想当然的认为null的长度为0,然后害怕不准确,外面还加个trim,这个真是看到不止一次两次,大家要注意。



那么NULL既不代表0,也不代表空字符串,那它代表什么啊? 它的含义可以分为两大类型,第一种是未知,第二种是不适用。而未知里面呢,又分两种,第一种是真未知,第二种尚未知。解释一下,真未知指的是什么?指的是其实某个属性是有可能有值的,但因为不知道这个值是什么,没填,所以未知。这种的,我称为真未知。声明一下,真未知这名字我起的,不见得贴切哈,大家领会这个意思就行了。

比如,一个员工入职,HR去填这个员工在前一份工作的工资,但因为员工不肯透露,实在不知道工资是多少,所以这个字段就置为空,那么这个字段的值就是NULL。

就是说,其实这个员工前一份工作是有工资的,但不知道。等未来知道了,还有可能补回去。

 

还有一种呢,是尚未知,这个指的是什么?指的是,这个字段未来有值,但当前还没发生,所以没法填,就留个空在那。比如,一个员工入职,要进某个部门,但因为手续还没办完,还不知道分哪去了,但未来总会有个去处,所以这种就是尚未知。

 

以上这两种都是未知,这是第一类,还有一类是不适用。不适用,比如当前围棋等级分的系统,对于一个围棋选手的描述,有个字段是性别,我们知道李世石,柯洁都是男的,芮乃伟是女的,但前阵子最火的ALPHAGO,它是AI,机器人,你说它什么性别? 因此这种就是不适用,也放空。再举个例子,比如记录一个人的身份证号,而恰好这个人不是中国人,他没有身份证ID,所以这种也是不适用。



介绍完了NULL的基本含义,我们再来好好看看为什么说NULL的使用要谨慎呢。它都有哪些隐患呢?

这里,我从五个方面进行描述。数学计算,where子句,join, 聚合函数以及子查询。



第一个,数学计算。

这个做数据仓库的同学,尤其计算最终报表层数据的,经常遇到这种坑,稍有不慎,就会出现数据计算错误,而且还很难排查。我们来看一个例子。

首先,我创建一张表,nullmath,表结构很简单,只有三个column,col1 char(1)的,col2和col3都是整型的。

我这里插入4条数据,

然后计算了一下col2+col3的值,我们会发现,凡是有null值的,计算出来的结果都是null,这个大家注意哈,这种场景实际上是非常常见的,相信做过统计报表的同学都不会陌生。比如某产品上个月一件没卖出去,这个月卖出去100件,然后我们要计算销售的增长量,逻辑其实很简单了,就本月的销售数字减去上个月的,但因为上个月没有数,所以你join的时候,上个月的销售数量就是null,然后用本月的100一减,结果是null而不是100,这个在统计的时候很容易出错,那怎么办呢,一般都用nvl函数去解决,更多细节我在这就不说了,大家有兴趣可以自己去看。这里再介绍一点有意思的东西。我们再看一个例子,我们刚才算的是加法,而实际上是所有的数学运算,遇到null出来的结果都是null。这里有个极端的例子,除法。我们看col1=’C’和’D’的值,因为它所对应的col3的值是0,按道理讲,如果0做除数是应该报错的。那么我们做一个除法看看效果。我们先看col1=‘C’的,运行了一下,如我们所料,出现错误。然后再看一下col1=’D’的,很奇妙吧,居然能求出结果。除以0也不报错,这个说明什么?说明啊,数据库在计算的时候会先检查是不是有null,如果有就直接得出结果就是null。



因此,数学计算我总结了一下,就是,所有NULL参与的计算,最后的结果都是NULL。


那么除了数学计算呢,还有当遇到null的时候,在where子句里面也会有一些隐患。

我这边举个例子:创建一张表,有两个字段,第一个是学生所在省份名称,province_code,第二个是学生人数。然后插入4条数据,北京, 1000, 上海, 800, 浙江,900, 还有一个是因为来自海外,没有所对应的省份(这个就是我们前面所提到的,不适用),因此设为NULL,有20人。我们先看看总学生人数2720。再看看来自北京的学生人数,很简单了。结果是1000哈, 那我们又想看看除了北京以外的,学生人数是多少,应该等于学生总数2720减去北京的1000,也就是1720. 但如果我们写SQL的时候忽视了NULL值,应该是下面的SQL,就会得到和我们的期待不一样的结果。这个看起来很简单,但其实编程的时候很容易忽视,不自觉的出错了。如果要得到正确的结果,就需要再加上一个条件,or province_cd is null。

还有比如,我们非常常见的,在报表查询的时候,有几个枚举项目,然后如果全选,我们期待的能够获得全部结果。但如果数据里面有null的话,就会丢一些数据,也是一样的道理。



因此,在where子句里面,我们需要注意的是当有NULL存在的时候,使用<>;以及报表的下拉列表中使用“全选”这些类似场景时容易带来的问题。


然后,第三个,我们再看join。看一个例子,这个也是不懂null含义的常见的错误。先建一张维度表dim_reg, 两个字段,reg_code和reg_name。再建事实表FACT_SALE两个字段,reg_code(区域编码),Sale_amt(销售金额); 然后向这两张表里插入数据,这里注意一下,DIM_REG这张表插入的第三条数据,null和unknown。FACT_SALE这张表的第三条数据是null, 20。让我们运行这句SQL:

selectb.reg_name, a.sale_amt

fromfale_sales a , dim_reg b

ona.reg_code=b.reg_code

 

对NULL特性属性的同学一定知道了,null和null的join是查不出数据的。所以最终的结果是这样的,‘unknown’对应的sale_amt没有查到。如果要想查出数据来,需要用coalesce。这个其实是很多人都懂的道理,但真写程序的时候,有的时候不自觉的会犯这种错误,因此NULL在使用时要谨慎小心, 因为的确会有这种编程上的隐患。



因此Join需要注意的是, null值和null值join是得不到任何结果的。


我们再来看聚合函数,还是先建一张表。表非常简单,只有一个字段,起个名字叫numb。然后插入4条记录。然后我们运行一个这句SQL,看一下聚合函数的结果。

很明显,对于MAX,MIN,SUM, NULL就直接被忽略掉了。AVG呢,其实AVG等于SUM除以对应的COUNT,我们可以看到AVG的值是2,也就是说在求SUM和求COUNT的时候,NULL都被忽略了。再看后面两个,我们可以看到count(numb)会把NULL忽略掉,而COUNT(*)会把NULL算进来。这里我们需要注意一下,如果把NULL用0替换,那么MAX,MIN,AVG,COUNT都有可能受到影响,而SUM,如果所有值都是NULL,得到的结果也是NULL而不是0,但通常来说SUM被影响是比较小的。



一样的,我们总结一下,在使用聚合函数的时候,NULL如果当成0来处理,会带来不一样的结果。


下一个,子查询

这个也是我们经常遇到的错误。我们看一个例子。

建两张非常简单的表,nulltest1和nulltest2。分别插入如下数据。

然后我们看一下这句SQL,会出现什么样的结果:

如我们所期待的那样,得到A和B。

然后我们给nulltest2插入一个null。

在运行一次这句SQL,结果为空。

 

我们把这个SQL改一改,不用NOT IN,改成notexists的模式,看看这个效果:

又得到A和B。具体为什么会这样呢?个中细节我就不讲了,毕竟这些其实都是SQL编程的内容,不是建模的。大家回去自己试试。对于SQL经验丰富的老手,可能觉得我举的这些例子都不算什么,但是实际工作中,我们真的还是难免犯类似的错误,尤其是当你写出非常复杂的多重嵌套的SQL时,稍微一不留神,就会忽视null带来的这些隐患。而对于SQL新手来说,我相信你自己回去多尝试多琢磨,会学到很多东西。

 

此外呢, 还有一点,我就简单说一下,很多数据库对于NULL是没有索引的,因此可能会有一些性能上的影响。



这里我总结了五种比较常见的NULL可能带来的编程隐患,但NULL可能引起的隐患肯定不仅仅限于此,我只是给大家提个醒,要了解NULL的隐患,在写SQL的时候不要忽视NULL值的存在。当你需要使用它的时候,要注意哪些问题。对于NULL的使用,既不能过于随意,更不能因噎废食干脆不用。

 

好,因为篇幅太长,我们这节课先讲到这,下节课接着讲在数据库的设计中,对于NULL值的处理。

推荐 7
本文由 BAO胖子 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

3 个评论

这一集等的时间很长
呵呵,出了些意外的事,耽搁了好久,重新进入征途了
讲得很好很透彻,感谢无私分享!~

要回复文章请先登录注册