使用基于任意jsonb过滤器表达式的jsonb列从表中选择行

时间:2018-03-14 02:29:18

标签: sql json postgresql jsonb postgresql-9.6

测试数据

DROP TABLE t;
CREATE TABLE t(_id serial PRIMARY KEY, data jsonb);
INSERT INTO t(data) VALUES
   ('{"a":1,"b":2, "c":3}')
   , ('{"a":11,"b":12, "c":13}')
   , ('{"a":21,"b":22, "c":23}')

问题陈述:我想收到一个任意的JSONB参数,作为列t.data的过滤器,例如

{ "b":{ "from":0, "to":20 }, "c":13 }

并使用它来从我的测试表t中选择匹配的行。 在此示例中,我想要b介于0到20之间且c = 13的行。 如果过滤器指定t.data中不存在的“列”(或“标记”),则不需要错误 - 它只是找不到匹配项。

为了简单起见,我使用了数字值,但是想要一种可以推广到text的方法。

到目前为止我尝试了什么。我查看了包含方法,该方法适用于相等条件,但我对处理范围条件的一般方法感到困惑:

select * from t
where t.data@> '{"c":13}'::jsonb;

背景:在网站上构建通用表格预览页面(针对管理员用户)时出现此问题。 该页面根据选择用于预览的任何表格中的各个列显示过滤器。 然后将过滤器传递给Postgres DB中的函数,该函数将此动态过滤条件应用于表。 它返回与用户指定的过滤器匹配的行的jsonb数组。 然后使用此jsonb数组填充Preview结果集。 构成过滤器的列可能会发生变化。

我的Postgres版本是9.6 - 谢谢。

2 个答案:

答案 0 :(得分:1)

如果要解析{ "b":{ "from":0, "to":20 }, "c":13 },则需要解析器。它超出了json函数的范围,但你可以编写" generic"使用ANDOR进行查询以按此类json进行过滤,例如:

https://www.db-fiddle.com/f/jAPBQggG3p7CxqbKLMbPKw/0

with filt(f) as (values('{ "b":{ "from":0, "to":20 }, "c":13 }'::json))
select * 
from t
join filt on 
 (f->'b'->>'from')::int < (data->>'b')::int 
 and 
 (f->'b'->>'to')::int > (data->>'b')::int 
 and
 (data->>'c')::int = (f->>'c')::int
;

答案 1 :(得分:0)

感谢您的意见/建议。 当我有更多时间时,我一定会看看GraphQL - 我现在正在紧迫的截止日期前工作。 似乎共识是没有解析器就无法实现完全通用的解决方案。 但是,我得到了一份可行的初稿 - 它远非理想,但我们可以使用它。欢迎提出任何意见/改进......

测试数据(扩展到包括日期和文本字段)

DROP TABLE t;
CREATE TABLE t(_id serial PRIMARY KEY, data jsonb);
INSERT INTO t(data) VALUES
   ('{"a":1,"b":2, "c":3, "d":"2018-03-10", "e":"2018-03-10", "f":"Blah blah" }')
   , ('{"a":11,"b":12, "c":13, "d":"2018-03-14", "e":"2018-03-14", "f":"Howzat!"}')
   , ('{"a":21,"b":22, "c":23, "d":"2018-03-14", "e":"2018-03-14", "f":"Blah blah"}')

动态应用jsonb过滤器的第一个代码草案,但限制了支持的语法。 此外,如果提供的语法与预期的语法不匹配,它只会无声地失败。 时间戳也处理有点笨拙。

-- Handle timestamp & text types as well as int
-- See is_timestamp(text) function at bottom

with cte as (
    select t.data, f.filt, fk.key
    from t
    , ( values ('{ "a":11, "b":{ "from":0, "to":20 }, "c":13, "d":"2018-03-14", "e":{ "from":"2018-03-11", "to": "2018-03-14" }, "f":"Howzat!" }'::jsonb ) ) as f(filt) -- equiv to cross join
    , lateral (select * from jsonb_each(f.filt)) as fk
)
select data, filt       --, key, jsonb_typeof(filt->key), jsonb_typeof(filt->key->'from'), is_timestamp((filt->key)::text), is_timestamp((filt->key->'from')::text) 
from cte
where 
    case when (filt->key->>'from') is null then 
        case jsonb_typeof(filt->key) 
            when 'number' then (data->>key)::numeric = (filt->>key)::numeric
            when 'string' then 
                case is_timestamp( (filt->key)::text )
                    when true then (data->>key)::timestamp = (filt->>key)::timestamp
                    else (data->>key)::text = (filt->>key)::text
                end
            when 'boolean' then (data->>key)::boolean = (filt->>key)::boolean
            else false
        end
    else 
        case jsonb_typeof(filt->key->'from') 
            when 'number' then (data->>key)::numeric between (filt->key->>'from')::numeric and (filt->key->>'to')::numeric
            when 'string' then 
                case is_timestamp( (filt->key->'from')::text )
                    when true then (data->>key)::timestamp between (filt->key->>'from')::timestamp and (filt->key->>'to')::timestamp
                    else (data->>key)::text between (filt->key->>'from')::text and (filt->key->>'to')::text
                end
            when 'boolean' then false
            else false
        end
    end
group by data, filt
having count(*) = ( select count(distinct key) from cte ) -- must match on all filter elements
;

create or replace function is_timestamp(s text) returns boolean as $$
begin
  perform s::timestamp;
  return true;
exception when others then
  return false;
end;
$$ strict language plpgsql immutable;