【问题标题】:Simulate CREATE DATABASE IF NOT EXISTS for PostgreSQL?为 PostgreSQL 模拟 CREATE DATABASE IF NOT EXISTS?
【发布时间】:2013-08-25 16:42:20
【问题描述】:

我想通过 JDBC 创建一个不存在的数据库。与 MySQL 不同,PostgreSQL 不支持 create if not exists 语法。实现这一目标的最佳方法是什么?

应用程序不知道数据库是否存在。它应该检查数据库是否存在,应该使用它。因此,连接到所需的数据库是有意义的,如果由于数据库不存在而导致连接失败,则应创建新数据库(通过连接到默认的 postgres 数据库)。我检查了 Postgres 返回的错误代码,但找不到任何相同的相关代码。

实现此目的的另一种方法是连接到postgres 数据库并检查所需数据库是否存在并采取相应措施。第二个有点繁琐。

有没有办法在 Postgres 中实现这个功能?

【问题讨论】:

    标签: sql database postgresql jdbc ddl


    【解决方案1】:

    限制

    您可以询问系统目录pg_database - 可从同一数据库集群中的任何数据库访问。棘手的部分是CREATE DATABASE 只能作为单个语句执行。 The manual:

    CREATE DATABASE 不能在事务块内执行。

    因此它不能直接在函数或DO 语句中运行,它会隐式在事务块中。 SQL 过程,随 Postgres 11 引入,cannot help with this either

    psql 中的解决方法

    您可以在 psql 中通过有条件地执行 DDL 语句来解决它:

    SELECT 'CREATE DATABASE mydb'
    WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'mydb')\gexec
    

    The manual:

    \gexec

    将当前查询缓冲区发送到服务器,然后将查询输出的每一行的每一列(如果有)视为要执行的 SQL 语句。

    shell 的解决方法

    使用\gexec 你只需要调用psql 一次

    echo "SELECT 'CREATE DATABASE mydb' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'mydb')\gexec" | psql
    

    您的连接可能需要更多 psql 选项;角色、端口、密码……请参阅:

    不能用psql -c "SELECT ...\gexec" 调用相同的方法,因为\gexec 是一个psql 元命令,而-c 选项需要一个命令the manual states:

    command 必须是服务器完全可解析的命令字符串(即,它不包含特定于 psql 的功能),或者是单个反斜杠命令.因此,您不能在 -c 选项中混合使用 SQL 和 psql 元命令。

    Postgres 事务中的解决方法

    您可以使用dblink 连接回到当前数据库,该数据库在事务块之外运行。因此效果也不能回滚。

    为此安装附加模块 dblink(每个数据库一次):

    然后:

    DO
    $do$
    BEGIN
       IF EXISTS (SELECT FROM pg_database WHERE datname = 'mydb') THEN
          RAISE NOTICE 'Database already exists';  -- optional
       ELSE
          PERFORM dblink_exec('dbname=' || current_database()  -- current db
                            , 'CREATE DATABASE mydb');
       END IF;
    END
    $do$;
    

    同样,您可能需要更多的 psql 选项来进行连接。请参阅 Ortwin 添加的答案:

    dblink详解:

    您可以将其设为重复使用的功能。

    【讨论】:

    • 从远程在 AWS RDS Postgres 上创建数据库时遇到了这个问题。 RDS主用户不是超级用户,因此不允许使用dblink_connect
    • 如果您没有超级用户权限,您可以使用密码进行连接。详情:dba.stackexchange.com/a/105186/3684
    • 工作起来就像一个魅力,在 Docker 容器内的 init.sql 脚本中使用。谢谢!
    • 当我从 shell 运行第一个查询时,我不得不删除 \gexec,但它起作用了。
    【解决方案2】:

    另一种选择,以防万一您想要一个 shell 脚本,如果数据库不存在则创建数据库,否则保持原样:

    psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'my_db'" | grep -q 1 || psql -U postgres -c "CREATE DATABASE my_db"
    

    我发现这对 devops 配置脚本很有帮助,您可能希望在同一个实例上运行多次。

    对于那些想要解释的人:

    -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
    

    【讨论】:

    • 它对我不起作用。 c:\Program Files\PostgreSQL\9.6\bin $ psql.exe -U admin -tc "SELECT 1 FROM pg_database WHERE datname = 'my_db'" | grep -q 1 || psql -U admin -c "CREATE DATABASE my_db" 'grep' is not recognized as an internal or external command, operable program or batch file.我做错了什么?
    • 您的路径中没有grep。在 Windows 上,默认情况下不安装 grep。您可以搜索 gnu grep windows 以查找可以在 Windows 上运行的版本。
    • 谢谢@Rod。安装 grep 后,这个脚本对我有用。
    • @AntonAnikeev:可以通过没有 grep 的单个 psql 调用来完成。我为我的答案添加了解决方案。
    • 我发现首先我们 pg_isready 检查连接是否可能很有用;如果连接不可用(错误的主机名、网络故障等),脚本将尝试创建数据库并失败并可能出现令人困惑的错误消息
    【解决方案3】:

    如果你不关心数据,你可以先删除数据库,然后重新创建:

    DROP DATABASE IF EXISTS dbname;
    CREATE DATABASE dbname;
    

    【讨论】:

    • 非常优雅的解决方案。如果您确实关心数据,请不要忘记先备份数据库。尽管这是我的首选解决方案,但对于测试情况。
    【解决方案4】:

    PostgreSQL 不支持 IF NOT EXISTS for CREATE DATABASE 语句。仅在 CREATE SCHEMA 中受支持。此外,CREATE DATABASE 不能在事务中发出,因此它不能在 DO 块中捕获异常。

    当发出CREATE SCHEMA IF NOT EXISTS 并且架构已经存在时,会引发带有重复对象信息的通知(不是错误)。

    要解决这些问题,您需要使用dblink 扩展,它会打开与数据库服务器的新连接并在不进入事务的情况下执行查询。您可以通过提供空字符串重用连接参数。

    下面是PL/pgSQL 代码,它完全模拟CREATE DATABASE IF NOT EXISTS,其行为与CREATE SCHEMA IF NOT EXISTS 相同。它通过dblink 调用CREATE DATABASE,捕获duplicate_database 异常(当数据库已经存在时发出)并通过传播errcode 将其转换为通知。字符串消息附加了, skipping,其方式与CREATE SCHEMA IF NOT EXISTS相同。

    CREATE EXTENSION IF NOT EXISTS dblink;
    
    DO $$
    BEGIN
    PERFORM dblink_exec('', 'CREATE DATABASE testdb');
    EXCEPTION WHEN duplicate_database THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
    END
    $$;
    

    此解决方案没有像其他答案那样的任何竞争条件,其中数据库可以由外部进程(或相同脚本的其他实例)在检查数据库是否存在和自己创建之间创建。

    此外,当CREATE DATABASE 因数据库已存在以外的其他错误而失败时,此错误将作为错误传播,而不是静默丢弃。只有duplicate_database 错误的捕获。所以它确实表现得像IF NOT EXISTS 应该的那样。

    您可以将此代码放入自己的函数中,直接调用或从事务中调用。只是回滚(恢复删除的数据库)是行不通的。

    测试输出(通过 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=# CREATE EXTENSION IF NOT EXISTS dblink;
    CREATE EXTENSION
    postgres=# DO $$
    postgres$# BEGIN
    postgres$# PERFORM dblink_exec('', 'CREATE DATABASE testdb');
    postgres$# EXCEPTION WHEN duplicate_database THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
    postgres$# END
    postgres$# $$;
    DO
    postgres=# 
    postgres=# CREATE EXTENSION IF NOT EXISTS dblink;
    NOTICE:  42710: extension "dblink" already exists, skipping
    LOCATION:  CreateExtension, extension.c:1539
    CREATE EXTENSION
    postgres=# DO $$
    postgres$# BEGIN
    postgres$# PERFORM dblink_exec('', 'CREATE DATABASE testdb');
    postgres$# EXCEPTION WHEN duplicate_database THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
    postgres$# END
    postgres$# $$;
    NOTICE:  42P04: database "testdb" already exists, skipping
    LOCATION:  exec_stmt_raise, pl_exec.c:3165
    DO
    postgres=# 
    postgres=# CREATE DATABASE testdb;
    ERROR:  42P04: database "testdb" already exists
    LOCATION:  createdb, dbcommands.c:467
    

    【讨论】:

    • 这是目前这里唯一正确的答案,它不受竞争条件的影响,并使用必要的选择性错误处理。很遗憾,这个答案出现在(不完全正确的)最佳答案获得超过 70 分之后。
    • 好吧,其他答案对于处理所有可能发生的极端情况并不那么精确。你也可以多次并行调用我的 PL/pgSQL 代码,它不会失败。
    • 即使这个答案也可能会受到竞争条件的影响,如此处所述。 stackoverflow.com/a/63106063/1866530
    【解决方案5】:

    我不得不使用@Erwin Brandstetter 使用的稍微扩展的版本:

    DO
    $do$
    DECLARE
      _db TEXT := 'some_db';
      _user TEXT := 'postgres_user';
      _password TEXT := 'password';
    BEGIN
      CREATE EXTENSION IF NOT EXISTS dblink; -- enable extension 
      IF EXISTS (SELECT 1 FROM pg_database WHERE datname = _db) THEN
        RAISE NOTICE 'Database already exists';
      ELSE
        PERFORM dblink_connect('host=localhost user=' || _user || ' password=' || _password || ' dbname=' || current_database());
        PERFORM dblink_exec('CREATE DATABASE ' || _db);
      END IF;
    END
    $do$
    

    我必须启用dblink 扩展,另外我必须提供 dblink 的凭据。 适用于 Postgres 9.4。

    【讨论】:

      【解决方案6】:

      如果你会用shell,试试

      psql -U postgres -c 'select 1' -d $DB &>dev/null || psql -U postgres -tc 'create database $DB'
      

      我觉得psql -U postgres -c "select 1" -d $DBSELECT 1 FROM pg_database WHERE datname = 'my_db'简单,只需要一种引用,更容易和sh -c结合。

      我在我的 ansible 任务中使用它

      - name: create service database
        shell: docker exec postgres sh -c '{ psql -U postgres -tc "SELECT 1" -d {{service_name}} &> /dev/null && echo -n 1; } || { psql -U postgres -c "CREATE DATABASE {{service_name}}"}'
        register: shell_result
        changed_when: "shell_result.stdout != '1'"
      

      【讨论】:

        【解决方案7】:

        在阅读完所有这些我认为复杂的解决方案后,由于缺少用于创建 postgres 用户的 IF NOT EXIST 选项,这些解决方案非常糟糕,我几乎忘记了有一种简单的方法可以在 shell 级别处理它。尽管这可能不是某些人想要的,但我认为很多人想要简单,而不是创建程序和复杂的结构。

        我正在使用 docker,这里是我的 bash 脚本中重要的 sn-ps,它在 devsetup 中加载数据:

        execute_psql_command_pipe () {
                 $DOCKER_COMMAND exec -it $POSTGRES_CONTAINER bash -c "echo \"$1\"| psql -h localhost -U postgres || echo psql command failed - object likely exists"
        }
        
        read -r -d '' CREATE_USER_COMMANDS << EOM
        create user User1 WITH PASSWORD 'password';
        create user User2 WITH PASSWORD 'password';
        EOM
        
        execute_psql_command_pipe "$CREATE_USER_COMMANDS"
        

        它有一些问题,但这是我能找到的让它做我想做的最简单的方法:在脚本的第一遍创建,在存在时继续第二遍。 顺便说一句,回显输出没有显示,但是命令继续,因为回显命令以 0 退出。

        对于任何命令(如 db create)都可以这样做。 对于可能发生的任何其他错误,这显然会失败(或成功,取决于视角),但您会获得 psql 输出打印机,因此可以添加更多处理。

        【讨论】:

          【解决方案8】:

          只需使用createdb CLI 工具创建数据库:

          PGHOST="my.database.domain.com"
          PGUSER="postgres"
          PGDB="mydb"
          createdb -h $PGHOST -p $PGPORT -U $PGUSER $PGDB
          

          如果数据库存在,则返回错误:

          createdb: database creation failed: ERROR:  database "mydb" already exists
          

          【讨论】:

          • OP 不想要发送错误的东西,因为它可能会使脚本崩溃。
          • script.sh &amp;&gt; /dev/null 所以它不会崩溃
          • 数据库创建可能会在其他条件下崩溃,而不是已经存在。其他原因将在您的解决方案中默默隐藏。
          【解决方案9】:

          最好的方法就是运行 SQL。

          CREATE DATABASE MY_DATABASE; 
          

          如果数据库已经存在,它会抛出“数据库已经存在错误”,你可以做任何你想做的事情,否则它会创建数据库。我认为它不会在您的数据库之上创建一个新数据库。 :D

          【讨论】:

            【解决方案10】:

            我最终使用的一种简单干净的方法:

            createdb $DATABASE 2> /dev/null || echo "database already exists"
            

            如果您预计除database "x" already exists 之外的其他错误显然不会起作用(例如权限被拒绝)。无论如何,如果这是一个问题,总是可以在此之前执行此类检查。

            不要忘记为DATABASE 设置值,并为createdb 命令传递所需的开关。最好你也可以这样做:

            export PGHOST=localhost
            export PGUSER=user
            export PGPASSWORD=p455w0rd
            ...
            

            【讨论】:

              【解决方案11】:

              升级到 PostgreSQL 9.5 或更高版本。如果(不)存在是在 9.5 版本中引入的。

              【讨论】:

              猜你喜欢
              • 2021-07-06
              • 2018-07-17
              • 1970-01-01
              • 2015-05-16
              • 1970-01-01
              • 2014-06-02
              • 1970-01-01
              • 1970-01-01
              • 2011-09-25
              相关资源
              最近更新 更多