【问题标题】:Test for null in function with varying parameters测试具有不同参数的函数中的空值
【发布时间】:2013-06-25 13:29:37
【问题描述】:

我有一个 Postgres 函数:

create function myfunction(integer, text, text, text, text, text, text) RETURNS 
table(id int, match text, score int, nr int, nr_extra character varying, info character varying, postcode character varying,
street character varying, place character varying, country character varying, the_geom geometry)
AS $$
BEGIN

return query (select a.id, 'address' as match, 1 as score, a.ad_nr, a.ad_nr_extra,a.ad_info,a.ad_postcode, s.name as street, p.name place , c.name country, a.wkb_geometry as wkb_geometry from "Addresses" a 
    left join "Streets" s on a.street_id = s.id 
        left join "Places" p on s.place_id = p.id 
            left join "Countries" c on p.country_id = c.id 
            where c.name = $7 
                and p.name = $6
                    and s.name = $5
                    and a.ad_nr = $1 
                    and a.ad_nr_extra = $2
                    and a.ad_info = $3
                    and ad_postcode = $4);
END;
$$
LANGUAGE plpgsql;

当输入的一个或多个变量为 NULL 时,此函数无法给出正确的结果,因为 ad_postcode = NULL 将失败。

如何在查询中测试 NULL?

【问题讨论】:

  • 您可以使用函数参数名称代替$1..$9。类似于myfunction(v_ad_nr integer, v_ad_nr_extra text, ...... a.ad_nr = v_ad_nr ...。它使代码更具可读性和更易于修改。
  • 并阅读一些关于 SQL 查询格式化的文章。目前这很可怕。或者,至少,使用自动查询美化器。
  • 感谢您提示我使用参数名称,感谢。

标签: postgresql null postgresql-9.1 plpgsql dynamic-sql


【解决方案1】:

你可以使用

c.name IS NOT DISTINCT FROM $7

如果c.name$7 相等或两者都是null,它将返回true

或者你可以使用

(c.name = $7 or $7 is null )

如果c.name$7 相等或$7 为空,它将返回true

【讨论】:

    【解决方案2】:

    如果你可以修改查询,你可以做类似的事情

    and (ad_postcode = $4 OR $4 IS NULL)
    

    【讨论】:

    • 谢谢,尽管这在之前的回答中也有说明,但我非常感谢您的努力!
    【解决方案3】:

    几件事...

    首先,作为旁注:您的查询的语义可能需要重新审视。 where 子句中的某些内容实际上可能属于您的 join 子句,例如:

    from ...
    left join ... on ... and ...
    left join ... on ... and ...
    

    如果他们不这样做,您最应该使用inner join,而不是left join

    其次,有一个is not distinct from 运算符,它偶尔可以方便地代替=a is not distinct from b 基本等同于a = b or a is null and b is null

    但是请注意,is not distinct from 确实使用索引,而 =is null 实际上使用索引。您可以在您的特定情况下使用(field = $i or $i is null),如果您使用的是最新版本的 Postgres,它将产生最佳计划:

    https://gist.github.com/ddebernardy/5884267

    【讨论】:

    • 我不得不不同意“最好的选择”,并提供了一个答案来支持它。
    • 哇,自从我上次需要它以来,Postgres 在这方面做了一些重大改进。我添加了一个要点,其中包含具有两个不同参数的相同预准备语句的示例。 9.2 似乎根据适用的谓词准备了几个计划。很酷的东西。
    • 这是 9.2 的重大更新。 Some details in the release notes. BTW,如果您不@-回复,我不会收到您的回答通知,因为我们正在评论 您的 答案。偶然看到这个。
    • 感谢您的回答和cmets。对更好地组织我的代码的提示表示赞赏。
    【解决方案4】:

    我不同意其他答案中的一些建议。这可以通过 PL/pgSQL 来完成,我认为它主要远远优于在客户端应用程序中组装查询。它更快、更干净,并且该应用程序仅在请求中通过网络发送最低限度的请求。 SQL 语句保存在数据库中,这样更易​​于维护 - 除非您想在客户端应用程序中收集所有业务逻辑,否则这取决于一般架构。

    带有动态 SQL 的 PL/pgSQL 函数

    CREATE OR REPLACE FUNCTION func(
          _ad_nr       int  = NULL
        , _ad_nr_extra text = NULL
        , _ad_info     text = NULL
        , _ad_postcode text = NULL
        , _sname       text = NULL
        , _pname       text = NULL
        , _cname       text = NULL)
      RETURNS TABLE(id int, match text, score int, nr int, nr_extra text
                  , info text, postcode text, street text, place text
                  , country text, the_geom geometry)
      LANGUAGE plpgsql AS
    $func$
    BEGIN
       -- RAISE NOTICE '%', -- for debugging
       RETURN QUERY EXECUTE concat(
       $$SELECT a.id, 'address'::text, 1 AS score, a.ad_nr, a.ad_nr_extra
            , a.ad_info, a.ad_postcode$$
    
       , CASE WHEN (_sname, _pname, _cname) IS NULL THEN ', NULL::text' ELSE ', s.name' END  -- street
       , CASE WHEN (_pname, _cname) IS NULL         THEN ', NULL::text' ELSE ', p.name' END  -- place
       , CASE WHEN _cname IS NULL                   THEN ', NULL::text' ELSE ', c.name' END  -- country
       , ', a.wkb_geometry'
    
       , concat_ws('
       JOIN   '
       , '
       FROM   "Addresses" a'
       , CASE WHEN NOT (_sname, _pname, _cname) IS NULL THEN '"Streets"   s ON s.id = a.street_id' END
       , CASE WHEN NOT (_pname, _cname) IS NULL         THEN '"Places"    p ON p.id = s.place_id' END
       , CASE WHEN _cname IS NOT NULL                   THEN '"Countries" c ON c.id = p.country_id' END
       )
    
       , concat_ws('
       AND    '
          , '
       WHERE  TRUE'
          , CASE WHEN $1 IS NOT NULL THEN 'a.ad_nr = $1' END
          , CASE WHEN $2 IS NOT NULL THEN 'a.ad_nr_extra = $2' END
          , CASE WHEN $3 IS NOT NULL THEN 'a.ad_info = $3' END
          , CASE WHEN $4 IS NOT NULL THEN 'a.ad_postcode = $4' END
          , CASE WHEN $5 IS NOT NULL THEN 's.name = $5' END
          , CASE WHEN $6 IS NOT NULL THEN 'p.name = $6' END
          , CASE WHEN $7 IS NOT NULL THEN 'c.name = $7' END
       )
       )
       USING $1, $2, $3, $4, $5, $6, $7;
    END
    $func$;
    

    呼叫:

    SELECT * FROM func(1, '_ad_nr_extra', '_ad_info', '_ad_postcode', '_sname');
    
    SELECT * FROM func(1, _pname := 'foo');
    

    由于所有函数参数都有默认值,您可以使用位置表示法、命名表示法或混合符号在你的函数调用中选择。见:

    动态SQL基础的更多解释:

    concat() 函数有助于构建字符串。它是在 Postgres 9.1 中引入的。

    CASE 语句的 ELSE 分支在不存在时默认为 NULL。简化代码。

    EXECUTEUSING 子句使得 SQL 注入成为不可能,因为值作为 传递,并允许直接使用参数值,就像在准备好的语句中一样。

    NULL 值用于忽略此处的参数。它们实际上并不用于搜索。

    SELECTRETURN QUERY 之间不需要括号。

    简单的 SQL 函数

    可以使用普通的 SQL 函数来完成,避免使用动态 SQL。在某些情况下,这可能会更快。但我不希望在 这种情况下。在没有不必要的连接和谓词的情况下规划查询通常会产生最佳结果。像这样的简单查询的计划成本几乎可以忽略不计。

    CREATE OR REPLACE FUNCTION func_sql(
         _ad_nr       int  = NULL
       , _ad_nr_extra text = NULL
       , _ad_info     text = NULL
       , _ad_postcode text = NULL
       , _sname       text = NULL
       , _pname       text = NULL
       , _cname       text = NULL)
      RETURNS TABLE(id int, match text, score int, nr int, nr_extra text
                  , info text, postcode text, street text, place text
                  , country text, the_geom geometry)
      LANGUAGE sql AS 
    $func$
    SELECT a.id, 'address' AS match, 1 AS score, a.ad_nr, a.ad_nr_extra
         , a.ad_info, a.ad_postcode
         , s.name AS street, p.name AS place
         , c.name AS country, a.wkb_geometry
    FROM   "Addresses"      a
    LEFT   JOIN "Streets"   s ON s.id = a.street_id
    LEFT   JOIN "Places"    p ON p.id = s.place_id
    LEFT   JOIN "Countries" c ON c.id = p.country_id
    WHERE ($1 IS NULL OR a.ad_nr = $1)
    AND   ($2 IS NULL OR a.ad_nr_extra = $2)
    AND   ($3 IS NULL OR a.ad_info = $3)
    AND   ($4 IS NULL OR a.ad_postcode = $4)
    AND   ($5 IS NULL OR s.name = $5)
    AND   ($6 IS NULL OR p.name = $6)
    AND   ($7 IS NULL OR c.name = $7)
    $func$;
    

    相同的调用。

    要有效地忽略具有NULL 值的参数

    ($1 IS NULL OR a.ad_nr = $1)
    

    要实际使用 NULL 值作为参数,请改用以下构造:

    ($1 IS NULL AND a.ad_nr IS NULL OR a.ad_nr = $1)  -- AND binds before OR
    

    这也允许使用索引
    对于手头的情况,将LEFT JOIN 的所有实例替换为JOIN

    dbfiddle here - simple demo 适用于所有变体。
    sqlfiddle

    旁白

    • 不要使用nameid 作为列名。它们不是描述性的,当你加入一堆表时(就像你在关系数据库中对 a lot 所做的那样),你最终会得到几个名为 nameid 的列,并且必须附加别名才能对乱七八糟的。

    • 请至少在提出公开问题时正确格式化您的 SQL。但也可以私下做,为了你自己好。

    【讨论】:

    • "$3 is null" 给我“查询失败:错误:42P08:无法确定参数 $3 的数据类型”。 $3 是一个有 4 个字符的字符串。
    • 参数的类型是从上下文中推断出来的,所以 ($1 IS NULL AND a.ad_nr IS NULL OR a.ad_nr = $1) 对我不起作用(Postgresql 不知道类型1 美元),但 (a.ad_nr = $1 OR $1 IS NULL AND a.ad_nr IS NULL) 有效。
    • @Rodrigo:你测试了哪个函数?我无法复制您评论的内容。所有参数的数据类型都是预定义的。你在 Postgres 上的版本是什么?你的确切电话是什么?我在使用 Postgres 9.1 发布之前对此进行了测试。刚刚使用 pg 9.3 和 9.4 再次测试了所有变体,发现它们工作正常。我在答案中添加了一个小提琴演示:sqlfiddle.com/#!15/58e6c/2
    • "所有参数的数据类型都是预定义的",其实不是必须的(9.4或9.3)。看起来如果我们开始比较 $1 和 NULL,SQL 就没有上下文可以学习。但是,如果我们开始将已知列与 $1 进行比较,那么它会“学习”$1 的类型。不指定类型的好处只是实现速度的一小部分增益。到目前为止,它似乎有效。
    • @ErwinBrandstetter 很抱歉提出这个非常古老的话题,但是,阅读 Denis 的评论和下面的要点 - 在版本 >= 9.2(在我的情况下为 10.4)是否仍然首选动态方法,或者简单的 SQL(多个 $1 IS NULL 或 col=$1)在每个参数组合的计划意义上会以相同的方式执行?仅影响 where 而不影响连接的参数呢? p.s 开始处理 postgres,您对 SO 的回答非常宝贵!赞一个!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-11-10
    • 2021-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-09
    • 1970-01-01
    相关资源
    最近更新 更多