PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Comment utiliser RETURNING avec ON CONFLICT dans PostgreSQL ?

La réponse actuellement acceptée semble correcte pour une seule cible de conflit, peu de conflits, de petits tuples et aucun déclencheur. Cela évite le problème de simultanéité 1 (voir ci-dessous) avec force brute. La solution simple a son attrait, les effets secondaires peuvent être moins importants.

Dans tous les autres cas, cependant, ne pas mettre à jour les lignes identiques sans besoin. Même si vous ne voyez aucune différence en surface, il y a divers effets secondaires :

  • Il pourrait déclencher des déclencheurs qui ne devraient pas être déclenchés.

  • Il verrouille en écriture les lignes "innocentes", ce qui peut entraîner des coûts pour les transactions simultanées.

  • Cela peut donner l'impression que la ligne est nouvelle, bien qu'elle soit ancienne (horodatage de la transaction).

  • Le plus important , avec le modèle MVCC de PostgreSQL, une nouvelle version de ligne est écrite pour chaque UPDATE , que les données de la ligne aient changé ou non. Cela entraîne une pénalité de performance pour l'UPSERT lui-même, un gonflement de table, un gonflement d'index, une pénalité de performance pour les opérations ultérieures sur la table, VACUUM Coût. Un effet mineur pour quelques doublons, mais massif pour la plupart des dupes.

Plus , parfois il n'est pas pratique ou même possible d'utiliser ON CONFLICT DO UPDATE . Le manuel :

Pour ON CONFLICT DO UPDATE , un conflict_target doit être fourni.

Un célibataire "cible de conflit" n'est pas possible si plusieurs index/contraintes sont impliqués. Mais voici une solution connexe pour plusieurs index partiels :

  • UPSERT basé sur la contrainte UNIQUE avec des valeurs NULL

De retour sur le sujet, vous pouvez obtenir (presque) la même chose sans mises à jour vides ni effets secondaires. Certaines des solutions suivantes fonctionnent également avec ON CONFLICT DO NOTHING (pas de "cible de conflit"), pour attraper tous les conflits éventuels qui pourraient survenir - qui peuvent ou non être souhaitables.

Sans charge d'écriture simultanée

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, ins AS (
   INSERT INTO chats (usr, contact, name) 
   SELECT * FROM input_rows
   ON CONFLICT (usr, contact) DO NOTHING
   RETURNING id  --, usr, contact              -- return more columns?
   )
SELECT 'i' AS source                           -- 'i' for 'inserted'
     , id  --, usr, contact                    -- return more columns?
FROM   ins
UNION  ALL
SELECT 's' AS source                           -- 's' for 'selected'
     , c.id  --, usr, contact                  -- return more columns?
FROM   input_rows
JOIN   chats c USING (usr, contact);           -- columns of unique index

Le source colonne est un ajout facultatif pour montrer comment cela fonctionne. Vous pourriez en avoir besoin pour faire la différence entre les deux cas (un autre avantage par rapport aux écritures vides).

Les derniers JOIN chats fonctionne car les lignes nouvellement insérées à partir d'un CTE de modification de données attaché ne sont pas encore visibles dans la table sous-jacente. (Toutes les parties de la même instruction SQL voient les mêmes instantanés des tables sous-jacentes.)

Depuis les VALUES l'expression est autonome (pas directement attachée à un INSERT ) Postgres ne peut pas dériver les types de données des colonnes cibles et vous devrez peut-être ajouter des conversions de type explicites. Le manuel :

Lorsque VALUES est utilisé dans INSERT , les valeurs sont toutes automatiquement contraintes au type de données de la colonne de destination correspondante. Lorsqu'il est utilisé dans d'autres contextes, il peut être nécessaire de spécifier le type de données correct. Si les entrées sont toutes des constantes littérales entre guillemets, forcer la première est suffisante pour déterminer le type supposé pour toutes.

La requête elle-même (sans compter les effets secondaires) peut être un peu plus chère pour quelques dupes, en raison de la surcharge du CTE et du SELECT supplémentaire (ce qui devrait être bon marché puisque l'index parfait est là par définition - une contrainte unique est implémentée avec un index).

Peut être (beaucoup) plus rapide pour beaucoup doublons. Le coût effectif des écritures supplémentaires dépend de nombreux facteurs.

Mais il y a moins d'effets secondaires et de coûts cachés dans tous les cas. C'est probablement moins cher dans l'ensemble.

Les séquences attachées sont toujours avancées, puisque les valeurs par défaut sont renseignées avant test des conflits.

À propos des CTE :

  • Les requêtes de type SELECT sont-elles les seuls types pouvant être imbriqués ?
  • Dédupliquer les instructions SELECT dans la division relationnelle

Avec charge d'écriture simultanée

En supposant par défaut READ COMMITTED isolement des transactions. Connexe :

  • Les transactions simultanées entraînent une condition de concurrence avec une contrainte unique sur l'insertion

La meilleure stratégie pour se défendre contre les conditions de concurrence dépend des exigences exactes, du nombre et de la taille des lignes dans la table et dans les UPSERT, du nombre de transactions simultanées, de la probabilité de conflits, des ressources disponibles et d'autres facteurs...

Problème de simultanéité 1

Si une transaction simultanée a été écrite dans une ligne que votre transaction tente maintenant d'UPSERT, votre transaction doit attendre que l'autre se termine.

Si l'autre transaction se termine par ROLLBACK (ou toute erreur, c'est-à-dire un ROLLBACK automatique ), votre transaction peut se dérouler normalement. Effet secondaire possible mineur :lacunes dans les numéros séquentiels. Mais pas de lignes manquantes.

Si l'autre transaction se termine normalement (COMMIT implicite ou explicite ), votre INSERT détectera un conflit (l'élément UNIQUE index / contrainte est absolu) et DO NOTHING , donc ne renvoie pas non plus la ligne. (Impossible également de verrouiller la ligne comme illustré dans le problème de concurrence 2 ci-dessous, puisqu'il n'est pas visible .) Le SELECT voit le même instantané depuis le début de la requête et ne peut pas non plus renvoyer la ligne encore invisible.

Ces lignes manquent dans le jeu de résultats (même si elles existent dans la table sous-jacente) !

Cela peut être correct tel quel . Surtout si vous ne renvoyez pas de lignes comme dans l'exemple et que vous êtes satisfait de savoir que la ligne est là. Si cela ne suffit pas, il existe plusieurs façons de contourner cela.

Vous pouvez vérifier le nombre de lignes de la sortie et répéter l'instruction si elle ne correspond pas au nombre de lignes de l'entrée. Peut être assez bon pour les cas rares. Le but est de démarrer une nouvelle requête (peut être dans la même transaction), qui verra alors les nouvelles lignes validées.

Ou vérifier les lignes de résultats manquantes dans la même requête et écraser ceux avec le truc de la force brute démontré dans la réponse d'Alextoni.

WITH input_rows(usr, contact, name) AS ( ... )  -- see above
, ins AS (
   INSERT INTO chats AS c (usr, contact, name) 
   SELECT * FROM input_rows
   ON     CONFLICT (usr, contact) DO NOTHING
   RETURNING id, usr, contact                   -- we need unique columns for later join
   )
, sel AS (
   SELECT 'i'::"char" AS source                 -- 'i' for 'inserted'
        , id, usr, contact
   FROM   ins
   UNION  ALL
   SELECT 's'::"char" AS source                 -- 's' for 'selected'
        , c.id, usr, contact
   FROM   input_rows
   JOIN   chats c USING (usr, contact)
   )
, ups AS (                                      -- RARE corner case
   INSERT INTO chats AS c (usr, contact, name)  -- another UPSERT, not just UPDATE
   SELECT i.*
   FROM   input_rows i
   LEFT   JOIN sel   s USING (usr, contact)     -- columns of unique index
   WHERE  s.usr IS NULL                         -- missing!
   ON     CONFLICT (usr, contact) DO UPDATE     -- we've asked nicely the 1st time ...
   SET    name = c.name                         -- ... this time we overwrite with old value
   -- SET name = EXCLUDED.name                  -- alternatively overwrite with *new* value
   RETURNING 'u'::"char" AS source              -- 'u' for updated
           , id  --, usr, contact               -- return more columns?
   )
SELECT source, id FROM sel
UNION  ALL
TABLE  ups;

C'est comme la requête ci-dessus, mais nous ajoutons une étape supplémentaire avec le CTE ups , avant de renvoyer le complet ensemble de résultats. Ce dernier CTE ne fera rien la plupart du temps. Ce n'est que si des lignes manquent dans le résultat renvoyé que nous utilisons la force brute.

Plus de frais généraux, encore. Plus il y a de conflits avec des lignes préexistantes, plus il y a de chances que cela surpasse l'approche simple.

Un effet secondaire :le 2e UPSERT écrit des lignes dans le désordre, il réintroduit donc la possibilité de blocages (voir ci-dessous) si trois ou plus les transactions écrivant dans les mêmes lignes se chevauchent. Si c'est un problème, vous avez besoin d'une solution différente - comme répéter l'intégralité de la déclaration comme mentionné ci-dessus.

Problème de simultanéité 2

Si des transactions simultanées peuvent écrire dans les colonnes concernées des lignes concernées et que vous devez vous assurer que les lignes que vous avez trouvées sont toujours là à un stade ultérieur de la même transaction, vous pouvez verrouiller les lignes existantes à moindre coût dans le CTE ins (qui autrement serait déverrouillé) avec :

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

Et ajoutez une clause de verrouillage au SELECT ainsi, comme FOR UPDATE .

Cela fait attendre les opérations d'écriture concurrentes jusqu'à la fin de la transaction, lorsque tous les verrous sont libérés. Alors soyez bref.

Plus de détails et d'explication :

  • Comment inclure des lignes exclues dans RETURNING from INSERT ... ON CONFLICT
  • SELECT ou INSERT est-il dans une fonction sujette à des conditions de concurrence ?

Des blocages ?

Défendez-vous contre les impasses en insérant des lignes dans un ordre cohérent . Voir :

  • Interblocage avec INSERTs multi-lignes malgré ON CONFLICT DO NOTHING

Types de données et conversions

Table existante comme modèle pour les types de données...

Conversions de type explicites pour la première ligne de données dans le VALUES autonome l'expression peut être gênante. Il y a des façons de contourner cela. Vous pouvez utiliser n'importe quelle relation existante (table, vue, ...) comme modèle de ligne. La table cible est le choix évident pour le cas d'utilisation. Les données d'entrée sont automatiquement converties en types appropriés, comme dans les VALUES clause d'un INSERT :

WITH input_rows AS (
  (SELECT usr, contact, name FROM chats LIMIT 0)  -- only copies column names and types
   UNION ALL
   VALUES
      ('foo1', 'bar1', 'bob1')  -- no type casts here
    , ('foo2', 'bar2', 'bob2')
   )
   ...

Cela ne fonctionne pas pour certains types de données. Voir :

  • Diffusion du type NULL lors de la mise à jour de plusieurs lignes

... et des noms

Cela fonctionne également pour tous types de données.

Lors de l'insertion dans toutes les colonnes (de tête) du tableau, vous pouvez omettre les noms de colonne. En supposant que la table chats dans l'exemple se compose uniquement des 3 colonnes utilisées dans UPSERT :

WITH input_rows AS (
   SELECT * FROM (
      VALUES
      ((NULL::chats).*)         -- copies whole row definition
      ('foo1', 'bar1', 'bob1')  -- no type casts needed
    , ('foo2', 'bar2', 'bob2')
      ) sub
   OFFSET 1
   )
   ...

À part :n'utilisez pas de mots réservés comme "user" comme identifiant. C'est une arme à pied chargée. Utilisez des identifiants légaux, en minuscules et sans guillemets. Je l'ai remplacé par usr .