Pourquoi Postgres génère-t-il une valeur PK déjà utilisée?


20

J'utilise Django, et de temps en temps j'obtiens cette erreur:

IntegrityError: la valeur de clé en double viole la contrainte unique "myapp_mymodel_pkey"
DÉTAIL: La clé (id) = (1) existe déjà.

Ma base de données Postgres a en fait un objet myapp_mymodel avec la clé primaire de 1.

Pourquoi Postgres tenterait-il d'utiliser à nouveau cette clé primaire? Ou, est-ce probablement mon application (ou l'ORM de Django) qui cause cela?

Ce problème s'est produit 3 fois de suite à l'instant. Ce que j'ai trouvé est que quand il ne se produit il arrive une ou plusieurs fois de suite pour une table donnée, pas encore. Cela semble se produire pour chaque table avant qu'elle ne s'arrête complètement pendant des jours, se produisant pendant au moins une minute par table lorsqu'elle se produit, et ne se produisant que par intermittence (pas toutes les tables tout de suite).

Le fait que cette erreur soit si intermittente (ne s'est produit qu'environ 3 fois en 2 semaines - aucune autre charge sur la base de données, juste moi testant mon application) est ce qui me rend si méfiant d'un problème de bas niveau.


Django déclare spécifiquement que la clé primaire est générée par le SGBD sauf indication contraire - maintenant, je ne sais pas ce que @orokusaky faisait dans son code python, mais je me suis retrouvé sur cette page parce que je suis assez confiant que je n'ai pas de code essayer d'utiliser une clé primaire spécifique et je n'ai jamais vu un SGBD essayer d'utiliser une mauvaise.
mccc

Réponses:


34

PostgreSQL n'essaiera pas d'insérer des valeurs en double de lui-même, c'est vous (votre application, ORM inclus) qui le fera.

Il peut s'agir soit d'une séquence alimentant les valeurs de l'ensemble PK dans la mauvaise position et le tableau contenant déjà la valeur égale à sa nextval()- soit simplement que votre application fait la mauvaise chose. Le premier est facile à corriger:

SELECT setval('your_sequence_name', (SELECT max(id) FROM your_table));

Le second signifie le débogage.

Django (ou tout autre framework populaire) ne réinitialise pas les séquences par lui-même - sinon nous aurions des questions similaires tous les deux jours.


Est-il intéressant de noter (également basé sur la réponse de @ andi ici) sur les différents niveaux d'isolement? Par exemple, si la deuxième requête arrive avant la fin de la première, est-il possible, étant donné un scénario où je n'utilise pas de transactions, d'insérer un enregistrement qui aboutit à l'obtention max(id)avant la fin de la première requête, puis à ce que les deux aient le même résultat?
orokusaki

5

Vous tentez probablement d'insérer une ligne dans une table pour laquelle la valeur de séquence de colonne série n'est pas mise à jour.

Envisagez de suivre la colonne de votre table qui est la clé primaire définie par Django ORM pour les postgres

id serial NOT NULL

Dont la valeur par défaut est définie sur

nextval('table_name_id_seq'::regclass)

La séquence n'est évaluée que lorsque le champ id est défini comme vide. Mais c'est un problème s'il y a déjà des entrées dans la table.

La question est pourquoi ces entrées précédentes n'ont-elles pas déclenché la mise à jour de la séquence? En effet, la valeur id a été explicitement fournie pour toutes les entrées précédentes.

Dans mon cas, ces entrées initiales ont été chargées à partir des appareils via les migrations.

Ce problème peut également devenir délicat via des entrées personnalisées avec une valeur PK aléatoire.

Dites par exemple. Il y a 10 entrées dans votre tableau. Vous effectuez une entrée explicite avec PK = 15. Les quatre insertions suivantes via le code fonctionneraient parfaitement, mais la 5ème soulèverait une exception.

DETAIL: Key (id)=(15) already exists.

Merci pour ce post. Cela fait longtemps que je débogue un cas comme celui-ci. Cela s'est très rarement produit. Il s'est avéré qu'une fonction d'administration "manuelle" spécifique pouvait insérer des identifiants par elle-même, laissant au compteur d'identité une ancienne valeur. C'est un véritable péril avec "GÉNÉRÉ PAR DÉFAUT COMME IDENTITÉ". J'y réfléchirai à deux fois avant d'utiliser "PAR DÉFAUT" au lieu de "TOUJOURS" la prochaine fois que je définirai une colonne d'identité.
Michael

4

Je me suis retrouvé ici avec la même erreur, qui se produisait rarement et était difficile à suivre, car je ne la cherchais pas où je devais.

Le défaut était la répétition JS qui faisait le POST au serveur deux fois! Donc, parfois, il vaut la peine de jeter un œil non seulement sur vos vues et formulaires django (ou tout autre framework Web), mais aussi sur ce qui se passe très en avant.


1

Ouais chose bizarre. Dans mon cas, quelque chose semble mal lors du chargement des données dans les migrations. J'ai ajouté une migration vide et écrit les lignes pour ajouter des données initiales, 6 enregistrements dans mon cas.

db_alias = schema_editor.connection.alias
bulk = []
for item in items:
    bulk.append(MyModel(
        id=item[0],
        value=item[1],
        slug=item[2],
        name=item[3],
    ))

MyModel.objects.using(db_alias).bulk_create(bulk)

Ensuite, dans le panneau d'administration, j'ai essayé d'ajouter un nouvel élément et j'ai obtenu:

Premier essai:

DETAIL:  Key (id)=(1) already exists.

Tentatives ultérieures:

DETAIL:  Key (id)=(2) already exists.
DETAIL:  Key (id)=(3) already exists.
DETAIL:  Key (id)=(4) already exists.
DETAIL:  Key (id)=(5) already exists.
DETAIL:  Key (id)=(6) already exists.

Et enfin 7e et les fois sont tous réussis

Je dis donc qu'il y a peut-être quelque chose en rapport avec bulk_create car j'ai chargé 6 éléments là-bas. Cela peut être quelque chose de similaire dans votre projet Django à l'origine de cela.

Django 1.9 PostgreSQL 9.3.14

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.