
Aujourd'hui, je souhaite présenter aux lecteurs de Habr un utilitaire écrit en Python pour travailler avec les dépendances des tables dans la base de données PostgreSQL.
L'API de l'utilitaire est simple et se compose de trois méthodes :
- archive_table â archivage/rĂ©cursif de la suppression des lignes avec les Primary Keys spĂ©cifiĂ©es
- get_table_references â recherche des dĂ©pendances d'une table (affiche les tables auxquelles la table spĂ©cifiĂ©e fait rĂ©fĂ©rence et celles qui y font rĂ©fĂ©rence)
- get_rows_references â recherche des lignes dans d'autres tables qui font rĂ©fĂ©rence aux lignes spĂ©cifiĂ©es dans la table souhaitĂ©e
Contexte
Je m'appelle Oleg Borzov, je suis dĂ©veloppeur dans l'Ă©quipe CRM pour les gestionnaires de prĂȘts hypothĂ©caires chez Domklik.
La base de données principale de notre systÚme CRM est l'une des plus volumineuses de l'entreprise. C'est aussi l'une des plus anciennes : elle est apparue dÚs le lancement du projet, lorsque les arbres étaient grands, que Domklik était une startup, et qu'au lieu d'un microservice sur un cadre Python asynchrone tendance, il y avait un énorme monolithe en PHP.
Le passage de PHP à Python a été trÚs long et a nécessité le soutien simultané des deux systÚmes, ce qui a eu un impact sur la conception de la base de données.
En consĂ©quence, nous avons une base de donnĂ©es avec un grand nombre de tables fortement liĂ©es et de taille Ă©norme, avec beaucoup d'index pour diffĂ©rents types de requĂȘtes. Tout cela affecte nĂ©gativement les performances de la base de donnĂ©es : en raison des grandes tables et des nombreuses relations entre elles, la complexitĂ© des requĂȘtes augmente constamment, ce qui est particuliĂšrement critique pour les tables les plus sollicitĂ©es.
Pour réduire la charge sur la base de données, nous avons décidé d'écrire un script qui déplacerait quotidiennement les anciennes entrées des tables les plus volumineuses et les plus sollicitées dans les tables d'archives (par exemple, depuis task dans task_archive).
Cette tĂąche est compliquĂ©e par le grand nombre de relations entre les tables : il ne suffit pas de dĂ©placer les lignes depuis task dans task_archive il faut au prĂ©alable faire la mĂȘme chose rĂ©cursivement avec toutes les tables qui y font rĂ©fĂ©rence. task Je vais dĂ©montrer avec l'exemple de
la base de données de démonstration du site postgrespro.ru :

Flights . De maniÚre simple, Postgres ne nous permettra pas de le faire : nous devons d'abord supprimer les enregistrements de toutes les tables qui y font référence, et cela de maniÚre récursive jusqu'aux tables auxquelles personne ne fait référence.Dans notre exemple, sur
Ticket_flights . De maniĂšre simple, Postgres ne nous permettra pas de le faire : nous devons d'abord supprimer les enregistrements de toutes les tables qui y font rĂ©fĂ©rence, et cela de maniĂšre rĂ©cursive jusqu'aux tables auxquelles personne ne fait rĂ©fĂ©rence. fait rĂ©fĂ©rence Ă , et celle-ci âBoarding_passes Donc, il faut supprimer dans cet ordre :.
Nous récupérons les valeurs des clés primaires (Primary Keys, PK) des lignes dans
- , qui font référence aux lignes à supprimer dans
, et celle-ci âNous obtenons les PK des lignes. De maniĂšre simple, Postgres ne nous permettra pas de le faire : nous devons d'abord supprimer les enregistrements de toutes les tables qui y font rĂ©fĂ©rence, et cela de maniĂšre rĂ©cursive jusqu'aux tables auxquelles personne ne fait rĂ©fĂ©rence.. - , qui font rĂ©fĂ©rence Ă
Donc, il faut supprimer dans cet ordre :, qui font rĂ©fĂ©rence Ă, et celle-ci â. - Nous supprimons les lignes par PK de la p.2 dans le tableau
Donc, il faut supprimer dans cet ordre :. - Nous supprimons les lignes par PK de la p.1 dans
, et celle-ci â. - Nous supprimons les lignes de
. De maniÚre simple, Postgres ne nous permettra pas de le faire : nous devons d'abord supprimer les enregistrements de toutes les tables qui y font référence, et cela de maniÚre récursive jusqu'aux tables auxquelles personne ne fait référence..
Le résultat est un utilitaire nommé PgGraph, que nous avons décidé de rendre open source.
Comment utiliser
L'utilitaire prend en charge deux modes d'utilisation :
- Appel depuis la ligne de commande (
pggraph âŠ). - Utilisation dans le code Python (classe
PgGraphApi).
Installation et configuration
Tout d'abord, il faut installer l'utilitaire depuis le dépÎt Pypi :
pip3 install pggraphEnsuite, créez un fichier config.ini sur votre machine locale avec la configuration de la base de données et du script d'archivage :
[db]
host = localhost
port = 5432
user = postgres
password = postgres
dbname = postgres
schema = public ; ParamÚtre optionnel, valeur par défaut indiquée
[archive] ; Cette section est facultative, valeurs par défaut indiquées ci-dessous
is_debug = false
chunk_size = 1000
max_depth = 20
to_archive = true
archive_suffix = 'archive'Lancement depuis la console
ParamĂštres
$ pggraph -h
usage: pggraph action [-h] --table TABLE [--ids IDS] [--config_path CONFIG_PATH]
arguments positionnels :
action action requise : archive_table, get_table_references, get_rows_references
arguments optionnels :
-h, --help afficher ce message d'aide et quitter
--table TABLE nom de la table
--ids IDS ids de clés primaires, séparés par des virgules, par exemple 1,2,3
--config_path CONFIG_PATH chemin vers config.ini
--log_path LOG_PATH chemin vers le répertoire des logs
--log_level LOG_LEVEL niveau de log (debug, info, error)Arguments positionnels :
actionâ action requise :archive_table,get_table_referencesouget_rows_references.
Arguments nommés :
--config_pathâ chemin vers le fichier de configuration ;--tableâ table sur laquelle effectuer l'action ;--idsâ liste d'id sĂ©parĂ©s par des virgules, par exemple,1,2,3(paramĂštre optionnel);--log_pathâ chemin vers le dossier des logs (paramĂštre optionnel, par dĂ©faut â dossier personnel);--log_levelâ niveau de journalisation (paramĂštre optionnel, par dĂ©faut â INFO).
Exemples de commandes
Archivage de la table
La fonction principale de l'utilitaire est l'archivage des données, c'est-à -dire le déplacement des lignes de la table principale vers la table d'archive (par exemple, de la table books dans books_archive).
La suppression sans archivage est également prise en charge : pour cela, il faut définir le paramÚtre dans config.ini to_archive = false).
Les paramĂštres obligatoires sont â config_path, table et ids.
AprÚs le lancement, les enregistrements seront supprimés de maniÚre récursive ids dans la table table et dans toutes les tables qui y font référence.
$ pggraph archive_table --config_path config.hw.local.ini --table flights --ids 1,2,3
2020-06-20 19:27:44 INFO: flights - DĂBUT
2020-06-20 19:27:44 INFO: flights - début archive_recursive 3 lignes (profondeur=0)
2020-06-20 19:27:44 INFO: DĂBUT ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: ticket_flights - début archive_recursive 3 lignes (profondeur=1)
2020-06-20 19:27:44 INFO: DĂBUT ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: boarding_passes - début archive_recursive 3 lignes (profondeur=2)
2020-06-20 19:27:44 INFO: DĂBUT ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: FIN ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: boarding_passes - archive_by_ids 3 lignes par ticket_no, flight_id
2020-06-20 19:27:44 INFO: boarding_passes - début archive_recursive 3 lignes (profondeur=2)
2020-06-20 19:27:44 INFO: DĂBUT ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: FIN ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: boarding_passes - archive_by_ids 3 lignes par ticket_no, flight_id
2020-06-20 19:27:44 INFO: boarding_passes - début archive_recursive 3 lignes (profondeur=2)
2020-06-20 19:27:44 INFO: DĂBUT ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: FIN ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: boarding_passes - archive_by_ids 3 lignes par ticket_no, flight_id
2020-06-20 19:27:44 INFO: boarding_passes - début archive_recursive 3 lignes (profondeur=2)
2020-06-20 19:27:44 INFO: DĂBUT ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: FIN ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: boarding_passes - archive_by_ids 3 lignes par ticket_no, flight_id
2020-06-20 19:27:44 INFO: FIN ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: ticket_flights - archive_by_ids 3 lignes par ticket_no, flight_id
2020-06-20 19:27:44 INFO: FIN ARCHIVE DES TABLES DE RĂFĂRENCE
2020-06-20 19:27:44 INFO: flights - archive_by_ids 3 lignes par id
2020-06-20 19:27:44 INFO: flights - FINRecherche des dépendances pour la table spécifiée
Fonction de recherche des dĂ©pendances de la table spĂ©cifiĂ©e table. Les paramĂštres obligatoires sont â config_path et table.
AprĂšs le lancement, un dictionnaire sera affichĂ© Ă l'Ă©cran oĂč :
in_refsâ dictionnaire des tables renvoyant Ă celle-ci, oĂč la clĂ© est le nom de la table, la valeur est une liste d'objets Foreign Key (pk_mainâ clĂ© primaire dans la table principale,pk_refâ clĂ© primaire dans la table rĂ©fĂ©rencĂ©e,fk_refâ nom de la colonne Ă©tant la clĂ© Ă©trangĂšre vers la table source) ;out_refsâ dictionnaire des tables vers lesquelles cette table renvoie.
$ pggraph get_table_references --config_path config.hw.local.ini --table flights
{'in_refs': {'ticket_flights': [ForeignKey(pk_main='flight_id', pk_ref='ticket_no, flight_id', fk_ref='flight_id')]},
'out_refs': {'aircrafts': [ForeignKey(pk_main='aircraft_code', pk_ref='flight_id', fk_ref='aircraft_code')],
'airports': [ForeignKey(pk_main='airport_code', pk_ref='flight_id', fk_ref='arrival_airport'),
ForeignKey(pk_main='airport_code', pk_ref='flight_id', fk_ref='departure_airport')]}}Recherche des liens vers les lignes avec les Primary Key spécifiés
Fonction de recherche des lignes dans d'autres tables qui renvoient via Foreign Key Ă des lignes ids de la table table. Les paramĂštres obligatoires sont â config_path, table et ids.
AprÚs le lancement, un dictionnaire sera affiché à l'écran avec la structure suivante :
{
pk_id_1: {
reffering_table_name_1: {
foreign_key_1: [
{row_pk_1: value, row_pk_2: value},
...
],
...
},
...
},
pk_id_2: {...},
...
}Exemple d'appel :
$ pggraph get_rows_references --config_path config.hw.local.ini --table flights --ids 1,2,3
{1: {'ticket_flights': {'flight_id': [{'flight_id': 1,
'ticket_no': '0005432816945'},
{'flight_id': 1,
'ticket_no': '0005432816941'}]}},
2: {'ticket_flights': {'flight_id': [{'flight_id': 2,
'ticket_no': '0005433101832'},
{'flight_id': 2,
'ticket_no': '0005433101864'},
{'flight_id': 2,
'ticket_no': '0005432919715'}]}},
3: {'ticket_flights': {'flight_id': [{'flight_id': 3,
'ticket_no': '0005432817560'},
{'flight_id': 3,
'ticket_no': '0005432817568'},
{'flight_id': 3,
'ticket_no': '0005432817559'}]}}}Utilisation dans le code
Outre l'exĂ©cution dans la console, la bibliothĂšque peut ĂȘtre utilisĂ©e dans du code Python. Voici des exemples d'appel dans un environnement interactif iPython.
Archivage de la table
>>> from pg_graph.main import setup_logging
>>> setup_logging(log_level='DEBUG')
>>> from pg_graph.api import PgGraphApi
>>> api = PgGraphApi('config.hw.local.ini')
>>> api.archive_table('flights', [4,5])
2020-06-20 23:12:08 INFO: flights - DĂBUT
2020-06-20 23:12:08 INFO: flights - commencer archive_recursive 2 lignes (profondeur=0)
2020-06-20 23:12:08 INFO: DĂBUT ARCHIVE DES TABLES RĂFĂRENCES
2020-06-20 23:12:08 DEBUG: ticket_flights - ForeignKey(pk_main='flight_id', pk_ref='flight_id, ticket_no', fk_ref='flight_id')
2020-06-20 23:12:08 DEBUG: SQL('SELECT flight_id, ticket_no FROM bookings.ticket_flights WHERE (flight_id) IN (%s, %s)')
2020-06-20 23:12:08 INFO: ticket_flights - commencer archive_recursive 30 lignes (profondeur=1)
2020-06-20 23:12:08 INFO: DĂBUT ARCHIVE DES TABLES RĂFĂRENCES
2020-06-20 23:12:08 DEBUG: boarding_passes - ForeignKey(pk_main='flight_id, ticket_no', pk_ref='flight_id, ticket_no', fk_ref='flight_id, ticket_no')
2020-06-20 23:12:08 INFO: boarding_passes - archive_by_fk 30 lignes par ForeignKey(pk_main='flight_id, ticket_no', pk_ref='flight_id, ticket_no', fk_ref='flight_id, ticket_no')
2020-06-20 23:12:08 DEBUG: SQL('CREATE TABLE IF NOT EXISTS bookings.boarding_passes_archive (LIKE bookings.boarding_passes)')
2020-06-20 23:12:08 DEBUG: DELETE FROM boarding_passes by FK flight_id, ticket_no - 30 lignes
2020-06-20 23:12:08 INFO: FIN ARCHIVE DES TABLES RĂFĂRENCES
2020-06-20 23:12:08 INFO: ticket_flights - archive_by_ids 30 lignes par flight_id, ticket_no
2020-06-20 23:12:08 DEBUG: SQL('CREATE TABLE IF NOT EXISTS bookings.ticket_flights_archive (LIKE bookings.ticket_flights)')
2020-06-20 23:12:08 DEBUG: DELETE FROM ticket_flights by flight_id, ticket_no - 30 lignes
2020-06-20 23:12:08 DEBUG: INSERT INTO ticket_flights_archive - 30 lignes
2020-06-20 23:12:08 INFO: ticket_flights - commencer archive_recursive 30 lignes (profondeur=1)
2020-06-20 23:12:08 INFO: DĂBUT ARCHIVE DES TABLES RĂFĂRENCES
2020-06-20 23:12:08 DEBUG: boarding_passes - ForeignKey(pk_main='flight_id, ticket_no', pk_ref='flight_id, ticket_no', fk_ref='flight_id, ticket_no')
2020-06-20 23:12:08 INFO: boarding_passes - archive_by_fk 30 lignes par ForeignKey(pk_main='flight_id, ticket_no', pk_ref='flight_id, ticket_no', fk_ref='flight_id, ticket_no')
2020-06-20 23:12:08 DEBUG: SQL('CREATE TABLE IF NOT EXISTS bookings.boarding_passes_archive (LIKE bookings.boarding_passes)')
2020-06-20 23:12:08 DEBUG: DELETE FROM boarding_passes by FK flight_id, ticket_no - 30 lignes
2020-06-20 23:12:08 INFO: FIN ARCHIVE DES TABLES RĂFĂRENCES
2020-06-20 23:12:08 INFO: ticket_flights - archive_by_ids 30 lignes par flight_id, ticket_no
2020-06-20 23:12:08 DEBUG: SQL('CREATE TABLE IF NOT EXISTS bookings.ticket_flights_archive (LIKE bookings.ticket_flights)')
2020-06-20 23:12:08 DEBUG: DELETE FROM ticket_flights by flight_id, ticket_no - 30 lignes
2020-06-20 23:12:08 DEBUG: INSERT INTO ticket_flights_archive - 30 lignes
2020-06-20 23:12:08 INFO: ticket_flights - commencer archive_recursive 30 lignes (profondeur=1)
2020-06-20 23:12:08 INFO: DĂBUT ARCHIVE DES TABLES RĂFĂRENCES
2020-06-20 23:12:08 DEBUG: boarding_passes - ForeignKey(pk_main='flight_id, ticket_no', pk_ref='flight_id, ticket_no', fk_ref='flight_id, ticket_no')
2020-06-20 23:12:08 INFO: boarding_passes - archive_by_fk 30 lignes par ForeignKey(pk_main='flight_id, ticket_no', pk_ref='flight_id, ticket_no', fk_ref='flight_id, ticket_no')
2020-06-20 23:12:08 DEBUG: SQL('CREATE TABLE IF NOT EXISTS bookings.boarding_passes_archive (LIKE bookings.boarding_passes)')
2020-06-20 23:12:08 DEBUG: DELETE FROM boarding_passes by FK flight_id, ticket_no - 30 lignes
2020-06-20 23:12:08 INFO: FIN ARCHIVE DES TABLES RĂFĂRENCES
2020-06-20 23:12:08 INFO: ticket_flights - archive_by_ids 30 lignes par flight_id, ticket_no
2020-06-20 23:12:08 DEBUG: SQL('CREATE TABLE IF NOT EXISTS bookings.ticket_flights_archive (LIKE bookings.ticket_flights)')
2020-06-20 23:12:08 DEBUG: DELETE FROM ticket_flights by flight_id, ticket_no - 30 lignes
2020-06-20 23:12:08 DEBUG: INSERT INTO ticket_flights_archive - 30 lignes
2020-06-20 23:12:08 INFO: ticket_flights - commencer archive_recursive 3 lignes (profondeur=1)
2020-06-20 23:12:08 INFO: DĂBUT ARCHIVE DES TABLES RĂFĂRENCES
2020-06-20 23:12:08 DEBUG: boarding_passes - ForeignKey(pk_main='flight_id, ticket_no', pk_ref='flight_id, ticket_no', fk_ref='flight_id, ticket_no')
2020-06-20 23:12:08 INFO: boarding_passes - archive_by_fk 3 lignes par ForeignKey(pk_main='flight_id, ticket_no', pk_ref='flight_id, ticket_no', fk_ref='flight_id, ticket_no')
2020-06-20 23:12:08 DEBUG: SQL('CREATE TABLE IF NOT EXISTS bookings.boarding_passes_archive (LIKE bookings.boarding_passes)')
2020-06-20 23:12:08 DEBUG: DELETE FROM boarding_passes by FK flight_id, ticket_no - 3 lignes
2020-06-20 23:12:08 INFO: FIN ARCHIVE DES TABLES RĂFĂRENCES
2020-06-20 23:12:08 INFO: ticket_flights - archive_by_ids 3 lignes par flight_id, ticket_no
2020-06-20 23:12:08 DEBUG: SQL('CREATE TABLE IF NOT EXISTS bookings.ticket_flights_archive (LIKE bookings.ticket_flights)')
2020-06-20 23:12:08 DEBUG: DELETE FROM ticket_flights by flight_id, ticket_no - 3 lignes
2020-06-20 23:12:08 DEBUG: INSERT INTO ticket_flights_archive - 3 lignes
2020-06-20 23:12:08 INFO: FIN ARCHIVE DES TABLES RĂFĂRENCES
2020-06-20 23:12:08 INFO: flights - archive_by_ids 2 lignes par flight_id
2020-06-20 23:12:09 DEBUG: SQL('CREATE TABLE IF NOT EXISTS bookings.flights_archive (LIKE bookings.flights)')
2020-06-20 23:12:09 DEBUG: DELETE FROM flights by flight_id - 2 lignes
2020-06-20 23:12:09 DEBUG: INSERT INTO flights_archive - 2 lignes
2020-06-20 23:12:09 INFO: flights - FINRecherche des dépendances pour la table spécifiée
>>> from pg_graph.api import PgGraphApi
>>> from pprint import pprint
>>> api = PgGraphApi('config.hw.local.ini')
>>> res = api.get_table_references('flights')
>>> pprint(res)
{'in_refs': {'ticket_flights': [ForeignKey(pk_main='flight_id', pk_ref='flight_id, ticket_no', fk_ref='flight_id')]},
'out_refs': {'aircrafts': [ForeignKey(pk_main='aircraft_code', pk_ref='flight_id', fk_ref='aircraft_code')],
'airports': [ForeignKey(pk_main='airport_code', pk_ref='flight_id', fk_ref='arrival_airport'),
ForeignKey(pk_main='airport_code', pk_ref='flight_id', fk_ref='departure_airport')]}}Recherche des liens vers les lignes avec les Primary Key spécifiés
>>> from pg_graph.api import PgGraphApi
>>> from pprint import pprint
>>> api = PgGraphApi('config.hw.local.ini')
>>> rows = api.get_rows_references('flights', [1,2,3])
>>> pprint(rows)
{1: {'ticket_flights': {'flight_id': [{'flight_id': 1,
'ticket_no': '0005432816945'},
{'flight_id': 1,
'ticket_no': '0005432816941'}]}},
2: {'ticket_flights': {'flight_id': [{'flight_id': 2,
'ticket_no': '0005433101832'},
{'flight_id': 2,
'ticket_no': '0005433101864'},
{'flight_id': 2,
'ticket_no': '0005432919715'}]}},
3: {'ticket_flights': {'flight_id': [{'flight_id': 3,
'ticket_no': '0005432817560'},
{'flight_id': 3,
'ticket_no': '0005432817568'},
{'flight_id': 3,
'ticket_no': '0005432817559'}]}}}Le code source de la bibliothÚque est disponible sur sous licence MIT, ainsi que dans le dépÎt .
Je serai heureux de recevoir des commentaires, des commits et des suggestions.
Je tenterai de répondre aux questions dans la mesure du possible ici et dans le dépÎt.
Source : habr.com
