2017-04-17 84 views
0

我有一个具有数百万行的10s的表。各种复杂的过滤查询产生行集以支持应用程序。这些行集的大小从单个行到整个表都是任意的。但是,出于特定领域的原因,它们始终保持特定关键点的高水平连续性。在WHERE子句中用一组文本范围过滤行

我需要在数据库和应用程序之间双向传递这些行集,并且以某种方式进行压缩会很好。你们中许多人可能熟悉UNIX cut,它需要像这样的字段规范:cut -f 2-6,7,9-21并返回相应的列。目前,我正在使用一个稍微有限的剪切域规范版本(例如,编号为17-)来表示行集。因此,例如24-923817,2827711-8471362,99188271表示占用34个字节的唯一一组6567445行。

我已经写了下面的程序将这些转换为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查询中。

我也写了一组返流功能的行规格:

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; 

此作品纯SQL与WHERE some_key IN (SELECT cut_string_to_set(...))。当然,对于规划人员来说,作为一系列范围的最佳表达方式,产生噩梦和冗长的查询计划,在解包方面效率相对较低,并且可能或不应该阻止规划人员使用索引。

任何人能提供上述难题的任何包装解决方案的这个,有可能为它自己的类型,可能与运营商定制,以允许在没有动态SQL在列语法健全基于索引的过滤在更广泛的参与查询?这是不可能的吗?

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

EDIT 1

大答案建议使用范围类型的数组。不幸的是,查询规划器似乎不愿意使用这样的查询索引。下面的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成本(注意小型测试表上超过100毫秒的6名并行工作人员)太高。我看不到任何额外的索引可以在这里帮助。

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

END EDIT低于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)); 

这是一个110MB的指数。这很重要。但它的工作。

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

回答

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 
+0

哇,真棒!这正是我所追求的。不幸的是,有一个致命的问题。PostgreSQL拒绝为此使用索引。请参阅上面的编辑器输出的编辑。有什么想法吗? – rg6

+0

@ rg6更新了链接到范围索引 –

+0

我承认PostgreSQL的这个特殊角落对我来说是新的,但它似乎并没有工作。请阅读编辑。 – rg6

0

根本不可能的。运营商不这样做。他们称职能。如果他们在这里调用函数,那么函数将不得不使用动态SQL。

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

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

我更喜欢第一种可能的选择。如果我需要制作DSL,我不会在PostgreSQL中完成。我在应用程序中执行此操作。

+0

运算符调用函数,但是它们提供的与现有功能接口的功能并不总是显而易见的。考虑一个模式匹配字符串数组的界面,它依次涉及1)一个反转LIKE参数顺序的函数,2)一个使用颠倒顺序的运算符,3)一个使用new运算符来使用的函数ANY结构,以及4)最后的@> ~~或@> ~~ *运算符。当然,这可以用不同的方式完成,但是我花了一段时间才意识到操作员如何允许与任何接口连接,例如ANY。 – rg6