Suppression de colonnes PostgreSQL 9.6 et effets secondaires sur les fonctions SQL avec CTE


15

Si j'avais un tableau avec 3 colonnes - disons A, B et D - et que je devais en introduire un nouveau - disons C pour remplacer la position actuelle de D. J'utiliserais la méthode suivante:

  1. Introduisez 2 nouvelles colonnes en C et D2.
  2. Copiez le contenu de D vers D2.
  3. Supprimer D.
  4. Renommez D2 en D.

Le nouvel ordre serait A, B, C et D.

Je pensais que c'était une pratique légitime car (jusqu'à présent) cela ne posait aucun problème.

Cependant, aujourd'hui, je suis tombé sur un problème lorsqu'une fonction exécutant une instruction sur la même table a renvoyé l'erreur suivante:

table row type and query-specified row type do not match

Et le détail suivant:

Query provides a value for a dropped column at ordinal position 13

J'ai essayé de redémarrer PostgreSQL, de faire VACUUM FULLet enfin de supprimer et de recréer la fonction comme suggéré ici et ici mais ces solutions n'ont pas fonctionné (à part le fait qu'ils essaient de résoudre une situation où une table système a été modifiée).

Ayant le luxe de travailler avec une très petite base de données, je l'ai exportée, supprimée puis réimportée et cela a résolu le problème de ma fonction.


J'étais conscient du fait qu'il ne fallait pas jouer avec l'ordre naturel des colonnes en modifiant les tables système (se salir les mains avec pg_attribute, etc.) comme on le voit ici:

Est-il possible de changer l'ordre naturel des colonnes dans Postgres?

À en juger par l'erreur générée par ma fonction, je me rends compte maintenant que le changement de l'ordre des colonnes avec ma méthode est également un non-non. Quelqu'un peut-il expliquer pourquoi ce que je fais est également mauvais?


La version Postgres est 9.6.0.

Voici la fonction:

CREATE OR REPLACE FUNCTION "public"."__post_users" ("facebookid" text, "useremail" text, "username" text) RETURNS TABLE (authentication_code text, id integer, key text, stripe_id text) AS '

-- First, select the user:
WITH select_user AS
(SELECT
users.id
FROM
users
WHERE
useremail = users.email),

-- Second, update the user (if user exists):
update_user AS
(UPDATE
users
SET
authentication_code = GEN_RANDOM_UUID(),
authentication_date = current_timestamp,
facebook_id = facebookid
WHERE EXISTS (SELECT * FROM select_user)
AND
useremail = users.email
RETURNING
users.authentication_code,
users.id,
users.key,
users.stripe_id),

-- Third, insert the user (if user does not exist):
insert_user AS
(INSERT INTO
users (authentication_code, authentication_date, email, key, name, facebook_id)
SELECT
GEN_RANDOM_UUID(),
current_timestamp,
useremail,
GEN_RANDOM_UUID(),
COALESCE(username, SUBSTRING(useremail FROM ''([^@]+)'')),
facebookid
WHERE NOT EXISTS (SELECT * FROM select_user)
RETURNING
users.authentication_code,
users.id,
users.key,
users.stripe_id)

-- Finally, select the authentication code, ID, key and Stripe ID:
SELECT
*
FROM
update_user
UNION ALL
SELECT
*
FROM
insert_user' LANGUAGE "sql" COST 100 ROWS 1
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER

J'ai effectué le changement de nom / réorganisation sur les deux colonnes facebook_idet stripe_id(une nouvelle colonne a été ajoutée avant celles-ci, ce qui est la raison du changement de nom, mais n'est pas touché par cette requête).

Avoir les colonnes dans un certain ordre est purement sans intérêt pour l'ordre. Cependant, la raison de poser cette question est de ne pas craindre qu'un simple changement de nom et suppression d'une colonne puisse déclencher de réels problèmes pour quelqu'un qui utilise des fonctions en mode production (comme cela m'est arrivé).


Jetez un œil à Comment puis-je modifier la position d'une colonne dans une table de base de données PostgreSQL? sur Stack Overflow qui pourraient être utiles, bien qu'ils suggèrent principalement de NE PAS réorganiser vos colonnes.
joanolo

Réponses:


16

Bogue probable sur 9.6 et 9.6.1

Cela ressemble complètement à un bug pour moi ...

Je ne sais pas pourquoi cela se produit, mais je peux confirmer que cela se produit. Il s'agit de la configuration la plus simple trouvée qui reproduit le problème (dans les versions 9.6.0 et 9.6.1).

CREATE TABLE users
(
    id SERIAL PRIMARY KEY,
    email TEXT NOT NULL,
    column_that_we_will_drop TEXT
) ;

-- Function that uses the previous table, and that has a CTE
CREATE OR REPLACE FUNCTION __post_users
    (_useremail text) 
RETURNS integer AS
$$
-- Need a CTE to produce the error. A 'constant' one suffices.
WITH something_even_if_useless(a) AS
(
    VALUES (1)
)
UPDATE
    users
SET
    id = id
WHERE 
    -- The CTE needs to be referenced, if the next
    -- condition were not in place, the problem is not reproduced
    EXISTS (SELECT * FROM something_even_if_useless)
    AND email = _useremail
RETURNING
    id
$$
LANGUAGE "sql" ;

Après cette configuration, la prochaine instruction fonctionne

SELECT * FROM __post_users('a@b.com');

À ce stade, nous DROP une colonne:

ALTER TABLE users 
    DROP COLUMN column_that_we_will_drop ;

Cette modification rend l'instruction suivante pour générer une erreur

SELECT * FROM __post_users('a@b.com');

qui est la même que celle mentionnée par @Andy:

ERROR: table row type and query-specified row type do not match
SQL state: 42804
Detail: Query provides a value for a dropped column at ordinal position 3.
Context: SQL function "__post_users" statement 1
    SELECT * FROM __post_users('a@b.com');

La suppression et la recréation de la fonction ne résout PAS le problème.
VACUUM FULL (la table ou la base de données entière) ne résout pas le problème.


Le rapport de bogue a été transmis à la liste de diffusion PostgreSQL appropriée et nous avons eu une réponse très rapide :

Je ne peux pas reproduire cela dans HEAD ou 9.6 branch tip. Je pense qu'il a déjà été corrigé par ce patch, qui est entré un peu après 9.6.1:

https://git.postgresql.org/gitweb/?p=postgresql.git&a=commitdiff&h=f4d865f22

Mais merci pour le rapport!

salutations, tom lane


Version 9.6.2

Le 2017-03-06, je peux confirmer que je ne peux pas reproduire ce comportement sur la version 9.6.2. Autrement dit, le bogue semble avoir été corrigé sur cette version.

MISE À JOUR

Par commentaire de @Jana: «Je peux confirmer que le bogue est présent dans 9.6.1 et a été corrigé dans 9.6.2. Le correctif est également répertorié sur le site Web de la version postgres : INSÉRER ou METTRE À JOUR sur une table avec une colonne supprimée "



4
J'utilise 9.6.0 et @joanolo est correct, je peux reproduire le bug avec sa méthode. Si Tom ne peut pas le reproduire, c'était probablement un bug isolé avec cette version spécifique (et 9.6.1 je suppose?). Comme mentionné dans la réponse, le problème apparaît lors de la suppression d' une colonne dans une table et de l' utilisation d'une fonction avec CTE affectant cette table. Ainsi, le problème n'est pas seulement lié à la réorganisation et c'est ce que j'essayais de comprendre avec ma question. Je vais modifier le titre pour refléter cela.
Andy

Je peux confirmer que le bogue est présent dans 9.6.1 et a été corrigé dans 9.6.2. Le correctif est également coté sur postgres release site : Correction des erreurs parasites « requête fournit une valeur pour une colonne » a chuté au cours INSERT ou UPDATE sur une table avec une colonne supprimée
Jana

0

Je sais que vous avez probablement déjà entendu cela auparavant, mais c'est une idée horrible.

  • L'ordre logique n'affecte que des choses comme SELECT *
  • L'effet de l'ordre logique est limité à l'apparence.

Donc, si cela n'a pas d'importance du tout ne vous dissuade pas et nous reconnaissons que nous ne faisons que jouer à Photoshop avec une structure de lignes et que nous sommes obsédés par l'affichage, expliquons encore quelques choses.

  • L'ordre logique n'existe pas dans PostgreSQL
  • La commande physique présente de réels avantages (emballage de table)
  • PostgreSQL ne vous donne aucun contrôle sur l'ordre physique CREATE TABLEnon plus (bien que ce soit une priorité beaucoup plus élevée)

PostgreSQL est donc une mauvaise couche d'affichage. Sur tout cela, bien que cela ALTERfonctionne bien, vous ne devriez pas tempérer le dragon. Les deux ALTER TABLE, et la section CAVEAT en font mention,

Certaines commandes DDL, actuellement uniquement TRUNCATEet les formes de réécriture de table de ALTER TABLE, ne sont pas sécurisées par MVCC. Cela signifie qu'après la validation de la troncature ou de la réécriture, la table apparaîtra vide pour les transactions simultanées, si elles utilisent un instantané pris avant la validation de la commande DDL. Cela ne sera un problème que pour une transaction qui n'a pas accédé à la table en question avant le démarrage de la commande DDL - toute transaction qui l'a fait contiendrait au moins un verrou de table ACCESS SHARE, qui bloquerait la commande DDL jusqu'à la fin de cette transaction. Ces commandes n'entraîneront donc aucune incohérence apparente dans le contenu de la table pour les requêtes successives sur la table cible, mais elles pourraient entraîner une incohérence visible entre le contenu de la table cible et d'autres tables de la base de données.

Et, si tout cela ne suffit pas, et vous voulez toujours prétendre que c'est une bonne idée plutôt une horrible idée. Alors essayez ceci,

  1. Vider la table avec pg_dump -t
  2. Réorganisez les colonnes à la main dans le vidage.
  3. Chargez le contenu dans une table TEMP.
  4. BEGIN une transaction
  5. DROP l'ancienne table entièrement,
  6. RENAME la table temp à la table prod.
  7. COMMIT

Si tout cela semble excessif, gardez à l'esprit que la mise à jour des lignes dans la base de données nécessite de réécrire les lignes (en supposant qu'elles ne sont pas TOAST . Vous devez analyser les données et reconstruire le schéma de la table, mais de toute façon vous devez réécrire Si je devais faire cette tâche, c'est comme ça que je le ferais.

Mais, tout cela parle d'une manière générale. Personne n'a reproduit vos résultats.

Vous avez donné un cas de test que nous ne pouvons pas exécuter

ERROR:  column users.authentication_code does not exist
LINE 24: users.authentication_code,

Et, vous ne nous avez pas dit la version exacte sur laquelle vous vous trouvez.


Merci pour l'explication approfondie, j'ai modifié la question pour inclure la version spécifique.
Andy

4
Le bug est reproductible dans la version 9.6.0
ypercubeᵀᴹ

0

J'ai contourné ce bogue en sauvegardant et en restaurant ma base de données.

Étapes pour Heroku

  • Activez le mode de maintenance: heroku maintenance:on
  • Sauvegarder la base de données: heroku pg:backups:capture
  • Restaurer la base de données: heroku pg:backups:restore
  • Redémarrez l'application: heroku restart
  • Désactivez le mode de maintenance: heroku maintenance:off

0

J'ai aussi rencontré ce bug. Pour ceux qui ne veulent pas sauvegarder / restaurer complètement leur base de données. Sachez que la simple copie de la table fonctionne. Il n'y a cependant aucun moyen "magique" de copier une table. Je l'ai fait en utilisant:

SELECT * INTO mytable_copy FROM mytable;
ALTER TABLE mytable RENAME TO mytable_backup; -- just in case. you never know
ALTER TABLE mytable_copy RENAME TO mytable;

Après cela, vous devrez toujours recréer manuellement vos index, clés étrangères et valeurs par défaut. Recréer la table comme ceci a fait disparaître le bug.

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.