¿Simular CREAR BASE DE DATOS SI NO EXISTE para PostgreSQL?

115

Quiero crear una base de datos que no existe a través de JDBC. A diferencia de MySQL, PostgreSQL no admite create if not existssintaxis. Cuál es la mejor manera de lograr esto?

La aplicación no sabe si la base de datos existe o no. Debe verificar y si la base de datos existe, debe usarse. Por lo tanto, tiene sentido conectarse a la base de datos deseada y si la conexión falla debido a la inexistencia de la base de datos, debería crear una nueva base de datos (conectándose a la postgresbase de datos predeterminada ). Revisé el código de error devuelto por Postgres, pero no pude encontrar ningún código relevante que tenga la misma especie.

Otro método para lograr esto sería conectarse a la postgresbase de datos y verificar si existe la base de datos deseada y tomar las medidas correspondientes. El segundo es un poco tedioso de resolver.

¿Hay alguna forma de lograr esta funcionalidad en Postgres?

Aman Deep Gautam
fuente

Respuestas:

111

Restricciones

Puede solicitar el catálogo del sistema pg_database, accesible desde cualquier base de datos en el mismo grupo de bases de datos. La parte complicada es que CREATE DATABASEsolo se puede ejecutar como una única declaración. El manual:

CREATE DATABASE no se puede ejecutar dentro de un bloque de transacciones.

Por lo tanto, no se puede ejecutar directamente dentro de una función o DOdeclaración, donde estaría implícitamente dentro de un bloque de transacción.

(Los procedimientos SQL, introducidos con Postgres 11, tampoco pueden ayudar con esto ).

Solución alternativa desde psql

Puede solucionarlo desde psql ejecutando la declaración DDL de forma condicional:

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

El manual:

\gexec

Envía el búfer de consulta actual al servidor, luego trata cada columna de cada fila de la salida de la consulta (si la hubiera) como una declaración SQL para ser ejecutada.

Solución alternativa desde el caparazón

Con \gexecsolo necesitas llamar a psql una vez :

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

Es posible que necesite más opciones de psql para su conexión; rol, puerto, contraseña, ... Ver:

No se puede llamar al mismo, psql -c "SELECT ...\gexec"ya que \gexeces un metacomando psql y la -copción espera un solo comando para el cual el manual dice:

commanddebe ser una cadena de comando que el servidor pueda analizar completamente (es decir, que no contenga funciones específicas de psql) o un solo comando de barra invertida. Por lo tanto, no puede mezclar metacomandos SQL y psql dentro de una -copción.

Solución alternativa desde dentro de la transacción de Postgres

Podrías usar un dblink conexión de regreso a la base de datos actual, que se ejecuta fuera del bloque de transacciones. Por lo tanto, los efectos tampoco se pueden revertir.

Instale el módulo adicional dblink para esto (una vez por base de datos):

Luego:

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

Nuevamente, es posible que necesite más opciones de psql para la conexión. Vea la respuesta agregada de Ortwin:

Explicación detallada de dblink:

Puede convertir esta función en una función para uso repetido.

Erwin Brandstetter
fuente
Me encontré con un problema con esto al crear una base de datos en AWS RDS Postgres de forma remota. El usuario maestro de RDS no es un superusuario y, por lo tanto, no se le permite usar dblink_connect.
Ondrej Burkert
Si no tiene privilegios de superusuario, puede utilizar una contraseña para la conexión. Detalles: dba.stackexchange.com/a/105186/3684
Erwin Brandstetter
Funcionó como un encanto, utilizado dentro de un script init.sql dentro del contenedor Docker. ¡Gracias!
Micheal J. Roberts
Tuve que eliminar el \gexeccuando ejecuté la primera consulta desde el shell, pero funcionó.
FilBot3
117

otra alternativa, en caso de que desee tener un script de shell que cree la base de datos si no existe y, de lo contrario, la mantiene 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"

Encontré que esto es útil en scripts de aprovisionamiento de devops, que es posible que desee ejecutar varias veces en la misma instancia.

andreasl
fuente
No funciona para mi. 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.Qué hice mal ?
Anton Anikeev
2
No tienes grepen tu camino. En Windows, grepno se instala de forma predeterminada. Puede buscar para gnu grep windowsencontrar una versión que funcione en Windows.
Rod
Gracias @Rod. Después de instalar grep, este script funcionó para mí.
Anton Anikeev
@AntonAnikeev: se puede hacer con una sola llamada psql sin grep. Agregué soluciones a mi respuesta.
Erwin Brandstetter
1
Encuentro útil que primero nos pg_isready para comprobar que es posible una conexión; si una conexión no está disponible (nombre de host incorrecto, red inactiva, etc.), el script intentará crear la base de datos y fallará con un mensaje de error posiblemente confuso
Oliver
8

Tuve que usar una versión ligeramente extendida que usó @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$

Tuve que habilitar la dblinkextensión, además tuve que proporcionar las credenciales para dblink. Funciona con Postgres 9.4.

Ortwin Angermeier
fuente
7

Si no le importan los datos, puede eliminar la base de datos primero y luego volver a crearla:

DROP DATABASE IF EXISTS dbname;
CREATE DATABASE dbname;
Andrey Semakin
fuente
Solución muy elegante. No olvide hacer una copia de seguridad de la base de datos primero si haces atención acerca de los datos. Para situaciones de prueba, esta es mi solución preferida.
Laryx Decidua
6

PostgreSQL no es compatible IF NOT EXISTSpara la CREATE DATABASEdeclaración. Solo se admite en CREATE SCHEMA. Además CREATE DATABASE, no se puede emitir en transacción, por lo que no puedeDO bloque con excepción de captura.

Cuando CREATE SCHEMA IF NOT EXISTS se emite y el esquema ya existe, se genera un aviso (no un error) con información de objeto duplicada.

Para resolver estos problemas, debe usar la dblinkextensión que abre una nueva conexión al servidor de la base de datos y ejecutar la consulta sin ingresar a la transacción. Puede reutilizar los parámetros de conexión proporcionando una cadena vacía.

A continuación se muestra el PL/pgSQLcódigo que simula completamente CREATE DATABASE IF NOT EXISTScon el mismo comportamiento que enCREATE SCHEMA IF NOT EXISTS . Llama a CREATE DATABASEtravés de la excepción de dblinkcaptura duplicate_database(que se emite cuando la base de datos ya existe) y la convierte en un aviso con propagación errcode. El mensaje de cadena se ha agregado , skippingde la misma manera que lo hace 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 solución no tiene ninguna condición de carrera como en otras respuestas, donde la base de datos puede ser creada por un proceso externo (u otra instancia del mismo script) entre verificar si la base de datos existe y su propia creación.

Además, cuando CREATE DATABASEfalla con otro error que la base de datos ya existe, este error se propaga como error y no se descarta silenciosamente. Solo hay una trampa para el duplicate_databaseerror. Entonces realmente se comporta como IF NOT EXISTSdebería.

Puede poner este código en su propia función, llamarlo directamente o desde la transacción. Solo la reversión (restaurar la base de datos eliminada) no funcionaría.

Prueba de salida (llamada dos veces a través de DO y luego directamente):

$ 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
fuente
1
Actualmente, esta es la única respuesta correcta aquí, que no sufre condiciones de carrera y utiliza el manejo de errores selectivo necesario. Es una lástima que esta respuesta apareciera después de que la respuesta principal (no del todo correcta) obtuviera más de 70 puntos.
vog
2
Bueno, otras respuestas no son tan precisas para manejar todos los posibles casos de esquina que puedan ocurrir. También puede llamar a mi código PL / pgSQL más veces en paralelo y no falla.
Pali
1

Si puede usar shell, intente

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

Creo que psql -U postgres -c "select 1" -d $DBes más fácil SELECT 1 FROM pg_database WHERE datname = 'my_db'y solo necesito un tipo de cotización, más fácil de combinar consh -c .

Uso esto en mi tarea 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
fuente
0

Simplemente cree la base de datos usando la createdbherramienta CLI:

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

Si la base de datos existe, devolverá un error:

createdb: database creation failed: ERROR:  database "mydb" already exists
James Wierzba
fuente