SQL vers RDF : principes de sérialisation et de jointure

Julien Garderon

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum. Commentez

- Titre : SQL vers RDF : principes de sérialisation et de jointure

- Auteur : Julien Garderon

- Parution : 26 août 2021

- Licence : Copyright ® 2020 Malick. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- RDF et gestion des connaissances- Partie 1 : apprendre les bases d’un triplestore REST en Python

- Python - RDF et gestion des connaissances - Partie 2 : fonctionnement avancé et opérationnel du triplestore

- Python - RDF et gestion des connaissances - Partie 3 : théorie et apports aux organisations

Sommaire

- I. Préambule

- II. Objectifs

- III. Prérequis et démarrage

- IV. Le format triplet

- IV-A. Exemple de résultat

- IV-B. Le principe

- IV-C. Les procédures : solution A – s’appuyer exclusivement sur les informations de la table

- IV-D. Les procédures : solution B – respecter une grammaire définie

- IV-E. La jointure : réalisation

- IV-F. La question des performances et de la sécurité ?

- V. Conclusions

- VI. Note de la rédaction de Developpez.com

I - Préambule

Afin de bien comprendre l’intérêt de cette pratique, il vous est vivement conseillé de lire l’article dédié à l’introduction de RDF [cf. mon article en cours de publication sur développez.com] ainsi qu’un autre d’exemple sur FOAF.

Il est vivement conseillé d’avoir quelques notions du modèle RDF et des requêtes standards SQL pour la lecture de cet article.

Note de l’auteur : d’après les fiches de travail de Tim Berners’Lee, RDF s’accommode très bien d’une base de données relationnelle pour le stockage des données. Nous démontrons une telle symbiose ici.

II - Objectifs

1. Nous allons sérialiser des données stockées dans une base de données relationnelle vers une présentation « triplet » classique, seulement avec SQL.

2. Nous allons réaliser l’équivalent d’une jointure entre deux tables et voir l’intérêt d’une telle procédure.

III - Prérequis et démarrage

Le code SQL fourni a été validé sur un Ubuntu 19.04 et la version 10.3 de MariaDB (paquet par défaut de la distribution). À noter que sur cette version, certaines fonctions de pivot de table prévues par le langage SQL ne sont pas disponibles directement.

Vous n’avez pas besoin d’autres outils qu’une connexion à votre base de données, avec des droits de création et de consultation de tables, de vues et de procédures stockées. Pour un souci de confort, je vous invite à tester les requêtes avec PHPMyAdmin ou une interface équivalente, surtout si vous n’êtes pas à l’aise avec les concepts abordés ou les requêtes SQL.

Nous admettrons ici que vous avez une base vierge sur laquelle nous allons créer deux tables et les peupler par quelques données (le format de la table n’a que peu d’importance, tant que chaque table dispose d’une clef primaire ou d’un index unique pour une des colonnes).

Table « factures » :

-- Version de PHP :  7.2.19-0ubuntu0.19.04.1

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";

--
-- Base de données :  `rdfxml`
--

-- --------------------------------------------------------

--
-- Structure de la table `factures`
--

CREATE TABLE `factures` (
`facture_id` int(11) NOT NULL,
`facture_emission` date DEFAULT NULL,
`facture_reglement` date DEFAULT NULL,
`client_id` int(11) NOT NULL,
`liste_id` int(11) NOT NULL,
`facture_total_ht` decimal(10,2) NOT NULL,
`facture_total_tva` decimal(10,2) NOT NULL,
`facture_total_ttc` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

--
-- Déchargement des données de la table `factures`
--

INSERT INTO `factures` (`facture_id`, `facture_emission`, `facture_reglement`, `client_id`, `liste_id`, `facture_total_ht`, `facture_total_tva`, `facture_total_ttc`) VALUES(1, '2019-08-01', NULL, 1, 1, '15.00', '3.00', '18.00');
INSERT INTO `factures` (`facture_id`, `facture_emission`, `facture_reglement`, `client_id`, `liste_id`, `facture_total_ht`, `facture_total_tva`, `facture_total_ttc`) VALUES(2, '2019-08-02', '2019-08-03', 1, 2, '8.50', '2.00', '10.50');

--
-- Index pour les tables déchargées
--

--
-- Index pour la table `factures`
--
ALTER TABLE `factures`
ADD PRIMARY KEY (`facture_id`);

--
-- AUTO_INCREMENT pour les tables déchargées
--

--
-- AUTO_INCREMENT pour la table `factures`
--
ALTER TABLE `factures`
MODIFY `facture_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
COMMIT;

Table « clients » :

-- phpMyAdmin SQL Dump
-- version 4.8.2
-- https://www.phpmyadmin.net/
--
-- Hôte : localhost
-- Généré le :  Dim 11 août 2019 à 12:23
-- Version du serveur :  10.3.13-MariaDB-2
-- Version de PHP :  7.2.19-0ubuntu0.19.04.1

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";

--
-- Base de données :  `rdfxml`
--

-- --------------------------------------------------------

--
-- Structure de la table `clients`
--

CREATE TABLE `clients` (
`client_id` int(11) NOT NULL,
`client_nom` varchar(255) NOT NULL,
`client_prenom` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

--
-- Déchargement des données de la table `clients`
--

INSERT INTO `clients` (`client_id`, `client_nom`, `client_prenom`) VALUES(1, 'Garderon', 'Julien');

--
-- Index pour les tables déchargées
--

--
-- Index pour la table `clients`
--
ALTER TABLE `clients`
ADD PRIMARY KEY (`client_id`);

--
-- AUTO_INCREMENT pour les tables déchargées
--

--
-- AUTO_INCREMENT pour la table `clients`
--
ALTER TABLE `clients`
MODIFY `client_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
COMMIT;

IV - Le format triplet

IV-A - Exemple de résultat

Notre objectif est de récupérer ici un retour de la base qui soit compatible avec le format triplet : c’est-à-dire ici 3 colonnes (sujet, prédicat, objet). Notre finalité est ici d’aboutir à un résultat similaire à ceci :

SELECT sujet, prédicat, objet FROM `rdf` ;

Notez que conformément au fonctionnement de RDF, il n’y a plus de notion de « clef » (primaire ou d’index) dans un tel résultat qui, dans notre cas, sera sous le format d’une jonction de vues.

IV-B - Le principe

Il n’est pas spécialement difficile de passer d’un format relationnel à un format triplet en SQL. Nous utiliserons pour cela la commande UNION ALL, qui permet de regrouper dans un résultat unique plusieurs requêtes de sélection (le nombre de colonnes et leur type doivent être identiques).

Exécutez dans votre éditeur la requête suivante :

SELECT `facture_id`, 'emission', `facture_emission` FROM `factures`

Le résultat est déjà un simulacre de RDF avec la première colonne qui serait le sujet (identifiant), la colonne « emission » qui serait la valeur de relation (prédicat) et le sujet – en l’occurrence la date d’émission de la facture :

C’est intéressant, mais nous n’avons là qu’une seule colonne pour une table. Exécutez maintenant :

SELECT `facture_id`   AS sujet,
'a'            AS predicat,
'sql:factures' AS objet
FROM   `factures`
UNION ALL
SELECT `facture_id`,
'emission',
`facture_emission`
FROM   `factures`
UNION ALL
SELECT `facture_id`,
'reglement',
`facture_reglement`
FROM   `factures`
UNION ALL
SELECT `facture_id`,
'client',
`client_id`
FROM   `factures`
UNION ALL
SELECT `facture_id`,
'items',
`liste_id`
FROM   `factures`
UNION ALL
SELECT `facture_id`,
'total#ht',
`facture_total_ht`
FROM   `factures`
UNION ALL
SELECT `facture_id`,
'total#tva',
`facture_total_tva`
FROM   `factures`
UNION ALL
SELECT `facture_id`,
'total#ttc',
`facture_total_ttc`
FROM   `factures`;

Notre retour contient désormais l’ensemble des données de la table, dans un format conforme aux triplets. Il reste que notre ontologie (c’est-à-dire la grammaire pour écrire les triplets et les rendre symboliquement compatibles dans un espace de sens donné) n’est pas conforme.

De plus, cette écriture, si elle reste faisable à la main, est loin d’être efficace pour un grand nombre de tables et c’est une source d’erreur (oubli). Une meilleure solution doit être trouvée.

IV-C - Les procédures : solution A – s’appuyer exclusivement sur les informations de la table

Notre première solution ne s’appuie que sur les informations déjà contenues dans nos tables. Elle a ma préférence si derrière, le système de traitement est capable de supporter des transformations de triplets ou des inférences afin de rendre conforme l’ontologie à celle de l’architecture visée.

Nous utiliserons pour cela une procédure stockée, qui produira à partir des informations sur la table visée (grâce à INFORMATION_SCHEMA.COLUMNS), une requête de création de vue. C’est donc à partir d’une vue de la table, qui est conforme à une présentation au format triplet, que nous pourrions réaliser des jointures.

DROP
PROCEDURE IF EXISTS `serialiser`;
DELIMITER | CREATE PROCEDURE `serialiser`(
IN table_nom varchar(25),
IN table_colref varchar(25),
IN debug boolean
) BEGINDECLARE stop_curseur_colonnes INT DEFAULT 0;
DECLARE colonne_nom VARCHAR(25);
DECLARE colonne_type VARCHAR(15);
DECLARE `argument_vide` CONDITION FOR SQLSTATE '45000';
DECLARE curseur_colonnes CURSOR FOR
SELECT
COLUMN_NAME,
data_type
FROM
information_schema.columns
WHERE
TABLE_NAME = table_nom;
DECLARECONTINUE HANDLER FOR SQLSTATE '02000'
SET
stop_curseur_colonnes = 1;
DECLARE EXIT HANDLER FOR sqlexceptionBEGIN
SELECT
Concat(
"erreur : la sérialisation a échoué ('",
@requete, "')."
);
END;
IF table_nom = "" THEN
SET
@requete = "le nom de la table est vide";
SIGNAL `argument_vide`;
ENDIF;
IF table_nom = "" THEN
SET
@requete = "le nom de la colonne identifiante est vide";
SIGNAL `argument_vide`;
ENDIF;
SET
@refSQL = Concat(
'CONCAT( "refSQL:facture#", ', table_colref,
' )'
);
SET
@requete = Concat(
'SELECT ', @refSQL, ' as sujet, "a" as predicat, "ontoSQL:',
table_nom, '" as objet FROM ', table_nom
);
OPEN curseur_colonnes;
REPEATFETCH curseur_colonnes INTO colonne_nom,
colonne_type;
IF stop_curseur_colonnes = 0 THEN
SET
@requete = concat(
@requete, ' UNION ALL SELECT ', @refSQL,
', "ontoSQL:', table_nom, '#', colonne_nom,
'", CONCAT( \'"\', IFNULL( ', colonne_nom,
', "") , \'"^^SQL:', colonne_type,
'\' ) FROM ', table_nom
);
ENDIF;
UNTIL stop_curseur_colonnes END repeat;
IF debug IS FALSE THEN
SET
@requetePrepare = concat(
'DROP VIEW IF EXISTS `rdf_', table_nom,
'`; '
);
PREPARE stmt
FROM
@requetePrepare;
EXECUTE Stmt;
SET
@requeteVue = Concat(
' CREATE VIEW `rdf_', table_nom,
'` AS ', @requete, ' ; '
);
PREPARE stmt
FROM
@requeteVue;
EXECUTE Stmt;
SET
@requeteSelection = concat(
' SELECT * FROM   `rdf_', table_nom,
'` ; '
);
PREPARE stmt
FROM
@requeteSelection;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
IF DEBUG IS TRUE THEN
SELECT
@requete;
END IF;
END | DELIMITER;

Cette procédure vous permettra de produire une requête (et éventuellement l’exécuter si DEBUG est à false ) qui produira elle-même une vue conforme à la table. Attention, pour que cette procédure aboutisse et soit « logique », il faut que votre sérialisation se produise avec une colonne « pivot » qui est la colonne identifiante (cf. la remarque en prérequis).

call `serialiser`( "factures", "facture_id", FALSE
-- la requête sera exécutée immédiatement
);

Vous devriez avoir des retours similaires à ceci :

Une vue a été créée (pensez à recharger la page si vous êtes sous PHPMyAdmin et qu’elle n’apparaît pas automatiquement) :

Vous noterez désormais qu’il y a une conformité plus grande des résultats de la vue avec le modèle RDF canonique : les valeurs d’objet indiquent leur type (int , decimal , date , etc.). Les attributs marqués NULL sont traités ici et affichés comme une date invalide. Nous aurions pu les extraire des résultats pour respecter une règle de RDF qui indique qu’il ne faut normalement pas afficher les colonnes marquées NULL, cependant, « l’esprit » de la table est ici d’avoir du sens à une absence de date pour la colonne « facture_reglement » (pas de date = pas de règlement).

La colonne des prédicats semble respecter davantage un début de grammaire et les sujets, si ce ne sont pas directement des URI, sont des références « uniques » à la base visée. Le système de traitement peut donc ensuite faire « pointer » correctement une telle référence à la donnée concernée.

Nous pourrions nous contenter d’une telle présentation et répéter la commande de sérialisation pour la table « clients ». Mais peut-être y a-t-il une autre solution ?

IV-D - Les procédures : solution B – respecter une grammaire définie

L’intérêt de SQL est de définir pour chaque colonne un type de valeur bien défini. C’est ainsi que dans la solution précédente, nous avons traité chaque cellule et produit pour chaque objet des triplets, la valeur et son type.

Cependant, la conformité n’est pas idéale et les formats SQL ne correspondent pas précisément aux formats RDF. Pire : le « casting » d’une valeur vers un type string est ici implicite. Ce n’est pas acceptable.

Aussi, nous allons créer dans notre base une nouvelle table qui comportera les colonnes, leur valeur de prédicat et leur valeur d’objet (avec le type) pour chaque table à sérialiser. Si une colonne n’est pas définie dans cet outil de démarrage d’une ontologie, alors elle ne sera jamais reproduite dans la vue RDF – logiquement.

Voici la définition d’une telle table, appelée « ontologies » :

-- phpMyAdmin SQL Dump
-- version 4.8.2
-- https://www.phpmyadmin.net/
--
-- Hôte : localhost
-- Généré le : Dim 11 août 2019 à 13:44
-- Version du serveur : 10.3.13-MariaDB-2
-- Version de PHP : 7.2.19-0ubuntu0.19.04.1
SET sql_mode = "NO_AUTO_VALUE_ON_ZERO";SET autocommit = 0;START TRANSACTION;SET time_zone = "+00:00";
--
-- Base de données : `rdfxml`
--
-- --------------------------------------------------------
--
-- Structure de la table `ontologies`
--CREATE TABLE `ontologies`
(
`onto_id`       int(11) NOT NULL auto_increment PRIMARY KEY,
`onto_table`    varchar(255) NOT NULL,
`onto_orga`     int(3) NOT NULL,INSERT INTO `ontologies`
(
`onto_table`,
`onto_orga`,
`onto_colonne`,
`onto_predicat`,
`onto_masque`
)
VALUES
(
'factures',
1,
'facture_id',
'a',
'refSQL:factures#facture_id=%s'
);INSERT INTO `ontologies`
(
`onto_table`,
`onto_orga`,
`onto_colonne`,
`onto_predicat`,
`onto_masque`
)
VALUES
(
'factures',
2,
'facture_emission',
'facture:date#emission',
'\"%s\"^^rdf:date'
);INSERT INTO `ontologies`
(
`onto_table`,
`onto_orga`,
`onto_colonne`,
`onto_predicat`,
`onto_masque`
)
VALUES
(
'factures',
3,
'facture_reglement',
'facture:date#reglement',
'\"%s\"^^rdf:date'
);INSERT INTO `ontologies`
(
`onto_table`,
`onto_orga`,
`onto_colonne`,
`onto_predicat`,
`onto_masque`
)
VALUES
(
'factures',
4,
'client_id',
'client#id',
'refSQL:clients#client_id=%s'
);INSERT INTO `ontologies`
(
`onto_table`,
`onto_orga`,
`onto_colonne`,
`onto_predicat`,
`onto_masque`
)
VALUES
(
'clients',
1,
'client_id',
'a',
'refSQL:clients#client_id=%s'
);INSERT INTO `ontologies`
(
`onto_table`,
`onto_orga`,
`onto_colonne`,
`onto_predicat`,
`onto_masque`
)
VALUES
(
'clients',
2,
'client_nom',
'foaf:familyName',
'\"%s\"^^rdf:string'
);

--
-- Index pour les tables déchargées
--
--COMMIT;

Vous devriez avoir une table avec les valeurs suivantes :

Notez la colonne « onto_orga » : elle permet de « classer » les triplets, mais surtout de déterminer le poids des colonnes. Dans ma logique, la valeur 1 dans cette colonne pour chaque table équivaut à la colonne de la clef primaire pour la table à sérialiser.

La valeur de prédicat est ici répétée. Nous aurions pu aussi faire des tables « prédicats » et « masque » pour éviter les répétitions de types – mais j’ai préféré la simplicité. Sachez simplement qu’ici, des jointures pour éviter les doublons seraient préférables.

Voyons maintenant la procédure de sérialisation modifiée et son exécution :

DROP
PROCEDURE IF EXISTS `serialiser`;
DELIMITER | CREATE PROCEDURE `serialiser`(
IN table_nom varchar(255),
IN table_colref varchar(255),
IN debug boolean
) BEGINDECLARE stop_curseur_colonnes INT default 0;
DECLARE colonne_orga INT(3);
DECLARE colonne_nom VARCHAR(255);
DECLARE colonne_relation VARCHAR(255);
DECLARE colonne_masque VARCHAR(255);
DECLARE `argument_vide` condition FOR sqlstate '45000';
DECLARE curseur_colonnes CURSOR FOR
SELECT
onto_orga,
onto_colonne,
onto_predicat,
onto_masque
FROM
ontologies
WHERE
onto_table = table_nom
ORDER BY
onto_orga ASC;
DECLARECONTINUE handler FOR sqlstate '02000'
SET
stop_curseur_colonnes = 1;
DECLARE EXIT HANDLER for sqlexceptionBEGIN
SELECT
Concat(
"erreur : la sérialisation a échoué ('",
@requete, "')."
);
END;
IF table_nom = "" then
SET
@requete = "le nom de la table est vide";
SIGNAL `argument_vide`;
ENDIF;
IF table_nom = "" then
SET
@requete = "le nom de la colonne identifiante est vide";
SIGNAL `argument_vide`;
ENDIF;
OPEN curseur_colonnes;
REPEATFETCH curseur_colonnes INTO colonne_orga,
colonne_nom,
colonne_relation,
colonne_masque;
IF colonne_orga = 1 then
SET
@refSQL = concat(
'REPLACE( \'', colonne_masque, '\', "%s", ',
table_colref, ' ) '
);
SET
@requete = Concat(
' SELECT ', @refSQL, '            AS sujet,        "',
colonne_relation, '" AS predicat,        "ontoSQL:',
table_nom, '" AS objet FROM   ',
table_nom
);
ENDIF;
IF colonne_orga > 1
AND stop_curseur_colonnes = 0 then
SET
@requete = concat(
@requete, ' UNION ALL SELECT ', @refSQL,
',        "', colonne_relation,
'",        REPLACE( \'', colonne_masque,
'\', "%s", IFNULL( CAST( ', colonne_nom,
' AS CHAR CHARACTER SET utf8 ), "" ) ) FROM ',
table_nom
);
ENDIF;
UNTIL stop_curseur_colonnes END repeat;
IF debug IS false THEN
SET
@requetePrepare = concat(
'DROP VIEW IF EXISTS `rdf_', table_nom,
'`;'
);
PREPARE stmt
FROM
@requetePrepare;
EXECUTE Stmt;
SET
@requeteVue = Concat(
' CREATE VIEW `rdf_', table_nom,
'` AS ', @requete, ' ; '
);
PREPARE stmt
FROM
@requeteVue;
EXECUTE Stmt;
SET
@requeteSelection = Concat(
' SELECT * FROM `rdf_', table_nom,
'` ; '
);
PREPARE stmt
FROM
@requeteSelection;
EXECUTE Stmt;
DEALLOCATE prepare stmt;
ENDIF;
IF debug IS true THEN
SELECT
@requete;
ENDIF;
END | delimiter;
CALL `serialiser`("factures", "facture_id", false);

Votre retour devrait être similaire à ceci :

La vue pour les factures « rdf_facture » devrait être mise à jour comme suit :

Pensez à exécuter la sérialisation pour la table « clients » :

CALL `serialiser`( "clients", "client_id", false ) ;

IV-E - La jointure : réalisation

Nous avons maintenant deux vues, l’une pour la table « factures » et l’autre pour la table « clients ». Pour l’amusement, vous pouvez aussi produire une vue de la table « ontologies », en pensant à ajouter les lignes correspondantes…

Nous allons produire maintenant une vue des vues disponibles, qui nous sera comme « base RDF » :

CREATE view `rdf`
AS
SELECT *
FROM   rdf_clients
UNION ALL
SELECT *
FROM   rdf_factures;

Ainsi, nous avons désormais une vue unique « rdf » qui peut être appelée et requêtée, avec seulement les colonnes sélectionnées dans la table « ontologies » et une grammaire respectueuse de RDF :

Vous pouvez vérifier en modifiant une valeur : votre vue basée sur une table SQL qui pourrait être en production est tout à fait conforme et reprend bien les valeurs d’origine.

Notre prochaine étape sera une jointure afin de récupérer la facture n°1 avec les informations disponibles au format RDF du client.

Testez une première requête pour saisir le principe que je vais développer après :

SELECT *
FROM   rdf
WHERE  ( sujet LIKE 'refSQL:clients#client_id=1%'
OR objet LIKE 'refSQL:clients#client_id=1%' );

Le résultat montre bien quelques informations qui correspondent à notre facture :

Pour renvoyer toutes les informations disponibles, il faut comprendre que la « jointure » se fera en prenant l’identifiant RDF de la facture et l’identifiant RDF du client. Et éventuellement des autres tables qui pourraient être concernées…

Voici une telle requête, qui pourrait facilement être incorporée dans une autre procédure stockée ou une fonction :

SET @refSQL = "refsql:factures#facture_id=1";
SET @refJointure = "client#id";

SELECT *
FROM   rdf
WHERE  sujet = @refSQL
UNION ALL
SELECT *
FROM   rdf
WHERE  sujet = (SELECT objet
FROM   rdf
WHERE  sujet LIKE @refSQL
AND predicat LIKE @refJointure);

La référence de la jointure entre les factures et les clients est donc un prédicat hérité de notre ontologie : client#id (c’est-à-dire : « la propriété Identifiant du concept de Client  »). Son exécution nous renvoie parfaitement les données disponibles suivant ce qui a été spécifié dans la table « ontologies » :

IV-F - La question des performances et de la sécurité ?

Pour la partie « sécurité », vous pouvez ajuster les droits afin de faire correspondre les vues produites à un profil particulier, qui n’aura donc que l’accès (en lecture) aux données qui correspondent à ce qui est défini dans la table « ontologies » - c’est donc plutôt un progrès, car vous choisissez très finement l’accès, colonne par colonne, pour chaque table sérialisée.

De plus, si vous modifiez l’organisation de vos tables, l’appel à la fonction de sérialisation modifie à la volée votre vue RDF de la table – c’est donc faisable « à chaud » avec une gestion intelligente des verrous.

Sur la question des performances, lors que les jointures sont nombreuses et les tables importantes, la notion de produits cartésiens – une épreuve mortelle pour votre base dans certains cas – se traite ici différemment. À proprement parler, il n’y a pas de produit cartésien, car nous ne sommes que sur une imbrication de requêtes de sélection. Parler de jointure serait donc impropre, mais l’idée, le résultat, c’est d’avoir les mêmes informations disponibles que dans une jointure SQL classique.

Enfin, l’usage « union all » évite un traitement des doublons (peu importe leur existence). L’usage des vues évite le peuplement de tables temporaires. Ce que je propose ici est donc probablement l’une des voies les plus efficaces pour sérialiser des tables en SQL vers des triplets RDF exploitables par un moteur dédié, en utilisant les possibilités offertes nativement par SQL.

Reste que cet usage n’est pas recommandé pour un grand nombre de données, car la génération de la vue n’est pas neutre en termes de délai (jusqu’à plusieurs secondes).

Cependant, il reste des points d’amélioration, en jouant justement sur des tables de résultats. Ainsi, des tests avec les requêtes sur un ensemble de données plus massif (145 k entrées, avec une dizaine de colonnes) renvoient des milliers de lignes (145 k triplets pour chaque colonne de la table SQL…).

Le temps de traitement sur la vue, sans jointure, avoisine alors 4.5 secondes (au lieu de 0.26 seconde avec une requête simple) et n’évolue pas, quel que soit le nombre de traitements ou de recherches appliqués. Dans la plupart des cas, c’est un délai inacceptable.

Une idée pour optimiser cela est l’utilisation d’une table pour stocker les données extraites (ce qui en réduit souvent le nombre), les sérialiser, puis les renvoyer (attention, la base est différente de celle indiquée plus haut, pour l’exemple) :

CREATE TABLE vers_rdfSELECT *
FROM   `lexique`
WHERE  `lemme` = 'zyeuter' ; -- cette requête retourne 8 éléments de la table d'origine "lexique"CREATE VIEW rdf_vers_rdf                                           AS
SELECT Replace( 'refSQL:lexique#lexique_id=%s', "%s", lexique_id ) AS sujet,
"a"                                                         AS predicat,
"ontoSQL:lexique"                                           AS objet
FROM   vers_rdf
UNION ALL
SELECT Replace( 'refSQL:lexique#lexique_id=%s', "%s", lexique_id ) ,
"lexique:orthographe",
replace( '"%s"^^rdf:string', "%s", ifnull( cast( ortho AS char characterSET utf8 ), "" ) )
FROM   vers_rdf
UNION ALL
select replace( 'refSQL:lexique#lexique_id=%s', "%s", lexique_id ) ,
"lexique:lemme",
replace( '"%s"^^rdf:string', "%s", ifnull( cast( lemme AS char characterSET utf8 ), "" ) )
FROM   vers_rdf
UNION ALL
select replace( 'refSQL:lexique#lexique_id=%s', "%s", lexique_id ) ,
"lexique:type",
replace( '"%s"^^rdf:string', "%s", ifnull( cast( cgram AS char characterSET utf8 ), "" ) )
FROM   vers_rdf ; -- cette requête peut être générée par une procédure, en utilisant les briques de la procédure "sérialiser" déjà indiquéeSELECT *
FROM   rdf_vers_rdf ; -- retourne les résultatsDROP TABLE vers_rdf;DROP VIEW rdf_vers_rdf;

L’ensemble des délais cumulés pour ces opérations, tombe alors à 0.2844 seconde, proche d’une requête SQL classique, en gardant la souplesse de RDF.

Si vous avez encore des doutes sur les performances globales du modèle RDF (au-delà du cas que j’évoque ici, qui s’apparente à un cas d’étude), je vous invite à lire l’introduction sur le Web Data du W3C :

Data is increasingly important to society and W3C has a mature suite of Web standards with plans for further work on making it easier for average developers to work with graph data and knowledge graphs. Linked Data is about the use of URIs as names for things, the ability to dereference these URIs to get further information and to include links to other data. There are ever increasing sources of Linked Open Data on the Web, as well as data services that are restricted to the suppliers and consumers of those services.
The digital transformation of industry is seeking to exploit advanced digital technologies. This will facilitate businesses to integrate horizontally along the supply and value chains, and vertically from the factory floor to the office floor. (…)
Traditional approaches to data have focused on tabular databases (SQL/RDBMS), Comma Separated Value (CSV) files, and data embedded in PDF documents and spreadsheets. We're now in midst of a major shift to graph data with nodes and labelled directed links between them. Graph data is :

- Faster than using SQL and associated JOIN operations

- Better suited to integrating data from heterogeneous sources

- Better suited to situations where the data model is evolving

Les données sont de plus en plus importantes pour la société et le W3C dispose d'une suite de normes Web bien préparée, qui devrait permettre aux développeurs moyens de travailler plus facilement avec les données graphiques et les graphiques de connaissances. Les données liées concernent l'utilisation des URI en tant que noms d'objets, la possibilité de déréférencer ces URI pour obtenir des informations supplémentaires et d'inclure des liens vers d'autres données. Il existe de plus en plus de sources de données ouvertes liées sur le Web, ainsi que de services de données réservés aux fournisseurs et aux consommateurs de ces services.
La transformation numérique de l'industrie cherche à exploiter les technologies numériques avancées. Cela facilitera l’intégration horizontale des entreprises dans les chaînes d’approvisionnement et de valeur, et l’intégration verticale de l’usine à l’étage. (…)
Les approches traditionnelles en matière de données se sont concentrées sur les bases de données tabulaires (SQL / SGBDR), les fichiers CSV (valeurs séparées par des points-virgules) et les données incorporées dans des documents PDF et des feuilles de calcul. Nous sommes actuellement en pleine mutation des données graphiques avec des nœuds et des liens dirigés étiquetés entre eux. Les données graphiques sont :

- plus rapides que SQL et les opérations JOIN associées ;

- mieux adaptées à l'intégration de données provenant de sources hétérogènes ;

- mieux adaptées aux situations où le modèle de données évolue.

V - Conclusions

Ce que j’explique ici est une première approche de ce que peut faire une application de SQL pour la production RDF dans des bases relationnelles.

Pour tester des scénarios et des développements, ou une production sémantique sur peu de tables ou peu de données par table, une telle solution suffit largement.

Si vous connaissez de meilleures méthodes que celle que j’ai démontrée ici, discutons-en sur le forum !

À très bientôt.

VI - Note de la rédaction de Developpez.com

Nous tenons à remercier escartefigue pour la relecture orthographique de ce tutoriel.