函数使用两次时的 PL/pgSQL 问题(缓存问题?)

Eri*_* Ly 6 postgresql plpgsql functions plan-cache postgresql-9.4

我面临一个绝对奇怪的问题,感觉更像是 Postgres 错误而不是算法问题。

我有这个功能:

CREATE FUNCTION sp_connect(mail character varying, passwd character varying, role character varying)
  RETURNS json LANGUAGE plpgsql STABLE AS
$$
DECLARE
    user_info record;
BEGIN
  IF role = 'Role1' THEN
    SELECT u.id, r.name INTO user_info
    FROM users u
    INNER JOIN users_roles ur ON ur.user_id = u.id
    INNER JOIN roles r ON ur.role_id = r.id
    WHERE u.email = mail
      AND u.password = encode(digest(CONCAT(passwd, u.password_salt), 'sha512'), 'hex')
      AND r.name = 'Role1';
  ELSIF role = 'Role2' THEN
    SELECT h.id, 'Role1' AS name INTO user_info
    FROM history h
    WHERE h.email = mail
      AND h.password = encode(digest(CONCAT(passwd, h.password_salt), 'sha512'), 'hex');
  ELSE
    RAISE 'USER_NOT_FOUND';
  END IF;

  IF NOT FOUND THEN
      RAISE 'USER_NOT_FOUND';
  ELSE
      RETURN row_to_json(row) FROM (SELECT user_info.id AS id, user_info.name AS role) row;
  END IF;
END;
$$;
Run Code Online (Sandbox Code Playgroud)

我面临的问题是,当我使用此功能以 Role1-user 登录时,当我将它与 Role2-user 一起使用时,我收到此错误消息:

type of parameter 7 (character varying) does not match that when preparing the plan (unknown)
Run Code Online (Sandbox Code Playgroud)

这是......好吧,我只是不明白它是从哪里来的。如果您擦除数据库并更改登录顺序(即角色 2 然后角色 1),这一次,角色 1 会收到错误。

奇怪的问题,奇怪的解决方案...如果我只是使用ALTER FUNCTION sp_connect但不修改函数内部的任何内容,那么神奇的是,这两个角色可以毫无问题地登录。我也试过这个解决方案:

  IF NOT FOUND THEN
      RAISE 'USER_NOT_FOUND';
  ELSE
      IF role = 'Seeker'
      THEN
          RETURN row_to_json(row) FROM (SELECT user_info.id AS id, user_info.name AS role) row;
      ELSE
          RETURN row_to_json(row) FROM (SELECT user_info.id AS id, user_info.name AS role) row;
  END IF;
Run Code Online (Sandbox Code Playgroud)

并通过添加IFELSE那绝对是无用的,使用相同的RETURN子句,这不会触发任何错误。

我知道 DBA StackExchange 不适合开发人员,但这种问题似乎更像是缓存问题或其他什么。有人可以告诉我 PostgreSQL 函数是否有问题吗?或者我可以从哪里得到这个奇怪问题的帮助?

Erw*_*ter 12

解释:

您声明user_inforecord. 手册:

记录变量类似于行类型变量,但它们没有预定义的结构。它们采用在SELECTorFOR命令期间分配给它们的行的实际行结构。记录变量的子结构可以在每次分配给 时更改

大胆强调我的。

您分配记录user_info,然后rowRETURN语句中从它派生行类型(实际上在您的代码中调用)。这一切都很好,很花哨,直到您在同一会话中的下一次调用中将不同数据类型的列分配给记录。这与准备好的语句的缓存查询计划不兼容并引发异常。

PL/pgSQL 将函数体中的所有 SQL 语句视为准备好的语句。准备语句的查询计划在会话期间缓存,除非任何涉及的对象发生更改(包括函数本身),否则会释放所有依赖的准备语句。这解释了为什么之后的下一次调用ALTER FUNCTION总是有效。更多解释:

解决方案

有多种方法可以解决这个问题。你自己已经找到了一些。只是避免将不同数据类型的参数提供给同一个(准备好的)语句

简单的解决方案是强制转换为相同的数据类型。你没有提供表定义,我认为 users.idhistory.idinteger搭配就好了。从错误信息我想来看roles.namevarchar,所以我投的字符串字面'Role1'varchar也,一切都应该工作。(您实际上可能是说'Role2',但这与问题是正交的。)

在某些类型的 SQL 语句中,无类型字符串文字被强制为匹配的数据类型,其中数据类型可以从上下文中派生。但这里的情况并非如此。如果没有明确的投字符串字面量的类型unknown,这是一样的varchar(或text)。这也显示在您的错误消息中。

CREATE FUNCTION sp_connect(mail varchar, passwd varchar, role varchar)
  RETURNS json AS
$func$
DECLARE
   user_info record;
BEGIN
  IF role = 'Role1' THEN
    SELECT u.id, r.name INTO user_info
    FROM   users u
    JOIN   users_roles ur ON ur.user_id = u.id
    JOIN   roles r ON ur.role_id = r.id
    WHERE  u.email = mail
    AND    u.password = encode(digest(CONCAT(passwd, u.password_salt), 'sha512'), 'hex')
    AND    r.name = 'Role1';

    RAISE NOTICE 'Role1: user_info.big_id: %; user_info.name: %'
                , pg_typeof(user_info.big_id), pg_typeof(user_info.name);  -- see data types
  ELSIF role = 'Role2' THEN
    SELECT h.id, 'Role1'::varchar AS name INTO user_info  -- Cast! And did you mean 'Role2'?
    FROM   history h
    WHERE  h.email = mail
    AND    h.password = encode(digest(CONCAT(passwd, h.password_salt), 'sha512'), 'hex');

    RAISE NOTICE 'Role2: %: user_info.big_id: %; user_info.name: %'
                , pg_typeof(user_info.big_id), pg_typeof(user_info.name);  -- see data types
  END IF;

  IF NOT FOUND THEN
      RAISE 'USER_NOT_FOUND';
  ELSE
      RETURN row_to_json(row) FROM (SELECT user_info.id AS id, user_info.name AS role) row;
  END IF;
END
$func$  LANGUAGE plpgsql STABLE;
Run Code Online (Sandbox Code Playgroud)

函数可以是STABLE,这是正确的波动率。

我还添加RAISE NOTICE了显示数据类型的语句,这应该可以帮助您进行调试。请注意,RAISE语句本身的计划也被缓存,因此单个RAISEafterEND IF会遇到相同的问题。