【问题标题】:Robust approach for building SQL queries programmatically以编程方式构建 SQL 查询的稳健方法
【发布时间】:2014-09-30 22:06:48
【问题描述】:

我不得不求助于 ORM 不足的原始 SQL(使用 Django 1.7)。问题是大多数查询最终都具有 80-90% 的相似性。在不违反可重用性的情况下,我无法找到一种强大且安全的方式来构建查询。

字符串连接是唯一的出路,即使用if-else条件构建无参数查询字符串,然后使用准备好的语句安全地包含参数(以避免SQL注入)。我想采用一种简单的方法为我的项目模板化 SQL,而不是重新发明一个迷你 ORM。

例如,考虑这个查询:

SELECT id, name, team, rank_score
FROM
  ( SELECT id, name, team
    ROW_NUMBER() OVER (PARTITION BY team
                       ORDER BY count_score DESC) AS rank_score
    FROM 
      (SELECT id, name, team
       COUNT(score) AS count_score
       FROM people
       INNER JOIN scores on (scores.people_id = people.id)
       GROUP BY id, name, team
      ) AS count_table
  ) AS rank_table
WHERE rank_score < 3

我该怎么做:

a) 在people 上添加可选的WHERE 约束
b) 将INNER JOIN 更改为LEFT OUTER
c) 将COUNT 更改为SUM
d) 完全跳过OVER / PARTITION 子句?

【问题讨论】:

  • 不是 Django - 但我最终创建了一个基于字典的方法,其中各种字典定义了数据以及表之间的关系。有没有一种方法可以将数据添加到 Django 模型中 - 或者甚至更好地创建一个 mixin 来创建您的查询。

标签: sql postgresql orm plpgsql dynamic-sql


【解决方案1】:

更好的查询

对于初学者,你可以修正语法,简化和澄清很多:

SELECT *
FROM  (
   SELECT p.person_id, p.name, p.team, sum(s.score)::int AS score
         ,rank() OVER (PARTITION BY p.team
                       ORDER BY sum(s.score) DESC)::int AS rnk
    FROM  person p
    JOIN  score  s USING (person_id)
    GROUP BY 1
   ) sub
WHERE  rnk < 3;
  • 基于我更新的表格布局。请参阅下面的小提琴。

  • 您不需要额外的子查询。窗口函数在聚合函数之后执行,所以你可以像演示的那样嵌套它。

  • 在谈论“排名”时,您可能想使用rank(),而不是row_number()

  • 假设people.people_id是PK,可以简化GROUP BY

  • 确保对所有可能不明确的列名进行表限定

PL/pgSQL 函数

然后我会编写一个 plpgsql 函数,它为您的可变部分获取参数。 实施a - c 的积分。 d不清楚,留给大家补充。

CREATE OR REPLACE FUNCTION f_demo(_agg text       DEFAULT 'sum'
                               , _left_join bool  DEFAULT FALSE
                               , _where_name text DEFAULT NULL)
  RETURNS TABLE(person_id int, name text, team text, score int, rnk int) AS
$func$
DECLARE
   _agg_op  CONSTANT text[] := '{count, sum, avg}';  -- allowed functions
   _sql     text;
BEGIN

-- assert --
IF _agg ILIKE ANY (_agg_op) THEN
   -- all good
ELSE
   RAISE EXCEPTION '_agg must be one of %', _agg_op;
END IF;

-- query --
_sql := format('
SELECT *
FROM  (
   SELECT p.person_id, p.name, p.team, %1$s(s.score)::int AS score
         ,rank() OVER (PARTITION BY p.team
                       ORDER BY %1$s(s.score) DESC)::int AS rnk
    FROM  person p
    %2$s  score  s USING (person_id)
    %3$s
    GROUP BY 1
   ) sub
WHERE  rnk < 3
ORDER  BY team, rnk'
   , _agg
   , CASE WHEN _left_join THEN 'LEFT JOIN' ELSE 'JOIN' END
   , CASE WHEN _where_name <> '' THEN 'WHERE p.name LIKE $1' ELSE '' END
);

-- debug   -- quote when tested ok
-- RAISE NOTICE '%', _sql;

-- execute -- unquote when tested ok
RETURN QUERY EXECUTE _sql
USING  _where_name;   -- $1

END
$func$  LANGUAGE plpgsql;

呼叫:

SELECT * FROM f_demo();
SELECT * FROM f_demo('sum', TRUE, '%2');    
SELECT * FROM f_demo('avg', FALSE);
SELECT * FROM f_demo(_where_name := '%1_'); -- named param

SQL Fiddle

【讨论】:

  • 您的查询清理看起来非常适合可维护性。谢谢你的提示。我将阅读更多内容以了解 PL/pgSQL function 功能部分。诚然,在 Python 中使用查询生成器对我来说会更容易,因为我更了解这一点。
  • @click:我添加了更多解释和链接。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-18
  • 2011-08-07
相关资源
最近更新 更多