24

我有一个表模式,其中包括一个 int 数组列,以及一个对数组内容求和的自定义聚合函数。换句话说,给定以下内容:

CREATE TABLE foo (stuff INT[]);

INSERT INTO foo VALUES ({ 1, 2, 3 });
INSERT INTO foo VALUES ({ 4, 5, 6 });

我需要一个可以返回的“sum”函数{ 5, 7, 9 }。正常工作的PL/pgSQL版本如下:

CREATE OR REPLACE FUNCTION array_add(array1 int[], array2 int[]) RETURNS int[] AS $$
DECLARE
    result int[] := ARRAY[]::integer[];
    l int;
BEGIN
  ---
  --- First check if either input is NULL, and return the other if it is
  ---
  IF array1 IS NULL OR array1 = '{}' THEN
    RETURN array2;
  ELSEIF array2 IS NULL OR array2 = '{}' THEN
    RETURN array1;
  END IF;

  l := array_upper(array2, 1);

  SELECT array_agg(array1[i] + array2[i]) FROM generate_series(1, l) i INTO result;

  RETURN result;
END;
$$ LANGUAGE plpgsql;

加上:

CREATE AGGREGATE sum (int[])
(
    sfunc = array_add,
    stype = int[]
);

使用大约 150,000 行的数据集,SELECT SUM(stuff)需要 15 秒以上才能完成。

然后我用 C 重写了这个函数,如下:

#include <postgres.h>
#include <fmgr.h>
#include <utils/array.h>

Datum array_add(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(array_add);

/**
 * Returns the sum of two int arrays.
 */
Datum
array_add(PG_FUNCTION_ARGS)
{
  // The formal PostgreSQL array objects:
  ArrayType *array1, *array2;

  // The array element types (should always be INT4OID):
  Oid arrayElementType1, arrayElementType2;

  // The array element type widths (should always be 4):
  int16 arrayElementTypeWidth1, arrayElementTypeWidth2;

  // The array element type "is passed by value" flags (not used, should always be true):
  bool arrayElementTypeByValue1, arrayElementTypeByValue2;

  // The array element type alignment codes (not used):
  char arrayElementTypeAlignmentCode1, arrayElementTypeAlignmentCode2;

  // The array contents, as PostgreSQL "datum" objects:
  Datum *arrayContent1, *arrayContent2;

  // List of "is null" flags for the array contents:
  bool *arrayNullFlags1, *arrayNullFlags2;

  // The size of each array:
  int arrayLength1, arrayLength2;

  Datum* sumContent;
  int i;
  ArrayType* resultArray;


  // Extract the PostgreSQL arrays from the parameters passed to this function call.
  array1 = PG_GETARG_ARRAYTYPE_P(0);
  array2 = PG_GETARG_ARRAYTYPE_P(1);

  // Determine the array element types.
  arrayElementType1 = ARR_ELEMTYPE(array1);
  get_typlenbyvalalign(arrayElementType1, &arrayElementTypeWidth1, &arrayElementTypeByValue1, &arrayElementTypeAlignmentCode1);
  arrayElementType2 = ARR_ELEMTYPE(array2);
  get_typlenbyvalalign(arrayElementType2, &arrayElementTypeWidth2, &arrayElementTypeByValue2, &arrayElementTypeAlignmentCode2);

  // Extract the array contents (as Datum objects).
  deconstruct_array(array1, arrayElementType1, arrayElementTypeWidth1, arrayElementTypeByValue1, arrayElementTypeAlignmentCode1,
&arrayContent1, &arrayNullFlags1, &arrayLength1);
  deconstruct_array(array2, arrayElementType2, arrayElementTypeWidth2, arrayElementTypeByValue2, arrayElementTypeAlignmentCode2,
&arrayContent2, &arrayNullFlags2, &arrayLength2);

  // Create a new array of sum results (as Datum objects).
  sumContent = palloc(sizeof(Datum) * arrayLength1);

  // Generate the sums.
  for (i = 0; i < arrayLength1; i++)
  {
    sumContent[i] = arrayContent1[i] + arrayContent2[i];
  }

  // Wrap the sums in a new PostgreSQL array object.
  resultArray = construct_array(sumContent, arrayLength1, arrayElementType1, arrayElementTypeWidth1, arrayElementTypeByValue1, arrayElementTypeAlignmentCode1);

  // Return the final PostgreSQL array object.
  PG_RETURN_ARRAYTYPE_P(resultArray);
}

这个版本只需要 800 毫秒就可以完成,这……好多了。

(在此处转换为独立扩展:https ://github.com/ringerc/scrapcode/tree/master/postgresql/array_sum )

我的问题是,为什么 C 版本的速度这么快? 我预计会有改进,但 20 倍似乎有点多。这是怎么回事?在 PL/pgSQL 中访问数组是否存在固有的缓慢问题?

我在 Fedora Core 8 64 位上运行 PostgreSQL 9.0.2。该机器是一个 High-Memory Quadruple Extra-Large EC2 实例。

4

2 回答 2

25

为什么?

为什么C版的速度这么快?

PostgreSQL 数组本身就是一个非常低效的数据结构。它可以包含任何数据类型,并且可以是多维的,因此无法进行很多优化。但是,正如您所见,在 C 中可以更快地使用相同的数组。

这是因为 C 中的数组访问可以避免 PL/PgSQL 数组访问中涉及的大量重复工作。看看src/backend/utils/adt/arrayfuncs.carray_ref。现在看看它是如何从src/backend/executor/execQual.cin调用的ExecEvalArrayRef它针对来自 PL/PgSQL 的每个单独的数组访问运行,您可以通过将 gdb 附加到从 找到的 pid select pg_backend_pid()、在 处设置断点ExecEvalArrayRef、继续并运行您的函数来看到。

更重要的是,在 PL/PgSQL 中,您执行的每条语句都通过查询执行器机制运行。即使考虑到它们是预先准备好的,这也会使小型、廉价的语句相当慢。就像是:

a := b + c

实际上是由 PL/PgSQL 执行的,更像:

SELECT b + c INTO a;

如果您将调试级别足够高,附加调试器并在合适的点中断,或者使用auto_explain带有嵌套语句分析的模块,您可以观察到这一点。为了让您了解当您运行大量微小的简单语句(如数组访问)时这会带来多少开销,请查看此示例回溯和我的注释。

每个 PL/PgSQL 函数调用也有很大的启动开销。它并不大,但是当它被用作聚合时,加起来就足够了。

C中更快的方法

在您的情况下,我可能会像您所做的那样在 C 中执行此操作,但是当作为聚合调用时,我会避免复制数组。您可以检查它是否在聚合上下文中被调用

if (AggCheckCallContext(fcinfo, NULL))

如果是这样,使用原始值作为可变占位符,修改它然后返回它而不是分配一个新的。我将编写一个演示来验证这是否可以在不久后使用数组......(更新)或不那么短,我忘记了在 C 中使用 PostgreSQL 数组是多么可怕。开始了:

// append to contrib/intarray/_int_op.c

PG_FUNCTION_INFO_V1(add_intarray_cols);
Datum           add_intarray_cols(PG_FUNCTION_ARGS);

Datum
add_intarray_cols(PG_FUNCTION_ARGS)
{
    ArrayType  *a,
           *b;

    int i, n;

    int *da,
        *db;

    if (PG_ARGISNULL(1))
        ereport(ERROR, (errmsg("Second operand must be non-null")));
    b = PG_GETARG_ARRAYTYPE_P(1);
    CHECKARRVALID(b);

    if (AggCheckCallContext(fcinfo, NULL))
    {
        // Called in aggregate context...
        if (PG_ARGISNULL(0))
            // ... for the first time in a run, so the state in the 1st
            // argument is null. Create a state-holder array by copying the
            // second input array and return it.
            PG_RETURN_POINTER(copy_intArrayType(b));
        else
            // ... for a later invocation in the same run, so we'll modify
            // the state array directly.
            a = PG_GETARG_ARRAYTYPE_P(0);
    }
    else 
    {
        // Not in aggregate context
        if (PG_ARGISNULL(0))
            ereport(ERROR, (errmsg("First operand must be non-null")));
        // Copy 'a' for our result. We'll then add 'b' to it.
        a = PG_GETARG_ARRAYTYPE_P_COPY(0);
        CHECKARRVALID(a);
    }

    // This requirement could probably be lifted pretty easily:
    if (ARR_NDIM(a) != 1 || ARR_NDIM(b) != 1)
        ereport(ERROR, (errmsg("One-dimesional arrays are required")));

    // ... as could this by assuming the un-even ends are zero, but it'd be a
    // little ickier.
    n = (ARR_DIMS(a))[0];
    if (n != (ARR_DIMS(b))[0])
        ereport(ERROR, (errmsg("Arrays are of different lengths")));

    da = ARRPTR(a);
    db = ARRPTR(b);
    for (i = 0; i < n; i++)
    {
            // Fails to check for integer overflow. You should add that.
        *da = *da + *db;
        da++;
        db++;
    }

    PG_RETURN_POINTER(a);
}

并将其附加到contrib/intarray/intarray--1.0.sql

CREATE FUNCTION add_intarray_cols(_int4, _int4) RETURNS _int4
AS 'MODULE_PATHNAME'
LANGUAGE C IMMUTABLE;

CREATE AGGREGATE sum_intarray_cols(_int4) (sfunc = add_intarray_cols, stype=_int4);

(更准确地说,您应该创建intarray--1.1.sqlintarray--1.0--1.1.sql更新intarray.control。这只是一个快速破解。)

利用:

make USE_PGXS=1
make USE_PGXS=1 install

编译和安装。

现在DROP EXTENSION intarray;(如果你已经拥有它)和CREATE EXTENSION intarray;.

您现在可以使用聚合函数sum_intarray_cols(如 your sum(int4[]),以及二操作数add_intarray_cols(如 your array_add)。

通过专门研究整数数组,一大堆复杂性就消失了。在聚合情况下避免了一堆复制,因为我们可以安全地就地修改“状态”数组(第一个参数)。为了保持一致,在非聚合调用的情况下,我们得到第一个参数的副本,因此我们仍然可以就地使用它并返回它。

这种方法可以推广到支持任何数据类型,方法是使用 fmgr 缓存来查找感兴趣类型的 add 函数等。我对此并不是特别感兴趣,所以如果你需要它(比如,对NUMERIC数组列求和)然后......玩得开心。

同样,如果您需要处理不同的数组长度,您可能可以从上面找出要做什么。

于 2013-06-08T06:04:10.563 回答
13

PL/pgSQL 擅长作为 SQL 元素的服务器端粘合剂。程序元素和大量任务不是它的强项。分配、测试或循环是相对昂贵的,并且只有在它们有助于走捷径时才值得保证,而这些捷径仅靠 SQL 是无法实现的。在 C 中实现的相同逻辑总是会更快,但您似乎很清楚......

通常,纯 SQL解决方案更快。在您的测试设置中比较这个简单、等效的解决方案:

SELECT array_agg(a + b)
FROM  (
   SELECT unnest('{1, 2, 3 }'::int[]) AS a
        , unnest('{4, 5, 6 }'::int[]) AS b
   ) x;

您可以将其包装成一个简单的 SQL 函数。或者,为了获得最佳性能,将其直接集成到您的大查询中:

SELECT tbl_id, array_agg(a + b)
FROM  (
   SELECT tbl_id
        , unnest(array1) AS a
        , unnest(array2) AS b
   FROM   tbl
   ORDER  BY tbl_id
   ) x
GROUP  BY tbl_id;

SELECT如果返回的行数相同,则集合返回函数仅在 a 中并行工作。即:仅适用于相等长度的数组。这种行为最终在 Postgres 10 中得到了清理。请参阅:

通常最好使用当前版本的 Postgres 进行测试。至少更新到最新的点版本(撰写本文时为 9.0.15)。可能是性能差异巨大的部分原因。

Postgres 9.4

现在有一个更简洁的并行取消嵌套解决方案:

于 2013-06-07T23:15:37.613 回答