How to fix PostgreSQL generic plan estimate for any($1) array parameter?

问题描述 投票:0回答:2

我遇到了一个问题,即 PostgreSQL(Windows 上的 13.1,默认配置)在执行 5 次后会开始更喜欢通用计划,因为计划估计显然是错误的。事实上,自定义计划比通用计划快 10 倍,在“最坏”情况下至少快 2.5 倍。我不想使用 set plan_cache_mode = force_custom_plan,因为查询是与多个线程上的连接池并行运行的软件的一部分,以及许多更小和更大的查询,感觉就像做一个 hacky workaround对于其他查询也可能是危险的。

我想我已经追踪到通用计划估计对于 where 子句中的 x.id = ANY($1) 部分是错误的,我有三个。

查询是这样的:

SELECT ... FROM ... WHERE ... x.id = ANY($1) AND y.id = ANY($2) AND z.id = ANY($3) ...

当然,这已经大大简化了,但我知道我在 99% 的情况下为第一个参数传递了 50 个条目的 int8[](因为查询是针对记录的分页视图,其中一页有 50 条记录) ,对于自定义查询计划,正确估计为 50 行,但对于通用查询计划,则为 10 行。

切换到使用 IN($1, $2, ... $50) 修复了这个问题,但我们目前正在尝试将 IN 子句移至 ANY,因为它比 JDBC 更有效,而且我们已经被参数限制 a 咬住了使用 IN 几次(使用 ANY 时不会发生这种情况,因为它只是一个参数)。此外,这会使参数的计数可变,因此查询计划器会经常得到不同的查询(在我们的产品系统上,只有 $1 几乎总是一个包含 50 个值的数组,其他的可能更少或更多,具体取决于很多因素)。

到目前为止我已经尝试过,但没有成功:

  • 投掷东西来暗示策划者
  • 创建各种相关表的统计信息
  • 增加涉及列的统计目标
  • using unnest(这修复了估计,但无论如何查询都慢了大约 20 倍,所以对我来说不是真正的解决方案) 以及其他几十种“hacky”解决方法,例如使用正则表达式拆分表等。

请注意,我知道为什么它在第 5 次执行后使用通用计划 - 因为它的估计值低于前 5 个自定义计划的平均估计值。但我不知道如何修正那个错误的估计。

这里是自定义计划的相关行:

->  Bitmap Heap Scan on sbuilding b  (cost=150.51..332.59 rows=50 width=29) (actual time=0.077..0.187 rows=50 loops=2)
    Recheck Cond: (id = ANY ('{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50}'::bigint[]))
    Filter: (deletedat IS NULL)
    Heap Blocks: exact=50
    ->  Bitmap Index Scan on sbuilding_pkey  (cost=0.00..150.50 rows=50 width=0) (actual time=0.065..0.066 rows=50 loops=2)
        Index Cond: (id = ANY ('{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50}'::bigint[]))

与通用计划相同的部分:

->  Index Scan using sbuilding_pkey on sbuilding b  (cost=0.28..82.84 rows=10 width=29) (actual time=0.049..0.229 rows=50 loops=2)
    Index Cond: (id = ANY ($2))
    Filter: (deletedat IS NULL)

我已经用数字 1-50 替换了实际的真实 ID,但真实 ID 都存在,因为它们是在此之前的步骤中从数据库中检索到的。

我宁愿不必发布完整的查询或查询计划,因为它们包含很多敏感信息(对我来说将是数百个单词和值),但我希望这些部分足以说明问题和希望你们中的一个人能够提供帮助。

编辑:我没有明确安装任何像 intArray 这样的数组扩展 - 找到了一个关于 intArray 扩展的估计的邮件列表线程,所以我想我会添加它。不确定它是否仍然适用?这是链接:https://www.postgresql.org/message-id/20150317191531.GE10492%40momjian.us

非常感谢!

Edit2:我刚刚用一个超级简单的查询重新测试,似乎查询规划器总是为通用计划假设 10 个值 = ANY(:arrayParam)。这似乎是我的罪魁祸首。有什么办法可以“修正”这个假设吗?最好不要安装任何需要配置更改的扩展,如 pg_hint_plan。谢谢!

Edit3:在postgres源代码selfuncs.c中发现如下内容:

/*
         * Arbitrarily assume 10 elements in the eventual array value (see
         * also estimate_array_length).  We don't risk an assumption of
         * disjoint probabilities here.
         */
        for (i = 0; i < 10; i++)
        {
            if (useOr)
                s1 = s1 + s2 - s1 * s2;
            else
                s1 = s1 * s2;
        }

/*
 * Estimate number of elements in the array yielded by an expression.
 *
 * It's important that this agree with scalararraysel.
 */
int
estimate_array_length(Node *arrayexpr)
{
    /* look through any binary-compatible relabeling of arrayexpr */
    arrayexpr = strip_array_coercion(arrayexpr);

    if (arrayexpr && IsA(arrayexpr, Const))
    {
        Datum       arraydatum = ((Const *) arrayexpr)->constvalue;
        bool        arrayisnull = ((Const *) arrayexpr)->constisnull;
        ArrayType  *arrayval;

        if (arrayisnull)
            return 0;
        arrayval = DatumGetArrayTypeP(arraydatum);
        return ArrayGetNItems(ARR_NDIM(arrayval), ARR_DIMS(arrayval));
    }
    else if (arrayexpr && IsA(arrayexpr, ArrayExpr) &&
             !((ArrayExpr *) arrayexpr)->multidims)
    {
        return list_length(((ArrayExpr *) arrayexpr)->elements);
    }
    else
    {
        /* default guess --- see also scalararraysel */
        return 10;
    }
}

我会看看我能从那里得到什么,但我仍然希望有人有一个解决方案,不需要我检查整个 postgres 源代码;)

postgresql sql-execution-plan postgresql-13 query-planner
2个回答
2
投票

这可以通过更改此查询的

plan_cache_mode
来完成:

BEGIN;
SET LOCAL plan_cache_mode = force_custom_plan;
SELECT /* your query */
COMMIT;

这将使优化器始终使用自定义计划。


0
投票

如果您不想采用 Laurenz Albe 建议的设置

plan_cache_mode
的解决方案,我有以下建议:

在准备好的语句中添加一个附加条件作为文字

  • 总是评估为真和
  • 每次都改变。

Epoch 可以使用例如:

SELECT ... FROM ... WHERE ... x.id = ANY($1) AND y.id = ANY($2) AND z.id = ANY($3) ... AND 1678193956123=1678193956123

SELECT ... FROM ... WHERE ... x.id = ANY($1) AND y.id = ANY($2) AND z.id = ANY($3) ... AND 1678193974456=1678193974456

SELECT ... FROM ... WHERE ... x.id = ANY($1) AND y.id = ANY($2) AND z.id = ANY($3) ... AND 1678193987789=1678193987789

© www.soinside.com 2019 - 2024. All rights reserved.