.Net程序员学用Oracle系列(15):DUAL、ROWID、NULL

时间:2021-12-12 04:09:33

1、DUAL 表

DUAL 是 Oracle 中的一个虚拟表,用来构成 SELECT 的语法规则,Oracle 系统保证 DUAL 里面永远只有一条记录。本人对“用来构成 SELECT 的语法规则”的理解是:由于 Oracle 中的 SELECT 语句必须包含 FROM 子句(这点与 SQL Server 等数据库不同),但很多时候我们只需要查询一些与任何真实表无关的变量或常量等,这时候如果没有诸如 DUAL 之类的虚拟表来满足语法规则,就不易实现了。素来以强大著称的 Oracle 当然是不会允许这种缺陷存在的,于是 DUAL 表应运而生。示例:

SELECT 'A' res FROM DUAL;           -- res: 'A',查询常量 'A'
SELECT 1+2 res1,3*7 res2 FROM DUAL; -- res1: 3,res2: 21,做四则运算,相当于计算器
SELECT fn_today res FROM DUAL; -- res: 2017-01-10,调用用户函数
SELECT ROWNUM res FROM DUAL; -- res: 1,调用系统函数
SELECT SYSDATE res FROM DUAL; -- res: 2017-02-05 21:31:05,查询当前系统时间
SELECT SYS_GUID() res FROM DUAL; -- res: '68A531826DEF4DA8BD7F03C1EE0C1A7E',获得一个GUID
SELECT DBMS_RANDOM.RANDOM res FROM DUAL; -- res: -212493338,获得一个随机数
SELECT USER res FROM DUAL; -- res: DEMO,查询当前连接用户名
SELECT SYS_CONTEXT('USERENV','TERMINAL') FROM DUAL; -- res: HZZ-PC,查询当前主机名

2、ROWID 类型

ROWID 是 Oracle 中较为复杂的一个特殊对象,开发人员一般无需对 ROWID 做深入了解。ROWID 不仅是一种数据类型,创建字段的时候可以指定字段类型为 ROWID。ROWID 还是 Oracle 表中行的唯一标识,通过 ROWID,Oracle 可以快速定位表中行数据的具体物理存储位置。

2.1、利用 ROWID 查询数据

有人把 ROWID 也称之为伪列,但它们在语法上有个明显的区别就是——ROWID 可以加表别名限定。这点从 ROWID 的用途来看也很好理解。

通过 ROWID 查询表中某一行数据的示例:

SELECT t.staff_id,t.staff_name,t.birthday FROM demo.t_staff t WHERE t.ROWID='AAAMpVAAEAAAABeAAA';

结果:

   STAFF_ID STAFF_NAME                                         BIRTHDAY
----------- -------------------------------------------------- -----------
1 小明 1988-05-08

通过 ROWID 查询表中主键重复的数据行数。示例:

SELECT COUNT(1) res FROM demo.t_staff t WHERE t.ROWID>(SELECT MIN(n.ROWID) FROM demo.t_staff n WHERE n.staff_id=t.staff_id); -- res: 0

通过 ROWID 来分页查询的示例(对员工表分页,一页 3 条数据,查第 1 页):

SELECT t4.staff_name,t4.dept_code,t4.gender,t4.birthday
FROM demo.t_staff t4
WHERE t4.ROWID IN(
SELECT t3.rid FROM(
SELECT t2.rid,ROWNUM rn FROM(
SELECT t1.ROWID rid,t1.birthday FROM demo.t_staff t1 ORDER BY t1.birthday
) t2 WHERE ROWNUM <= (1*3)
) t3 WHERE t3.rn >= ((1-1)*3+1)
) ORDER BY t4.birthday;

结果:

STAFF_NAME                          DEPT_CODE                                GENDER BIRTHDAY
----------------------------------- ---------------------------------------- ------ -----------
小萨 010102 1 1986-03-07
小明 010101 1 1988-05-08
李阳 010101 1 1989-01-14

2.2、利用 ROWID 更新数据

在 PL/SQL Developer 中,查询某个表的时候,只要在查询字段列表中包含 ROWID,即可直接修改已经查出来的数据。写法示例:

SELECT t.ROWID,t.* FROM demo.t_staff t; -- 执行查询之后,数据窗格上面会出现一个小锁,点一下就可以直接修改数据了

3、NULL 值

在 Oracle 中 NULL 是一个很特殊的值,任何类型的值都可以是 NULL。NULL 是未知的,它可以是任何类型的值,也可以不依赖于任何类型而单独存在(字面量 NULL),任何没有 NOT NULL 约束或主键约束的列都有可能出现 NULL 值。

3.1、NULL 与空字符串

NULL 的判断和比较运算规则:NULL 与任何值(包括 NULL 自身)既不相等也不不想等,换句话说它与任何值的比较结果都是未知的。很多人都说 NULL 与空字符串是等价的,其实是 Oracle 把它们做了等价处理,但 Oracle 并不建议把它们当成是一样的,至少在某些系统函数内部它们就不一样,而且以后可能还会变。事实上 NULL 可能是任何类型,也可能没有类型,而空字符串一定是字符类型,这是它们的本质区别。示例:

WITH
t1 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL=NULL),t2 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL<>NULL),
t3 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL=' '),t4 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL<>' '),
t5 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL=''),t6 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL<>''),
t7 AS(SELECT COUNT(1) res FROM DUAL WHERE ''=''),t8 AS(SELECT COUNT(1) res FROM DUAL WHERE ''<>''),
t9 AS(SELECT COUNT(1) res FROM DUAL WHERE ''=' '),t10 AS(SELECT COUNT(1) res FROM DUAL WHERE ''<>' ')
SELECT t1.res,t2.res,t3.res,t4.res,t5.res,t6.res,t7.res,t8.res,t9.res,t10.res FROM t1,t2,t3,t4,t5,t6,t7,t8,t9,t10;

结果(下面的结果进一步证明了 NULL != Any value & NULL !!= Any value'' <=> NULL):

       RES        RES        RES        RES        RES        RES        RES        RES        RES        RES
---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ----------
0 0 0 0 0 0 0 0 0 0

事实上除了不能用 =<>来比较 NULL,也不能用 >< 等其它比较运算符来比较 NULL。Oracle 提供了 IS NULLIS NOT NULL 来判别某个数据是否为 NULL 或不为 NULL。示例:

SELECT COUNT(1) res FROM DUAL WHERE NULL IS NULL; -- res:1
SELECT COUNT(1) res FROM DUAL WHERE 0 IS NULL; -- res:0
SELECT COUNT(1) res FROM DUAL WHERE '' IS NULL; -- res:1
SELECT COUNT(1) res FROM DUAL WHERE NULL IS ''; -- 报错(missing NULL keyword) SELECT COUNT(1) res FROM DUAL WHERE NULL IS NOT NULL; -- res:0
SELECT COUNT(1) res FROM DUAL WHERE 0 IS NOT NULL; -- res:1
SELECT COUNT(1) res FROM DUAL WHERE '' IS NOT NULL; -- res:0
SELECT COUNT(1) res FROM DUAL WHERE NULL IS NOT ''; -- 报错(missing NULL keyword)

IS NULLIS NOT NULL 是不可分割的整体,不能写成 IS ''IS NOT '',如上两组案例的最后 4 个也能证明这一点。

NULL 的算术和逻辑运算规则:NULL 在做一些算术运算时,如 +-*/ 等,结果一定还是 NULL。如果是连接操作符 ||,则会直接忽略 NULL。示例:

SELECT 1 + NULL res FROM DUAL;    -- res:NULL
SELECT 1 - NULL res FROM DUAL; -- res:NULL
SELECT 1 * NULL res FROM DUAL; -- res:NULL
SELECT 1 / NULL res FROM DUAL; -- res:NULL
SELECT '1' || NULL res FROM DUAL; -- res:'1'

如果把上面 5 条语句中的 NULL 替换成 '',结果不变。这也印证了在做逻辑运算时,Oracle 把 '' 做了与 NULL 等效的处理。

3.2、NULL 与函数

Oracle 中的很多函数都对 NULL 做了特殊处理,如聚合函数、DECODE 函数等。有些函数会直接忽略 NULL,而另一些函数则会对 NULL 做特殊处理,下面我们来看 4 个 NULL 与函数的示例:

LTRIM 函数

SELECT LTRIM('_ID','_') res FROM DUAL;  -- res:ID
SELECT LTRIM('_ID',NULL) res FROM DUAL; -- res:NULL

REPLACE 函数

SELECT REPLACE('_ID','_') res FROM DUAL;  -- res:ID
SELECT REPLACE('_ID',NULL) res FROM DUAL; -- res:'_ID'

DECODE 函数

语法:DECODE(expr, val1, res1, val2, res2, ...[, defval])

描述:如果第一个参数与第 2n(n>=1) 个参数相等,则返回第 2n+1 个参数,如果第一个参数与所有第 2n 个参数都不相等,有默认参数(当参数列表长度为 2n 时,最后一个参数就是默认参数)就返回默认参数,没有默认参数就返回 NULL。

示例:

SELECT DECODE(1,1,'A',2,'B','C') res FROM dual;   -- res:'A'
SELECT DECODE(2,1,'A',2,'B','C') res FROM dual; -- res:'B'
SELECT DECODE(3,1,'A',2,'B','C') res FROM dual; -- res:'C'
SELECT DECODE(3,1,'A',2,'B') res FROM dual; -- res:NULL
SELECT DECODE(NULL,1,'A',2,'B') res FROM dual; -- res:NULL
SELECT DECODE(NULL,1,'A',NULL,'B') res FROM dual; -- res:'B'

DECODE 函数很强大,用起来也很方便,但它是 Oracle 的方言,我们还是要尽可能少用。其实 DECODE 函数能实现的任何功能都能用 CASE 函数或 SQL 的 IF 语句来实现,只是书写起来相对繁琐。

CASE 函数

语法:

CASE [ expression ]
WHEN condition_1 THEN result_1
WHEN condition_2 THEN result_2
...
WHEN condition_n THEN result_n
ELSE res
END

示例(为方便示例间对比和节省篇幅,避免篇幅过长不方便阅读,下述 4 个示例均缩写成一行,实际开发中一般有断行更好阅读):

SELECT CASE 1 WHEN 1 THEN 'A' WHEN 2 THEN 'B' ELSE 'C' END res FROM DUAL; -- res: 'A'
SELECT CASE 2 WHEN 1 THEN 'A' WHEN 2 THEN 'B' ELSE 'C' END res FROM DUAL; -- res: 'B' SELECT CASE NULL WHEN NULL THEN 'A' ELSE 'B' END res FROM DUAL; -- res:'B'
SELECT CASE WHEN NULL IS NULL THEN 'NULL' ELSE 'NOT NULL' END res FROM DUAL; -- res: 'NULL'

为了能更灵活的对 NULL 进行处理,Oracle 提供了 NVL、NVL2、NULLIF、COALESCE 等函数来专门处理 NULL。下面来看看这 4 个函数的示例:

NVL 函数

语法:NVL(expr1, expr2)

描述:常用于某个值可能是 NULL,而查询结果中又不能出现空值的转换需求。如果第一个参数为 NULL,则返回第二个参数,否则返回第一个参数。

示例:

SELECT NVL(NULL,'b') res FROM DUAL; -- res:b
SELECT NVL('a','b') res FROM DUAL; -- res:a SELECT NVL(NULL,2) res FROM DUAL; -- res:2
SELECT NVL(1,2) res FROM DUAL; -- res:1

NVL2 函数

语法:NVL2(expr1, expr2, expr3)

描述:常用于无论某个值是否为 NULL,都需要转换的需求。如果第一个参数值为 NULL,则返回第三个参数,否则返回第二个参数。

示例:

SELECT NVL2(NULL,'b','c') res FROM DUAL; -- res:c
SELECT NVL2('a','b','c') res FROM DUAL; -- res:b SELECT NVL2(NULL,2,3) res FROM DUAL; -- res:3
SELECT NVL2(1,2,3) res FROM DUAL; -- res:2

NULLIF 函数

语法:NULLIF(expr1, expr2)

描述:如果第一个参数与第二个参数相等,则返回 NULL,否则返回第一个参数。此函数要求第一个参数与第二个参数类型相容,且第一个参数不能是 NULL,限制比较多,且可用 CASE 函数或 DECODE 函数代替,因此实际开发中用的不多。

示例:

SELECT NULLIF('a','a') res FROM DUAL; -- res:NULL
SELECT NULLIF('a','b') res FROM DUAL; -- res:a SELECT NULLIF(1,1) res FROM DUAL; -- res:NULL
SELECT NULLIF(1,2) res FROM DUAL; -- res:1

COALESCE 函数

语法:COALESCE(expr1, expr2, ..., exprn)

描述:此函数可接受两个及以上的参数,从左向右找到第一个不为 NULL 的参数并返回,若所有参数均为 NULL,则返回 NULL。此函数要求参数列表数据类型相容,且有短路计算功能,找到非 NULL 的参数后便不再继续。

案例:

SELECT COALESCE(NULL,NULL,'a') res FROM DUAL;  -- res:a
SELECT COALESCE('','','a') res FROM DUAL; -- res:a
SELECT COALESCE(NULL,0,1,2) res FROM DUAL; -- res:0
SELECT COALESCE(NULL,NULL,NULL) res FROM DUAL; -- res:NULL

3.3、NULL 与索引

众所周知,索引就是商业数据库中为加速对表中数据行的检索而创建的一种存储结构。Oracle 中的索引有很多种,其中最基本的单列索引和复合索引不会存储索引列全为 NULL 的行。证明示例:

-- 先给 demo.t_staff.hire_date 列创建一个索引
CREATE INDEX idx_hire_date ON demo.t_staff(hire_date); -- 然后查询一下索引中的数据行数,由于 hire_date 列全都为 NULL,结果是 0
SELECT t.table_owner,t.table_name,t.index_name,t.num_rows FROM SYS.USER_INDEXES t WHERE t.index_name='IDX_HIRE_DATE'; -- 将女员工的 hire_date 全都设置为 2016-06-01
UPDATE demo.t_staff t SET t.hire_date=TO_DATE('2016-06-01','yyyy-mm-dd') WHERE t.gender=0;
COMMIT; -- 重建 IDX_HIRE_DATE 索引
ALTER INDEX demo.idx_hire_date REBUILD; -- 再来查询索引中的数据行数,结果是 5
SELECT t.table_owner,t.table_name,t.index_name,t.num_rows FROM SYS.USER_INDEXES t WHERE t.index_name='IDX_HIRE_DATE';

这里将介绍一个利用 NULL 和基于函数的索引来优化查询的典型案例:假如人事部想要一个能方便给新员工指派导师的功能。稍微分析下需求就会发现,公司大多数员工都是正式员工,试用期的员工只占很小的一部分。而人事专员在实际使用过程中经常查询的恰恰是那少数新员工,正式员工则很少会查询。这时就可以只对新员工所属行建立索引,一方面节省了维护索引的开销,降低了索引的存储空间,另一方面包含行少的索引检索也更快。示例:

-- 给员工表添加一个记录试用状态的字段 prob_status
ALTER TABLE demo.t_staff ADD(prob_status NUMBER(1));
COMMENT ON COLUMN demo.t_staff.prob_status IS '试用状态{0已转正/1试用中}'; -- 根据上文的证明的结论,试用中的员工状态全部转为 NULL 了,自然就进不了索引了
CREATE INDEX idx_prob_status ON demo.t_staff(DECODE(prob_status,1,1,NULL)); -- 再来查询试用期员工时,就会走索引了,大家可以自行确认索引行数和分析下句的执行计划
SELECT * FROM demo.t_staff t WHERE DECODE(t.prob_status,1,1,NULL)=1;

3.4、NULL 与 SQL

当 DDL 遇上 NULL:因前文.Net程序员学用Oracle系列(6):表、字段、注释、约束、索引中已经介绍过基本的 DDL 语法了,也都比较简单,本节就不再赘述。但有关操作非空约束和默认值的 DDL 语句,却有很多与 NULL 相关的细节问题仍需注意,这里将直接给出 5 个实验结果,有兴趣的读者朋友们可自行验证。

  • 创建表的时候,如果同时给字段设置非空约束和默认值,那么 NULL/NOT NULL 必须放在 DEFAULT 后面。
  • 创建字段的时候,如果同时设置默认值,那么表中现有行的该列会自动更新为默认值,否则全部为 NULL。
  • 修改表字段的时候,如果同时修改非空约束和默认值,那么 NULL/NOT NULL 必须在 DEFAULT 后面。
  • 添加或修改默认值的时候,不会影响表中现有行的值。
  • 添加或修改非空约束的时候,如果字段原本是 NOT NULL 约束,无论表中是否有数据,都可以直接改为 NULL 约束。如果字段原本是 NULL 约束,且该列不包含 NULL 值,仍然可以修改为 NOT NULL 约束;倘若该列已经存在 NULL 值,那就不能再修改为 NOT NULL 约束了。

当子查询遇上 NULL:有些子查询结果集中包含 NULL 时,就可能会导致外部查询的“结果集异常”,如 IN/NOT IN 中的非相关子查询。

IN 非相关子查询示例:

WITH
t1 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL),
t2 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL)
SELECT cid,cname FROM t1 WHERE t1.cname IN(SELECT t2.cname FROM t2);

结果:

       CID CNAME
---------- -----
1 Trump

NOT IN 非相关子查询示例:

WITH
t1 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL),
t2 AS(SELECT 1 cid,'Hillary' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL)
SELECT cid,cname FROM t1 WHERE t1.cname NOT IN(SELECT t2.cname FROM t2);

结果:

       CID CNAME
---------- -----

因为 NULL 既不等于任何值,也不不等于任何值,所以上述两个单列非相关子查询的结果集,都比“预期”少了一行记录。多列子查询的规律相同,分析方法也是:NULL != Any value & NULL !!= Any value。下面再补充几个多列子查询的示例,即使你还不太明白,但相信你根据我给出的分析方法,再对比这几条语句和结果也能自己悟出其中的奥妙。

示例:

WITH
t1 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL),
t2 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL)
SELECT cid,cname FROM t1 WHERE (t1.cid,t1.cname) IN(SELECT t2.cid,t2.cname FROM t2);

结果:

       CID CNAME
---------- -----
1 Trump

示例:

WITH
t1 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL),
t2 AS(SELECT 1 cid,'Hillary' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL)
SELECT cid,cname FROM t1 WHERE (t1.cid,t1.cname) NOT IN(SELECT t2.cid,t2.cname FROM t2);

结果:

       CID CNAME
---------- -----
1 Trump

示例:

WITH
t1 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL),
t2 AS(SELECT 1 cid,'Hillary' cname FROM DUAL UNION ALL SELECT 3,NULL FROM DUAL)
SELECT cid,cname FROM t1 WHERE (t1.cid,t1.cname) NOT IN(SELECT t2.cid,t2.cname FROM t2);

结果:

       CID CNAME
---------- -----
1 Trump
2

如果你看了上面 3 个示例之后,结果还是不理解的话,那就看看下面 4 条语句的条件分析说明,看明白之后再去悟上面的 3 个示例吧。已经理解了的读者可以跳过本节了!

-- 该句的 WHERE 条件相当于 t.dept_code = '010101'
SELECT COUNT(1) res FROM demo.t_staff t WHERE t.dept_code IN('010101'); -- res:5 -- 该句的 WHERE 条件相当于 t.dept_code = '010101' OR t.dept_code = NULL
SELECT COUNT(1) res FROM demo.t_staff t WHERE t.dept_code IN('010101',NULL); -- res:5 -- 该句的 WHERE 条件相当于 t.dept_code != '010101'
SELECT COUNT(1) res FROM demo.t_staff t WHERE t.dept_code NOT IN('010101'); -- res:11 -- 该句的 WHERE 条件相当于 t.dept_code != '010101' AND t.dept_code != NULL
SELECT COUNT(1) res FROM demo.t_staff t WHERE t.dept_code NOT IN('010101',NULL); -- res:0

当集合查询和分组查询遇上 NULL:在.Net程序员学用Oracle系列(14):子查询、集合查询第 2 节中有讲到 INTERSECT、UNION、MINUS 三个操作符的处理结果是不包含重复行的,如果某列中含有 NULL,其它列与别的行相同则认为是重复行。示例:

SELECT 1 cid,'Trump' cname FROM DUAL
UNION
SELECT 2 cid,NULL cname FROM DUAL
UNION
SELECT 2 cid,NULL cname FROM DUAL;

结果:

       CID CNAME
---------- -----
1 Trump
2

在分组查询中也认为 NULL 和 NULL 是相等的。示例:

WITH t AS(
SELECT 1 cid,'Trump' cname FROM DUAL
UNION ALL
SELECT 2 cid,NULL cname FROM DUAL
UNION ALL
SELECT 2 cid,NULL cname FROM DUAL
)
SELECT t.cname,COUNT(1) cnt FROM t GROUP BY t.cname ORDER BY cnt;

结果:

CNAME        CNT
----- ----------
Trump 1
2

当排序遇上 NULL:排序子句 ORDER BY 默认是升序(ASC),这时候排序列值为 NULL 的行会排在最后,而如果指定了降序(DESC),NULL 则排在最前面。可以这么来记这个规律——排序时 NULL 最大。当然,在 Oracle 中可以显示的指定 NULL 的排序规则。在升序排列时需要 NULL 排在最前面,可以指定 NULLS FIRST,在降序排列时需要 NULL 排在最后面,可以指定 NULLS LAST。示例:

SELECT 1 cid,'Trump' cname FROM DUAL
UNION ALL
SELECT 2,'Hillary' FROM DUAL
UNION ALL
SELECT 3,NULL FROM DUAL
ORDER BY 2 NULLS FIRST;

结果:

       CID CNAME
---------- -------
3
2 Hillary
1 Trump

当 UPDATE 遇上 NULL:在.Net程序员学用Oracle系列(12):增删改查中已详述,本节不再赘述。

4、总结

本文主要讲述了 Oracle 中较为常用、又极其特殊的 3 个对象。其中 DUAL 和 ROWID 使用起来比较简单;NULL 则不然,不仅它本身特殊,很多常见的语句、操作符、函数等遇上它也变得特殊,个中的细节实在太多,限于篇幅和本人的精力,本文也只介绍了相对常见的一些情况,更多奥妙还有待大家在学习和工作中去发现。

本文链接http://www.cnblogs.com/hanzongze/p/oracle-dual-null.html

版权声明:本文为博客园博主 韩宗泽 原创,作者保留署名权!欢迎通过转载、演绎或其它传播方式来使用本文,但必须在明显位置给出作者署名和本文链接!本人初写博客,水平有限,若有不当之处,敬请批评指正,谢谢!