PostgreSQL:动态 WHERE 查询条件

Mic*_*ich 1 sql postgresql

我正在开发一个项目,需要在表中创建动态查询过滤规则,以用于过滤另一个表中的数据。我需要一些帮助来看看这是否可行,如果可行,如何形成查询。

       people                                rules
+----+--------+-----+     +----+--------+-------+----------+----------+
| id |  name  | age |     | id |  type  | field | operator | criteria |
+----+--------+-----+     +----+--------+-------+----------+----------+
|  1 | Emma   |  34 |     |  1 | people | age   | >        | 30       |
|  2 | Larry  |  25 |     +----+--------+-------+----------+----------+
|  3 | Alice  |  22 |
|  4 | Thomas |  31 |
+----+--------+-----+
Run Code Online (Sandbox Code Playgroud)

在此示例中,我想通过使用“rules”表中的条件来查询“people”表。我读到了 PostgreSQL 中的通用表表达式,并认为我可以在这里使用它,但到目前为止我还没有成功。到目前为止,这是我尝试过的:

WITH    cte_rule AS (SELECT field FROM rules WHERE id = 1)
SELECT  *,
FROM    people
WHERE   (SELECT field FROM cte_rule) < 30;
Run Code Online (Sandbox Code Playgroud)

这会导致从数据库返回以下错误:

Query 1 ERROR: ERROR:  operator does not exist: character varying < integer
LINE 4: WHERE (SELECT field FROM cte_rule) < 30;
                                           ^
HINT:  No operator matches the given name and argument types. You might need to add explicit type casts.
Run Code Online (Sandbox Code Playgroud)

我现在正在尝试对某些值进行硬编码,如果成功地将语句的一部分变为动态,则进一步将其扩展为完全动态。

任何指导将不胜感激。

小智 5

为此,您将需要动态 SQL。

由于两者的值列名称都是动态的,而且应用它们的上下文也有点棘手。

大致意思是:

create or replace function evaluate_rule(p_row record, p_rule_id int)
  returns boolean
as
$$
declare
  l_expression text;
  l_rule rules;
  l_result boolean;
  l_values jsonb;
  l_type text;
begin
  
  select *
    into l_rule
  from rules
  where id = p_rule_id
    and type = pg_typeof(p_row)::text;
  
  if not found then 
    return false;
  end if;
  
  l_values := to_jsonb(p_row);
  l_type := jsonb_typeof(l_values -> l_rule.field);
  
  if l_type = 'number' then 
    l_expression := format('select %s %s %s', l_values ->> l_rule.field, l_rule.operator, l_rule.criteria);
  else 
    l_expression := format('select %L %s %L', l_values ->> l_rule.field, l_rule.operator, l_rule.criteria);
  end if;
  
  execute l_expression 
    into l_result;
    
  return l_result;
end;
$$
language plpgsql
stable;
Run Code Online (Sandbox Code Playgroud)

不幸的是,为了从传递的行中动态选取列值而转换为 JSONB 确实会使您丢失数据类型信息。但我真的想不出一种不同的方法来动态创建一个列名和值事先未知的表达式。

如果规则指定了不存在的列,则将导致类似于该表达式的计算结果NULL > 42为 false。

该函数假设“type”和“id”的组合只有一个规则。如果你不关心请求的规则是否与传递的表匹配,你可以删除条件and type = pg_typeof(p_row)::text

鉴于此样本数据:

create table rules (id int,   type  text, field text, operator text, criteria text);
insert into rules values 
(1, 'people', 'age', '>', '30'), 
(2, 'people', 'name', '=', 'Alice');


create table people (id int, name text, age int);
insert into people
values
  (1, 'Emma', 34),
  (2, 'Larry', 25),
  (3, 'Alice', 22),
  (4, 'Thomas', 31)
;
Run Code Online (Sandbox Code Playgroud)

你可以这样使用它:

create or replace function evaluate_rule(p_row record, p_rule_id int)
  returns boolean
as
$$
declare
  l_expression text;
  l_rule rules;
  l_result boolean;
  l_values jsonb;
  l_type text;
begin
  
  select *
    into l_rule
  from rules
  where id = p_rule_id
    and type = pg_typeof(p_row)::text;
  
  if not found then 
    return false;
  end if;
  
  l_values := to_jsonb(p_row);
  l_type := jsonb_typeof(l_values -> l_rule.field);
  
  if l_type = 'number' then 
    l_expression := format('select %s %s %s', l_values ->> l_rule.field, l_rule.operator, l_rule.criteria);
  else 
    l_expression := format('select %L %s %L', l_values ->> l_rule.field, l_rule.operator, l_rule.criteria);
  end if;
  
  execute l_expression 
    into l_result;
    
  return l_result;
end;
$$
language plpgsql
stable;
Run Code Online (Sandbox Code Playgroud)

与第二条规则:

create table rules (id int,   type  text, field text, operator text, criteria text);
insert into rules values 
(1, 'people', 'age', '>', '30'), 
(2, 'people', 'name', '=', 'Alice');


create table people (id int, name text, age int);
insert into people
values
  (1, 'Emma', 34),
  (2, 'Larry', 25),
  (3, 'Alice', 22),
  (4, 'Thomas', 31)
;
Run Code Online (Sandbox Code Playgroud)

网上例子


编辑

我突然想到,使用 Postgres 12 或更高版本,这实际上可以在没有动态 SQL 或辅助函数的情况下完成:

with cte_rule as (
  select (concat('$.', field, ' ', operator, ' ', criteria))::jsonpath as rule
  from rules
  where id = 1
)
select p.*
from people p
where to_jsonb(p) @@ (select rule from cte_rule);
Run Code Online (Sandbox Code Playgroud)

操作符需要遵守SQL/JSON路径语言的规则,例如字符串需要使用==代替=和双引号。所以我的第二个示例规则需要是:

insert into rules values 
(2, 'people', 'name', '==', '"Alice"');
Run Code Online (Sandbox Code Playgroud)

网上例子


如果使用 JSON 路径是一种替代方案,您可能需要考虑将最终表达式存储在单个列中,而不是将列名称、运算符和值存储在三个不同的列中。

将表达式存储在jsonpath列中的好处是,当您尝试存储表达式时,将解析和验证语法。

create table rules (id int, type  text, expression jsonpath);
insert into rules values 
(1, 'people', '$.age > 30'), 
(2, 'people', '$.name == "Alice"');
Run Code Online (Sandbox Code Playgroud)

然后你可以使用:

select p.*
from people p
where to_jsonb(p) @@ (select expression from rules where id = 1)
Run Code Online (Sandbox Code Playgroud)

网上例子