令人感到很奇怪的是,SQL居然没有提供除法,必须用NOT EXISTS手动实现。具体的原因不再赘述,这里我只是想说说我对用SELECT语句构建关系代数除法的理解。
书上的除法构建过程是这样的:
- 为查询对象命名
- 寻找反例
- 通过SELECT构建反例
- NOT EXISTS构建的反例
- 得到最终结果
事实上我不太认同这个过程,我觉得按照这个过程容易出现偏差;当然也可能是我太菜了。我的过程是这样的:
- 写出查询对应的关系代数
- 明确查询对象并命名
- 如果有连接运算,选择连接对象
- 如果没有连接运算,选择“自己”
- 从右往左,从外往内构建SELECT
- 内层可以用*模糊
- 如果出现和查询目标连接的情况,忽略
- 有时候可能有多个检索条件
- 如果除法多于一次,每有一次除法,就重复一次步骤1~3
- 连续的简单除法(没有使用选择运算where的除法)可以用and连接进行优化
- 得到结果
接下来我拿书上的例子来说明一下我的观点。
例1. 查询通过住在New York的所有代理商订了货的顾客的cid值。
按照流程:
-
写出对应的关系代数:
O[cid, aid] ÷ (A where city='New York')[aid]
-
没有连接运算,明确查询对象是C的cid,并且命名为c.cid
-
然后开始从右往左,从外往内构建SELECT:
SELECT c.cid -- 查询目标 FROM C c WHERE NOT EXISTS( -- (A where city='New York')[aid] SELECT * FROM A a WHERE a.city = 'New York' and NOT EXISTS( -- O[cid, aid] SELECT * FROM O x WHERE x.cid = c.cid and x.aid = a.aid ) );
-
只有一次除法,结果如上。
再看一个例子:
例2. 查询住在New York或Duluth并订购了价格超过一美元的所有产品的代理商的aid值。
按照流程:
-
写出对应的关系代数:
(((O[aid, pid] ÷ (P where price > 1.00)[pid]) ⋈ A) where city = 'New York' or city = 'Duluth')[aid]
其实可能不需要这么多括号,只是出于语义考虑罢了。
-
有连接对象,明确查询对象是A的aid,并且命名为a.aid
-
然后开始从右往左,从外往内构建SELECT:
SELECT a.aid -- 查询目标 FROM A a WHERE (city = 'New York' -- "A" where city = 'New York' or city = 'Duluth' or city = 'Duluth') and NOT EXISTS( -- (P where price > 1.00)[pid]) SELECT * FROM P p WHERE p.price > 1.00 and NOT EXISTS( -- O[aid, pid] SELECT * FROM O x WHERE x.aid = a.aid and x.pid = p.pid ) );
-
只有一次除法,结果如上。
还有一个例子:
例3. 查询订购了产品p01和价格超过一美元的所有产品的代理商的aid值。
按照流程:
-
写出对应的关系代数:
(((O[aid, pid] ÷ (P where price > 1.00)[pid]) ⋈ O) where pid = 'p01')[aid]
-
有连接运算,明确查询对象是O的aid,并且命名为x.aid
-
然后开始从右往左,从外往内构建SELECT:
SELECT x.aid -- 查询目标 FROM O x WHERE x.pid = 'p01' -- "O" where pid = 'p01' and NOT EXISTS( -- (P where price > 1.00)[pid]) SELECT * FROM P p WHERE p.price > 1.00 and NOT EXISTS( -- O[aid, pid] SELECT * FROM O y WHERE y.aid = x.aid and y.pid = p.pid ) );
-
只有一次除法,结果如上。
其实核心是表示关系代数中除法的部分,并没有那么机械。并且,由于关系代数的表示方法并不唯一,写出来的结果也不一定相同,这只是一个思路罢了。
书上还有两道例题,不妨一试。
2019.09.27:补充多个除法运算嵌套的题目。
还有一些比较复杂的情况。如果检索条件比较复杂,也就是说,有多个检索条件,我们该怎么办呢?比如,对于下面这个例子:
例6. 查询满足要求的顾客的cid:如果顾客c006通过代理商y订购产品x,那么客户通过代理商y订购产品x。
原文:Getcids for customers with the following property: if customer c006 orders a product x through agent y, so the customer orders the product x through the agent y.
这个看起来跟除法没啥关系,但是如果表述成这样呢?
例6. 查询所有通过顾客c006订购过商品的代理商订购商品的顾客的cid。
这样的话,按照流程:
-
写出对应的关系代数:
O[cid, pid, aid] ÷ (O where cid = 'c006')[pid, aid]
-
无连接运算,明确查询对象是C的cid,并且命名为c.cid
-
然后开始从右往左,从外往内构建SELECT;因为有多个检索条件,所以在处理的时候需要增加:
SELECT c.cid -- 查询目标 FROM C c WHERE NOT EXISTS( -- (O where cid = 'c006')[pid, aid] SELECT * FROM O x, WHERE x.cid = 'c006' and NOT EXISTS( -- O[cid, pid, aid] SELECT * FROM O y WHERE y.cid = c.cid and y.pid = o.pid and y.aid = o.aid ) );
-
只有一次除法,结果如上。
还有一种特殊情况,如果有多个嵌套的除法呢?也就是说,描述里出现了多个ALL:
例7. 查询为通过该代理商订购了超过一美元的所有产品的所有顾客订购过产品的代理商。
原文:Get aids of agents who place orders for all customers who place orders for all products costing more than a dollar through the agent.
……这个表述有点复杂。还是按照流程来:
-
写出对应的关系代数:
O[aid, cid] ÷ (O[cid, pid] ÷ (P where price > 1.00)[pid])
-
无连接运算,明确右边的查询对象是C的cid,并且命名为c.cid
-
然后开始从右往左,从外往内构建SELECT(这一步的结果只是一个中间值):
SELECT c.cid -- 右边的查询目标 FROM C c WHERE NOT EXISTS( -- (P where price > 1.00)[pid]) SELECT * FROM P p WHERE p.pid > 1.00 and NOT EXISTS( -- O[cid, pid] SELECT * FROM O x WHERE x.pid = p.pid and x.cid = c.cid ) );
-
有两次除法,重复一次流程。这次的查询目标(也是真正的查询目标)是A的aid,命名为a.aid;另外,我们不妨假设上一步的结果是一张新表T:
SELECT a.aid -- 左边的查询目标 FROM A a WHERE NOT EXISTS( -- O[aid, cid] SELECT * FROM T t WHERE t.aid = a.aid and t.cid = t.cid );
-
展开与合并,结果如下:
SELECT a.aid -- 真正的查询目标 FROM A a WHERE NOT EXISTS( SELECT * FROM P p, C c -- O[cid, pid] WHERE p.pid > 1.00 -- (P where price > 1.00)[pid]) and NOT EXISTS( -- O[cid, pid, aid] SELECT * FROM O x WHERE x.aid = a.aid and x.pid = p.pid and x.cid = c.cid ) );
当然,在这种情况下,还有一种比较简单的特殊情况:多个连续的简单除法。看看下面这个例子:
例8. 查询为所有订购过所有产品的顾客下过订单的代理商的aid。
原文:Get aids of agents who place orders for all customers who place orders for all products.
这个相对来说有所简化,按照流程:
-
写出对应的关系代数:
O[aid, cid] ÷ (O[cid, pid] ÷ O[pid])
-
无连接运算,明确右边的查询对象是C的cid,并且命名为c.cid
-
然后开始从右往左,从外往内构建SELECT(这一步的结果只是一个中间值):
SELECT c.cid -- 右边的查询目标 FROM C c WHERE NOT EXISTS( -- O[pid] SELECT * FROM P p WHERE NOT EXISTS( -- O[cid, pid] SELECT * FROM O x WHERE x.pid = p.pid and x.cid = c.cid ) );
-
有两次除法,重复一次流程。这次的查询目标(也是真正的查询目标)是A的aid,命名为a.aid;另外,我们不妨假设上一步的结果是一张新表T:
SELECT a.aid -- 左边的查询目标 FROM A a WHERE NOT EXISTS( -- O[aid, cid] SELECT * FROM T t WHERE t.aid = a.aid and t.cid = t.cid );
-
展开与合并。因为是连续的简单查询,可以用and连接。结果如下:
SELECT a.aid -- 真正的查询目标 FROM A a WHERE NOT EXISTS( SELECT * FROM P p WHERE NOT EXISTS( SELECT * FROM O x WHERE x.pid = p.pid and x.cid = c.cid ) and NOT EXISTS( SELECT * FROM O y WHERE y.aid = a.aid and y.cid = c.cid ) );
参考解法
例4. 查询订购了所有被顾客c006订购的产品的顾客的cid值。
还是按照流程:
-
写出对应的关系代数:
O[cid, pid] ÷ (O where cid = 'c006')[pid]
-
无连接运算,明确查询对象是C的cid,并且命名为c.cid
-
然后开始从右往左,从外往内构建SELECT:
SELECT c.cid -- 查询目标 FROM C c WHERE NOT EXISTS( -- (O where cid = 'c006')[pid]` SELECT * FROM O x WHERE x.cid = 'c006' and NOT EXISTS( -- O[cid, pid] SELECT * FROM O y WHERE y.cid = c.cid and y.pid = x.pid ) );
-
只有一次除法,结果如上。
例5. 查询被所有住在Duluth的顾客订购的产品的pid值。
还是按照流程:
-
写出对应的关系代数:
O[pid, cid] ÷ (C where city = 'Duluth')[cid]
-
无连接运算,明确查询对象是P的pid,并且命名为p.pid
-
然后开始从右往左,从外往内构建SELECT:
SELECT p.pid -- 查询目标 FROM P p WHERE NOT EXISTS( -- (P where price > 1.00)[pid]) SELECT * FROM C c WHERE c.city = 'Duluth' and NOT EXISTS( -- O[aid, pid] SELECT * FROM O x WHERE x.pid = p.pid and x.cid = c.cid ) );
-
只有一次除法,结果如上。