大数据量查询

Querying Large Quantities of Data

    越快剔除不需要的数据,查询的后续阶段必须处理的数据量就越少,自然查询的效率就越高,这听起来显而易见。集合操作符(set operator)是这一原理的绝佳应用,其中的union使用最为广泛,我们经常看到通过union操作将几个表在一起。中等复杂程度的union语句较为常见,大多数被连接的表都会同时出现在union两端的select 语句中。例如下面这段代码:

 select ...
from A,
B,
C,
D,
E1
where (condition on E1)
and (joins and other conditions)

union
select ...
from A,
B,
C,
D,
E2
where (condition on E2)
and (joins and other conditions)  

    这类查询是典型的照搬式编程。为了提高效率,可以仅对代码中非共用的表(本例中即E1E2)使用union,然后配合筛选条件,把 union 语句降级为内嵌视图。代码如下:

select ...
from A,
B,
C,
D,
(select ...
from E1
where (condition on E1)
union
select ...
from E2
where (condition on E2)) E
where (joins and other conditions)   


    另一个查询条件用错了地方的经典例子,和在含有 group by 子句的查询中进行过滤操作有关。你可以过滤分了组的字段,也可以过滤聚合(aggregate)结果(例如检查 count() 的结果是否小于某阈值),或者同时过滤两者;SQL 允许在 having 子句中使用这类条件,但应该在 group by 完成后才进行过滤(比如排序之后再进行聚合操作)。任何影响聚合函数(aggregate function)结果的条件都应放在 having 子句中,因为在 group by 之前无从知道聚合函数的结果。任何与聚合无关的条件都应放在 where 子句中,从而减少为进行group by而必须执行的排序操作所处理的数据量。

现在回过头来看客户与订单的例子,我承认先前处理订单的方法比较复杂。在订单完成之前,必须经历几个阶段,这些都记录在表orderstatus中,该表的主要字段有:ordid(订单ID)、statusstatusdate(时间戳)等,主键由ordidstatusdate组成。我们的需求是列出所有尚未标记为完成状态的订单(假设所有交易都已终止)的下列字段:订单号、客户名、订单的最后状态,以及设置状态的时间。最终,我们写出下列查询,滤掉已完成的订单,并找出订单当前状态:

select c.custname, o.ordid, os.status, os.statusdate
from customers c,
orders o,
orderstatus os
where o.ordid = os.ordid
and not exists (select null
from orderstatus os2
where os2.status = 'COMPLETE'
and os2.ordid = o.ordid)
and os.statusdate = (select max(statusdate)
from orderstatus os3
where os3.ordid = o.ordid)
and o.custid = c.custid  

乍一看,这个查询很合理,但事实上,它让人非常担心。首先,上面代码中有两个子查询,但它们嵌入的方式和前一个例子的方式不同,它们只是彼此间接相关的。最让人担心的是,这两个子查询访问相同的表,而且该表在外层已经被访问过。我们编写的过滤条件质量如何呢?因为只检查了订单是否完成,所以它不是非常精确。

这个查询如何执行的呢?很显然,可以扫描 orders 表,检查每一条订单记录是否为已完成状态——注意,仅通过表 orders 即可找出所要信息似乎令人高兴,但实际情况并非如此,因为只有上述活动之后,才能检查最新状态的日期,即必须按照子查询编写的顺序来执行。

上述两个子查询是关联子查询,这很不好。因为必须要扫描 orders 表,这意味着我们必须检查 orders 的每条订单记录状态是否为 “COMPLETE”,虽然检查状态的子查询执行很快,但多次重复执行就不那么快了。而且,若第一个子查询没找到 “COMPLETE” 状态时,还必须执行第二个子查询。那么,何不试试非关联子查询呢?
要编写非关联子查询,最简单的办法是在第二个子查询上做文章。事实上,在某些 SQL 方言中,我们可以这么写:

and (o.ordid, os.statusdate) = (select ordid, max(statusdate)
from orderstatus
group by ordid)  

这个子查询会对 orderestatus 全扫描,但未必是坏事,下面会对此加以解释。
重写的子查询条件中,等号左端的字段对有点别扭,因为这两个字段来自不同的表,其实不必这样。我们想让ordersorderstatus的订单ID相等,但优化器能感知这一点吗?答案是不一定。所以优化器可能依然先执行子查询,依然要把ordersorderstatus这两个表连接起来。我们应该将查询稍加修改,使优化器更容易明白我们的描述,最终按照先获得子查询的结果,然后再连接ordersorderstatus的顺序工作:

and (os.ordid, os.statusdate) = (select ordid, max(statusdate)
from orderstatus
group by ordid) 

这次,等号左端的字段来自相同的表,从而不必连接ordersorderstatus这两个表了。尽管好的优化器可能会帮我们做到这一点,但保险起见,一开始就指定这两个字段来自相同的表是更明智的选择。为优化器保留最大的自由度总是上策。

前面已经看到了,非关联子查询可以变成内嵌视图,且改动不大。下面,我们写出列出待办订单的整个查询语句:

select c.custname, o.ordid, os.status, os.statusdate
from customers c,
orders o,
orderstatus os,
(select ordid, max(statusdate) laststatusdate
from orderstatus
group by ordid) x
where o.ordid = os.ordid
and not exists (select null
from orderstatus os2
where os2.status = 'COMPLETE'
and os2.ordid = o.ordid)
and os.statusdate = x.laststatusdate
and os.ordid = x.ordid
and o.custid = c.custid   

但还有问题,如果最终状态确实是“COMPLETE”,我们就没有必要用子查询检查其最新状态了。内嵌视图能帮我们找出最后状态,无论它是不是“COMPLETE”。所以我们把查询改为检查已知的最新状态,这个过滤条件非常令人满意:

select c.custname, o.ordid, os.status, os.statusdate
from customers c,
orders o,
orderstatus os,
(select ordid, max(statusdate) laststatusdate
from orderstatus
group by ordid) x
where o.ordid = os.ordid
and os.statusdate = x.laststatusdate
and os.ordid = x.ordid
and os.status != 'COMPLETE'
and o.custid = c.custid  

如果进一步利用 OLAP SQL 引擎提供的分析功能,还可以避免对orderstatus的重复参照。不过就此打住,来思考一下我们是如何修改查询的,更重要的是执行路径(execution path为何。基本上,正常路径是先扫描orders表,接着利用orderstatus表上预计非常高效的索引进行访问。在最后一版的代码中,我们改用完整扫描orderstatus的方法,这是为了执行group byorderstatus中的记录条数一定会比 orders 中的大好几倍,然而,只以要扫描的数据量来看,估计前者比较小(而且可能小很多),这取决于为每张订单保存了多少信息。

无法确定哪种方法一定更好,这一切都取决于实际数据。补充说明一点,最好别在预期会增大的表上做全表扫描操作(若能把搜索限制在最近一个月或几个月的数据上则会好些)。不过,最后一版的代码肯定比第一版的(在where子句用子查询)要好。

在结束大数据量查询的话题之前,有个特殊情况值得一提。当查询要返回非常大量的数据时,该查询很可能不是某个用户坐在电脑前敲入的命令,而是来自于某个批处理操作。即便预备阶段稍长,只要整个处理能达到令人满意的结果,就是可以接受的。当然,不要忘了,无论是不是预备阶段,都会需要资源——CPU、内存,可能还有临时磁盘空间。即使最基本的查询完全相同,优化器在返回大量数据时所选择的路径,仍可能会与返回少量数据时完全不同,了解这一点是有用的。

总结:尽早过滤掉不需要的数据。

你可能感兴趣的:(大数据量)