Vous appréciez mon travail ?
Je serais ravi de prendre un café !

Vous prenez du plaisir à lire mes articles ? Vous apprenez de nouvelles choses ? Je serais ravis que vous supportiez mon travail avec une petite participation

1 café Merci, vous financez ma dose quotidienne de théïne (oui, en vrai je ne bois pas de café).
5 cafés Génial, ça couvre mes frais de serveur mensuels.
10 cafés Fantastique, avec ça je peux investir dans du matériel et approfondir mes connaissances.
BazinGa's - Tips & tuto IT

Importer des données binaires dans PostgreSQL

Aujourd’hui on va s’atteler à une fonctionnalité de PostgreSQL que j’ai mis pas mal de temps à comprendre car la documentation n’est soit pas très fournit, soit peut compréhensible (pour moi-même).

Nous allons parler de l’import des données binaires. Il s’agit de fichiers de données dans divers formats comme des images, des sons, des vidéos, des programmes… Car oui, PostgreSQL peut stocker tout type de fichiers !

Stockage

Avant de parler import, parlons stockage. Bien comprendre le stockage permettra de bien comprendre comment importer des données.

Tout fichier informatique (y compris les programme car ils sont eux-même constitués de fichiers) est composé de données dans différents langages mais dont le support de stockage est le binaire : une suite de 0 et de 1. Ainsi, lorsqu’un fichier est copié ou déplacé, l’opération est effectué sur le code binaire, cette suite de 0 et de 1.

PostgreSQL, comme d’autres SGBD, permet de stocker ce type de données : les données binaires. Pour cela, deux solutions, que l’on peut apparenter au type « standard » BLOB (Binary Large OBject), sont disponibles :

  • Les large objects (ou objets larges).
  • Le type de données bytea associé au système de stockage TOAST.

Large object

Le stockage large object repose sur l’importation des fichiers binaires directement dans les répertoires de la base de données. Chaque fichier est découpé en morceaux (nommés les pages). Coté base, chaque fichier est donc identifié par un oid et est lié à une série de numéro de pages (l’ensemble des morceaux du fichier).

Les large objects sont particuliers et ne fonctionnent pas comme les autres objets de la base pour lesquels on définit un schéma, un tablespace, un nom… Ils sont tous regroupés et stockés au même endroit. Deux tables systèmes sont dédiées au stockage des large objects :

  • La table pg_largeobject_metadata liste chaque large object ainsi que son propriétaire et les droits d’accès associés.
  • La table pg_largeobject contient les données découpées en pages : une page par ligne. Chaque ligne contient ainsi l’id de l’objets correspondant, le numéro de page (en démarrant de 0) et la donnée binaire (d’une taille maximale de 2048 octets par défaut).
    Ceci permet d’accéder à des morceaux du fichier sans avoir à récupérer la totalité de la données (par exemple pour ne modifier qu’une partie du fichier).
-- Visualiser la liste des large objects stockés en base
SELECT 
	oid,
	*
FROM 
	pg_largeobject_metadata
;
-- Visualiser les données des large objects stockés en base
SELECT 
	*
FROM 
	pg_largeobject
;

Attention, le stockage de la donnée binaire est effectué dans une colonne de type bytea mais il n’a rien à voir avec le stockage binaire détaillé dans le paragraphe suivant.

Les large objects sont si particuliers qu’ils doivent être manipulés à travers une API : des fonctions spécifiques, dédiés à la manipulation des large objects. Ces fonctions sont appelées via des requêtes SQL mais travaillent sur les large objects directement, sans SQL. Elles permettent de créer, d’écrire, de lire, d’importer, d’exporter…

Ainsi, l’import d’un large object ne se fera pas via un simple INSERT. Ce qui est intéressant de noter, c’est que l’import se fait directement via transfert des données binaires brutes. Ce qui permet d’être rapide sans trop charger l’utilisation du processeur.

Bytea et TOAST

Le type de données bytea est directement accessible en SQL sous PostgreSQL. Il permet de définir une colonne qui contiendra de la donnée au format binaire brute.

L’intérêt est la facilité d’utilisation, ce type est associé à une colonne standard PostgreSQL : le nom est libre, elle peut faire partie de n’importe quelle table (et même plusieurs par table), le tablespace peut être défini…

L’import et l’export de données dans ces colonnes se fait au moyen de requêtes SQL standard ce qui impose la conversion de la donnée dans un format textuel pour pouvoir l’écrire dans la requête. Plusieurs formats sont disponibles :

  • Hex : encodage des données binaires sur 2 valeurs hexadécimales par bit (plus d’info ici). Le poids de la données se trouve alourdie d’un facteur 2.
  • Escape : encodage des données binaires sur des caractères ASCII (plus d’info ici). Le poids de la données se trouve alourdie d’un facteur 1 à 4.
  • Base64 : encodage des données binaires sur 64 caractères (plus d’info ici). Le poids de la données se trouve alourdie d’un facteur 4/3 (1/3 de plus) au moins.

La conversion des données d’un format textuel comme ceux cités précédemment au format binaire brut implique une consommation de ressource CPU ainsi qu’un temps de traitement.

Un exemple :

Valeur textuelleEncodage ‘Hex’Encodage ‘Escape’Encodage ‘base64’
arthurbazin61727468757262617a696earthurbazinYXJ0aHVyYmF6aW4=

Et les biscottes TOAST dans tout ça ? Il s’agit en fait d’un fonctionnement interne de PostgreSQL qui permet de stocker de grosses valeurs dans les attributs d’une table.

TOAST = The Oversized-Attribute Storage Technique

Ce type de stockage s’applique à certains types de données (numérique, textuel, géométrique et dans notre cas binaire) dont la longueur est variable (un exemple de type à longueur variable : varchar(20)).

Pour chaque tuple (= ligne d’une table), PostgreSQL stocke la donnée dans ce que l’on appelle une « page ». Un tuple ne peut être stocké que dans une seul page maximum mais une page peut contenir plusieurs tuples. Ces pages ne peuvent excéder 8 ko et donc un tuple non plus. Ainsi, lorsqu’une donnée excède cette taille, le système TOAST s’enclenche pour outrepasser cette limite.

Le système TOAST compresse puis divise les données dans plusieurs « sous-tuples » (de nouvelles lignes) ce qui permet de répartir le poids des données et de les stocker de façon classique, sans dépasser la taille limitée d’une page. Ces lignes supplémentaires sont créées dans des tables spécifiques nommées pg_toast_xxx (le xxx étant remplacé par l’oid de la table d’origine) et stockées dans le schéma système pg_toast.

Ce fonctionnement est entièrement automatisé et totalement transparent pour l’utilisateur qui n’a rien à gérer. L’activation du système est paramétré par la variable de configuration TOAST_TUPLE_THRESHOLD qui fixe par défaut à 2 ko la limite à partir de laquelle la données est TOASTée.

Notez que la structure des tables pg_toast_xxx est identique à la structure de la table pg_largeobject bien que le système de stockage soit différent.

Pour identifier la table pg_toast_xxx liée à une table, vous pouvez utiliser la requête suivante :

SELECT
	reltoastrelid::regclass as nom_table,
	pg_relation_filepath(reltoastrelid) as emplacement_stockage,
	pg_size_pretty(pg_relation_size(reltoastrelid)) as taille
FROM
	pg_class
WHERE
	relnamespace = 'mon_schema'::regnamespace
AND 
	relname = 'ma_table'
;

Import

L’import de données binaires se résume donc à un déplacement de données du lieu d’origine vers l’instance PostgreSQL. Selon le type de stockage utilisé, le traitement sera donc différent :

  • Format large object : il faut obligatoirement utiliser l’API PostgreSQL dédiée.
  • Type de donnée bytea : la donnée doit être encodée dans un format binaire textuel puis envoyé au moyen d’une requête.

Voici un exemple, Je souhaite importer une image dans une colonne au format bytea : je dois d’abord convertir mon image dans un format binaire textuel comme par exemple le base64 via un outil (par exemple celui-ci) puis l’importer via la fonction decode().

-- Import dans un champ bytea d'une image encodée en base64
INSERT INTO ma_table (
	mon_champ_bytea)
SELECT
	decode('iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAY[...]3+/wA93TsnnJMLsQAAAABJRU5ErkJggg==', 'base64')
;

Pour utiliser une image stockée au format binaire, il est possible de l’encoder au format base64 pour l’utiliser dans du HTML par exemple.

-- Création d'une chaine HTML pour afficher une image stockée au format binaire
SELECT
	'<img src="data:image/png;base64, ' || encode(champ_bytea, 'base64') || '" alt="mon_image" width="150" height="150">'
FROM
	ma_table
;

On le voit donc tout de suite : pour du stockage dans une colonne en bytea, il faut d’abord convertir la donnée d’origine avant de pouvoir l’inclure dans une requête. Ceci implique donc l’utilisation d’un logiciel tiers permettant cet encodage… Pas très simple.

La suite de cet article va donc s’intéresser à une méthode d’import de données en tant que large object qui permet ensuite de récupérer ce large object dans une colonne de type bytea.

Le logiciel client utilisé est tout simplement PSQL.

Attention, le programme qui s’occupe de l’import du fichier (l’instance de PostgreSQL ou le logiciel client) doit avoir les autorisations d’accès en lecture sur ce fichier.

Pour rappel, toute requête SQL est envoyée par le logiciel client vers le serveur PostgreSQL qui va exécuter cette requête. Cela veut dire que le serveur ne peut atteindre des fichiers situés sur la machine cliente. C’est donc au logiciel client d’envoyer ces fichiers binaires au serveur.

A l’inverse, le serveur PostgreSQL est capable d’atteindre tout fichier présent sur la machine sur laquelle l’instance est installée.

Données coté serveur : server-side

Lorsque les données à importées sont stockées dans un répertoire accessible par l’instance de PostgreSQL (sur la même machine ou dans un répertoire réseau monté sur la machine), il suffit de faire appel aux fonctions fournies par l’API large object au travers de requêtes SQL.

L’importation du fichier utilise la fonction lo_import() qui renvoie l’oid du large object créé.

-- Importation d'un fichier situé sur le serveur
SELECT lo_import(/emplacement/de/mon/fichier.ext);

Un large object peut être récupéré au format bytea grâce à la fonction lo_get() à laquelle on passe l’oid du large object à récupérer.

-- Récupération d'un large object au format bytea
SELECT lo_get(123456);

Cette fonction peut donc être utilisée dans une requête d’UPDATE pour importer le large object dans une colonne de type bytea.

-- Importation d'un large object dans une colonne de type bytea
UPDATE mon_schema.ma_table
SET ma_colonne_bytea = lo_get(123456)
;

La fonction lo_unlink() permet de supprimer un large object en indiquant l’oid de celui-ci.

-- Récupération d'un large object au format bytea
SELECT lo_unlink(123456);

Il est possible de grouper les différentes étapes dans une fonction d’import de données dans le type bytea :

CREATE OR REPLACE FUNCTION bytea_import(emplacement_fichier text) 
RETURNS bytea AS
$corps$
	DECLARE
		-- oid du large object importé
		v_oid_objet oid;
		-- Données du fichier au format binaire
		v_donnees;
	BEGIN
		-- Import du fichier sous forme de large objet
		SELECT lo_import(emplacement_fichier) INTO v_oid_objet;
		-- Récupération des données du large objet
		SELECT lo_get(v_oid_objet) INTO v_donnees;
		-- Suppression du large objet
		PERFORM lo_unlink(v_oid_objet);

		-- Renvoi des données
		RETURN v_donnees;
	END;
$corps$
LANGUAGE PLPGSQL
;

Données coté client : client-side

Lorsque les données à importées sont stockées dans un répertoire situé sur la machine cliente et donc non accessibles par l’instance de PostgreSQL, il faut passer par un logiciel client capable d’importer les fichiers. Nous allons ici faire appel à PSQL qui possède la même fonction que l’API de PostgreSQL : la méta commande \lo_import.

L’importation du fichier utilise la méta commande \lo_import() qui renvoie l’oid du large object créé.

-- Importation d'un fichier situé sur la machine cliente
\lo_import(/emplacement/de/mon/fichier.ext);

Les autres fonctions sont identiques à la version serveur car une fois le fichier importé, il se trouve coté serveur. C’est donc via des requêtes SQL que le large object sera ensuite traité.

Dans ce cas là, il n’est pas possible d’avoir une fonction qui regroupe les différentes étapes comme dans la partie précédente car la méta commande \lo_import n’est disponible que coté client alors que la fonction sera exécutée cotée serveur.

Import en masse

L’import d’objet en masse est un peu différent car il nécessite de faire appel aux fonctions vues précédemment et de les combiner avec d’autres mécanismes pour correctement récupérer les données et leurs informations.

Server-side

Bien que ça ne soit pas obligatoire, nous allons passer par une procédure pour exécuter toutes les requêtes ce qui permettra l’utilisation de variables et pour réutiliser le code plus facilement.

DO
$fonction$
DECLARE

	-- Table d'import
	schema_import TEXT := 'public';
	table_import TEXT := 'import_fichier';

	-- Programme à utiliser pour localiser les fichiers à importer (sous Windows)
		-- Lister tous les fichiers du répertoire tmp
		-- WHERE "C:\tmp:*"
	
		-- Lister tous les fichiers jpeg et png du répertoire tmp
		-- WHERE "C:\tmp:*.jpg" "C:\tmp:*.png"
	
		-- Lister tous les fichiers du répertoire tmp et de ses sous-répertoires
		-- DIR "C:\tmp" /b /S /a-d
	liste_fichier TEXT := 'WHERE "C:\tmp:*.jpg"';


	-- Autres variables
	-- Date et heure de démarrage
	var_timestamp timestamp := now();


BEGIN
	
	-- Création de la table d'import
	EXECUTE 
		$a$CREATE TABLE IF NOT EXISTS $a$ || quote_ident(schema_import) || $a$.$a$ || quote_ident(table_import) || $a$ (
		-- Un identifiant
		id serial PRIMARY KEY,
		-- L'emplacement d'origine du fichier
		emplacement_fichier TEXT,
		-- Le nom d'origine du fichier
		nom_fichier TEXT,
		-- L'extension d'origine du fichier
		extension_fichier TEXT,
		-- L'oid du large object créé
		lo_oid oid,
		-- Le fichier stocké au format binaire
		data bytea,
		-- Le timestamp d'import
		date_import timestamp
	) $a$
	;



	-- Récupérer la liste des fichiers situés dans un dossier
	EXECUTE 
		$a$COPY $a$ || quote_ident(schema_import) || $a$.$a$ || quote_ident(table_import) || $a$ (emplacement_fichier) 
			FROM PROGRAM '$a$ || liste_fichier || $a$' 
			WITH (format 'csv')$a$ 
	;



	-- Calcul des métadonnées des fichiers
	EXECUTE
		$a$UPDATE $a$ || quote_ident(schema_import) || $a$.$a$ || quote_ident(table_import) || $a$
			SET 
				-- Récupération du nom du fichier
				nom_fichier = (SELECT regexp_matches(emplacement_fichier,'\\([^\\]*)\.(\w+)$'))[1],
				-- Récupération de l'extension du fichier
				extension_fichier = (SELECT regexp_matches(emplacement_fichier,'\.(\w+)$'))[1],
				-- Création d'un timestamp
				date_import = $a$ || quote_literal(var_timestamp)
	;



	-- Import du document et récupération de l'oid
	EXECUTE
		$a$UPDATE $a$ || quote_ident(schema_import) || $a$.$a$ || quote_ident(table_import) || $a$
			SET 
				lo_oid = lo_import(emplacement_fichier) $a$
	;



	-- Récupération des large objects dans la colonne de type bytea
	EXECUTE
		$a$UPDATE $a$ || quote_ident(schema_import) || $a$.$a$ || quote_ident(table_import) || $a$
			SET 
				data = lo_get(lo_oid) $a$
	;



	-- Suppression des large objects
	EXECUTE
		$a$SELECT 
			lo_unlink(lo_oid)
		FROM
			$a$ || quote_ident(schema_import) || $a$.$a$ || quote_ident(table_import) 
	;


END
$fonction$

Client-side

Le principe est similaire à la version server-side mais avec quelques subtilités.

Comme nous allons avoir plusieurs fichiers à traiter, l’idéal est de passer par un script batch qui va générer un fichier SQL qui sera ensuite exécuté par PSQL.

@echo off
setlocal enabledelayedexpansion

:: ======================================
:: Définition des variables parametrables
:: ======================================

:: Connexion BDD
	SET param.host=127.0.0.1
	SET param.port=5432
	SET param.bdd=ma_bdd
	SET param.user=postgres
	SET param.pwd=postgres

:: Table d'import
	SET param.schema_import=public
	SET param.table_import=import_fichier

:: Programme à utiliser pour localiser les fichiers à importer
	:: Lister tous les fichiers du répertoire tmp
	:: WHERE "C:\tmp:*"

	:: Lister tous les fichiers jpeg et png du répertoire tmp
	:: WHERE "C:\tmp:*.jpg" "C:\tmp:*.png"

	:: Lister tous les fichiers du répertoire tmp et de ses sous-répertoires
	:: DIR "C:\tmp" /b /S /a-d

	SET param.liste_fichier=WHERE "C:\tmp:*.jpg"



:: Autres variables
	SET var_dir_install=%~dp0
	SET scriptfile="%var_dir_install%script_import_bytea.sql"
	:: Date et heure de démarrage
	:: Le timestamp permettra de gérer plusieurs imports en parallèle si besoin
	SET var_timestamp=%date:~6,4%-%date:~3,2%-%date:~0,2% %time:~0,2%:%time:~3,2%:%time:~6,5%
	SET "var_timestamp=%var_timestamp:,=.%"


:: Initialisation du script SQL
(
	echo -- Script d'import SQL
) > "%scriptfile%"



:: Création de la table d'import
(
	echo -- Table d'import
	echo CREATE TABLE IF NOT EXISTS "%param.schema_import%"."%param.table_import%" ^(
	echo 	-- Un identifiant
	echo 	id serial PRIMARY KEY,
	echo 	-- L'emplacement d'origine du fichier
	echo 	emplacement_fichier TEXT,
	echo 	-- Le nom d'origine du fichier
	echo 	nom_fichier TEXT,
	echo 	-- L'extension d'origine du fichier
	echo 	extension_fichier TEXT,
	echo 	-- L'oid du large object créé
	echo 	lo_oid oid,
	echo 	-- Le fichier stocké au format binaire
	echo 	data bytea,
	echo 	-- Le timestamp d'import
	echo 	date_import timestamp
	echo ^)
	echo ;
	echo.
	echo.
	echo.
) >> "%scriptfile%"



:: Boucle pour chaque fichier à importer 
FOR /f "usebackq delims=|" %%a in (`%param.liste_fichier%`) do (

	SET import_file=%%a
	SET "import_file=%import_file:\=/%"


	(
		echo -- Fichier : %import_file%
		echo.
	) >> "%scriptfile%"

	:: Insertion de l'emplacement du fichier dans la table d'import
	(
		echo -- Insertion emplacement du fichier
		echo INSERT INTO "%param.schema_import%"."%param.table_import%" ^(
		echo 	emplacement_fichier,
		echo 	date_import
		echo ^)
		echo VALUES 
		echo 	^('%import_file%', '%var_timestamp%'^)
		echo ;
		echo.
	) >> "%scriptfile%"

	:: Importation du fichier avec la méta commande \lo_import
	(
		echo -- Importation du fichier avec la méta commande \lo_import
		echo \lo_import '%import_file%'
		echo.
	) >> "%scriptfile%"

	:: Récupération de l'OID du large object créé
	(
		echo -- Récupération de l'OID du large object créé
		echo UPDATE "%param.schema_import%"."%param.table_import%"
		echo 	SET lo_oid = :LASTOID
		echo WHERE emplacement_fichier = '%import_file%'
		echo AND date_import = '%var_timestamp%'
		echo ;
		echo.
		echo.
	) >> "%scriptfile%"

)



:: Calcul des métadonnées des fichiers
(
	echo.
	echo.
	echo -- Calcul des métadonnées des fichiers
	echo UPDATE "%param.schema_import%"."%param.table_import%"
	echo 	SET
	echo 		-- Récupération du nom du fichier
	echo 		nom_fichier = ^(SELECT regexp_matches^(emplacement_fichier,'/^([^^/]*^)\.\w+$'^)^)[1],
	echo 		-- Récupération de l'extension du fichier
	echo 		extension_fichier = ^(SELECT regexp_matches^(emplacement_fichier,'\.^(\w+^)$'^)^)[1]
	echo WHERE
	echo 	date_import = '%var_timestamp%'
	echo ;
	echo.
) >> "%scriptfile%"



:: Récupération des large objects dans la colonne de données binaire
(
	echo.
	echo.
	echo -- Récupération des large objects dans la colonne de données binaire
	echo UPDATE "%param.schema_import%"."%param.table_import%"
	echo 	SET
	echo 		data = lo_get^(lo_oid^)
	echo WHERE
	echo 	date_import = '%var_timestamp%'
	echo ;
	echo.
) >> "%scriptfile%"



:: Suppression des large objects
(
	echo.
	echo.
	echo -- Suppression des large objects
	echo SELECT 
	echo 	lo_unlink^(lo_oid^)
	echo FROM
	echo 	"%param.schema_import%"."%param.table_import%"
	echo WHERE
	echo 	date_import = '%var_timestamp%'
	echo ;
	echo.
) >> "%scriptfile%"



:: Execution du script SQL
psql -d "postgresql://%param.user%:%param.pwd%@%param.host%:%param.port%/%param.bdd%" -f "%scriptfile%"
-- Script d'import SQL
-- Table d'import
CREATE TABLE IF NOT EXISTS "public"."import_fichier" (
	-- Un identifiant
	id serial PRIMARY KEY,
	-- L'emplacement d'origine du fichier
	emplacement_fichier TEXT,
	-- Le nom d'origine du fichier
	nom_fichier TEXT,
	-- L'extension d'origine du fichier
	extension_fichier TEXT,
	-- L'oid du large object créé
	lo_oid oid,
	-- Le fichier stocké au format binaire
	data bytea,
	-- Le timestamp d'import
	date_import timestamp
)
;



-- Fichier : C:/tmp/mon_image_1.jpg

-- Insertion emplacement du fichier
INSERT INTO "public"."import_fichier" (
	emplacement_fichier,
	date_import
)
VALUES 
	('C:/tmp/mon_image_1.jpg', '2022-02-06 21:00:00.00')
;

-- Importation du fichier avec la méta commande \lo_import
\lo_import 'C:/tmp/mon_image_1.jpg'

-- Récupération de l'OID du large object créé
UPDATE "public"."import_fichier"
	SET lo_oid = :LASTOID
WHERE emplacement_fichier = 'C:/tmp/mon_image_1.jpg'
AND date_import = '2022-02-06 21:00:00.00'
;


-- Fichier : C:/tmp/mon_image_2.jpg

-- Insertion emplacement du fichier
INSERT INTO "public"."import_fichier" (
	emplacement_fichier,
	date_import
)
VALUES 
	('C:/tmp/mon_image_2.jpg', '2022-02-06 21:00:00.00')
;

-- Importation du fichier avec la méta commande \lo_import
\lo_import 'C:/tmp/mon_image_2.jpg'

-- Récupération de l'OID du large object créé
UPDATE "public"."import_fichier"
	SET lo_oid = :LASTOID
WHERE emplacement_fichier = 'C:/tmp/mon_image_2.jpg'
AND date_import = '2022-02-06 21:00:00.00'
;



-- Calcul des métadonnées des fichiers
UPDATE "public"."import_fichier"
	SET
		-- Récupération du nom du fichier
		nom_fichier = (SELECT regexp_matches(emplacement_fichier,'/([^/]*)\.\w+$'))[1],
		-- Récupération de l'extension du fichier
		extension_fichier = (SELECT regexp_matches(emplacement_fichier,'\.(\w+)$'))[1]
WHERE
	date_import = '2022-02-06 21:00:00.00'
;



-- Récupération des large objects dans la colonne de données binaire
UPDATE "public"."import_fichier"
	SET
		data = lo_get(lo_oid)
WHERE
	date_import = '2022-02-06 21:00:00.00'
;



-- Suppression des large objects
SELECT 
	lo_unlink(lo_oid)
FROM
	"public"."import_fichier"
WHERE
	date_import = '2022-02-06 21:00:00.00'
;

Pour en savoir plus

Un comparatif entre les stockages large object et binary : https://blog-postgresql.verite.pro/2017/11/15/large-objects-bytea.html

Un bel article sur les TOAST : https://medium.com/gojekengineering/a-toast-from-postgresql-83b83d0d0683

Un autre article sur les TOAST : https://fljd.in/2020/10/12/toast-la-meilleure-chose-depuis-le-pain-en-tranches/

Cet article fait parti du cours sur PostgreSQL, partie 8 du Vade-mecum.


Cet article vous a plu ?

N'hésitez pas à le partager, il interessera surement certains de vos contacts.

Les thèmes suivants contiennent des articles en lien avec celui-ci, allez faire un tour :

BDDPostgreSQLProgrammationSQL binairebyteaimportlarge objectTOAST

50%