【问题标题】:Create PostgreSQL ROLE (user) if it doesn't exist如果不存在,则创建 PostgreSQL ROLE(用户)
【发布时间】:2026-02-13 19:00:01
【问题描述】:

如何编写一个 SQL 脚本在 PostgreSQL 9.1 中创建一个 ROLE,但如果它已经存在却不会引发错误?

当前脚本只有:

CREATE ROLE my_user LOGIN PASSWORD 'my_password';

如果用户已经存在,这将失败。我想要类似的东西:

IF NOT EXISTS (SELECT * FROM pg_user WHERE username = 'my_user')
BEGIN
    CREATE ROLE my_user LOGIN PASSWORD 'my_password';
END;

...但这不起作用 - IF 在纯 SQL 中似乎不受支持。

我有一个创建 PostgreSQL 9.1 数据库、角色和其他一些东西的批处理文件。它调用 psql.exe,传入要运行的 SQL 脚本的名称。到目前为止,所有这些脚本都是纯 SQL,如果可能的话,我想避免使用 PL/pgSQL 等。

【问题讨论】:

    标签: sql postgresql roles dynamic-sql


    【解决方案1】:

    您可以通过解析以下输出在您的批处理文件中执行此操作:

    SELECT * FROM pg_user WHERE usename = 'my_user'
    

    如果角色不存在,则再次运行psql.exe

    【讨论】:

    • “用户名”列不存在。它应该是“用户名”。
    • "usename" 是不存在的。 :)
    • 请参考pg_user查看文档。 7.4-9.6版本没有“用户名”列,“用户名”是正确的。
    【解决方案2】:

    在 9.x 上,您可以将其包装到 DO 语句中:

    do 
    $body$
    declare 
      num_users integer;
    begin
       SELECT count(*) 
         into num_users
       FROM pg_user
       WHERE usename = 'my_user';
    
       IF num_users = 0 THEN
          CREATE ROLE my_user LOGIN PASSWORD 'my_password';
       END IF;
    end
    $body$
    ;
    

    【讨论】:

    • Select 应该是 `SELECT count(*) into num_users FROM pg_roles WHERE rolname = 'data_rw';` 否则不会工作
    【解决方案3】:

    按照您的想法进行简化:

    DO
    $do$
    BEGIN
       IF NOT EXISTS (
          SELECT FROM pg_catalog.pg_roles  -- SELECT list can be empty for this
          WHERE  rolname = 'my_user') THEN
    
          CREATE ROLE my_user LOGIN PASSWORD 'my_password';
       END IF;
    END
    $do$;
    

    (以@a_horse_with_no_name 的回答为基础,并通过@Gregory's comment 进行了改进。)

    例如,与 CREATE TABLE 不同,CREATE ROLE 没有 IF NOT EXISTS 子句(至少到第 12 页)。而且您不能在纯 SQL 中执行动态 DDL 语句。

    除非使用另一个 PL,否则您“避免 PL/pgSQL”的请求是不可能的。 DO statement 使用 plpgsql 作为默认程序语言。语法允许省略显式声明:

    DO [ LANGUAGE lang_name ] code
    ...
    lang_name
    编写代码的过程语言的名称。如果 省略,默认为plpgsql

    【讨论】:

    • @Alberto: pg_userpg_roles 都是正确的。当前版本 9.3 中的情况仍然如此,并且不会很快改变。
    • @Ken:如果$在你的客户端有特殊含义,你需要根据你客户端的语法规则进行转义。尝试在 Linux shell 中使用 \$ 转义 $。或者开始一个新问题 - cmets 不是这个地方。您可以随时链接到这个以获取上下文。
    • 我使用的是 9.6,如果用户是使用 NOLOGIN 创建的,他们不会出现在 pg_user 表中,但会出现在 pg_roles 表中。 pg_roles 在这里会是更好的解决方案吗?
    • @ErwinBrandstetter 这不适用于具有 NOLOGIN 的角色。它们出现在 pg_roles 中,但不在 pg_user 中。
    • 此解决方案存在竞争条件。更安全的变体是documented in this answer
    【解决方案4】:

    或者如果角色不是任何可以使用的数据库对象的所有者:

    DROP ROLE IF EXISTS my_user;
    CREATE ROLE my_user LOGIN PASSWORD 'my_password';
    

    但前提是放弃该用户不会造成任何伤害。

    【讨论】:

      【解决方案5】:

      这是一个使用 plpgsql 的通用解决方案:

      CREATE OR REPLACE FUNCTION create_role_if_not_exists(rolename NAME) RETURNS TEXT AS
      $$
      BEGIN
          IF NOT EXISTS (SELECT * FROM pg_roles WHERE rolname = rolename) THEN
              EXECUTE format('CREATE ROLE %I', rolename);
              RETURN 'CREATE ROLE';
          ELSE
              RETURN format('ROLE ''%I'' ALREADY EXISTS', rolename);
          END IF;
      END;
      $$
      LANGUAGE plpgsql;
      

      用法:

      posgres=# SELECT create_role_if_not_exists('ri');
       create_role_if_not_exists 
      ---------------------------
       CREATE ROLE
      (1 row)
      posgres=# SELECT create_role_if_not_exists('ri');
       create_role_if_not_exists 
      ---------------------------
       ROLE 'ri' ALREADY EXISTS
      (1 row)
      

      【讨论】:

        【解决方案6】:

        我的团队遇到了在一台服务器上存在多个数据库的情况,具体取决于您连接到的数据库,SELECT * FROM pg_catalog.pg_user 没有返回有问题的 ROLE,正如 @erwin-brandstetter 和 @a_horse_with_no_name 所建议的那样。条件块执行,我们点击role "my_user" already exists

        很遗憾,我们不确定确切的条件,但此解决方案可以解决问题:

                DO  
                $body$
                BEGIN
                    CREATE ROLE my_user LOGIN PASSWORD 'my_password';
                EXCEPTION WHEN others THEN
                    RAISE NOTICE 'my_user role exists, not re-creating';
                END
                $body$
        

        可能会更具体地排除其他例外情况。

        【讨论】:

        • pg_user 表似乎只包含具有 LOGIN 的角色。如果一个角色有 NOLOGIN,它不会出现在 pg_user 中,至少在 PostgreSQL 10 中是这样。
        【解决方案7】:

        Bash 替代方案(用于Bash 脚本):

        psql -h localhost -U postgres -tc \
        "SELECT 1 FROM pg_user WHERE usename = 'my_user'" \
        | grep -q 1 \
        || psql -h localhost -U postgres \
        -c "CREATE ROLE my_user LOGIN PASSWORD 'my_password';"
        

        (不是问题的答案!仅适用于可能有用的人)

        【讨论】:

        • 它应该是 FROM pg_roles WHERE rolname 而不是 FROM pg_user WHERE usename
        • 请注意,这不仅会受到竞争条件的影响,还会向数据库添加完整的往返。
        【解决方案8】:

        建议使用模式的一些答案:检查角色是否不存在,如果不存在则发出CREATE ROLE 命令。这有一个缺点:竞争条件。如果其他人在检查和发出CREATE ROLE 命令之间创建了一个新角色,那么CREATE ROLE 显然会失败并出现致命错误。

        为了解决上述问题,更多其他答案已经提到了PL/pgSQL 的用法,无条件发出CREATE ROLE,然后从该调用中捕获异常。这些解决方案只有一个问题。他们默默地删除任何错误,包括那些不是由角色已经存在的事实产生的错误。 CREATE ROLE 也可以抛出其他错误,模拟 IF NOT EXISTS 应该只在角色已经存在时静默错误。

        CREATE ROLE 在角色已经存在时抛出duplicate_object 错误。异常处理程序应该只捕获这一个错误。正如其他答案所提到的,将致命错误转换为简单通知是个好主意。其他 PostgreSQL IF NOT EXISTS 命令将 , skipping 添加到他们的消息中,所以为了保持一致性,我也在此处添加它。

        这里是模拟CREATE ROLE IF NOT EXISTS 的完整 SQL 代码,带有正确的异常和 sqlstate 传播:

        DO $$
        BEGIN
        CREATE ROLE test;
        EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
        END
        $$;
        

        测试输出(通过 DO 调用两次,然后直接调用):

        $ sudo -u postgres psql
        psql (9.6.12)
        Type "help" for help.
        
        postgres=# \set ON_ERROR_STOP on
        postgres=# \set VERBOSITY verbose
        postgres=# 
        postgres=# DO $$
        postgres$# BEGIN
        postgres$# CREATE ROLE test;
        postgres$# EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
        postgres$# END
        postgres$# $$;
        DO
        postgres=# 
        postgres=# DO $$
        postgres$# BEGIN
        postgres$# CREATE ROLE test;
        postgres$# EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
        postgres$# END
        postgres$# $$;
        NOTICE:  42710: role "test" already exists, skipping
        LOCATION:  exec_stmt_raise, pl_exec.c:3165
        DO
        postgres=# 
        postgres=# CREATE ROLE test;
        ERROR:  42710: role "test" already exists
        LOCATION:  CreateRole, user.c:337
        

        【讨论】:

        • 谢谢。没有竞争条件,严格的异常捕获,包装 Postgres 自己的消息而不是重写你自己的。
        • 确实!这是目前这里唯一正确的答案,它不受竞争条件的影响,并使用必要的选择性错误处理。很遗憾,这个答案出现在(不完全正确的)最佳答案获得超过 100 分之后。
        • 不客气!我的解决方案还传播 SQLSTATE,因此如果您使用 SQL 连接器从其他 PL/SQL 脚本或其他语言调用语句,您将收到正确的 SQLSTATE。
        • 这很棒。让我们希望它尽快投票到顶部!我编辑了自己的答案,以参考您的答案以加快流程。
        【解决方案9】:

        Simulate CREATE DATABASE IF NOT EXISTS for PostgreSQL? 相同的解决方案应该可以工作 - 将CREATE USER … 发送到\gexec

        psql 中的解决方法

        SELECT 'CREATE USER my_user'
        WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec
        

        shell 的解决方法

        echo "SELECT 'CREATE USER my_user' WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec" | psql
        

        更多详情请见accepted answer there

        【讨论】:

        【解决方案10】:

        如果您有权访问 shell,则可以这样做。

        psql -tc "SELECT 1 FROM pg_user WHERE usename = 'some_use'" | grep -q 1 || psql -c "CREATE USER some_user"
        

        对于那些想要解释的人:

        -c = run command in database session, command is given in string
        -t = skip header and footer
        -q = silent mode for grep 
        || = logical OR, if grep fails to find match run the subsequent command
        

        【讨论】:

          【解决方案11】:

          基于此处的其他答案,我希望能够对 .sql 文件执行一次 psql 以使其执行一组初始化操作。我还希望能够在执行时注入密码以支持 CI/CD 场景。

          -- init.sql
          
          CREATE OR REPLACE FUNCTION pg_temp.create_myuser(theUsername text, thePassword text)
          RETURNS void AS
          $BODY$
          DECLARE
            duplicate_object_message text;
          BEGIN
            BEGIN
              EXECUTE format(
                'CREATE USER %I WITH PASSWORD %L',
                theUsername,
                thePassword
              );
            EXCEPTION WHEN duplicate_object THEN
              GET STACKED DIAGNOSTICS duplicate_object_message = MESSAGE_TEXT;
              RAISE NOTICE '%, skipping', duplicate_object_message;
            END;
          END;
          $BODY$
          LANGUAGE 'plpgsql';
          
          SELECT pg_temp.create_myuser(:'vUsername', :'vPassword');
          

          使用psql 调用:

          NEW_USERNAME="my_new_user"
          NEW_PASSWORD="password with 'special' characters"
          
          psql --no-psqlrc --single-transaction --pset=pager=off \
            --tuples-only \
            --set=ON_ERROR_STOP=1 \
            --set=vUsername="$NEW_USERNAME" \
            --set=vPassword="$NEW_PASSWORD" \
            -f init.sql
          

          这将允许 init.sql 在本地或通过 CI/CD 管道运行。


          注意事项:

          • 我没有找到直接在 DO 匿名函数中引用文件变量 (:vPassword) 的方法,因此使用完整的 FUNCTION 来传递 arg。 (see @Clodoaldo Neto's answer)
          • @Erwin Brandstetter's answer 解释了为什么我们必须使用 EXECUTE 而不能直接使用 CREATE USER
          • @Pali's answer 解释了 EXCEPTION 防止竞争条件的必要性(这就是不推荐使用 \gexec 方法的原因)。
          • 该函数必须在SELECT 语句中调用。使用psql 命令中的-t/--tuples-only 属性来清理日志输出,正如@villy393's answer 中所指出的那样。
          • 该函数是在临时架构中创建的,因此会自动删除。
          • 引号处理得当,因此密码中的特殊字符不会导致错误或更严重的安全漏洞。

          【讨论】:

            最近更新 更多