我不得不求助于 ORM 不足的原始 SQL(使用 Django 1.7)。问题是大多数查询最终有 80-90% 的相似度。我想不出一种健壮和安全的方法来构建查询而不违反可重用性。
字符串连接是唯一的出路吗,即使用 if-else
条件构建无参数查询字符串,然后使用准备好的语句安全地包含参数(以避免 SQL 注入(inject))。我想遵循一种简单的方法为我的项目模板化 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
子句?
最佳答案
更好的查询
修复语法,简化和阐明:
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;
基于我更新的表格布局。请参阅下面的 fiddle 。
您不需要额外的子查询。窗口函数在聚合函数之后执行,因此您可以像演示那样嵌套它。
在谈论“排名”时,您可能想使用 rank()
,而不是 row_number()
。
假设 people.people_id
是 PK,您可以简化 GROUP BY
子句。
确保对所有可能不明确的列名进行表限定。
PL/pgSQL 函数
我会编写一个 PL/pgSQL 函数,它为您的可变部分获取参数。实现 a
- c
您的观点。 d
不清楚,留给你补充。
CREATE TABLE person (
person_id serial PRIMARY KEY
, name text NOT NULL
, team text
);
CREATE TABLE score (
score_id serial PRIMARY KEY
, person_id int NOT NULL REFERENCES person
, score int NOT NULL
);
-- dummy values
WITH ins AS (
INSERT INTO person(name, team)
SELECT 'Jon Doe ' || p, t
FROM generate_series(1,20) p -- 20 guys x
, unnest ('{team1,team2,team3}'::text[]) t -- 3 teams
RETURNING person_id
)
INSERT INTO score(person_id, score)
SELECT i.person_id, (random() * 100)::int
FROM ins i, generate_series(1,5) g; -- 5 scores each
功能:
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 numeric, rnk bigint)
LANGUAGE plpgsql AS
$func$
DECLARE
_agg_op CONSTANT text[] := '{count, sum, avg}'; -- allowed agg 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)::numeric AS score
, rank() OVER (PARTITION BY p.team ORDER BY %1$s(s.score) DESC) 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 -- %1$s
, CASE WHEN _left_join THEN 'LEFT JOIN' ELSE 'JOIN' END -- %2$s
, CASE WHEN _where_name <> '' THEN 'WHERE p.name LIKE $1' ELSE '' END -- %3$s
);
-- debug -- inspect query first
-- RAISE NOTICE '%', _sql;
-- execute -- unquote when tested ok
RETURN QUERY EXECUTE _sql
USING _where_name; -- $1
END
$func$;
调用:
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
您需要对 PL/pgSQL 有深入的了解。否则,解释太多了。您可以在 plpgsql 下的 SO 上找到相关答案对于答案中的每个细节。
所有参数都经过安全处理,不可能有 SQL 注入(inject)。见:
请特别注意,在传递
_where_name
时,如何有条件地添加WHERE
子句,其中位置参数$1
查询刺痛。该值被传递给EXECUTE
作为值 与USING
clause .没有类型转换,没有转义,没有 SQL 注入(inject)的机会。示例:对函数参数使用
DEFAULT
值,因此您可以自由提供任何参数或不提供任何参数。更多:函数
format()
有助于以安全和干净的方式构建复杂的动态 SQL 字符串。
关于sql - 以编程方式构建 SQL 查询的可靠方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25186353/