SQL经典实例——分层查询

分层查询

数据中可能存在层次关系,本章介绍表达这种关系的实例。对于层次数据,相比于对其进行存储,对其进行检索并以层次方式呈现出来通常更难。

几年前,MySQL 引入了递归式 CTE,现在大多数RDBMS 支持这种功能。因此,使用递归式 CTE 已成为编写分层查询的标准方法。

先来看看 EMP 表中 EMPNO 和 MGR 之间的层次关系。

sql 复制代码
select empno,mgr
from emp
order by 2;

 empno | mgr  
-------+------
  7902 | 7566
  7788 | 7566
  7521 | 7698
  7844 | 7698
  7654 | 7698
  7900 | 7698
  7499 | 7698
  7934 | 7782
  7876 | 7788
  7782 | 7839
  7698 | 7839
  7566 | 7839
  7369 | 7902
  7839 |     
(14 rows)

如果仔细观察,你将发现每个 MGR 值都是一个 EMPNO,这意味着 EMP 表中的每位管理者也同样是员工,且未被存储在其他地方。MGR 和 EMPNO 之间为父子关系,因为EMPNO 对应的 MGR 值是它的直接父节点。​(对于特定的员工,其管理者之上可能还有管理者,而这些管理者之上也有管理者,以此类推,形成 n 层层次结构。​)对于没有管理者的员工,其 MGR 值为 NULL。

1、呈现父子关系

问题:你想在返回子记录中数据的同时,返回父记录中的信息。例如,你想显示每位员工的名字以及其管理者的名字。换言之,你想返回如下结果集。

sql 复制代码
EMPS_AND_MGRS
------------------------------
FORD works for JONES
SCOTT works for JONES
JAMES works for BLAKE
TURNER works for BLAKE
MARTIN works for BLAKE
WARD works for BLAKE
ALLEN works for BLAKE
MILLER works for CLARK
ADAMS works for SCOTT
CLARK works for KING
BLAKE works for KING
JONES works for KING
SMITH works for FORD

解决方案:基于 MGR 和 EMPNO 相等自连接 EMP 表,以找出每位员工的管理者的名字。然后,使用 RDBMS 提供的字符串拼接函数生成所需的字符串。

DB2、Oracle 和 PostgreSQL:自连接 EMP 表,然后使用表示拼接运算符的双竖线(||)​。

sql 复制代码
select a.ename || ' works for ' || b.ename as emps_and_mgrs
from emp a, emp b
where a.mgr = b.empno;

     emps_and_mgrs      
------------------------
 SMITH works for FORD
 ALLEN works for BLAKE
 WARD works for BLAKE
 JONES works for KING
 MARTIN works for BLAKE
 BLAKE works for KING
 CLARK works for KING
 SCOTT works for JONES
 TURNER works for BLAKE
 ADAMS works for SCOTT
 JAMES works for BLAKE
 FORD works for JONES
 MILLER works for CLARK
(13 rows)

MySQL:自连接 EMP 表,然后使用拼接函数 CONCAT。

sql 复制代码
select concat(a.ename, ' works for ',b.ename) as emps_and_mgrs
from emp a, emp b
where a.mgr = b.empno;

SQL Server:自连接 EMP 表,然后使用表示拼接运算符的加号(+)​。

sql 复制代码
select a.ename + ' works for ' + b.ename as emps_and_mgrs
from emp a, emp b
where a.mgr = b.empno;

2、呈现子--父--祖父关系

问题:员工 CLARK 是 KING 的下属,要表示这种关系,可以使用上一节中的解决方案。如果员工 CLARK 还是另一位员工的管理者,那么该如何表示这种关系呢?请看下面的查询。

sql 复制代码
select ename,empno,mgr
from emp
where ename in ('KING','CLARK','MILLER');

ENAME        EMPNO     MGR
--------- -------- -------
CLARK         7782    7839
KING          7839
MILLER        7934    7782

如你所见,员工 MILLER 是 CLARK 的下属,而 CLARK是 KING 的下属。你要呈现从 MILLER 到 KING 的完整层次结构。换言之,你想返回如下结果集。

sql 复制代码
LEAF___BRANCH___ROOT
---------------------
MILLER-->CLARK-->KING

然而,上一节使用的单次自连接方法无法呈现上述完整关系。虽然可以编写执行两次自连接的查询,但使用遍历层次结构的通用方法更佳。

解决方案:本实例不同于上一个实例,因为它要呈现的关系包含 3层。Oracle 提供了遍历树型数据的功能,如果你使用的 RDBMS 没有提供这种功能,则可以使用 CTE 来解决这个问题。

DB2 和 SQL Server:使用递归式 WITH 找出 MILLER 的管理者 CLARK,再找出 CLARK 的管理者 KING。下面的解决方案使用的是SQL Server 字符串拼接运算符 +。

sql 复制代码
   with x (tree,mgr,depth)
     as (
 select cast(ename as varchar(100)),
        mgr, 0
   from emp
  where ename = 'MILLER'
  union all
 select cast(x.tree+'-->'+e.ename as varchar(100)),
        e.mgr, x.depth+1
   from emp e, x
  where x.mgr = e.empno
 )
 select tree leaf___branch___root
   from x
  where depth = 2;

只要修改拼接运算符,就可以将该解决方案用于其他数据库。换言之,用于 DB2 时,可以将拼接运算符改为||。

MySQL 和 PostgreSQL:MySQL 和 PostgreSQL 解决方案与上述解决方案类似,只是需要添加关键字 RECURSIVE。

sql 复制代码
WITH RECURSIVE x (tree, mgr, depth) AS (
    SELECT CAST(ename AS CHAR(255)),
           mgr,
           0
    FROM emp
    WHERE ename = 'MILLER'

    UNION ALL

    SELECT CONCAT(x.tree, '-->', e.ename),
           e.mgr,
           x.depth + 1
    FROM emp e
    JOIN x ON x.mgr = e.empno
)
SELECT tree AS leaf___branch___root
FROM x
WHERE depth = 2;

Oracle:使用函数 SYS_CONNECT_BY_PATH 返回 MILLER、MILLER 的管理者 CLARK 以及 CLARK 的管理者KING,并使用 CONNECT BY 子句遍历树。

sql 复制代码
select ltrim(
         sys_connect_by_path(ename,'-->'),
       '-->') leaf___branch___root
 from emp
where level = 3
start with ename = 'MILLER'
connect by prior mgr = empno;

3、创建基于表的分层视图

问题:你想返回一个结果集,将整张表的层次结构呈现出来。在EMP 表中,员工 KING 之上没有管理者,因此 KING 为根节点。你想从 KING 开始,显示其所有下属以及这些下属的所有下属。换言之,你想返回如下结果集。

sql 复制代码
EMP_TREE
------------------------------
KING
KING - BLAKE
KING - BLAKE - ALLEN
KING - BLAKE - JAMES
KING - BLAKE - MARTIN
KING - BLAKE - TURNER
KING - BLAKE - WARD
KING - CLARK
KING - CLARK - MILLER
KING - JONES
KING - JONES - FORD
KING - JONES - FORD - SMITH
KING - JONES - SCOTT
KING - JONES - SCOTT - ADAMS

解决方案

DB2、PostgreSQL 和 SQL Server:使用递归式 WITH 子句生成一个层次结构,其中包含KING 及其管理的所有员工。下面展示的是 DB2 解决方案(使用的是 DB2 拼接运算符 ||)​。要将该解决方案用于 SQL Server 和 MySQL,只需在其中分别使用拼接运算符 + 和拼接函数 CONCAT。

sql 复制代码
with RECURSIVE x (ename,empno)
as (
	select cast(ename as varchar(100)),empno
	from emp
	where mgr is null
	union all
	select cast(x.ename||' - '||e.ename as varchar(100)), e.empno
	from emp e, x
	where e.mgr = x.empno
)
select ename as emp_tree
from x
order by 1;

           emp_tree           
------------------------------
 KING
 KING - BLAKE
 KING - BLAKE - ALLEN
 KING - BLAKE - JAMES
 KING - BLAKE - MARTIN
 KING - BLAKE - TURNER
 KING - BLAKE - WARD
 KING - CLARK
 KING - CLARK - MILLER
 KING - JONES
 KING - JONES - FORD
 KING - JONES - FORD - SMITH
 KING - JONES - SCOTT
 KING - JONES - SCOTT - ADAMS
(14 rows)

MySQL:在 MySQL 中,还需添加关键字 RECURSIVE。

sql 复制代码
WITH RECURSIVE x (ename, empno) AS (
    SELECT CAST(ename AS CHAR(100)), empno
    FROM emp
    WHERE mgr IS NULL

    UNION ALL

    SELECT CAST(CONCAT(x.ename, ' - ', e.ename) AS CHAR(255)), e.empno
    FROM emp e
    JOIN x ON e.mgr = x.empno
)
SELECT ename AS emp_tree
FROM x
ORDER BY 1;

Oracle:使用函数 CONNECT BY 定义层次结构,并使用函数SYS_CONNECT_BY_PATH 设置输出的格式。

sql 复制代码
 select ltrim(
          sys_connect_by_path(ename,' - '),
        ' - ') emp_tree
   from emp
   start with mgr is null
 connect by prior empno=mgr
   order by 1;

相比于上一节的解决方案,该解决方案的不同之处在于没有使用基于伪列 LEVEL 的筛选器。删除这个筛选器后,将显示所有可能的树(符合条件 PRIOR EMPNO=MGR 的树)​。

4、找出给定父行的所有子行

问题:你想找出 JONES 的所有下属,包括直接下属和间接下属(JONES 的下属的下属)​。下面列出了 JONES 及其所有下属。

sql 复制代码
ENAME
----------
JONES
SCOTT
ADAMS
FORD
SMITH

解决方案 :能够定位到树的顶部或底部很有用。在本解决方案中,不需要特殊的格式设置。这里的目标很简单,就是返回JONES 下属的所有员工,包括 JONES 自己。这种查询充分展示了递归式 SQL 扩展(比如 Oracle 的 CONNECTBY 以及 SQL Server 和 DB2 的 WITH 子句)的威力。

DB2、PostgreSQL 和 SQL Server:使用递归式 WITH 子句找出是 JONES 下属的所有员工。从 JONES 开始,在 UNION ALL 上半部分的查询中指定WHERE ENAME = JONES。

sql 复制代码
   with x (ename,empno)
     as (
 select ename,empno
   from emp
  where ename = 'JONES'
  union all
 select e.ename, e.empno
   from emp e, x
  where x.empno = e.mgr
 )
 select ename
   from x;

Oracle:使用 CONNECT BY 子句并指定 START WITH ENAME =JONES,以找出 JONES 下属的所有员工。

sql 复制代码
select ename
from emp
start with ename = 'JONES'
connect by prior empno = mgr;

5、确定叶子节点、分支节点和根节点

问题:你想判断给定的行是哪种类型的节点:叶子节点、分支节点还是根节点。在本实例中,叶子节点指的是不是管理者的员工,分支节点指的是自己是管理者且还有上级管理者的员工,而根节点指的是没有上级管理者的员工。对于层次结构中的每一行,你都要返回 1(TRUE)或 0(FALSE)​,以指出其状态。你希望返回的结果集如下所示。

sql 复制代码
ENAME         IS_LEAF  IS_BRANCH    IS_ROOT
---------- ---------- ---------- ----------
KING                0          0          1
JONES               0          1          0
SCOTT               0          1          0
FORD                0          1          0
CLARK               0          1          0
BLAKE               0          1          0
ADAMS               1          0          0
MILLER              1          0          0
JAMES               1          0          0
TURNER              1          0          0
ALLEN               1          0          0
WARD                1          0          0
MARTIN              1          0          0
SMITH               1          0          0

解决方案:EMP 表建立的是树型层次结构,而不是递归层次结构,因为根节点的 MGR 为 NULL,认识到这一点很重要。如果EMP 建立的是递归层次结构,那么根节点将指向自己(也就是说,员工 KING 的 MGR 值将为他的 EMPNO)​。我们发现,指向自己是不合常理的,因此将根节点的 MGR 设置为了 NULL。使用 Oracle 的 CONNECT BY 以及 DB2和 SQL Server 的 WITH 子句时,你会发现树型层次结构比递归层次结构更容易处理,效率也更高。使用CONNECT BY 或 WITH 处理递归层次结构时务必小心,因为最终编写的 SQL 代码可能包含循环。如果处理递归层次结构时出现问题,那么请务必检查这种循环。

DB2、PostgreSQL、MySQL 和 SQL Server:使用 3 个标量子查询在每个节点类型列中返回正确的"布尔"值(1 或 0)​。

sql 复制代码
select e.ename,
			(select sign(count(*)) from emp d
				where 0 =
					(select count(*) from emp f
						where f.mgr = e.empno)) as is_leaf,
			(select sign(count(*)) from emp d
				where d.mgr = e.empno
					and e.mgr is not null) as is_branch,
			(select sign(count(*)) from emp d
				where d.empno = e.empno
					and d.mgr is null) as is_root
 from emp e
order by 4 desc,3 desc;

Oracle:上述子查询解决方案也适用于 Oracle。如果你使用的是Oracle Database 10g 以前的版本,那么也应该使用这种解决方案。下面的解决方案使用了 Oracle 提供的内置函数 CONNECT_BY_ROOT 和 CONNECT_BY_ISLEAF(这些内置函数是 Oracle Database 10g 引入的)来找出根行和叶子行。

sql 复制代码
  select ename,
         connect_by_isleaf is_leaf,
         (select count(*) from emp e
           where e.mgr = emp.empno
             and emp.mgr is not null
             and rownum = 1) is_branch,
         decode(ename,connect_by_root(ename),1,0) is_root
    from emp
   start with mgr is null
 connect by prior empno = mgr
 order by 4 desc, 3 desc;