数据库 SQL 语句中关于 NULL 的那些坑

debugtalk · 2016年04月25日 · 最后由 nyani 回复于 2017年07月19日 · 4481 次阅读

问题描述

今天在跟进公司内部测试平台线上问题的时候,发现一个忽略已久的问题。

为了简化问题描述,将其进行了抽象。

有一张数据表qms_branch,里面包含了一批形式如下所示的数据:

id name types
1 dashboard_trunk dashboard
2 monkey_trunk monkey
3 dashboard_projects_10_9_9 dashboard
4 performance_trunk
5 performance_projects_10_9_8 performance

在系统的某个页面中,需要展示出所有dashboard类型以外的分支,于是就采用如下方式进行查询(Rails)。

branches = Qms::Branch.where("types!='dashboard'")

这个方式有问题么?

之前我是觉得没什么问题。但是在代码上线后,实际使用时发现部分分支没有加载出来,这就包括了performance_trunk分支。

然后就是问题定位,到 MySQL 的控制台采用 SQL 语句进行查询:

SELECT * FROM qms_branch WHERE types != 'dashboard'

发现在查询结果中的确没有包含performance_trunk分支。

这是什么原因呢?为什么在第 4 条数据中,types属性的值明明就不是dashboard,但是采用types!='dashboard'就无法查询得到结果呢?

原因追溯

查看数据表qms_branch的结构,看到types字段的属性为:DEFAULT NULL

经过查询资料,在w3schools上找到了答案。

  • NULL is used as a placeholder for unknown or inapplicable values, it is treated differently from other values.
  • It is not possible to test for NULL values with comparison operators, such as =, <, or <>. We will have to use the IS NULL and IS NOT NULL operators instead.

也就是说,在 SQL 中,NULL并不能采用!=与数值进行比较,若要进行比较,我们只能采用IS NULLIS NOT NULL

于是,我们将 SQL 语句改为如下形式:

SELECT * FROM qms_branch WHERE types IS NULL or types != 'dashboard'

再次查询时,结果集就包含performance_trunk分支了。

问题延伸

通过上面例子,我们知道在对 NULL 进行判断处理时,只能采用IS NULLIS NOT NULL,而不能采用=, <, <>, !=这些操作符。

那除此之外,还有别的可能存在的坑么?

再看一个例子:

有一张数据表table_foo,其中有一个字段value_field,我们想从这张表中筛选出所有value_field为'value1','value2'或 NULL 的记录。

那么,我们采用IN操作符,通过如下 SQL 语句进行查询。

SELECT * FROM table_foo WHERE value_field IN ('value1', 'value2', NULL)

这会存在问题么?我们并没有采用=, <, <>, !=对 NULL 进行比较哦。

答案是同样存在问题!

因为在 SQL 中,IN语句会被转换为多个=语句。例如,上面例子中的 SQL 在执行时就会被转换为如下 SQL 语句:

SELECT * FROM table_foo WHERE value_field = 'value1' OR value_field = 'value2' OR value_field = NULL

而这个时候,执行value_field = NULL时就会出现问题了。

正确的做法应该是将NULL相关的判断独立出来,如下 SQL 才是正确的写法。

SELECT * FROM table_foo WHERE value_field IN ('value1', 'value2') OR value_field IS NULL

关于作者

笔名九毫,英文名 Leo Lee。

专注于软件测试行业,享受在墙角安静地 debug,也喜欢在博客上分享文字。

个人博客:http://debugtalk.com

1 楼 已删除

这是常识

这一块 ransack 就很智能,虽然我不喜欢用,):D https://github.com/activerecord-hackery/ransack/pull/370

#1 楼 @hooopo 暴露自己是新手了,😄

我记得 PostgreSQL 里是没有关系的

数据库实现不同?

9 楼 已删除

建议你们写代码前先读读书

#9 楼 @night_7th 试了,的确可以,谢谢

#10 楼 @mizuhashi 虽然问题有难易之分,但对对于分享应该持鼓励,毕竟别人遇到的问题你也可能会遇到。

shoushen 我觉的 Ruby China 对新手不太友好 提及了此话题。 07月07日 11:06

我是来改变倒数第三个帖子的

我是新手 来看倒数第三个帖子 哪个是?

franklinyu 回复

感谢向导

更好的做法請使用(Rails 4+):

Qms::Branch.where.not(types: 'dashboard')

一般來說更推薦用 ActiveRecord 而非手捏 SQL,有諸多好處。譬如:

Foo.where(prop: 'a')
Foo.where(prop: nil)

會分別被自動翻譯成:

SELECT ... FROM ... WHERE col = 'a'
SELECT ... FROM ... WHERE col IS NULL

:-)

需要 登录 后方可回复, 如果你还没有账号请 注册新账号