PgSQL的外连接选择率计算
上文讲述了PostgreSQL内连接选择率计算原理,本文介绍下外连接的选择率计算。外连接包括left join、right join、full join、anti join。
1、需要理解的前提概念:ON条件和下推条件
外连接中需要区分join中的ON条件和下推条件,而内连接却不需要,这里需要理解一下为什么。首先ON条件就是join的连接条件,那么下推条件是什么呢?
Pushed-down quals就是可以下推到左表(非空侧)扫描的条件,作为普通过滤条件。也就是说可以识别一部分连接条件,将他下推到左表侧提前过滤。为什么只有左表才可以下推呢?因为是左连接,按照他的语义,除了匹配的值外,不匹配时,右表需要以NULL来补齐,和左表值构成join结果。如果提前将它下推到右表,可能会将join结果过滤掉。
比如:SELECT * FROM a LEFT JOIN bON a.id1 = b.id1 WHERE b.id2 > 10;
ON条件a.id1=b.id1就是ON谓词,而下推条件是WHERE中的b.id2>10,假设a表(id1,id2)有(1,1),(20,2);b表(id1,id2)有(1,3),(2,3)。
1)如果该条件不下推连接后的结果是:
apache
a.id1 a.id2 b.id1 b.id21 1 1 320 2 NULL NULL
然后在此结果基础上进行b.id2>10过滤,会发现没有一个结果满足条件,所以返回NULL。
因为外连接的语义:必须为每个外表行输出至少一行,没有匹配的时候,内表行以NULL进行填充
2)如果条件下推下去,则右表先进行过滤,结果只有(20,2),然后在此基础上与a表进行连接,发现没有匹配的值,但是因为是左连接,所以左表的值需要全部输出,右表以NULL进行填充,结果是:
cpp
a.id1 a.id2 b.id1 b.id21 1 NULL NULL20 2 NULL NULL
而我们的条件要求b.id2>10,显然这个结果是不符合要求的。也就是如果WHERE条件在空侧,也就是右表,则不可以下推下去。如果在左表则可以下推下去,因为左表下推下去和不下推的结果一样,下推下去可以提前过滤左表数据,减少探测次数。
同理,如果是内连接,则也可以下推下去。因为下推和不下推场景下结果一样。
判断是否能够下推的逻辑在函数distribute_qual_to_rels中。
2、left join的基数估算

首先会将约束条件区分为joinquals也就是ON连接条件和下推条件pushedquals,通过clauselist_selectivity分别计算出join选择率和过滤选择率,计算结果为外表行数和内部行数乘积再乘以join选择率,此时需要保证输出行数至少是外表行数,然后再乘以过滤选择率,以此结果作为left join的最终估算基数。
3、left join的选择率
这里主要通过clauselist_selectivity函数计算join选择率和过滤条件选择率。对于join选择率来说,常见等值操作符为"=",使用join_selectivity->eqjoinsel 函数计算选择率(和inner join的选择率一样)。而过滤条件">"选择率使用restriction_selectivity->scalargtsel 。在此处,需要关注pg_operator.dat文件,该文件中注册了所有操作符元数据信息,包括OID和对应的操作函数,当然还有对应的选择率计算函数,比如>和<=操作符,它对应的选择率计算函数分别是scalargtsel和scalarlesel:

对于eqjoinsel来说利用MCV、等品直方图等统计信息来计算选择率,我们看下计算逻辑:

Inner join和left join以及full join的选择率是一样的。关于内连接选择率计算即eqjoinsel_inner函数见:PgSQL的等值inner join选择率
对于>的选择率 即函数scalargtsel :它调用scalarineqsel_wrapper函数来进行计算,而该函数确保变量在左侧,如果不是,则需要交换操作符并调整isgt标志,也就是大于改成小于或者小于改成大于。然后调用scalarineqsel函数计算标量类型不等式操作符(<, <=, >, >=)的选择率,通过结合 MCV(最常见值)和直方图统计信息来估计满足条件的行比例。这里分为两种场景:第一种场景是无统计信息,第二种场景是有统计信息,我们先看有统计信息的:

首先计算MCV的频率,也就是mcv_selectivity_ext函数判断常量在MCV中的频率和以及匹配个数。然后利用等频直方图,每个桶的行数近似相等,并且每隔桶里面的数据假设均匀分布,比如直方图边界值[10,30,50,70,90],那么就有4个桶,查询<=45时,的计算步骤:
1)每个桶的频率相同,为1/4
2)桶内均匀,估算时利用线性插值,45落在第2个桶里面
binfrac=(val-low)/(high-low),也就是(45-30)/(50-30)=0.75
3)完整频率:(2-1)*(1/4)+0.75*(1/4)
4)如果是<,则需要等号修正,get_variable_numdistinctt获取非重复值总数total:从pg_statistic中获取stadistinct,如果该值>0,则返回绝对值,否则表示相对比例,需要乘以表总行数
5)非MCV不同值个数othrdistinct=total-mcv个数
6)非MCV不同值等值选择率eq_selec=1/otherdistinct,此时需要将完整频率减去eq_selec。
最终mcv频率+直方图计算的频率作为最终的选择率。
下面看下无统计信息怎么计算:
如果Var是CTID 则特殊处理:基于表页数和元组密度计算选择率,否则使用默认选择率:返回 DEFAULT_INEQ_SEL(0.3333333333333333)。CTID的选择率计算如下图所示:

主要基于CTID 的物理存储特性,假设元组在表中按物理顺序分布,通过 CTID 的位置估算其在表中的相对位置,从而计算选择率,忽略了死元组行指针的影响,因为缺乏相关信息。
block+offset/density就是该<=该ctid的所有页数,然后除以总页数,就是选择率值。
元组密度density表示一页有多少元组数,而offset表示该页到此的元组数,offset/density就是这么多元组占几页,加上block就是小于等于该ctid的所有页数。