Simular CREATE DATABASE IF NOT EXISTS para PostgreSQL?

115

Quero criar um banco de dados que não existe por meio de JDBC. Ao contrário do MySQL, PostgreSQL não oferece suporte à create if not existssintaxe. Qual é o melhor jeito de concluir isso?

O aplicativo não sabe se o banco de dados existe ou não. Deve verificar e se o banco de dados existe deve ser usado. Portanto, faz sentido conectar-se ao banco de dados desejado e se a conexão falhar devido à inexistência de banco de dados, deve-se criar um novo banco de dados (conectando-se ao postgresbanco de dados padrão ). Eu verifiquei o código de erro retornado pelo Postgres, mas não consegui encontrar nenhum código relevante que especifique o mesmo.

Outro método para conseguir isso seria conectar-se ao postgresbanco de dados e verificar se o banco de dados desejado existe e agir de acordo. O segundo é um pouco tedioso de trabalhar.

Existe alguma maneira de obter essa funcionalidade no Postgres?

Aman Deep Gautam
fonte

Respostas:

111

Restrições

Você pode perguntar ao catálogo do sistema pg_database- acessível a partir de qualquer banco de dados no mesmo cluster de banco de dados. A parte complicada é que CREATE DATABASEsó pode ser executado como uma única instrução. O manual:

CREATE DATABASE não pode ser executado dentro de um bloco de transação.

Portanto, ele não pode ser executado diretamente dentro de uma função ou DOinstrução, onde estaria implicitamente dentro de um bloco de transação.

(Os procedimentos SQL, introduzidos no Postgres 11, também não podem ajudar nisso .)

Solução alternativa de dentro do psql

Você pode contornar isso de dentro do psql executando a instrução DDL condicionalmente:

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

O manual:

\gexec

Envia o buffer de consulta atual para o servidor e, a seguir, trata cada coluna de cada linha da saída da consulta (se houver) como uma instrução SQL a ser executada.

Solução alternativa do shell

Com \gexecvocê só precisa chamar o psql uma vez :

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

Você pode precisar de mais opções do psql para sua conexão; função, porta, senha, ... Veja:

O mesmo não pode ser chamado, psql -c "SELECT ...\gexec"pois \gexecé um meta-comando psql e a -copção espera um único comando para o qual o manual indica:

commanddeve ser uma string de comando completamente analisável pelo servidor (ou seja, não contém recursos específicos do psql) ou um único comando de barra invertida. Portanto, você não pode misturar meta-comandos SQL e psql dentro de uma -copção.

Solução alternativa de dentro da transação Postgres

Você pode usar uma dblinkconexão de volta ao banco de dados atual, que funciona fora do bloco de transação. Portanto, os efeitos também não podem ser revertidos.

Instale o módulo adicional dblink para isso (uma vez por banco de dados):

Então:

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$;

Novamente, você pode precisar de mais opções do psql para a conexão. Veja a resposta adicionada de Ortwin:

Explicação detalhada para dblink:

Você pode tornar esta função para uso repetido.

Erwin Brandstetter
fonte
Tive problemas com isso ao criar um banco de dados remoto no AWS RDS Postgres. O usuário mestre RDS não é um superusuário e, portanto, não tem permissão para usá-lo dblink_connect.
Ondrej Burkert
Se você não tiver privilégios de superusuário, pode usar uma senha para a conexão. Detalhes: dba.stackexchange.com/a/105186/3684
Erwin Brandstetter
Funcionou perfeitamente, usado em um script init.sql dentro do contêiner do Docker. Obrigado!
Micheal J. Roberts
Tive que descartar o \gexecquando executei a primeira consulta do shell, mas funcionou.
FilBot3
117

outra alternativa, apenas no caso de você querer ter um script de shell que cria o banco de dados se ele não existir e, caso contrário, apenas o mantenha como está:

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

Achei isso útil em scripts de provisionamento devops, que você pode querer executar várias vezes na mesma instância.

andreasl
fonte
Isso não funciona para mim. 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.O que eu fiz errado ?
Anton Anikeev
2
Você não tem grepem seu caminho. No Windows, grepnão é instalado por padrão. Você pode pesquisar para gnu grep windowsencontrar uma versão que funcione no Windows.
Rod
Thx @Rod. Depois que instalei o grep, esse script funcionou para mim.
Anton Anikeev
@AntonAnikeev: Pode ser feito com uma única chamada do psql sem grep. Eu adicionei soluções à minha resposta.
Erwin Brandstetter
1
Acho útil primeiro nos pg_isready para verificar se uma conexão é possível; se uma conexão não estiver disponível (nome de host incorreto, rede inativa etc), o script tentará criar o banco de dados e falhará com a mensagem de erro possivelmente confusa
Oliver
8

Tive que usar uma versão ligeiramente estendida que @Erwin Brandstetter usou:

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$

Tive que habilitar a dblinkextensão, além de fornecer as credenciais para dblink. Funciona com Postgres 9.4.

Ortwin Angermeier
fonte
7

Se você não se importa com os dados, pode primeiro descartar o banco de dados e depois recriá-lo:

DROP DATABASE IF EXISTS dbname;
CREATE DATABASE dbname;
Andrey Semakin
fonte
Solução muito elegante. Só não se esqueça de fazer o backup do banco de dados primeiro se faz o cuidado sobre os dados. Para testar situações, esta é minha solução preferida.
Laryx Decidua
6

O PostgreSQL não apoiar IF NOT EXISTSpara CREATE DATABASEdeclaração. É compatível apenas com CREATE SCHEMA. Além disso, CREATE DATABASEnão pode ser emitido na transação, portanto, não pode ser em DObloco com captura de exceção.

Quando CREATE SCHEMA IF NOT EXISTSé emitido e o esquema já existe, é gerado um aviso (não erro) com informações de objeto duplicadas.

Para resolver esses problemas, você precisa usar a dblinkextensão que abre uma nova conexão com o servidor de banco de dados e executa a consulta sem entrar em transação. Você pode reutilizar os parâmetros de conexão fornecendo uma string vazia.

Abaixo está o PL/pgSQLcódigo que simula totalmente o CREATE DATABASE IF NOT EXISTSmesmo comportamento do CREATE SCHEMA IF NOT EXISTS. Ele chama CREATE DATABASEvia dblink, catch duplicate_databaseexception (que é emitido quando o banco de dados já existe) e o converte em aviso com propagação errcode. A mensagem de string foi anexada , skippingda mesma maneira que ela 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
$$;

Esta solução é sem nenhuma condição de corrida como nas outras respostas, onde o banco de dados pode ser criado por processo externo (ou outra instância do mesmo script) entre a verificação da existência do banco de dados e sua própria criação.

Além disso, quando CREATE DATABASEfalha com outro erro que não o banco de dados já existe, esse erro é propagado como erro e não descartado silenciosamente. Só existe captura para o duplicate_databaseerro. Portanto, realmente se comporta como IF NOT EXISTSdeveria.

Você pode colocar este código em sua própria função, chamá-lo diretamente ou a partir da transação. Apenas a reversão (restauração do banco de dados eliminado) não funcionaria.

Saída de teste (chamada duas vezes via DO e depois diretamente):

$ 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
Pali
fonte
1
Esta é atualmente a única resposta correta aqui, que não sofre de condições de corrida e usa o tratamento de erro seletivo necessário. É uma pena que esta resposta tenha aparecido depois que a resposta principal (não totalmente correta) coletou mais de 70 pontos.
vog
2
Bem, outras respostas não são tão precisas para lidar com todos os casos esquivos possíveis que podem acontecer. Você também pode chamar meu código PL / pgSQL mais vezes em paralelo e ele não falha.
Pali
1

Se você pode usar shell, tente

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

Eu acho que psql -U postgres -c "select 1" -d $DBé mais fácil do que SELECT 1 FROM pg_database WHERE datname = 'my_db', e só preciso de um tipo de citação, mais fácil de combinar sh -c.

Eu uso isso na minha tarefa 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'"
Wener
fonte
0

Basta criar o banco de dados usando a createdbferramenta CLI:

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

Se o banco de dados existir, ele retornará um erro:

createdb: database creation failed: ERROR:  database "mydb" already exists
James Wierzba
fonte
-11

Atualize para PostgreSQL 9.5 ou superior. Se (não) existir foi introduzido na versão 9.5.

Brlinton
fonte
16
Não há if not existspara CREATE DATABASE- nem mesmo no Postgres 11 postgresql.org/docs/current/static/sql-createdatabase.html
a_horse_with_no_name