Simuler CREATE DATABASE IF NOT EXISTS pour PostgreSQL?


115

Je souhaite créer une base de données qui n'existe pas via JDBC. Contrairement à MySQL, PostgreSQL ne prend pas en charge la create if not existssyntaxe. Quelle est la meilleure façon d'y parvenir?

L'application ne sait pas si la base de données existe ou non. Il doit vérifier et si la base de données existe, elle doit être utilisée. Il est donc logique de se connecter à la base de données souhaitée et si la connexion échoue en raison de l'inexistence de la base de données, elle doit créer une nouvelle base de données (en se connectant à la postgresbase de données par défaut ). J'ai vérifié le code d'erreur renvoyé par Postgres mais je n'ai trouvé aucun code pertinent qui spécifie la même chose.

Une autre méthode pour y parvenir serait de se connecter à la postgresbase de données et de vérifier si la base de données souhaitée existe et d'agir en conséquence. Le second est un peu fastidieux à élaborer.

Existe-t-il un moyen d'obtenir cette fonctionnalité dans Postgres?

Réponses:


111

Restrictions

Vous pouvez demander le catalogue système pg_database- accessible depuis n'importe quelle base de données dans le même cluster de bases de données. La partie la plus délicate est qu'elle CREATE DATABASEne peut être exécutée que comme une seule instruction. Le manuel:

CREATE DATABASE ne peut pas être exécuté dans un bloc de transaction.

Il ne peut donc pas être exécuté directement dans une fonction ou une DOinstruction, où il se trouverait implicitement dans un bloc de transaction.

(Les procédures SQL, introduites avec Postgres 11, ne peuvent pas non plus aider .)

Solution de contournement à partir de psql

Vous pouvez le contourner à partir de psql en exécutant l'instruction DDL de manière conditionnelle:

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

Le manuel:

\gexec

Envoie le tampon de requête actuel au serveur, puis traite chaque colonne de chaque ligne de la sortie de la requête (le cas échéant) comme une instruction SQL à exécuter.

Solution de contournement depuis le shell

Avec \gexecvous n'avez besoin d'appeler psql qu'une seule fois :

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

Vous aurez peut-être besoin de plus d'options psql pour votre connexion; rôle, port, mot de passe, ... Voir:

La même chose ne peut pas être appelée psql -c "SELECT ...\gexec"car il \gexecs'agit d'une méta-commande psql et l' -coption attend une seule commande pour laquelle le manuel indique:

commanddoit être soit une chaîne de commande entièrement analysable par le serveur (c'est-à-dire qu'elle ne contient aucune fonctionnalité spécifique à psql), soit une seule commande de barre oblique inverse. Ainsi, vous ne pouvez pas mélanger les méta-commandes SQL et psql dans une -coption.

Solution de contournement depuis la transaction Postgres

Vous pouvez utiliser une dblinkconnexion de retour à la base de données actuelle, qui s'exécute en dehors du bloc de transaction. Les effets ne peuvent donc pas non plus être annulés.

Installez le module supplémentaire dblink pour cela (une fois par base de données):

Ensuite:

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

Encore une fois, vous aurez peut-être besoin de plus d'options psql pour la connexion. Voir la réponse ajoutée d'Ortwin:

Explication détaillée pour dblink:

Vous pouvez en faire une fonction pour une utilisation répétée.


J'ai rencontré un problème lors de la création d'une base de données sur AWS RDS Postgres à distance. L'utilisateur maître RDS n'est pas un super utilisateur et n'est donc pas autorisé à l'utiliser dblink_connect.
Ondrej Burkert le

Si vous ne disposez pas des privilèges de superutilisateur, vous pouvez utiliser un mot de passe pour la connexion. Détails: dba.stackexchange.com/a/105186/3684
Erwin Brandstetter

A fonctionné comme un charme, utilisé dans un script init.sql à l'intérieur du conteneur Docker. Merci!
Micheal J. Roberts

J'ai dû abandonner le \gexeclorsque j'ai exécuté la première requête à partir du shell, mais cela a fonctionné.
FilBot3

117

une autre alternative, juste au cas où vous voudriez avoir un script shell qui crée la base de données si elle n'existe pas et la garde autrement telle quelle:

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

J'ai trouvé cela utile dans les scripts de provisioning devops, que vous voudrez peut-être exécuter plusieurs fois sur la même instance.


Ça ne marche pas pour moi. 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'ai-je fait de mal ?
Anton Anikeev

2
Vous n'avez pas grepsur votre chemin. Sous Windows, grepn'est pas installé par défaut. Vous pouvez rechercher gnu grep windowsune version qui pourrait fonctionner sur Windows.
Rod

Merci @Rod. Après avoir installé grep, ce script a fonctionné pour moi.
Anton Anikeev

@AntonAnikeev: Peut être fait avec un seul appel psql sans grep. J'ai ajouté des solutions à ma réponse.
Erwin Brandstetter

1
Je trouve utile de commencer par nous pg_isready pour vérifier qu'une connexion est possible; si une connexion n'est pas disponible (mauvais nom d'hôte, réseau en panne, etc.), le script tentera de créer la base de données et échouera avec un message d'erreur potentiellement déroutant
Oliver

8

J'ai dû utiliser une version légèrement étendue @Erwin Brandstetter utilisée:

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$

J'ai dû activer l' dblinkextension, plus j'ai dû fournir les informations d'identification pour dblink. Fonctionne avec Postgres 9.4.


7

Si vous ne vous souciez pas des données, vous pouvez d'abord supprimer la base de données, puis la recréer:

DROP DATABASE IF EXISTS dbname;
CREATE DATABASE dbname;

Solution très élégante. N'oubliez pas de sauvegarder d'abord la base de données si vous vous souciez des données. Pour tester des situations, c'est ma solution préférée.
Laryx Decidua

6

PostgreSQL ne prend pas en charge IF NOT EXISTSde CREATE DATABASEdéclaration. Il est pris en charge uniquement dans CREATE SCHEMA. De plus, CREATE DATABASEil ne peut pas être émis en transaction, il ne peut donc pas être en DObloc avec capture d'exception.

Lorsque CREATE SCHEMA IF NOT EXISTSest émis et que le schéma existe déjà, une notification (pas d'erreur) avec des informations d'objet en double est générée.

Pour résoudre ces problèmes, vous devez utiliser l' dblinkextension qui ouvre une nouvelle connexion au serveur de base de données et exécute la requête sans entrer dans la transaction. Vous pouvez réutiliser les paramètres de connexion en fournissant une chaîne vide.

Vous trouverez ci-dessous du PL/pgSQLcode qui simule entièrement CREATE DATABASE IF NOT EXISTSavec le même comportement que dansCREATE SCHEMA IF NOT EXISTS . Il appelle CREATE DATABASEvia dblink, catch duplicate_databaseexception (qui est émis lorsque la base de données existe déjà) et le convertit en avis avec propagation errcode. Le message de chaîne a été ajouté , skippingde la même manière 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
$$;

Cette solution est sans condition de concurrence comme dans d'autres réponses, où la base de données peut être créée par un processus externe (ou une autre instance du même script) entre la vérification de l'existence de la base de données et sa propre création.

De plus, en cas d' CREATE DATABASEéchec avec une autre erreur que la base de données existe déjà, cette erreur est propagée en tant qu'erreur et n'est pas supprimée en silence. Il n'y a qu'un piège pour l' duplicate_databaseerreur. Donc, il se comporte vraiment comme il se IF NOT EXISTSdoit.

Vous pouvez mettre ce code dans sa propre fonction, l'appeler directement ou à partir d'une transaction. La simple restauration (restaurer la base de données supprimée) ne fonctionnerait pas.

Sortie de test (appelée deux fois via DO puis directement):

$ 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

1
C'est actuellement la seule réponse correcte ici, qui ne souffre pas des conditions de course et utilise la gestion sélective des erreurs nécessaire. Il est vraiment dommage que cette réponse soit apparue après que la première réponse (pas tout à fait correcte) ait recueilli plus de 70 points.
vog

2
Eh bien, les autres réponses ne sont pas aussi précises pour gérer tous les cas de coin possibles qui peuvent arriver. Vous pouvez également appeler mon code PL / pgSQL plusieurs fois en parallèle et cela n'échoue pas.
Pali

1

Si vous pouvez utiliser shell, essayez

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

Je pense que psql -U postgres -c "select 1" -d $DBc'est plus facile SELECT 1 FROM pg_database WHERE datname = 'my_db'et n'a besoin que d'un seul type de devis, plus facile à combiner sh -c.

J'utilise ceci dans ma tâche 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'"

0

Créez simplement la base de données à l'aide de l' createdboutil CLI:

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

Si la base de données existe, elle renverra une erreur:

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.