PostgreSQL反模式:将集合和选择传递给SQL

开发人员有时需要将一组参数传递给请求,甚至需要传递整个 “输入” 选择 。 有时会遇到非常奇怪的解决方案。

让我们从相反的角度出发,看看它不值得做什么,为什么以及如何做得更好。

直接将值插入请求主体


通常看起来像这样:

query = "SELECT * FROM tbl WHERE id = " + value 

...左右:

 query = "SELECT * FROM tbl WHERE id = :param".format(param=value) 

关于这种方法,据说, 甚至写得很丰富:



几乎总是这样,这是通向SQL注入直接路径,并且给业务逻辑带来了额外的负担,这被迫“粘合”查询字符串。

仅当需要在PostgreSQL 10及更低版本的PostgreSQL中使用section以获得更有效的计划时,这种方法才可以部分证明是合理的。 在这些版本中,即使不考虑传输的参数,也仅基于请求主体来确定扫描部分的列表。

$ n参数


使用参数占位符是好的,它允许您使用PREPARED STATEMENTS ,减少业务逻辑(查询字符串仅生成和传输一次)和数据库服务器(不需要为每个请求实例重新解析和计划)的负载。

可变数量的参数


当我们想提前传递未知数量的参数时,问题将等待我们:

 ... id IN ($1, $2, $3, ...) -- $1 : 2, $2 : 3, $3 : 5, ... 

如果您以这种形式保留请求,那么尽管它可以使我们免于潜在的注入,但仍然需要根据参数数量对每个选项的请求进行粘合/解析。 比每次都做的更好,但是您可以不用它。

仅传递一个包含数组序列化表示形式的参数就足够

 ... id = ANY($1::integer[]) -- $1 : '{2,3,5,8,13}' 

唯一的区别是需要将参数显式转换为所需的数组类型。 但这不会引起问题,因为我们已经提前知道了要解决的问题。

样品转移(矩阵)


通常,这些都是用于“在一个请求中”将数据集插入数据库的各种选项:

 INSERT INTO tbl(k, v) VALUES($1,$2),($3,$4),... 

除了上述“重新粘贴”请求问题之外,这还可能导致内存不足和服务器崩溃。 原因很简单-PG为参数保留了额外的内存,并且集合中的记录数仅受所应用的愿望清单业务逻辑的限制。 在特别是临床情况下,人们不得不看到“编号”的论点大于9,000美元 -无需这样做。

我们使用“两级”序列化重写请求:

 INSERT INTO tbl SELECT unnest[1]::text k , unnest[2]::integer v FROM ( SELECT unnest($1::text[])::text[] -- $1 : '{"{a,1}","{b,2}","{c,3}","{d,4}"}' ) T; 

是的,在数组内部为“复杂”值的情况下,它们需要用引号引起来。
显然,您可以用任意数量的字段“扩展”选择范围。

巢,巢,...


定期有传输选项,而不是几个“列数组”的“数组数组”,我在上一篇文章中提到

 SELECT unnest($1::text[]) k , unnest($2::integer[]) v; 

使用这种方法,在生成不同列的值列表时会出错,因此获得完全意外的结果非常简单,这也取决于服务器版本:

 -- $1 : '{a,b,c}', $2 : '{1,2}' -- PostgreSQL 9.4 k | v ----- a | 1 b | 2 c | 1 a | 2 b | 1 c | 2 -- PostgreSQL 11 k | v ----- a | 1 b | 2 c | 

杰森


从9.3版开始,PostgreSQL引入了完善的函数来处理json类型。 因此,如果在浏览器中定义了输入参数,则可以此处为SQL查询创建一个json对象

 SELECT key k , value v FROM json_each($1::json); -- '{"a":1,"b":2,"c":3,"d":4}' 

对于以前的版本, 每个(hstore)都可以使用相同的方法,但是在hstore中转义复杂对象的正确“卷积”会导致问题。

json_populate_recordset


如果预先知道来自“输入” json数组的数据将填充某种表,则可以使用json_populate_recordset函数在“解引用”字段并将其转换为必要的类型方面节省很多:

 SELECT * FROM json_populate_recordset( NULL::pg_class , $1::json -- $1 : '[{"relname":"pg_class","oid":1262},{"relname":"pg_namespace","oid":2615}]' ); 

json_to_recordset


而且此函数只是简单地将转移的对象数组“扩展”到选择中,而无需依赖表格式:
 SELECT * FROM json_to_recordset($1::json) T(k text, v integer); -- $1 : '[{"k":"a","v":1},{"k":"b","v":2}]' k | v ----- a | 1 b | 2 

临时表


但是,如果传输的样本中的数据量非常大,则将其放入一个序列化参数很困难,有时甚至是不可能的,因为它需要一次性分配大量内存 。 例如,您需要长期,长时间地从外部系统收集有关事件的大数据包,然后希望在数据库端对其进行一次处理。

在这种情况下,最好的解决方案是使用临时表

 CREATE TEMPORARY TABLE tbl(k text, v integer); ... INSERT INTO tbl(k, v) VALUES($1, $2); --  -  ... --   -       

该方法适用于罕见的大量数据传输。
从描述其数据结构的角度来看,临时表与pg_class系统表中的“正常”功能只有一个功能不同,而在pg_type,pg_depend,pg_attribute,pg_attrdef等方面则完全没有。

因此,在每个都有大量短期连接的Web系统中,这样的表每次都会生成新的系统记录,并在与数据库的连接关闭时将其删除。 结果, 对TEMP TABLE不受控制的使用导致pg_catalog中的表“膨胀”,并减慢了使用它们的许多操作。
当然,这可以借助VACUUM FULL定期遍历系统目录表来解决。

会话变量


假设对于单个SQL查询,前一种情况下的数据处理非常复杂,但是您要经常这样做。 也就是说,我们希望在DO块中使用过程处理,但是通过临时表使用数据传输将太昂贵。

我们也将无法使用$ n参数来转移到匿名块。 会话变量和current_setting函数将帮助我们摆脱这种情况。

在9.2版之前,有必要为“您的”会话变量预先配置custom_variable_classes 命名空间 。 在当前版本中,您可以编写如下内容:

 SET my.val = '{1,2,3}'; DO $$ DECLARE id integer; BEGIN FOR id IN (SELECT unnest(current_setting('my.val')::integer[])) LOOP RAISE NOTICE 'id : %', id; END LOOP; END; $$ LANGUAGE plpgsql; -- NOTICE: id : 1 -- NOTICE: id : 2 -- NOTICE: id : 3 

其他受支持的过程语言可以找到其他解决方案。

你知道更多方法吗? 分享评论!

Source: https://habr.com/ru/post/zh-CN481122/


All Articles