Je gère une application qui a une très grande base de données Oracle (près de 1 To de données avec plus de 500 millions de lignes dans une table). La base de données ne fait vraiment rien (pas de SProcs, pas de déclencheurs ou quoi que ce soit), c'est juste un magasin de données.
Chaque mois, nous devons purger les enregistrements des deux tableaux principaux. Les critères de purge varient et sont une combinaison de l'âge des lignes et de quelques champs d'état. Nous finissons généralement par purger entre 10 et 50 millions de lignes par mois (nous ajoutons environ 3 à 5 millions de lignes par semaine via les importations).
Actuellement, nous devons effectuer cette suppression par lots d'environ 50 000 lignes (par exemple, supprimer 50000, comit, supprimer 50000, valider, répéter). Si vous tentez de supprimer le lot entier en une seule fois, la base de données ne répond plus pendant environ une heure (en fonction du nombre de lignes). La suppression des lignes en lots comme celui-ci est très rude sur le système et nous devons généralement le faire "si le temps le permet" au cours d'une semaine; permettre au script de s'exécuter en continu peut entraîner une dégradation des performances inacceptable pour l'utilisateur.
Je crois que ce type de suppression par lots dégrade également les performances de l'index et a d'autres impacts qui finissent par dégrader les performances de la base de données. Il y a 34 index sur une seule table, et la taille des données d'index est en fait plus grande que les données elles-mêmes.
Voici le script utilisé par l'un de nos informaticiens pour effectuer cette purge:
BEGIN
LOOP
delete FROM tbl_raw
where dist_event_date < to_date('[date]','mm/dd/yyyy') and rownum < 50000;
exit when SQL%rowcount < 49999;
commit;
END LOOP;
commit;
END;
Cette base de données doit être en hausse de 99,99999% et nous n'avons qu'une fenêtre de maintenance de 2 jours une fois par an.
Je cherche une meilleure méthode pour supprimer ces enregistrements, mais je n'en ai pas encore trouvé. Aucune suggestion?
La logique avec 'A' et 'B' peut être "cachée" derrière une colonne virtuelle sur laquelle vous pouvez faire le partitionnement:
alter session set nls_date_format = 'yyyy-mm-dd';
drop table tq84_partitioned_table;
create table tq84_partitioned_table (
status varchar2(1) not null check (status in ('A', 'B')),
date_a date not null,
date_b date not null,
date_too_old date as
( case status
when 'A' then add_months(date_a, -7*12)
when 'B' then date_b
end
) virtual,
data varchar2(100)
)
partition by range (date_too_old)
(
partition p_before_2000_10 values less than (date '2000-10-01'),
partition p_before_2000_11 values less than (date '2000-11-01'),
partition p_before_2000_12 values less than (date '2000-12-01'),
--
partition p_before_2001_01 values less than (date '2001-01-01'),
partition p_before_2001_02 values less than (date '2001-02-01'),
partition p_before_2001_03 values less than (date '2001-03-01'),
partition p_before_2001_04 values less than (date '2001-04-01'),
partition p_before_2001_05 values less than (date '2001-05-01'),
partition p_before_2001_06 values less than (date '2001-06-01'),
-- and so on and so forth..
partition p_ values less than (maxvalue)
);
insert into tq84_partitioned_table (status, date_a, date_b, data) values
('B', date '2008-04-14', date '2000-05-17',
'B and 2000-05-17 is older than 10 yrs, must be deleted');
insert into tq84_partitioned_table (status, date_a, date_b, data) values
('B', date '1999-09-19', date '2004-02-12',
'B and 2004-02-12 is younger than 10 yrs, must be kept');
insert into tq84_partitioned_table (status, date_a, date_b, data) values
('A', date '2000-06-16', date '2010-01-01',
'A and 2000-06-16 is older than 3 yrs, must be deleted');
insert into tq84_partitioned_table (status, date_a, date_b, data) values
('A', date '2009-06-09', date '1999-08-28',
'A and 2009-06-09 is younger than 3 yrs, must be kept');
select * from tq84_partitioned_table order by date_too_old;
-- drop partitions older than 10 or 3 years, respectively:
alter table tq84_partitioned_table drop partition p_before_2000_10;
alter table tq84_partitioned_table drop partition p_before_2000_11;
alter table tq84_partitioned_table drop partition p2000_12;
select * from tq84_partitioned_table order by date_too_old;
La solution classique est de partitionner vos tables, par ex. par mois ou par semaine. Si vous ne les avez jamais rencontrés auparavant, une table partitionnée est comme plusieurs tables structurées de manière identique avec un UNION
implicite lors de la sélection, et Oracle stockera automatiquement une ligne dans la partition appropriée lors de son insertion en fonction des critères de partitionnement. Vous mentionnez les index - eh bien, chaque partition obtient également ses propres index partitionnés. C'est une opération très bon marché dans Oracle de supprimer une partition (c'est analogue à un TRUNCATE
en termes de charge car c'est ce que vous faites vraiment - tronquer ou supprimer une de ces sous-tables invisibles). Ce sera une quantité importante de traitement à répartir "après coup", mais cela n'a aucun sens de pleurer sur le lait renversé - les avantages de le faire l'emportent jusqu'à présent sur les coûts. Chaque mois, vous diviseriez la partition supérieure pour créer une nouvelle partition pour les données du mois suivant (vous pouvez facilement automatiser celle-ci avec un DBMS_JOB
).
Et avec les partitions, vous pouvez également exploiter requête parallèle et élimination de partition , ce qui devrait rendre vos utilisateurs très heureux ...
Un aspect à considérer est la proportion des performances de suppression des index et celle de la table brute. Chaque enregistrement supprimé de la table nécessite la même suppression de la ligne de chaque index btree. Si vous avez plus de 30 index btree, je pense que la plupart de votre temps est consacré à la maintenance des index.
Cela a un impact sur l'utilité du partitionnement. Disons que vous avez un index sur le nom. Un index Btree standard, tout en un segment, peut avoir à effectuer quatre sauts pour passer du bloc racine au bloc feuille et une cinquième lecture pour obtenir la ligne. Si cet index est partitionné en 50 segments et que vous n'avez pas la clé de partition dans le cadre de la requête, chacun de ces 50 segments devra être vérifié. Chaque segment sera plus petit, vous n'aurez donc peut-être qu'à effectuer 2 sauts, mais vous pouvez toujours finir par faire 100 lectures au lieu des 5 précédentes.
S'il s'agit d'index bitmap, les équations sont différentes. Vous n'utilisez probablement pas d'index pour identifier des lignes individuelles, mais plutôt des ensembles d'entre elles. Ainsi, plutôt qu'une requête utilisant 5 E/S pour renvoyer un seul enregistrement, elle utilisait 10 000 E/S. En tant que tel, la surcharge supplémentaire dans les partitions supplémentaires pour l'index n'aura pas d'importance.
la suppression de 50 millions d'enregistrements par mois par lots de 50 000 n'est que 1 000 itérations. si vous supprimez 1 toutes les 30 minutes, cela devrait répondre à vos besoins. une tâche planifiée pour exécuter la requête que vous avez publiée mais supprimez la boucle afin qu'elle ne s'exécute qu'une seule fois ne devrait pas entraîner une dégradation sensible pour les utilisateurs. Nous faisons à peu près le même volume d'enregistrements dans notre usine de fabrication qui fonctionne à peu près 24h/24 et 7j/7 et qui répond à nos besoins. Nous l'étalons en fait un peu plus de 10 000 enregistrements toutes les 10 minutes, qui s'exécutent en environ 1 ou 2 secondes sur nos serveurs Oracle Unix.
Si l'espace disque n'est pas limité, vous pouvez créer une copie "de travail" de la table, par exemple my_table_new
, en utilisant CTAS (Create Table As Select) avec des critères qui omettraient les enregistrements à supprimer. Vous pouvez faire l'instruction create en parallèle et avec l'indicateur d'ajout pour la rendre rapide, puis créer tous vos index. Ensuite, une fois terminé, (et testé), renommez la table existante en my_table_old
et renommez la table "work" en my_table
. Une fois que vous êtes à l'aise avec tout drop my_table_old purge
pour se débarrasser de l'ancienne table. S'il y a un tas de restrictions de clés étrangères, jetez un œil à dbms_redefinition
package PL/SQL . Il clonera vos index, contraintes, etc. lors de l'utilisation des options appropriées. Ceci est un résumé d'une suggestion de Tom Kyte de AskTom renommée. Après la première exécution, vous pouvez tout automatiser, et la table de création devrait aller beaucoup plus vite, et peut être effectuée pendant que le système est en marche, et le temps d'arrêt des applications serait limité à moins d'une minute pour renommer les tables. L'utilisation de CTAS sera beaucoup plus rapide que plusieurs suppressions de lots. Cette approche peut être particulièrement utile si vous n'avez pas de licence de partitionnement.
Exemple de CTAS, en conservant les lignes avec les données des 365 derniers jours et flag_inactive = 'N'
:
create /*+ append */ table my_table_new
tablespace data as
select /*+ parallel */ * from my_table
where some_date >= sysdate -365
and flag_inactive = 'N';
-- test out my_table_new. then if all is well:
alter table my_table rename to my_table_old;
alter table my_table_new rename to my_table;
-- test some more
drop table my_table_old purge;
lors de la suppression d'une partition, vous laissez les index globaux inutilisables, qui doivent être reconstruits, la reconstruction des index globaux serait un gros problème, comme si vous le faites en ligne, ce sera assez lent, sinon vous avez besoin de temps d'arrêt. dans les deux cas, ne peut pas répondre à l'exigence.
"Nous finissons généralement par purger entre 10 et 50 millions de lignes par mois"
je recommanderais d'utiliser la suppression par lots PL/SQL, plusieurs heures est ok je pense.