【问题标题】:PostgreSQL partitioning with joined table - partition constraint not used in query plan带有连接表的 PostgreSQL 分区 - 查询计划中未使用分区约束
【发布时间】:2013-07-22 05:18:21
【问题描述】:

我在 PostgreSQL 9.2 中有一个大表,已分区为 described in the manual。嗯……差不多!我真正的分区键不在分区表本身,而是在一个连接表中,像这样(简化):

-- millions to tens of millions of rows
CREATE TABLE data
(
  slice_id integer NOT NULL,
  point_id integer NOT NULL,
  -- ... data columns ...,
  CONSTRAINT pk_data PRIMARY KEY (slice_id, point_id),
  CONSTRAINT fk_data_slice FOREIGN KEY (slice_id) REFERENCES slice (id)
  CONSTRAINT fk_data_point FOREIGN KEY (point_id) REFERENCES point (id)
)

-- hundreds to thousands of rows
CREATE TABLE slice
(
  id serial NOT NULL,
  partition_date timestamp without time zone NOT NULL,
  other_date timestamp without time zone NOT NULL,
  int_key integer NOT NULL
  CONSTRAINT pk_slice PRIMARY KEY (id)
)

-- about 40,000 rows
CREATE TABLE point
(
  -- ... similar to "slice" ...
)

要分区的表 (data) 包含 pointslice 的每个组合的行,每个组合都有一个复合键。我只想将它分区到一个键列partition_date,它是slice 的一部分。当然,我的子表上的检查约束不能直接包含它,所以我包含了与 partition_date 对应的所有 slice.id 值的范围,如下所示:

ALTER TABLE data_part_123 ADD CONSTRAINT ck_data_part_123
    CHECK (slice_id >= 1234 AND slice_id <= 1278);

这一切都适用于插入数据。但是,查询不使用上述 CHECK 约束。例如。

SELECT *
FROM data d
JOIN slice s ON d.slice_id = s.id
WHERE s.partition_date = '2013-07-23'

我可以在查询计划中看到这仍然会扫描所有子表。我已经尝试以多种方式重写查询,包括 CTE 和子选择,但这没有帮助。

有什么方法可以让规划者“理解”我的分区方案?我真的不想在data 表中复制分区键数百万次。

查询计划如下所示:

Aggregate  (cost=539243.88..539243.89 rows=1 width=0)
  ->  Hash Join  (cost=8.88..510714.02 rows=11411945 width=0)
        Hash Cond: (d.slice_id = s.id)
        ->  Append  (cost=0.00..322667.41 rows=19711542 width=4)
              ->  Seq Scan on data d  (cost=0.00..0.00 rows=1 width=4)
              ->  Seq Scan on data_part_123 d  (cost=0.00..135860.10 rows=8299610 width=4)
              ->  Seq Scan on data_part_456 d  (cost=0.00..186807.31 rows=11411931 width=4)
        ->  Hash  (cost=7.09..7.09 rows=143 width=4)
              ->  Seq Scan on slice s  (cost=0.00..7.09 rows=143 width=4)
                    Filter: (partition_date = '2013-07-23 00:00:00'::timestamp without time zone)

【问题讨论】:

    标签: sql performance postgresql database-design partitioning


    【解决方案1】:

    实现它的唯一方法是使查询动态化:

    create function select_from_data (p_date date)
    returns setof data as $function$
    
    declare
        min_slice_id integer,
        max_slice_id integer;
    
    begin
        select min(slice_id), max(slice_id)
        into min_slice_id, max_slice_id
        from slice
        where partition_date = p_date;
    
    return query execute
        $dynamic$
            select *
            from data
            where slice_id between $1 and $2
        $dynamic$
        using min_slice_id, max_slice_id;
    
    end;
    $function$ language plpgsql;
    

    这将为给定日期构建具有适当切片范围的查询,并在运行时计划它,此时计划器将拥有检查确切分区所需的信息。

    要使函数更通用而又不失去规划器在运行时获取信息的能力,请在过滤器中使用or parameter is null 构造。

    create function select_from_data (
        p_date date,
        value_1 integer default null,
        value_2 integer default null
    )
    returns setof data as $function$
    
    declare
        min_slice_id integer,
        max_slice_id integer;
    
    begin
        select min(slice_id), max(slice_id)
        into min_slice_id, max_slice_id
        from slice
        where partition_date = p_date;
    
    return query execute
        $dynamic$
            select *
            from data
            where
                slice_id between $1 and $2
                and (some_col = $3 or $3 is null)
                and (another_col = $4 or $4 is null)
        $dynamic$
        using min_slice_id, max_slice_id, value_1, value_2;
    
    end;
    $function$ language plpgsql;
    

    现在,如果某些参数以null 的形式传递,它将不会干扰查询。

    【讨论】:

    • +1 表示一个有趣的想法,但我不确定我是否会这样做。这确实只考虑正确的分区,从其中选择所有行时速度更快。但是,当添加进一步的过滤器(在其他列上)时,它会慢得多。我相信这是因为 plpgsql 函数对优化器是不透明的,所以它从函数返回分区中所有 500 万左右的行,然后过滤它们。
    • @EM 您究竟是如何添加更多过滤器的?函数内部?如果是这样,请说明您是如何做到的,因为这可能会对规划者的愿景产生破坏性影响。请注意,您可以通过以下函数过滤集合 returnedselect * from select_from_data('2013-07-23'::date) where another_column = some_value
    • 是的,我过滤了 outside 函数,就像在你的例子中一样 - 这就是慢的。函数内部的过滤应该很快,但当然需要为每个查询编写一个单独的函数(或将 SQL 作为字符串传递给函数)。
    • @EM 我更新了该函数的通用版本,但我仍然希望看到您尝试使用其explain 输出的查询。
    • 我无法确定这是可怕还是真棒,但无论如何 +1。
    【解决方案2】:

    这个方案行不通。 constraint_exclusion 简单而愚蠢。它必须能够通过检查查询在计划期间证明该查询不能触及某些分区才能排除它们。

    目前不支持在查询执行期间排除分区。 Pg 提供的基本分区支持还有很大的改进空间,执行时间约束排除只是可以使用工作的领域之一。

    您的应用程序需要了解分区及其约束,并且需要明确加入仅所需分区的联合。

    在这种情况下,我不确定 PostgreSQL 是如何做到你想要的。我猜你希望它通过连接上的复合键来投射约束,断言由于查询指定了s.partition_date = '2013-07-23',并且对具有s.partition_date = '2013-07-23' 的所有切片ID 的查询在slice_id &gt;= 1234 AND slice_id &lt;= 1278 范围内找到它们,那么只有分区@987654325 @ 应该被扫描。

    问题在于在计划时 PostgreSQL 完全不知道s.partition_date = '2013-07-23 对应于特定范围的切片ID。如果保留它们,它可能能够从相关统计信息中找出它,但表统计信息只是近似值,而不是分区所需的证明

    我怀疑您需要对数据进行一些反规范化,如果您希望按其分区,则在每个 data 行中复制 slice.partition_date。您可以尝试确保不要让它们不同步,或者(我会做的)在slice(id, partition_date) 上创建UNIQUE 约束,然后将FOREIGN KEY 引用从data 的分区添加到@987654333 @,从而确保它们不能以一些额外的索引维护和插入成本为代价而失去同步。

    【讨论】:

    • 感谢详细的解释,有道理。正如您所说,关键是仅在计划期间才考虑约束 - 我没有意识到这一点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-02-08
    • 1970-01-01
    • 2022-01-17
    • 2016-09-16
    • 2017-05-22
    • 2020-05-07
    相关资源
    最近更新 更多