在 PL/pgSQL 函数中使用变量

Using variables in a PL/pgSQL function

Postgres PL/pgSQL docs say:

For any SQL command that does not return rows, for example INSERT without a RETURNING clause, you can execute the command within a PL/pgSQL function just by writing the command.

Any PL/pgSQL variable name appearing in the command text is treated as a parameter, and then the current value of the variable is provided as the parameter value at run time.

但是当我在查询中使用变量名时出现错误:

ERROR:  syntax error at or near "email"
LINE 16: ...d,email,password) values(identity_id,current_ts,''email'',''...

这是我的功能:

CREATE OR REPLACE FUNCTION app.create_identity(email varchar,passwd varchar)
RETURNS integer as $$
DECLARE
    current_ts          integer;
    new_identity_id     integer;
    int_max             integer;
    int_min             integer;
BEGIN
    SELECT extract(epoch FROM now())::integer INTO current_ts;
    int_min:=-2147483648;
    int_max:= 2147483647;
    LOOP
        BEGIN
            SELECT floor(int_min + (int_max - int_min + 1) * random()) INTO new_identity_id;
            IF new_identity_id != 0 THEN
                INSERT into app.identity(identity_id,date_inserted,email,password) values(identity_id,current_ts,''email'',''passwd'');
                RETURN new_identity_id;
            END IF;
        EXCEPTION
            WHEN unique_violation THEN
        END;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

为什么当我在查询中使用变量时,Postgres 会抛出错误。这应该怎么写?

您不能将参数名称放在单引号中 (''email'' 并且您不能使用参数 email "as is" 因为它与table。这种名称冲突是强烈建议 不要 使用与 [=55= 之一中的列同名的变量或参数的原因之一]s. 你有三种选择来处理这个问题:

  1. 重命名变量。常见的命名约定是在参数前加上 p_ 前缀,例如p_email,然后在 insert

    中使用明确的名称
    INSERT into app.identity(identity_id,date_inserted,email,password) 
    values(identity_id,current_ts,p_email,p_password);
    
  2. 第一个参数使用</code>,第二个参数使用<code>

    INSERT into app.identity(identity_id,date_inserted,email,password) 
    values(identity_id,current_ts,,);
    
  3. 在参数名前加上函数名:

    INSERT into app.identity(identity_id,date_inserted,email,password) 
    values(identity_id,current_ts,create_identity.email,create_identity.password);
    

我强烈建议选择选项 1


无关,但是:如果您不从 table 中检索这些值,则不需要 SELECT 语句来分配变量值。

SELECT extract(epoch FROM now())::integer INTO current_ts;

可以简化为:

current_ts := extract(epoch FROM now())::integer;

SELECT floor(int_min + (int_max - int_min + 1) * random()) INTO new_identity_id;

new_identity_id := floor(int_min + (int_max - int_min + 1) * random());

您的实际问题并澄清引用问题和命名冲突。

关于引用:

  • Insert text with single quotes in PostgreSQL

关于命名冲突(plpgsql 的行为随时间略有变化):

  • Set a default return value for a Postgres function

更好的解决方案

我建议开始使用完全不同的方法:

CREATE OR REPLACE FUNCTION app.create_identity(_email text, _passwd text
                                             , OUT new_identity_id int) AS
$func$
DECLARE
   _current_ts int := extract(epoch FROM now());
BEGIN
   LOOP
      --+ Generate compeltely random int4 numbers +-----------------------------
      -- integer (= int4) in Postgres is a signed integer occupying 4 bytes   --
      -- int4 ranges from -2147483648 to +2147483647, i.e. -2^31 to 2^31 - 1  --
      -- Multiply bigint 4294967296 (= 2^32) with random() (0.0 <= x < 1.0)   --
      --   trunc() the resulting (positive!) float8 - cheaper than floor()    -- 
      --   add result to -2147483648 and cast the next result back to int4    --
      -- The result fits the int4 range *exactly*                             --
      --------------------------------------------------------------------------
      INSERT INTO app.identity
            (identity_id, date_inserted,  email ,  password)
      SELECT _random_int, _current_ts  , _email , _passwd
      FROM  (SELECT (bigint '-2147483648'       -- could be int, but sum is bigint anyway
                   + bigint '4294967296' * random())::int) AS t(_random_int)  -- random int
      WHERE  _random_int <> 0                   -- exclude 0 (no insert)
      ON     CONFLICT (identity_id) DO NOTHING  -- no exception raised!
      RETURNING identity_id                     -- return *actually* inserted identity_id
      INTO   new_identity_id;                   -- OUT parameter, returned at end

      EXIT WHEN FOUND;                          -- exit after success
      -- maybe add counter and raise exception when exceeding n (100?) iterations
   END LOOP;
END
$func$  LANGUAGE plpgsql;

要点

  • 您的随机整数计算将导致 integer out of range 错误,因为中间项 int_max - int_min + 1 与 [=13= 运算],但结果不合适。 我建议使用上面更便宜的正确算法。

  • 进入带有例外条款的区块比没有例外条款要昂贵得多。幸运的是,您实际上不需要一开始就引发异常。使用 UPSERT (INSERT ... ON CONFLICT ... DO NOTHING),以廉价而优雅的方式解决这个问题(Postgres 9.5+)。
    The manual:

    Tip: A block containing an EXCEPTION clause is significantly more expensive to enter and exit than a block without one. Therefore, don't use EXCEPTION without need.

  • 您也不需要额外的 IF 结构。使用 SELECTWHERE.

  • 使 new_identity_id 成为 OUT 参数以简化。

  • 使用RETURNING子句并将结果identity_id直接插入到OUT参数中。除了更简单的代码和更快的执行之外,还有一个额外的、微妙的好处:您获得 实际 插入的值。如果 table 上有触发器或规则,这可能与您使用 INSERT 发送的内容不同。

  • PL/pgSQL 中的分配相对昂贵。将这些减少到最低限度以实现高效代码。
    您也可以删除最后一个剩余的变量 _current_ts ,并在子查询中进行计算,那么您根本不需要 DECLARE 。我留下了那个,因为计算它 一次 可能有意义,如果函数循环多次...

  • 剩下的就是一个SQL命令,包装成LOOP重试直到成功。

  • 如果您的 table 有可能溢出(使用全部或大部分 int4 数字)- 严格来说,总是 机会 - 我会添加一个计数器并在大约 100 次迭代后引发异常以避免无限循环。