使用一组文本范围过滤 WHERE 子句中的行

Filtering rows in WHERE clause with a textual set of ranges

我有一个 table 有数百万行。各种复杂的过滤查询会生成行集以支持应用程序。这些行集的大小是任意的,从单行到并包括完整的 table。但是,由于特定于域的原因,它们始终沿特定键保持高级别的连续性。

我需要在数据库和应用程序之间双向传递这些行集,最好能以某种方式对其进行压缩。你们中的许多人可能熟悉 UNIX cut,它采用如下字段规范:cut -f 2-6,7,9-21 和 returns 对应的列。我目前正在使用一个略有限制的 cut 字段规范版本(例如 no 17-)来表示行集。因此,例如 24-923817,2827711-8471362,99188271 表示一组唯一的 6567445 行,同时占用 34 个字节。

我已经编写了以下程序,使用 BETWEEN 语法将它们转换为 SQL WHERE 过滤器

CREATE OR REPLACE FUNCTION cut_string_to_sql_filter( TEXT, TEXT ) RETURNS TEXT AS $$
SELECT
    CASE 
        WHEN '' THEN 'FALSE'
        ELSE
            (SELECT
                '(' || STRING_AGG( REGEXP_REPLACE( REGEXP_REPLACE( str, '(\d+)-(\d+)', QUOTE_IDENT(  ) || ' BETWEEN  AND ' ), '^(\d+)$', QUOTE_IDENT(  ) || '=' ), ' OR ' ) || ')' AS sql
                FROM
                    REGEXP_SPLIT_TO_TABLE( , ',' ) AS t(str))
        END;
$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE;

第一个参数是行集规范,第二个参数是 table 的键字段名称。对于上面的示例,SELECT cut_string_to_sql_filter( '24-923817,2827711-8471362,99188271', 'some_key' ) returns:

(some_key BETWEEN 24 AND 923817 OR some_key BETWEEN 2827711 AND 8471362 OR some_key=99188271)

问题在于,目前任何使用此类行集规范的查询都必须使用动态 SQL,因为我想不出一种方法来使用自定义运算符或任何其他语法功能来嵌入它在普通 SQL 查询中产生效果。

我还写了一个行规范的返回函数:

CREATE OR REPLACE FUNCTION cut_string_to_set( TEXT ) RETURNS SETOF INTEGER AS $$
DECLARE
    _i TEXT;
    _j TEXT;
    _pos INTEGER;
    _start INTEGER;
    _end INTEGER;
BEGIN
    IF  <> '' THEN
        FOR _i IN SELECT REGEXP_SPLIT_TO_TABLE( , ',' ) LOOP
            _pos := POSITION( '-' IN _i );
            IF _pos > 0 THEN
                _start := SUBSTRING( _i FROM 1 FOR _pos - 1 )::INTEGER;
                _end := SUBSTRING( _i FROM _pos + 1 )::INTEGER;
                FOR _j IN _start.._end LOOP
                    RETURN NEXT _j;
                END LOOP;
            ELSE
                RETURN NEXT _i;
            END IF;
        END LOOP;
    END IF;
END
$$ LANGUAGE PLPGSQL IMMUTABLE STRICT PARALLEL SAFE;

这在 SQL 和 WHERE some_key IN (SELECT cut_string_to_set(...)) 中有效。当然,将最适合作为一组范围表达给规划器的内容拆包效率相对较低,会产生噩梦般和冗长的查询计划,并且可能会或可能不会阻止规划器在本来可以而且应该使用索引的情况下使用索引。

任何人都可以提供解决上述难题的任何解决方案来打包它,可能是它自己的类型,可能带有自定义运算符,以允许在没有动态的列上进行语法上合理的基于索引的过滤 SQL 在更广泛的查询中?这根本不可能吗?

如果您发现任何机会,请随时提出改进程序的建议。谢谢!

编辑 1

下面的好答案建议使用范围类型数组。不幸的是,查询规划器似乎不愿意在这样的查询中使用索引。 运行 的小测试 table.

中的 Planner 输出如下
Gather  (cost=1000.00..34587.33 rows=38326 width=45) (actual time=0.395..112.334 rows=1018 loops=1)
Workers Planned: 6
Workers Launched: 6
->  Parallel Seq Scan on test  (cost=0.00..29754.73 rows=6388 width=45) (actual time=91.525..107.354 rows=145 loops=7)
        Filter: (test_ref <@ ANY ('{"[24,28)","[29,51)","[999,1991)"}'::int4range[]))
        Rows Removed by Filter: 366695
Planning time: 0.214 ms
Execution time: 116.779 ms

CPU 成本(注意 6 个 worker 在小测试中并行超过 100 毫秒 table)太高了。我看不出任何额外的索引在这里有什么帮助。

作为对比,这里是使用 BETWEEN 过滤器的规划器输出。

Bitmap Heap Scan on test  (cost=22.37..1860.39 rows=1031 width=45) (actual time=0.134..0.430 rows=1018 loops=1)
Recheck Cond: (((test_ref >= 24) AND (test_ref <= 27)) OR ((test_ref >= 29) AND (test_ref <= 50)) OR ((test_ref >= 999) AND (test_ref <= 1990)))
Heap Blocks: exact=10
->  BitmapOr  (cost=22.37..22.37 rows=1031 width=0) (actual time=0.126..0.126 rows=0 loops=1)
        ->  Bitmap Index Scan on test_test_ref_index  (cost=0.00..2.46 rows=3 width=0) (actual time=0.010..0.010 rows=4 loops=1)
            Index Cond: ((test_ref >= 24) AND (test_ref <= 27))
        ->  Bitmap Index Scan on test_test_ref_index  (cost=0.00..2.64 rows=21 width=0) (actual time=0.004..0.004 rows=22 loops=1)
            Index Cond: ((test_ref >= 29) AND (test_ref <= 50))
        ->  Bitmap Index Scan on test_test_ref_index  (cost=0.00..16.50 rows=1007 width=0) (actual time=0.111..0.111 rows=992 loops=1)
            Index Cond: ((test_ref >= 999) AND (test_ref <= 1990))
Planning time: 0.389 ms
Execution time: 0.660 ms

结束编辑 1

编辑 2

下面的答案建议使用范围索引。据我了解,问题是我不需要索引范围类型。好吧,也许键列被转换为操作的范围,所以我可以对其应用 GIST 索引,规划器将使用它。

CREATE INDEX test_test_ref_gist_index ON test USING GIST (test_ref);
ERROR:  data type integer has no default operator class for access method "gist"
HINT:  You must specify an operator class for the index or define a default operator class for the data type.

这并不奇怪。因此,让我们将键列转换为范围并对其进行索引。

CREATE INDEX test_test_ref_gist_index ON test USING GIST (INT4RANGE( test_ref, test_ref ));

哇,一个 110 MB 的索引。那是沉重的。但是有用吗

Gather  (cost=1000.00..34587.33 rows=38326 width=45) (actual time=0.419..111.009 rows=1018 loops=1)
Workers Planned: 6
Workers Launched: 6
->  Parallel Seq Scan on test_mv  (cost=0.00..29754.73 rows=6388 width=45) (actual time=90.229..105.866 rows=145 loops=7)
        Filter: (test_ref <@ ANY ('{"[24,28)","[29,51)","[999,1991)"}'::int4range[]))
        Rows Removed by Filter: 366695
Planning time: 0.237 ms
Execution time: 114.795 ms

没有。我并不太惊讶。我希望该索引适用于 "contains" 而不是 "contained by" 操作。不过我没有这方面的经验。

结束编辑 2

根本不可能。运营商不这样做。他们调用函数。如果他们在这里调用一个函数,该函数将不得不使用动态 SQL.

要不使用动态 SQL,您必须破解 PostgreSQL 词法分析器。 PostgreSQL 是一个 SQL 数据库。您的语法不是 SQL。你可以做两件事,

  1. 使用SQL.
  2. 编译SQL.

我尽可能选择第一个选项。如果我需要制作 DSL,我不会在 PostgreSQL 中制作。我在应用程序中进行。

传递一个范围数组:

select *
from t
where
    k <@ any (array[
        '[24,923817]','[2827711,8471362]','[99188271,99188271]'
    ]::int4range[])

检查范围类型的索引:https://www.postgresql.org/docs/current/static/rangetypes.html#RANGETYPES-INDEXING

如果无法找到合适的范围索引,请连接具体化范围:

select *
from
    t
    inner join
    (
        select generate_series(lower(a),upper(a) - 1) as k
        from unnest(array[ 
            '[24,27]','[29,50]','[999,1990]'
        ]::int4range[]) a(a)
    ) s using (k)

可以避免连接所有范围值。比较范围的下限和上限:

select *
from
    t
    cross join
    (
        select lower(a) as l, upper(a) - 1 as u
        from unnest(array[
            '[24,27]','[29,50]','[999,1990]'
        ]::int4range[]) a(a)
    ) s
where k between l and u