使用文本范围

时间:2017-04-17 16:16:40

标签: postgresql where-in

我有一张包含数百万行的表格。各种复杂的过滤查询生成行集以支持应用程序。这些行集具有从单行到包括完整表的任意大小。但是,出于特定领域的原因,它们始终在特定密钥上保持高水平的连续性。

我需要在数据库和应用程序之间双向传递这些行集,并且以某种方式压缩它会很好。许多人可能熟悉UNIX cut,它采用如下字段规范:cut -f 2-6,7,9-21并返回相应的列。我目前正在使用切割字段规范的略微限制版本(例如,没有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 $1
        WHEN '' THEN 'FALSE'
        ELSE
            (SELECT
                '(' || STRING_AGG( REGEXP_REPLACE( REGEXP_REPLACE( str, '(\d+)-(\d+)', QUOTE_IDENT( $2 ) || ' BETWEEN \1 AND \2' ), '^(\d+)$', QUOTE_IDENT( $2 ) || '=\1' ), ' OR ' ) || ')' AS sql
                FROM
                    REGEXP_SPLIT_TO_TABLE( $1, ',' ) AS t(str))
        END;
$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE;

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

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

这个问题是目前任何使用这种行集规范的查询都必须使用动态SQL,因为我想不出一种方法来使用自定义运算符或任何其他语法功能将此效果嵌入到纯SQL查询中

我还为行规范写了一个set-returns函数:

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 $1 <> '' THEN
        FOR _i IN SELECT REGEXP_SPLIT_TO_TABLE( $1, ',' ) 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;

这适用于使用WHERE some_key IN (SELECT cut_string_to_set(...))的纯SQL。当然,将打包最好表达的内容解压缩为一组范围,产生噩梦般和冗长的查询计划,并且可能会或可能不会阻止规划者在其他方式可能和应该使用索引时,效率相对较低。 / p>

任何人都可以提供任何解决上述难题的解决方案,可能作为自己的类型,可能使用自定义运算符,以允许在更广泛的相关查询中没有动态SQL的列的语法上理智的基于索引的过滤?这根本不可能吗?

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

编辑1

下面的答案很好,建议使用一系列范围类型。不幸的是,查询规划器似乎不愿意使用带有这种查询的索引。计划器输出低于在小型测试台上运行。

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个并行超过100毫秒的工作人员)太高。我无法看到任何额外的索引如何在这里提供帮助。

相比之下,这是使用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 ));

Whew,一个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

不。我并不太惊讶。我希望这个索引适用于“包含”而不是“包含”操作。我虽然没有经验。

END EDIT 2

2 个答案:

答案 0 :(得分:1)

传递一系列范围:

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

答案 1 :(得分:0)

根本不可能。经营者不这样做。他们称功能。如果他们在这里调用了一个函数,那么函数就必须使用动态SQL。

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

  1. 使用SQL。
  2. 编译SQL。
  3. 我更喜欢第一种选择。如果我需要制作DSL,我不会在PostgreSQL中这样做。我是在应用程序中完成的。