Du SQL à R : l’accès aux données

Dans les articles précédents nous avons appris à transposer les retraitements SQL standards en R. Cependant, il manque quelque chose d’essentiel : R travaille avec des objets en mémoire, c’est à dire chargés dans des data.frames, vecteurs, listes etc. alors que vos données sont probablement rangées bien au chaud dans une base de données. Allez-vous devoir exporter manuellement des giga-octets de fichiers CSV, parquet ou autre pour pouvoir faire vos petits calculs ?

Que nenni car entre en scène une extension de dplyr : dbplyr !

Qu’est ce que dbplyr ?

dbplyr est un sous-projet parallèle à dplyr qui est désormais intégré. Son rôle est de proposer une couche d’abstraction pour tout simplement utiliser au maximum la syntaxe dplyr afin de manipuler directement une ou des bases de données.

dbplyr fonctionne autant que possible de façon transparente pour migrer les données de ou vers la base de données afin de décharger une partie du travail du poste local.

En pratique, dans la majorité des cas, dbplyr vas transformer un pipline dplyr en requête SQL pour la faire tourner sur le serveur de base de données et ensuite importer le résultat pour continuer le traitement localement.

C’est un peu confus tout ça !

Je suis d’accord et c’est pour cela que je vous propose de commencer d’emblée par la mise en oeuvre.

Premier problème, avoir accès à une base de données

Pour pouvoir étudier un exemple réel, il nous faut des données réelles. Je ne peux malheureusement pas deviner les données que chaque lecteur a ou n’a pas à disposition.

Je vous ai donc concocté un pseudo-DPI que vous allez trouver sous un format bien connu : SQLITE.

SQLITE est un serveur embarqué de base de données qu’il est possible de charger à la volée dans un programme et qui sauvegarde les données dans un unique fichier qu’il va simplement falloir télécharger :

Les librairies nécessaires

Pour notre exercice, nous allons avoir besoin de certaines librairies qu’il faudra ajouter si nécessaire :

  • dplyr, bien sûr
  • DBI, la couche d’abstraction de base de données
  • Rsqlite, la couche spécifique au format SQLITE

Il nous faut aussi tidyr pour illustrer un retraitement en R

Si nous avions tenté de nous connecter à une base Oracle, MySQL/MariaDB, PostgreSQL, etc. il aurait fallu alors charger la librairie spécifique (qu’on appelle backend). Vous avez sur la droite la liste des moteurs de base de données officiellement supportés par dbplyr. Les principales y sont. De plus avec ODBC, cela ouvre – si vous êtes sur une machine adaptée- tous les drivers génériques qui pourraient y être liés.

Dans notre cas, nous voulons SQLITE donc c’est bon !

Par ailleurs, la dernière ligne stipule Teradata qui est le moteur de base de données qui a été déployé l’année dernière sur la plateforme d’accès aux données de santé de l’ATIH. Et cette dernière a décidé de migrer totalement ses bases, applications et webapps de reporting de SAS vers R/Teradata ! L’avenir du requêtage national du PMSI sera donc à portée de vos doigts à la fin de cet article.

Se connecter à un fichier SQLITE

Une fois le fichier récupéré et enregistré sur votre disque (pour ma part, je l’ai dans mon répertoire de travail), il faut s’y connecter.

# Chargeons les librairies
library(tidyr)
library(DBI)
library(RSQLite)
library(dplyr)

# Et connectons-nous
con <- dbConnect(SQLite(),
                 "dpi_demo.sqlite",
                 extended_types = TRUE #nécessaire pour les types DATE
)
R

A partir de maintenant, nous pouvons utiliser le contenu de ce fichier. con= est un descripteur qui va permettre à toutes le futures commandes que nous allons appliquer d’interagir avec les tables qui y sont stockées.

Le contenu du fichier d’exemple

Nous pouvons par exemple demander la liste des tables contenues dans le fichier avec la fonction DBI dbListTables() puis leurs colonnes avec dbListFields() :


> dbListTables(con)
[1] "diags"    "patients" "rums"     "sejours" 

> dbListFields(con,"patients")
[1] "NOM"    "PRENOM" "DDN"    "SEXE"   "IPP"  
> dbListFields(con,"sejours")
[1] "GHMC"  "GHS"   "DENT"  "DSORT" "IPP"   "DS"    "NDA"  
> dbListFields(con,"rums")
[1] "MENT"      "MSORT"     "DENT_RUM"  "DSORT_RUM" "NRUM"      "NDA"      
> dbListFields(con,"diags")
[1] "DIAG"      "TYPE"      "NRUM"      "NDIAG"    
R

Comme vous pouvez le voir la structure mime un minuscule « pseudo DPI » avec des SEJOURS, une relation 1-à-plusieurs vers des RUMs, eux-mêmes ayant une relation 1-à-plusieurs vers des DIAGS et chaque séjour dépendant d’un PATIENT (relation plusieurs-à-un).

Attacher des tables

Telles qu’elles ces tables pourraient être chargées « brutalement » en mémoire via dbReadTable() qui charge la totalité de son contenu en mémoire au sein d’un data.frame. Mais ce n’est pas notre but. Nous allons donc définir un lien entre la table dans la base de données et un objet R via tbl() :

patients_tbl <- tbl(con, "patients")
patients_tbl
# Source:   table<`patients`> [?? x 5]
# Database: sqlite 3.47.1 [C:\Users\fsenis\Documents\R\atih_demo.sqlite]
   NOM         PRENOM    DDN        SEXE  IPP       
   <chr>       <chr>     <date>     <chr> <chr>     
 1 SUDRE       PIERRETTE 1942-10-19 F     P000004646
 2 MOTTIN      ALAIN     1960-09-19 H     P000003594
 3 ROSTAING    PATRICK   1955-08-21 H     P000004313
 4 LHOTE       NOELE     1951-01-02 F     P000003113
 5 FERCHAUD    MIGAEL    1981-04-08 H     P000001973
 6 NEGRIER     JEREMIE   1979-12-19 H     P000003669
 7 MEYRE       MONIQUE   1941-07-22 F     P000003468
 8 KESKAS      MURIELLE  1962-03-14 F     P000002723
 9 BERRIE      GUY       1945-04-17 H     P000000490
10 MONCOURRIER GUY       1943-02-01 H     P000003530
# ℹ more rows
# ℹ Use `print(n = ...)` to see more rows
R

Comme vous pouvez le voir dans le volet environnement de RStudio, cet objet n’est pas du tout un dataframe :

mais un objet complexe qu’heureusement vous n’avez pas le besoin d’explorer pour l’utiliser :

Si vous regarder en détail le résultat de sortie, on repère des différences notables dans la sortie :

Le dataframe « patients » a un nombre de lignes défini (5000, entièrement chargées en mémoire), tandis que la tbl « patients » ne connait pas sa taille et possède des informations de source en plus. En cas d’appel pour affichage, tbl() va aller chercher un échantillon limité des données dans la base, mais uniquement pour vous faire plaisir.

Si nous voulions récupérer toute la table, nous utiliserions plutôt la fonction DBI dbReadTable() :

> dbReadTable(con, "patients") %>% as_tibble()
# A tibble: 5,000 × 5
   NOM         PRENOM    DDN        SEXE  IPP       
   <chr>       <chr>     <date>     <chr> <chr>     
 1 SUDRE       PIERRETTE 1942-10-19 F     P000004646
 2 MOTTIN      ALAIN     1960-09-19 H     P000003594
 3 ROSTAING    PATRICK   1955-08-21 H     P000004313
 4 LHOTE       NOELE     1951-01-02 F     P000003113
 5 FERCHAUD    MIGAEL    1981-04-08 H     P000001973
 6 NEGRIER     JEREMIE   1979-12-19 H     P000003669
 7 MEYRE       MONIQUE   1941-07-22 F     P000003468
 8 KESKAS      MURIELLE  1962-03-14 F     P000002723
 9 BERRIE      GUY       1945-04-17 H     P000000490
10 MONCOURRIER GUY       1943-02-01 H     P000003530
# ℹ 4,990 more rows
# ℹ Use `print(n = ...)` to see more rows
R

Dans ce cas, nous recevons un data.frame qu’on transforme en tibble et nous avons une stricte égalité entre les 2 sorties.

Monter une requête d’extraction

Il faut bien avouer, récupérer la totalité d’une table depuis une base de données est exactement la négation de la raison d’être d’une base de données relationnelle. Au contraire, son but est de n’en extraire que les données pertinentes.

Supposons donc que nous voulions les patients s’appelant « GUY ». En SQL, nous utiliserions :

SELECT * FROM patients WHERE PRENOM = "GUY";
SQL

Et nous pouvons directement l’appliquer en DBI avec dbGetQuery() :

#attention à la bonne gestion des guillements simplpes et doubles
> dbGetQuery(con, 'SELECT * FROM patients WHERE PRENOM = "GUY";') %>% as_tibble
# A tibble: 29 × 5
   NOM            PRENOM DDN        SEXE  IPP       
   <chr>          <chr>  <date>     <chr> <chr>     
 1 BERRIE         GUY    1945-04-17 H     P000000490
 2 MONCOURRIER    GUY    1943-02-01 H     P000003530
 3 JACQUET        GUY    1938-07-24 H     P000002618
 4 LACOTE         GUY    1947-07-29 H     P000002793
 5 HERNANDEZ      GUY    1931-06-16 H     P000002510
 6 FOUILLEUL      GUY    1937-07-17 H     P000002071
 7 BASSELFALL     GUY    1948-05-11 H     P000000338
 8 NAVARRO        GUY    1943-02-01 H     P000003659
 9 COURTADEGRANGE GUY    1947-08-13 H     P000001287
10 TANGUY         GUY    1936-03-15 H     P000004666
# ℹ 19 more rows
# ℹ Use `print(n = ...)` to see more rows
R

Mais nous pouvons utiliser la syntaxe dplyr pour construire la requête :

> tbl(con, "patients") %>% filter(PRENOM == "GUY")
# Source:   SQL [?? x 5]
# Database: sqlite 3.47.1 [C:\Users\fsenis\Documents\R\atih_demo.sqlite]
   NOM            PRENOM DDN        SEXE  IPP       
   <chr>          <chr>  <date>     <chr> <chr>     
 1 BERRIE         GUY    1945-04-17 H     P000000490
 2 MONCOURRIER    GUY    1943-02-01 H     P000003530
 3 JACQUET        GUY    1938-07-24 H     P000002618
 4 LACOTE         GUY    1947-07-29 H     P000002793
 5 HERNANDEZ      GUY    1931-06-16 H     P000002510
 6 FOUILLEUL      GUY    1937-07-17 H     P000002071
 7 BASSELFALL     GUY    1948-05-11 H     P000000338
 8 NAVARRO        GUY    1943-02-01 H     P000003659
 9 COURTADEGRANGE GUY    1947-08-13 H     P000001287
10 TANGUY         GUY    1936-03-15 H     P000004666
# ℹ more rows
# ℹ Use `print(n = ...)` to see more rows
R

Notez que nous sommes toujours en live dans la base de données donc la requête ne connait pas sa taille et renvoit juste un aperçu. Pour récupérer définitivement (et figer) les données, nous avons une dernière action à réaliser : collecter les données avec… collect()

>  tbl(con, "patients") %>% filter(PRENOM == "GUY") %>% collect
# A tibble: 29 × 5
   NOM            PRENOM DDN        SEXE  IPP       
   <chr>          <chr>  <date>     <chr> <chr>     
 1 BERRIE         GUY    1945-04-17 H     P000000490
 2 MONCOURRIER    GUY    1943-02-01 H     P000003530
 3 JACQUET        GUY    1938-07-24 H     P000002618
 4 LACOTE         GUY    1947-07-29 H     P000002793
 5 HERNANDEZ      GUY    1931-06-16 H     P000002510
 6 FOUILLEUL      GUY    1937-07-17 H     P000002071
 7 BASSELFALL     GUY    1948-05-11 H     P000000338
 8 NAVARRO        GUY    1943-02-01 H     P000003659
 9 COURTADEGRANGE GUY    1947-08-13 H     P000001287
10 TANGUY         GUY    1936-03-15 H     P000004666
# ℹ 19 more rows
# ℹ Use `print(n = ...)` to see more rows
R

Maintenant, si patients_tbl a été initalisé par tbl(con, "patients"), nous pouvons appeler :

> patient_tbl %>% filter(PRENOM == "GUY") %>% collect
R

Et vous avez ici toute la puissance de dbplyr : vous écrivez votre pipeline dplyr mais à base de tbl() et collect() génère le jeu de données résultat. 2 exemples pratiques :

#Quelques valeurs de base définies dans R
cim_sp <- "Z515"
ghm_sp <- c("23Z02Z", "23Z02T")

# les tables de la base de données
patients_tbl <- tbl(con, "patients")
sejours_tbl <- tbl(con, "sejours")
rums_tbl <- tbl(con, "rums")
diags_tbl <- tbl(con, "diags")

# Traitements :

# Séjours en GHM de soins palliatifs ou soins de support
> sejours_tbl %>% filter(GHMC %in% ghm_sp) %>% collect
# A tibble: 613 × 7
   GHMC     GHS DENT                DSORT               IPP           DS NDA       
   <chr>  <dbl> <dttm>              <dttm>              <chr>      <int> <chr>     
 1 23Z02T  7991 2019-10-01 14:00:00 2019-10-01 17:20:00 P000003538     0 S000000001
 2 23Z02Z  7994 2019-10-03 14:50:00 2019-10-06 15:30:00 P000003249     3 S000000002
 3 23Z02Z  7994 2019-10-07 10:39:00 2019-10-20 19:25:00 P000002627    13 S000000003
 4 23Z02T  7991 2019-10-07 13:56:00 2019-10-07 17:00:00 P000001893     0 S000000004
 5 23Z02T  7991 2019-10-08 09:30:00 2019-10-08 16:00:00 P000003130     0 S000000005
 6 23Z02Z  7994 2019-10-09 08:59:00 2019-10-22 08:10:00 P000001346    13 S000000006
 7 23Z02Z  7994 2019-10-10 08:48:00 2019-10-14 09:00:00 P000001607     4 S000000007
 8 23Z02Z  7994 2019-10-11 11:21:00 2019-10-20 08:00:00 P000002174     9 S000000008
 9 23Z02T  7991 2019-10-14 13:25:00 2019-10-14 18:00:00 P000002550     0 S000000009
10 23Z02T  7991 2019-10-14 13:41:00 2019-10-14 15:30:00 P000001103     0 S000000010
# ℹ 603 more rows
# ℹ Use `print(n = ...)` to see more rows

# Séjours porteurs de code de soins palliatifs hors du GHM de soins palliatifs
> sejours_tbl %>% filter(!(GHMC %in% ghm_sp)) %>% semi_join(rums_tbl %>% inner_join(diags_tbl %>% filter(DIAG== cim_sp))) %>% collect
Joining with `by = join_by(NRUM)`
Joining with `by = join_by(NDA)`
# A tibble: 27 × 7
   GHMC     GHS DENT                DSORT               IPP           DS NDA       
   <chr>  <dbl> <dttm>              <dttm>              <chr>      <int> <chr>     
 1 90Z00Z  9999 2019-12-09 14:00:00 2019-12-09 17:30:00 P000001980     0 S000000059
 2 05M124  1770 2020-12-08 15:37:00 2020-12-18 07:55:00 P000002870    10 S000000511
 3 06M133  2178 2020-12-21 15:28:00 2021-02-02 18:30:00 P000004800    43 S000000685
 4 10M163  3959 2020-12-31 18:35:00 2021-01-07 13:00:00 P000002883     7 S000000765
 5 10M163  3959 2021-03-09 11:22:00 2021-03-16 18:45:00 P000001941     7 S000001612
 6 06C14J  1982 2021-03-17 06:51:00 2021-03-17 16:22:00 P000002748     0 S000001733
 7 04M093  1163 2021-03-18 08:18:00 2021-04-05 09:45:00 P000000510    18 S000001746
 8 09C03J  3323 2021-03-22 09:46:00 2021-03-22 18:00:00 P000000597     0 S000001787
 9 11M041  4293 2021-03-27 07:34:00 2021-03-28 12:00:00 P000004806     1 S000001859
10 06M04T  2199 2021-04-04 10:19:00 2021-04-05 16:00:00 P000002220     1 S000001967
# ℹ 17 more rows
# ℹ Use `print(n = ...)` to see more rows
R

Il est tout à fait possible de sauvegarder la requête en l’attribuant à un objet :

# création de la requête
> requete <- sejours_tbl %>% filter(!(GHMC %in% ghm_sp)) %>% semi_join(rums_tbl %>% inner_join(diags_tbl %>% filter(DIAG== cim_sp)))
Joining with `by = join_by(NRUM)`
Joining with `by = join_by(NDA)`
# (pas de sortie écran lors de la création,
# les "Joining with" apparaissent parce que je n'ai pas explicitement
# spécifié les jointures et dplyr les a déduites)

# exécution de la requête (on pourrait bien sûr l'attribuer à une
# variable vu que c'est un tibble tout ce qu'il y a de plus standard)
> requete %>% collect
# A tibble: 27 × 7
   GHMC     GHS DENT                DSORT               IPP           DS NDA       
   <chr>  <dbl> <dttm>              <dttm>              <chr>      <int> <chr>     
 1 90Z00Z  9999 2019-12-09 14:00:00 2019-12-09 17:30:00 P000001980     0 S000000059
 2 05M124  1770 2020-12-08 15:37:00 2020-12-18 07:55:00 P000002870    10 S000000511
 3 06M133  2178 2020-12-21 15:28:00 2021-02-02 18:30:00 P000004800    43 S000000685
 4 10M163  3959 2020-12-31 18:35:00 2021-01-07 13:00:00 P000002883     7 S000000765
 5 10M163  3959 2021-03-09 11:22:00 2021-03-16 18:45:00 P000001941     7 S000001612
 6 06C14J  1982 2021-03-17 06:51:00 2021-03-17 16:22:00 P000002748     0 S000001733
 7 04M093  1163 2021-03-18 08:18:00 2021-04-05 09:45:00 P000000510    18 S000001746
 8 09C03J  3323 2021-03-22 09:46:00 2021-03-22 18:00:00 P000000597     0 S000001787
 9 11M041  4293 2021-03-27 07:34:00 2021-03-28 12:00:00 P000004806     1 S000001859
10 06M04T  2199 2021-04-04 10:19:00 2021-04-05 16:00:00 P000002220     1 S000001967
# ℹ 17 more rows
# ℹ Use `print(n = ...)` to see more rows
R

Explorer la projection vers la base de données

Comme expliqué, dbplyr sert à requêter une base en écrivant un pipeline dplyr au lieu du code SQL, cependant celui-ci reste explorable. Nous avons 2 fonctions pour cela

show_query()

Cette fonction, appliquée à une requête, affiche le code qui va être transmis au moteur de base de données. Dans notre exemple :

> requete %>% show_query()
<SQL>
SELECT `LHS`.*
FROM (
  SELECT `sejours`.*
  FROM `sejours`
  WHERE (NOT((`GHMC` IN ('23Z02Z', '23Z02T'))))
) AS `LHS`
WHERE EXISTS (
  SELECT 1 FROM (
  SELECT `rums`.*, `DIAG`, `TYPE`, `NDIAG`
  FROM `rums`
  INNER JOIN (
    SELECT `diags`.*
    FROM `diags`
    WHERE (`DIAG` = 'Z515')
  ) AS `RHS`
    ON (`rums`.`NRUM` = `RHS`.`NRUM`)
) AS `RHS`
  WHERE (`LHS`.`NDA` = `RHS`.`NDA`)
)
R

Comme vous pouvez le voir, dbplyr a traduit le code dplyr en code SQL nécessaire à notre requête ; il a fait les substitutions de variables et de fonctions nécessaires pour que le code soit adapté au moteur et a même adapté semi_join() qui est un type de jointure qui n’est pas géré normalement.

Ca n’est pas obligatoirement le code que vous auriez produit à la main, mais il fonctionne.

Le plan d’exécution interne

Il est aussi possible d’obtenir le plan d’exécution interne d’une requête c’est à dire comment elle va être découpée et exécutée par le moteur. Pour cela on demande explain() (explain est une fonction générique qui peut s’appliquer à divers objets en R, dans notre cas, vous pouvez l’appliquer sur une requête mais aussi sur une tbl) :

> requete %>% explain
<SQL>
SELECT `LHS`.*
FROM (
  SELECT `sejours`.*
  FROM `sejours`
  WHERE (NOT((`GHMC` IN ('23Z02Z', '23Z02T'))))
) AS `LHS`
WHERE EXISTS (
  SELECT 1 FROM (
  SELECT `rums`.*, `DIAG`, `TYPE`, `NDIAG`
  FROM `rums`
  INNER JOIN (
    SELECT `diags`.*
    FROM `diags`
    WHERE (`DIAG` = 'Z515')
  ) AS `RHS`
    ON (`rums`.`NRUM` = `RHS`.`NRUM`)
) AS `RHS`
  WHERE (`LHS`.`NDA` = `RHS`.`NDA`)
)

<PLAN>
  id parent notused                                                                  detail
1  2      0     216                                                            SCAN sejours
2 12      0       0                                            CORRELATED SCALAR SUBQUERY 4
3 17     12     216                                                               SCAN rums
4 33     12      53 SEARCH diags USING AUTOMATIC PARTIAL COVERING INDEX (DIAG=? AND NRUM=?)
R

Nous avons alors la requête SQL mais aussi un plan d’exécution expliquant ce que va faire le moteur SQLITE. Cette sortie par contre n’est pas standardisée pour un retraitement informatique, c’est plus ou moins à destination d’un humain. Voici par exemple ci-dessous la sortie explain() d’une requête sur les bases nationales donc via le moteur teradata :

Le but de cette fonctionnalité est de permettre de débugger votre requête si les données sont incohérentes ou les performances décevantes.

Combiner la puissance de la base de données avec les retraitements R

Lors d’un traitement, nous pouvons décider qui travaille en fonction d’où on place le collect() :

Ce qui est avant le collect() est traité par la base de données, ce qui est après est traité par R.

> sejours_tbl %>%
    inner_join(patients_tbl) %>%
    mutate(CMD = substr(GHMC, 1, 2)) %>%
    group_by(SEXE, CMD) %>%
    summarise(n = n(),dms = mean(DS)) %>% 
    # traité par la BDD jusqu'ici
    collect %>% ##############################################################
    # traité par R à partir d'ici
    pivot_wider(names_from="SEXE",values_from=c("n","dms"))
Joining with `by = join_by(IPP)`
`summarise()` has grouped output by "SEXE". You can override using the `.groups` argument.
# A tibble: 25 × 5
   CMD     n_F   n_H  dms_F  dms_H
   <chr> <int> <int>  <dbl>  <dbl>
 1 01      215   201 1.81   1.90  
 2 02      218   189 0.0183 0.0794
 3 03       48    63 0.5    0.222 
 4 04      239   195 3.90   3.99  
 5 05      282   281 1.52   2.21  
 6 06      670   544 1.21   1.29  
 7 07       95   109 5.71   3.70  
 8 08      414   369 3.07   4.32  
 9 09      142   115 3.30   3.69  
10 10      102    82 5.45   5.09  
# ℹ 15 more rows
# ℹ Use `print(n = ...)` to see more rows
R

Le résultat sera strictement le même si vous mettez le collect() avant le group_by(), cependant le traitement du regroupement sera fait pas R et non par le moteur de base. Ce qui peut avoir des impacts en terme de performance (selon le nombre de lignes à récupérer via le réseau par exemple).

Dans le cas d’uns base de données hébergée sur un serveur dédié (généralement bien dimensionné), il est souvent plus « rentable » de lui faire faire le maximum du traitement.

Les limites du système

L’équivalence automatique des verbes et fonctions R vers SQL

Bien entendu, dbplyr ne va pas pouvoir TOUT transformer en code SQL, ce langage ayant un but précis limitant sa polyvalence. De même, il n’est pas toujours possible de convertir une donnée ou fonction spécifique ; seules les plus fréquentes sont prévues, n’espérez donc pas que votre fonction personnalisée soit automatiquement transcrite en SQL ou en procédure stockée.

Par contre, il est tout à fait possible, si vous avez défini des fonctions personnalisées directement sur la base de données ou si vous voulez appeler des fonctions précises de la base dans vos traitements de les passer lors de la construction de requête. Par exemple :

> patients_tbl %>%
   mutate(
     alea_perso= sql("random()/92233720368547758.07"),
     alea_dbplyr = runif()) %>%
   show_query()
<SQL>
SELECT
  `patients`.*,
  random()/92233720368547758.07 AS `alea_perso`,
  (0.5 + RANDOM() / 18446744073709551616.0) AS `alea_dbplyr`
FROM `patients`
R

Nous voyons ci dessus alea_perso= créé manuellement en passant du code SQL brut tandis que alea_dbplyr= est produit par traduction de la fonction runif(). Par contre si vous passez une fonction inconnue (tant en auto-traduction ou par le moteur de base), la requête échouera.

L’accès à des ressources composites

Imaginons que patients et sejours sont 2 dataframes contenant la même chose que les tables pointées par patients_tbl et sejours_tbl :

patients <- dbReadTable(con, "patients") # un tibble/dataframe
patients_tbl <- tbl(con, "patients")     # un tbl dynamique

sejours <- dbReadTable(con, "sejours")   # un tibble/dataframe
sejours_tbl <- tbl(con, "sejours")      # un tbl dynamique
R

Alors on pourrais être tenté de faire

patients %>% left_join(sejours_tbl)

patients_tbl %>% left_join(sejours)
R

Ces deux appels mélangeant des pommes et des poires (tibbles locaux et tbls en base) finissent avec une erreur mais dont le message nous donne un espoir :

Error in `auto_copy()`:
! `x` and `y` must share the same src.
 `x` is a <tbl_SQLiteConnection/tbl_dbi/tbl_sql/tbl_lazy/tbl> object.
 `y` is a <tbl_df/tbl/data.frame> object.
 Set `copy = TRUE` if `y` can be copied to the same source as `x` (may be slow).
R

En effet, selon la configuration de la base de données et les quantités de données dans les tables concernées, l’ajout du paramètre copy= réglé à TRUE dans la jointure va entrainer une recopie de la table non disponible vers l’autre mode de stockage (soumis à droits divers pour les bases de données). Ainsi :

> patients_tbl %>% inner_join(sejours, copy=TRUE) %>% show_query()
Joining with `by = join_by(IPP)`
<SQL>
SELECT `patients`.*, `GHMC`, `GHS`, `DENT`, `DSORT`, `DS`, `NDA`
FROM `patients`
INNER JOIN `dbplyr_eQn29OQvG0`
  ON (`patients`.`IPP` = `dbplyr_eQn29OQvG0`.`IPP`)
# On voit bien le nom de la table temporaire crée>

> patients_tbl %>% inner_join(sejours, copy=TRUE) 
Joining with `by = join_by(IPP)`
# Source:   SQL [?? x 11]
# Database: sqlite 3.47.1 [C:\Users\fsenis\Documents\R\atih_demo.sqlite]
   NOM      PRENOM    DDN        SEXE  IPP        GHMC     GHS DENT               
   <chr>    <chr>     <date>     <chr> <chr>      <chr>  <dbl> <dttm>             
 1 SUDRE    PIERRETTE 1942-10-19 F     P000004646 23M20Z  7990 2021-02-04 15:11:00
 2 MOTTIN   ALAIN     1960-09-19 H     P000003594 16M133  6194 2022-02-08 14:02:00
 3 ROSTAING PATRICK   1955-08-21 H     P000004313 06M12T  5381 2021-10-28 15:22:00
 4 LHOTE    NOELE     1951-01-02 F     P000003113 23Z02T  7991 2020-12-22 13:50:00
 5 LHOTE    NOELE     1951-01-02 F     P000003113 23Z02Z  7994 2021-04-29 09:45:00
 6 FERCHAUD MIGAEL    1981-04-08 H     P000001973 06K04J  2121 2021-02-01 07:08:00
 7 NEGRIER  JEREMIE   1979-12-19 H     P000003669 06M033  2131 2021-07-21 18:54:00
 8 NEGRIER  JEREMIE   1979-12-19 H     P000003669 07M02T  5409 2021-07-19 12:24:00
 9 MEYRE    MONIQUE   1941-07-22 F     P000003468 07M071  2529 2022-01-05 13:58:00
10 KESKAS   MURIELLE  1962-03-14 F     P000002723 11M044  4296 2020-12-25 19:45:00
# ℹ more rows
# ℹ 3 more variables: DSORT <dttm>, DS <int>, NDA <chr>
# ℹ Use `print(n = ...)` to see more rows
R

On voit bien de part les entêtes de résultat que nous sommes dans l’affichage d’une requête côté base de données dont il faudra collect()er le contenu pour l’avoir dans R.

Nous pouvons faire le traitement inverse (données principales locales, jointure avec données distantes) :

> patients %>% inner_join(sejours_tbl, copy=TRUE)
Joining with `by = join_by(IPP)`
# A tibble: 6,895 × 11
   NOM      PRENOM    DDN        SEXE  IPP        GHMC     GHS DENT               
   <chr>    <chr>     <date>     <chr> <chr>      <chr>  <dbl> <dttm>             
 1 SUDRE    PIERRETTE 1942-10-19 F     P000004646 06K04J  2121 2021-01-06 08:58:00
 2 MOTTIN   ALAIN     1960-09-19 H     P000003594 02C05J   424 2021-12-06 06:00:00
 3 MOTTIN   ALAIN     1960-09-19 H     P000003594 02C05J   424 2022-01-03 06:00:00
 4 ROSTAING PATRICK   1955-08-21 H     P000004313 23M20T  7989 2021-09-20 15:54:00
 5 LHOTE    NOELE     1951-01-02 F     P000003113 04M111  5194 2021-07-01 15:40:00
 6 FERCHAUD MIGAEL    1981-04-08 H     P000001973 02C05J   424 2020-12-07 11:00:00
 7 FERCHAUD MIGAEL    1981-04-08 H     P000001973 08C481  2885 2021-11-21 17:00:00
 8 NEGRIER  JEREMIE   1979-12-19 H     P000003669 20Z051  5797 2021-03-27 01:04:00
 9 MEYRE    MONIQUE   1941-07-22 F     P000003468 08C38J  2840 2021-02-08 06:00:00
10 MEYRE    MONIQUE   1941-07-22 F     P000003468 06K04J  2121 2022-01-26 06:56:00
# ℹ 6,885 more rows
# ℹ 3 more variables: DSORT <dttm>, DS <int>, NDA <chr>
# ℹ Use `print(n = ...)` to see more rows
R

Le résultat est cette fois-ci directement un tibble vu que nous sommes en local.

Il n’en reste pas moins que ces manœuvres sont une consommation inutile de ressources en règle générale. Il vaut donc mieux planifier et passer par un prétraitement avec un collect() puis un traitement local en R.

Si vous attaquez directement le serveur de base de données principale de votre DPI1, il est TRES probable que les droits utilisateurs vous interdisent l’accès en écriture et en création de table (ce qui vous permettrait de totalement casser la base live du DPI), attendez-vous à ce que ce type de traitement échoue dans le sens R -> BdD !

Conclusion

Avec cet article, vous êtes fins prêt à bidouiller mon mini-DPI et quand vous vous sentirez prêt d’attaquer votre DPI ou les bases nationales. Et comme d’habitude, n’hésitez pas à me contacter si vous avez besoin de précisions.

La prochaine fois, en bonus, diverses infos sur d’autres pans mais non indispensables pour la manipulation de données en lecture.

  1. C’est MAL de faire comme ça. Il est fortement conseillé de passer par une réplication temps réel sur un autre serveur (moins puissant que le serveur de production) sur lequel les droits sont réglés en lecture exclusive. ↩︎

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *