PostgreSQL:查询的动态 WHERE 条件

时间:2021-01-13 11:21:37

标签: 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 |
+----+--------+-----+

在这个例子中,我想通过使用“rules”表中的条件来查询“people”表。我阅读了 PostgreSQL 中的 Common Table Expressions 并认为我可以在这里使用它,但到目前为止我还没有成功。这是我迄今为止尝试过的:

WITH    cte_rule AS (SELECT field FROM rules WHERE id = 1)
SELECT  *,
FROM    people
WHERE   (SELECT field FROM cte_rule) < 30;

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

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.

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

非常感谢任何指导。

1 个答案:

答案 0 :(得分:3)

为此您将需要动态 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;

不幸的是,为了从传递的行中动态选择列值而转换为 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)
;

你可以这样使用它:

select p.*
from people p
where evaluate_rule(p, 1)

id | name   | age
---+--------+----
 1 | Emma   |  34
 4 | Thomas |  31

使用第二条规则:

select p.*
from people p
where evaluate_rule(p, 2)

id | name  | age
---+-------+----
 3 | Alice |  22

Online example


编辑

我突然想到,使用 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);

运营商需要遵守SQL/JSON path language的规则,例如您需要对字符串使用 == 而不是 = 和双引号。所以我的第二个示例规则需要是:

insert into rules values 
(2, 'people', 'name', '==', '"Alice"');

Online example


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

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

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

然后你可以使用:

select p.*
from people p
where to_jsonb(p) @@ (select expression from rules where id = 1)

Online example