Je dois ajouter une contrainte unique à une table existante. C'est bien, sauf que la table a déjà des millions de lignes et que beaucoup d'entre elles violent la contrainte unique que je dois ajouter.
Quelle est l’approche la plus rapide pour supprimer les lignes incriminées? J'ai une instruction SQL qui trouve les doublons et les supprime, mais cela prend une éternité pour s'exécuter. Y a-t-il un autre moyen de résoudre ce problème? Peut-être sauvegarder la table, puis restaurer après l'ajout de la contrainte?
Par exemple, vous pourriez:
CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;
Certaines de ces approches semblent un peu compliquées, et je le fais généralement comme:
Étant donné la table table
, souhaitez l’unifier sur (champ1, champ2) en conservant la ligne avec le champ max3:
DELETE FROM table USING table alias
WHERE table.field1 = alias.field1 AND table.field2 = alias.field2 AND
table.max_field < alias.max_field
Par exemple, j'ai une table, user_accounts
, et je veux ajouter une contrainte unique au courrier électronique, mais j'ai des doublons. Dites aussi que je veux conserver le dernier créé (max id parmi les doublons).
DELETE FROM user_accounts USING user_accounts ua2
WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
USING
n'est pas un langage SQL standard, c'est une extension PostgreSQL (mais une très utile), mais la question d'origine mentionne spécifiquement PostgreSQL.Au lieu de créer une nouvelle table, vous pouvez également réinsérer des lignes uniques dans la même table après l'avoir tronquée. Faites tout en une seule transaction . Vous pouvez éventuellement supprimer la table temporaire à la fin de la transaction automatiquement avec ON COMMIT DROP
. Voir ci-dessous.
Cette approche n'est utile que lorsqu'il y a beaucoup de lignes à supprimer de la table. Pour quelques doublons, utilisez un simple DELETE
.
Vous avez mentionné des millions de lignes. Pour que l'opération soit rapide , vous souhaitez allouer suffisamment de mémoires tampons temporaires pour la session. Le réglage doit être ajusté avant tout tampon temporaire est utilisé dans votre session en cours. Découvrez la taille de votre table:
SELECT pg_size_pretty(pg_relation_size('tbl'));
Ensemble temp_buffers
en conséquence. Arrondissez généreusement car la représentation en mémoire nécessite un peu plus de RAM.
SET temp_buffers = 200MB; -- example value
BEGIN;
-- CREATE TEMPORARY TABLE t_tmp ON COMMIT DROP AS -- drop temp table at commit
CREATE TEMPORARY TABLE t_tmp AS -- retain temp table after commit
SELECT DISTINCT * FROM tbl; -- DISTINCT folds duplicates
TRUNCATE tbl;
INSERT INTO tbl
SELECT * FROM t_tmp;
-- ORDER BY id; -- optionally "cluster" data while being at it.
COMMIT;
Cette méthode peut être supérieure à la création d'une nouvelle table si des objets dépendants existent. Vues, index, clés étrangères ou autres objets faisant référence à la table. TRUNCATE
vous oblige de toute façon à commencer avec une ardoise vierge (nouveau fichier en arrière-plan) et est beaucoup plus rapide que DELETE FROM tbl
avec de grandes tables (DELETE
peut être plus rapide avec de petites tables).
Pour les grandes tables, il est régulièrement plus rapide de supprimer les index et les clés étrangères, de remplir la table et de recréer ces objets. En ce qui concerne les contraintes de fk, vous devez bien sûr être certain que les nouvelles données sont valables, sinon vous rencontrerez une exception si vous essayez de créer le fk.
Notez que TRUNCATE
requiert un verrouillage plus agressif que DELETE
. Cela peut poser un problème pour les tables à forte charge simultanée.
Si TRUNCATE
n’est pas une option ou généralement pour les tables petites à moyennes , il existe une technique similaire avec un modification des données). CTE (Postgres 9.1 +):
WITH del AS (DELETE FROM tbl RETURNING *)
INSERT INTO tbl
SELECT DISTINCT * FROM del;
-- ORDER BY id; -- optionally "cluster" data while being at it.
Plus lent pour les grandes tables, car TRUNCATE
est plus rapide là-bas. Mais peut être plus rapide (et plus simple!) Pour les petites tables.
Si vous n'avez aucun objet dépendant du tout, vous pouvez créer une nouvelle table et supprimer l'ancienne, mais vous ne gagnerez presque rien par rapport à cette approche universelle.
Pour les très grandes tables qui ne rentrent pas dans la mémoire vive disponible , la création d'une table nouvelle sera considérablement plus rapide. Vous devrez peser cela par rapport à d'éventuels problèmes/frais généraux avec des objets dépendants.
Vous pouvez utiliser oid ou ctid, qui est normalement une colonne "non visible" dans la table:
DELETE FROM table
WHERE ctid NOT IN
(SELECT MAX(s.ctid)
FROM table s
GROUP BY s.column_has_be_distinct);
La fonction de fenêtre PostgreSQL est pratique pour résoudre ce problème.
DELETE FROM tablename
WHERE id IN (SELECT id
FROM (SELECT id,
row_number() over (partition BY column1, column2, column3 ORDER BY id) AS rnum
FROM tablename) t
WHERE t.rnum > 1);
Voir Supprimer les doublons.
Requête généralisée pour supprimer les doublons:
DELETE FROM table_name
WHERE ctid NOT IN (
SELECT max(ctid) FROM table_name
GROUP BY column1, [column 2, ...]
);
La colonne ctid
est une colonne spéciale disponible pour chaque table, mais non visible sauf mention contraire. La valeur de la colonne ctid
est considérée comme unique pour chaque ligne d'une table.
De ne ancienne liste de diffusion postgresql.org :
create table test ( a text, b text );
insert into test values ( 'x', 'y');
insert into test values ( 'x', 'x');
insert into test values ( 'y', 'y' );
insert into test values ( 'y', 'x' );
insert into test values ( 'x', 'y');
insert into test values ( 'x', 'x');
insert into test values ( 'y', 'y' );
insert into test values ( 'y', 'x' );
insert into test values ( 'x', 'y');
select oid, a, b from test;
select o.oid, o.a, o.b from test o
where exists ( select 'x'
from test i
where i.a = o.a
and i.b = o.b
and i.oid < o.oid
);
Remarque: PostgreSQL ne supporte pas les alias sur la table mentionnée dans la clause from
d’une suppression.
delete from test
where exists ( select 'x'
from test i
where i.a = test.a
and i.b = test.b
and i.oid < test.oid
);
Je viens d'utiliser réponse d'Erwin Brandstetter avec succès pour supprimer les doublons dans une table de jointure (une table dépourvue de ses propres ID principaux), mais j'ai constaté qu'il y avait une mise en garde importante.
Comprenant ON COMMIT DROP
_ signifie que la table temporaire sera supprimée à la fin de la transaction. Pour moi, cela signifiait que la table temporaire n'était plus disponible au moment où je suis allée l'insérer!
Je viens de faire CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl;
et tout a bien fonctionné.
La table temporaire est supprimée à la fin de la session.
Tout d’abord, vous devez décider lequel de vos "doublons" vous allez conserver. Si toutes les colonnes sont égales, vous pouvez supprimer l’une d’entre elles ... Mais vous voulez peut-être ne conserver que le critère le plus récent ou un autre?
Le moyen le plus rapide dépend de votre réponse à la question ci-dessus, ainsi que du% de doublons sur la table. Si vous jetez 50% de vos lignes, vous feriez mieux de faire CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;
, et si vous supprimez 1% des lignes, utiliser DELETE est préférable.
Également pour les opérations de maintenance comme celle-ci, il est généralement bon de définir work_mem
sur une bonne partie de votre RAM: lancez EXPLAIN, vérifiez le nombre N de tris/hachages, et définissez work_mem sur votre RAM/2/N. Utilisez beaucoup de RAM; c'est bon pour tant que vous n’avez qu’une connexion simultanée ...
DELETE FROM table
WHERE something NOT IN
(SELECT MAX(s.something)
FROM table As s
GROUP BY s.this_thing, s.that_thing);
Cette fonction supprime les doublons sans supprimer les index et le fait dans n'importe quelle table.
Utilisation: select remove_duplicates('mytable');
--- --- remove_duplicates (nom_table) supprime les enregistrements en double d'une table (conversion d'un jeu unique) --- CREATE OR REPLACE FUNCTION remove_duplicates (text) RETURNS void AS $$ DECLARE Nom_table ALIAS FOR $ 1; COMMENCER EXÉCUTER 'CRÉER UNE TABLE TEMPORAIRE _DISTINCT_' || nom_table || 'AS (SELECT DISTINCT * FROM' || nom_table || ');'; EXECUTE 'DELETE FROM' || nom_table || ';'; EXECUTE 'INSERT INTO' || nom_table || '(SELECT * FROM _DISTINCT_' || tablename || ');'; EXECUTE 'DROP TABLE _DISTINCT_' || nom_table || ';'; RETURN; END; $$ LANGUAGE plpgsql;
Si vous avez seulement une ou quelques entrées dupliquées et qu'elles sont bien dupliquées (c'est-à-dire qu'elles apparaissent deux fois), vous pouvez utiliser la colonne "hidden" ctid
, comme proposé ci-dessus. , avec LIMIT
:
DELETE FROM mytable WHERE ctid=(SELECT ctid FROM mytable WHERE […] LIMIT 1);
Cela ne supprimera que la première des lignes sélectionnées.
Cela fonctionne très bien et est très rapide:
CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;
DELETE FROM tablename
WHERE id IN (SELECT id
FROM (SELECT id,ROW_NUMBER() OVER (partition BY column1, column2, column3 ORDER BY id) AS rnum
FROM tablename) t
WHERE t.rnum > 1);
Supprimer les doublons par colonne (s) et conserver la ligne avec l'id le plus bas. Le motif est pris dans le postgres wiki
En utilisant les CTE, vous pouvez obtenir une version plus lisible de ce qui précède grâce à cette
WITH duplicate_ids as (
SELECT id, rnum
FROM num_of_rows
WHERE rnum > 1
),
num_of_rows as (
SELECT id,
ROW_NUMBER() over (partition BY column1,
column2,
column3 ORDER BY id) AS rnum
FROM tablename
)
DELETE FROM tablename
WHERE id IN (SELECT id from duplicate_ids)
CREATE TABLE test (col text);
INSERT INTO test VALUES
('1'),
('2'), ('2'),
('3'),
('4'), ('4'),
('5'),
('6'), ('6');
DELETE FROM test
WHERE ctid in (
SELECT t.ctid FROM (
SELECT row_number() over (
partition BY col
ORDER BY col
) AS rnum,
ctid FROM test
ORDER BY col
) t
WHERE t.rnum >1);
Je travaille avec PostgreSQL 8.4. Lorsque j'ai exécuté le code proposé, j'ai constaté qu'il ne supprimait pas les doublons. Lors de l'exécution de certains tests, j'ai constaté que l'ajout de "DISTINCT ON (duplicate_column_name)" et de "ORDER BY duplicate_column_name" faisait l'affaire. Je ne suis pas un gourou du langage SQL, je l’ai trouvé dans la doc SELECT ... DISTINCT PostgreSQL 8.4.
CREATE OR REPLACE FUNCTION remove_duplicates(text, text) RETURNS void AS $$
DECLARE
tablename ALIAS FOR $1;
duplicate_column ALIAS FOR $2;
BEGIN
EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_' || tablename || ' AS (SELECT DISTINCT ON (' || duplicate_column || ') * FROM ' || tablename || ' ORDER BY ' || duplicate_column || ' ASC);';
EXECUTE 'DELETE FROM ' || tablename || ';';
EXECUTE 'INSERT INTO ' || tablename || ' (SELECT * FROM _DISTINCT_' || tablename || ');';
EXECUTE 'DROP TABLE _DISTINCT_' || tablename || ';';
RETURN;
END;
$$ LANGUAGE plpgsql;