logo

Magie de DuckDB 🩆

DuckDB en quelques mots

DuckDB est un logiciel permettant d’exĂ©cuter du code SQL sur diffĂ©rents types de donnĂ©es de maniĂšre trĂšs lĂ©gĂšre, avec zĂ©ro administration, et une optimisation pour les opĂ©rations “analytiques”.
Qu’entend-on par “analytique” ? En gĂ©nĂ©ral, c’est un terme Ă  opposer Ă  “transactionnel”, le travail historique demandĂ© aux bases de donnĂ©es, Ă  savoir la mise Ă  jour de donnĂ©es avec un systĂšme de confirmation / restauration des opĂ©rations. La partie “analytique”, elle, correspond plutĂŽt Ă  l’aspect de requĂȘtage et de traitement statistique des donnĂ©es.
Si DuckDB s’avùre finalement trùs bon sur l’aspect transactionnel, c’est d’abord pour des besoins analytiques qu’on l’utilise, et ce sera le cas ici.

Sources de données possibles (dans R) :

  • un data.frame 📰 (je n’ai pas trouvé d’emoji plus convaincant pour “data.frame” 😅)
  • un fichier dĂ©limitĂ© đŸ§Ÿ type CSV / sĂ©parateur tabulation / etc.
  • un ou plusieurs fichiers Parquet (en collaboration avec Arrow)đŸ—ƒïž

Voyons cela en détail, et en chronométrant les temps de traitement (sur un PC vieux de quelques années, avec le package {tictoc}) pour donner un ordre de grandeur.

DuckDB 101 : comment démarrer ?

Chaque utilisateur aura sa propre base de donnĂ©es DuckDB, pilotĂ©e depuis R. Evidemment la premiĂšre chose Ă  faire sera d’activer le package {duckdb}. Puis de dĂ©marrer une session DuckDB, qui sera associĂ©e Ă  un fichier physique. Le rĂŽle de celui-ci est de stocker si besoin des donnĂ©es volumineuses en cours de traitement.

library(duckdb)
tictoc::tic()
bd <- dbConnect(duckdb())
tictoc::toc()

 

0.05 sec elapsed

La syntaxe ci-dessus est le minimum vital pour dĂ©marrer une session DuckDB fonctionnelle. Le dialogue avec cette session passera dĂ©sormais par l’objet nommĂ© bd (le nom est bien sĂ»r libre, il s’appelle con dans quasiment tous les exemples que j’ai pu lire ; vous pouvez aussi l’appeler coincoin ou howard).

On peut ajouter des options, particuliĂšrement utiles dans un environnement contraint (serveur R partagĂ© avec d’autres utilisateurs) pour Ă©viter un accaparement excessif des ressources.

tictoc::tic()
bd <- dbConnect(duckdb(
  dbdir = "fichier_stockage_permanent.duckdb",
  config = list(temp_directory = "fichier_stockage_temporaire.duckdb",
                threads = "4", 
                memory_limit = "4GB")
))
tictoc::toc()

 

0.05 sec elapsed
  • 📁Le fichier de l’option dbdir servira Ă  stocker des tables créées au cours de la session DuckDB (principalement via la syntaxe SQL CREATE TABLE) ou d’autres objets SQL (vue, schĂ©ma, etc.). Ce fichier est conservĂ© Ă  la fin de la session et peut ĂȘtre rĂ©utilisĂ© dans une autre session pour repartir de ces tables.
    Indiquez dans cette option un fichier non partagĂ© (on ne peut pas avoir plusieurs sessions DuckDB simultanĂ©es qui pointent vers le mĂȘme fichier), dans un rĂ©pertoire oĂč il y a de la place libre. En particulier, ne pointez pas vers un fichier temporaire comme ce que crĂ©e tempfile() car la taille de ces fichiers est souvent limitĂ©e.
  • 📁Le fichier de l’option temp_directory servira le temps de la session DuckDB Ă  stocker des donnĂ©es sur disque si la mĂ©moire allouĂ©e Ă  la session n’est pas suffisante (dĂ©charge ou spill). Ce fichier est dĂ©truit Ă  la fin de la session DuckDB et tout ce qui y est stockĂ© est perdu. Le stockage n’y est jamais volontaire de la part de l’utilisateur, contrairement Ă  dbdir.
    Indiquez dans cette option un fichier non partagĂ© (on ne peut pas avoir plusieurs sessions DuckDB qui pointent vers le mĂȘme fichier), dans un rĂ©pertoire oĂč il y a de la place libre. En particulier, ne pointez pas vers un fichier temporaire comme ce que crĂ©e tempfile() car la taille de ces fichiers est souvent limitĂ©e.
  • đŸ€L’option threads indique le nombre de coeurs de calculs utilisables pour parallĂ©liser les requĂȘtes. Par courtoisie pour vos collĂšgues, Ă©vitez d’utiliser plus du tiers des coeurs du serveur.
  • 🧠L’option memory_limit indique la RAM associĂ©e Ă  la session DuckDB. Quand cette quantitĂ© sera saturĂ©e, DuckDB Ă©crira sur le disque dans le fichier indiquĂ© prĂ©cĂ©demment. LĂ  encore il s’agit de ne pas privatiser un serveur au dĂ©triment des collĂšgues. Si la quantitĂ© de RAM indiquĂ©e ici est basse, l’exĂ©cution sera juste plus lente.

Une liste exhaustive des options se trouve ici.

Mettre des données à disposition de DuckDB

Evidemment, dans une base de donnĂ©es, il faut des donnĂ©es. Il ne s’agit pas forcĂ©ment de les importer dans DuckDB, une des forces de cet outil Ă©tant de pouvoir travailler sur des liens vers des sources de donnĂ©es.

Travailler sur un data.frame

Si on veut réellement dupliquer un data.frame dans DuckDB, on pourra utiliser la fonction dplyr::copy_to mais la solution optimale pour des données volumineuses consiste à simplement déclarer un lien.

library(nycflights13)
tictoc::tic()
data(flights)

duckdb_register(bd, "tb_vols", flights)
tictoc::toc()

 

0.82 sec elapsed

✈A titre d’exemple je prends les donnĂ©es du package {nycflights13} avec les 336 776 lignes de la table flights. Le code ci-dessus s’exĂ©cute instantanĂ©ment, la fonction duckdb_register se contentant de faire un lien vers le data.frame flights. Le 2e argument de la fonction est le nom sous lequel DuckDB connaĂźtra ces donnĂ©es, sous forme d’une pseudo table.

Rien ne transparaĂźt Ă  l’issue de ce code : pas d’objet visible dans l’Environnement, pas d’affichage dans la Console. Si DuckDB a bien fait connaissance avec les donnĂ©es, il ne nous en dit rien. Nous allons donc ajouter une derniĂšre Ă©tape : un objet R qui fera le lien avec cette pseudo-table et nous permettra de les afficher et surtout de les requĂȘter.

tictoc::tic()
duck_vols <- dplyr::tbl(bd, "tb_vols")
# affichage des premiĂšres lignes
duck_vols

 

# Source:   table<tb_vols> [?? x 19]
# Database: DuckDB 1.4.4 [PC@Windows 10 x64:R 4.5.3/C:\Users\PC\Documents\pro\linkedin\fichier_stockage_permanent.duckdb]
    year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
   <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
 1  2013     1     1      517            515         2      830            819
 2  2013     1     1      533            529         4      850            830
 3  2013     1     1      542            540         2      923            850
 4  2013     1     1      544            545        -1     1004           1022
 5  2013     1     1      554            600        -6      812            837
 6  2013     1     1      554            558        -4      740            728
 7  2013     1     1      555            600        -5      913            854
 8  2013     1     1      557            600        -3      709            723
 9  2013     1     1      557            600        -3      838            846
10  2013     1     1      558            600        -2      753            745
# â„č more rows
# â„č 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
#   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
#   hour <dbl>, minute <dbl>, time_hour <dttm>
tictoc::toc()

 

0.19 sec elapsed

Petite remarque sur cette configuration : si on modifie le data.frame vers lequel pointe DuckDB, ces modifications sont automatiquement appliquĂ©es dans la pseudo-table DuckDB, puisqu’aucune copie n’est rĂ©alisĂ©e, ce ne sont que des liens.

👍 IntĂ©rĂȘts par rapport Ă  du code R sur ce mĂȘme data.frame ?

  • DuckDB va parallĂ©liser et optimiser les requĂȘtes (en regardant globalement les opĂ©rations Ă  effectuer, alors que {dplyr} s’exĂ©cute linĂ©airement)
  • DuckDB Ă©crira sur le disque s’il a un trop-plein d’information par rapport au quota de mĂ©moire qu’on lui a autorisĂ© ; aucun risque de saturer l’Environnement et donc la RAM d’un serveur partagĂ©

Travailler sur un fichier à séparateur

Avoir un gros data.frame comme point de dĂ©part de notre travail sur DuckDB c’est bien, mais ce serait encore mieux si on n’avait pas besoin d’avoir un objet volumineux dans l’Environnement R. Si notre source de donnĂ©es est un fichier plat Ă  sĂ©parateur (virgule, point-virgule ou tabulation, peu importe) alors DuckDB va importer le fichier et le stocker sur le disque dans un format optimisĂ© qui lui sera propre.

🎬 Pour illustrer ce cas, j’ai rĂ©cupĂ©rĂ© sur le site d’IMDB (Internet Movie DataBase) la liste des films connus de cette base de donnĂ©es assez exhaustive, depuis les dĂ©buts du cinĂ©ma Ă  la fin XIXe siĂšcle, en incluant mĂȘme des tĂ©lĂ©films et des sĂ©ries. Les informations sont ici et le fichier lĂ  (c’est title.basics.tsv.gz). J’ai dĂ©zippĂ© le fichier : il s’avĂšre qu’il s’agit d’un fichier Ă  sĂ©parateur tabulation.

Une premiĂšre version trĂšs simple consiste Ă  indiquer uniquement le nom du fichier Ă  DuckDB. 👃 Le “sniffer” fait le reste : deviner le sĂ©parateur et le type des colonnes.

tictoc::tic()
dbExecute(bd, "DROP TABLE IF 
EXISTS tb_films") # Ă  tout hasard

 

[1] 0
dbExecute(bd,
          "CREATE TABLE tb_films AS
          SELECT *
          FROM 'C:/olivier/imdb/title.basics.tsv'")

 

[1] 12621902
tictoc::toc()

 

12.7 sec elapsed

Le rĂ©sultat de dbExecute est le nombre de lignes renvoyĂ©es par la requĂȘte : 0 pour le DROP TABLE mais 12,6 millions pour l’import !

On va utiliser {dplyr} pour inspecter le résultat.

library(dplyr, quietly = TRUE)
tictoc::tic()
tbl(bd, "tb_films") %>% glimpse()

 

Rows: ??
Columns: 9
Database: DuckDB 1.4.4 [PC@Windows 10 x64:R 4.5.3/C:\Users\PC\Documents\pro\linkedin\fichier_stockage_permanent.duckdb]
$ tconst         <chr> "tt0000001", "tt0000002", "tt0000003", "tt0000004", "tt

$ titleType      <chr> "short", "short", "short", "short", "short", "short", "

$ primaryTitle   <chr> "Carmencita", "Le clown et ses chiens", "Poor Pierrot",

$ originalTitle  <chr> "Carmencita", "Le clown et ses chiens", "Pauvre Pierrot

$ isAdult        <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

$ startYear      <chr> "1894", "1892", "1892", "1892", "1893", "1894", "1894",

$ endYear        <chr> "\\N", "\\N", "\\N", "\\N", "\\N", "\\N", "\\N", "\\N",

$ runtimeMinutes <chr> "1", "5", "5", "12", "1", "1", "1", "1", "45", "1", "1"

$ genres         <chr> "Documentary,Short", "Animation,Short", "Animation,Come

tictoc::toc()

 

0.03 sec elapsed

Tout n’est pas parfait dans les suppositions du sniffer. La chaĂźne de caractĂšres \\N est visiblement une donnĂ©e manquante, et les informations sur l’annĂ©e de sortie (startYear) et la durĂ©e (runtimeMinutes) devraient ĂȘtre numĂ©riques, comme endYear qui n’est renseignĂ©e que pour les sĂ©ries. Ajoutons des options Ă  la fonction DuckDB read_csv pour expliciter tous nos choix (la doc est ici de tout ce qu’on peut indiquer ; voir cependant la courte remarque sur l’encodage ci-dessous).

tictoc::tic()
dbExecute(bd, "DROP TABLE IF 
EXISTS tb_films") # supprime le 1er essai

 

[1] 0
dbExecute(bd,
          "CREATE TABLE tb_films AS
          SELECT *
          FROM read_csv('C:/olivier/imdb/title.basics.tsv',
                        delim='\t',
                        nullstr='\\N',
                        header=true,
                        columns={
                          'tconst'         : 'VARCHAR',
                          'titleType'      : 'VARCHAR',
                          'primaryTitle'   : 'VARCHAR',
                          'originalTitle'  : 'VARCHAR',
                          'isAdult'        : 'SMALLINT',
                          'startYear'      : 'INTEGER',
                          'endYear'        : 'INTEGER',
                          'runtimeMinutes' : 'FLOAT',
                          'genres'         : 'VARCHAR'
                        }
          )")

 

[1] 12621902
tictoc::toc()

 

6.4 sec elapsed
tictoc::tic()
tbl(bd, "tb_films") %>% glimpse()

 

Rows: ??
Columns: 9
Database: DuckDB 1.4.4 [PC@Windows 10 x64:R 4.5.3/C:\Users\PC\Documents\pro\linkedin\fichier_stockage_permanent.duckdb]
$ tconst         <chr> "tt0000001", "tt0000002", "tt0000003", "tt0000004", "tt

$ titleType      <chr> "short", "short", "short", "short", "short", "short", "

$ primaryTitle   <chr> "Carmencita", "Le clown et ses chiens", "Poor Pierrot",

$ originalTitle  <chr> "Carmencita", "Le clown et ses chiens", "Pauvre Pierrot

$ isAdult        <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

$ startYear      <int> 1894, 1892, 1892, 1892, 1893, 1894, 1894, 1894, 1894, 1

$ endYear        <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,

$ runtimeMinutes <dbl> 1, 5, 5, 12, 1, 1, 1, 1, 45, 1, 1, 1, 1, 1, 2, 1, 1, 1,

$ genres         <chr> "Documentary,Short", "Animation,Short", "Animation,Come

tictoc::toc()

 

0.03 sec elapsed

On a indiquĂ© un sĂ©parateur tabulation, un en-tĂȘte, que \\N est une valeur NA, et les types de toutes les colonnes. Sur ce dernier point, si on veut typer une colonne, il faut le faire pour toutes : l’énumĂ©ration dans columns ne peut pas concerner que certaines colonnes du fichier Ă  lire. Les types possibles sont rĂ©pertoriĂ©s ici.

Courte remarque sur l’encodage

Il n’y a pas d’option pour l’encodage du fichier plat qu’on souhaite importer, mĂȘme si la doc en propose une. En rĂ©alitĂ©, seul l’encodage UTF-8 est supportĂ©. Si votre fichier est encodĂ© d’une autre maniĂšre, il faut en faire une copie prĂ©alable avec un changement d’encodage vers UTF-8 (sous Unix, iconvfonctionne parfaitement et on l’appelle depuis R avec la fonction system) et importer cette copie.

Fin de la courte remarque

👍 IntĂ©rĂȘts par rapport Ă  un import du fichier dans R et du code R classique ?

  • le fichier n’est pas importĂ© en RAM
  • et mĂȘmes avantages que sur un data.frame

Travailler sur un fichier Parquet

La Rolls ✹ de nos configurations reste cependant d’avoir comme point de dĂ©part des donnĂ©es au format Parquet, plutĂŽt qu’un fichier Ă  sĂ©parateur. Car dans ce cas, aucun import n’est nĂ©cessaire. On va utiliser la chaĂźne de liens suivante :

  • R fait un lien âžĄïž vers DuckDB
  • DuckDB fait un lien âžĄïž vers Arrow
  • Arrow fait un lien âžĄïž vers des fichiers Parquet

Tout ça semble bien compliquĂ©, mais au final, tout se tient en 3 fonctions qui n’ont aucune complexitĂ© particuliĂšre, et on Ă©vite toute duplication des donnĂ©es. Comment est-ce possible ? Parce que le format Parquet reprend l’organisation interne des donnĂ©es DuckDB : il permet donc Ă  DuckDB d’exĂ©cuter, via Arrow, ses requĂȘtes directement sur les fichiers Parquet.

Rapide apartĂ© : d’un CSV Ă  un Parquet

Ce n’est pas parce que vos sources sont en fichier dĂ©limitĂ© que l’option de travailler sur des fichiers Parquet vous est bouchĂ©e. Car DuckDB peut vous convertir d’un format dans un autre, sans encombrer exagĂ©rĂ©ment la RAM. Le principe est dĂ©crit dans cet article, et adaptĂ© ci-dessous Ă  nos donnĂ©es IMDB. Par rapport au code de l’article, il est utile d’indiquer un temp_directory quand on crĂ©e la session DuckDB, comme indiquĂ© plus haut.

tictoc::tic()
dbSendQuery(bd,
            "COPY(
               SELECT *
               FROM read_csv('C:/olivier/imdb/title.basics.tsv',
                             delim='\t',
                             nullstr='\\N',
                             header=true,
                             columns={
                               'tconst'         : 'VARCHAR',
                               'titleType'      : 'VARCHAR',
                               'primaryTitle'   : 'VARCHAR',
                               'originalTitle'  : 'VARCHAR',
                               'isAdult'        : 'SMALLINT',
                               'startYear'      : 'INTEGER',
                               'endYear'        : 'INTEGER',
                               'runtimeMinutes' : 'FLOAT',
                               'genres'         : 'VARCHAR'
                             })
               ) TO 'c:/olivier/imdb/films/films.parquet' 
                 (FORMAT parquet, COMPRESSION zstd)")
tictoc::toc()

 

3.25 sec elapsed

🕐 En 3 secondes, les 12 millions de lignes sont traitĂ©es. La RAM a Ă  peine frĂ©mi. Le fichier .duckdb de temp_directory a servi de tampon.đŸ˜Č

Quant au code, on voit que c’est celui du travail sur le fichier CSV, recyclĂ© pour convertir en fichier Parquet.

Fin de l’apartĂ©, travaillons sur une source Parquet

Nous parlions donc de 3 fonctions pour Ă©tablir la connexion R âžĄïž DuckDB âžĄïž Arrow âžĄïž fichiers Parquet. Ce sont :

  1. arrow::open_dataset qui va Ă©tablir le lien Arrow âžĄïž Parquet. L’argument dans cette fonction est le rĂ©pertoire contenant un ou plusieurs fichiers Parquet.
    ⚠ Attention : s’il y a plusieurs fichiers ils doivent ĂȘtre de mĂȘme structure car Arrow les verra comme un tout. S’il y a des sous-rĂ©pertoires, ils seront explorĂ©s et leurs contenus concatĂ©nĂ©s de mĂȘme. Et il ne doit y avoir que des fichiers Parquet dans tout ce rĂ©pertoire et ses sous-dossiers.
  2. duckdb::duckdb_register_arrow est une variante du duckdb_register vu avec les data.frames. On dĂ©clare une pseudo-table dans DuckDB qui pointe vers une source de donnĂ©es extĂ©rieure, ici le lien Arrow vers Parquet. A ce stade on crĂ©e donc la connexion DuckDB âžĄïž Arrow et on a Ă©tabli la chaĂźne DuckDB âžĄïž Arrow âžĄïž fichiers Parquet.
  3. dplyr::tbl dĂ©jĂ  vu prĂ©cĂ©demment, qui permet d’avoir l’objet R pointant vers la (pseudo) table DuckDB. La chaĂźne est maintenant complĂšte avec ce dernier lien R âžĄïž DuckDB.
tictoc::tic()
duckdb_register_arrow(bd, "tb_films",
                      arrow::open_dataset("c:/olivier/imdb/films"))

films <- tbl(bd, "tb_films")
tictoc::toc()

 

0.17 sec elapsed

Si vous organisez de maniĂšre bien carrĂ©e vos rĂ©pertoires de fichiers Parquet, alors rien n’est compliquĂ© : avec ces 3 fonctions, R peut travailler sur des donnĂ©es Parquet sans aucun import ni duplication, en un processus qui est instantanĂ© puisqu’à ce stade, aucune opĂ©ration autre que des liens n’a Ă©tĂ© rĂ©alisĂ©e.

👍 IntĂ©rĂȘts par rapport Ă  un import du fichier dans R et du code R classique ?

  • le fichier n’est pas importĂ© en RAM
  • et mĂȘmes avantages que sur un data.frame
  • et ça va super vite.

Construire des requĂȘtes

La suite de cette histoire est assez classique puisqu’elle passe par le package {dbplyr} qui traduit du code {dplyr} en SQL pour que DuckDB l’exĂ©cute. La syntaxe est ici la mĂȘme qu’avec d’autres bases de donnĂ©es et les documentations sur Internet ne manquent pas.

Notons que DuckDB sait également réaliser des opérations de pivots comme pivot_longer et pivot_wider de {tidyr} que {dbplyr} traduira.

Autre point important : il faut toujours activer les packages dont sont issues les fonctions à traduire. Par exemple pour travailler sur des dates, il faut activer {dbplyr} et {lubridate} pour qu’une fonction comme month ou year soit traduite.

library(dbplyr)

tictoc::tic()
films %>% 
  mutate(decennie = floor(startYear/10)*10) %>% 
  filter( ! is.na(decennie)) %>% 
  count(decennie) %>% 
  arrange(desc(decennie))

 

# Source:     SQL [?? x 2]
# Database:   DuckDB 1.4.4 [PC@Windows 10 x64:R 4.5.3/C:\Users\PC\Documents\pro\linkedin\fichier_stockage_permanent.duckdb]
# Ordered by: desc(decennie)
   decennie       n
      <dbl>   <dbl>
 1     2110       1
 2     2030      21
 3     2020 3020433
 4     2010 3953305
 5     2000 1681824
 6     1990  794647
 7     1980  502336
 8     1970  447882
 9     1960  361829
10     1950  179321
11     1940   31519
12     1930   33840
13     1920   37025
14     1910   72627
15     1900   25799
16     1890    6117
17     1880      79
18     1870      32
tictoc::toc()

 

0.26 sec elapsed

Trois stratĂ©gies sont possibles en fin de requĂȘte :

  • on ne fait rien de spĂ©cial et la requĂȘte est traduite en SQL. Si on l’affecte Ă  un objet, c’est une lazy query, c’est Ă  dire que rien n’est exĂ©cutĂ© sur le moment. Le SQL est mĂ©morisĂ©, et quand DuckDB sera obligĂ© de l’exĂ©cuter (pour afficher le rĂ©sultat, faire une jointure, crĂ©er un data.frame R) tout l’enchaĂźnement des requĂȘtes passĂ©es sera considĂ©rĂ©, optimisĂ© et exĂ©cutĂ©. Si on ne l’affecte pas, les premiĂšres lignes du rĂ©sultat sont calculĂ©es et affichĂ©es dans la Console
  • on ajoute collect() en fin de requĂȘte. Dans ce cas la requĂȘte est exĂ©cutĂ©e et le rĂ©sultat envoyĂ© intĂ©gralement Ă  R. Attention si ce rĂ©sultat est volumineux !
  • on ajoute compute("nom_table_duckdb") en fin de requĂȘte. La requĂȘte est exĂ©cutĂ©e et son rĂ©sultat, stockĂ© dans DuckDB. La table DuckDB (une vraie table, cette fois, pas une sĂ©rie de liens) sera stockĂ©e dans le fichier dĂ©fini par dbdir.
library(stringr)
tictoc::tic()
lazy <- films %>% 
          filter(str_detect(str_to_lower(originalTitle),"superman") 
                 & ! str_detect(str_to_lower(titleType), "tv")) %>% 
          select(startYear, titleType, originalTitle, 
                 runtimeMinutes, genres) %>% 
          arrange(startYear, titleType)
# à ce stade, rien n'est calculé
tictoc::toc()

 

0.01 sec elapsed
tictoc::tic()
lazy %>% show_query() # affiche le SQL

 

<SQL>
SELECT startYear, titleType, originalTitle, runtimeMinutes, genres
FROM tb_films
WHERE (REGEXP_MATCHES(LOWER(originalTitle), 'superman') AND NOT(REGEXP_MATCHES(LOWER(titleType), 'tv')))
ORDER BY startYear, titleType
tictoc::toc()

 

0.04 sec elapsed
tictoc::tic()
lazy %>% count() # nombre de lignes de la requĂȘte

 

# Source:     SQL [?? x 1]
# Database:   DuckDB 1.4.4 [PC@Windows 10 x64:R 4.5.3/C:\Users\PC\Documents\pro\linkedin\fichier_stockage_permanent.duckdb]
# Ordered by: startYear, titleType
      n
  <dbl>
1   304
tictoc::toc()

 

0.31 sec elapsed
tictoc::tic()
lazy # affichage des premiers résultats

 

# Source:     SQL [?? x 5]
# Database:   DuckDB 1.4.4 [PC@Windows 10 x64:R 4.5.3/C:\Users\PC\Documents\pro\linkedin\fichier_stockage_permanent.duckdb]
# Ordered by: startYear, titleType
   startYear titleType originalTitle                 runtimeMinutes genres      
       <int> <chr>     <chr>                                  <dbl> <chr>       
 1      1941 short     Superman                                  10 Action,Adve

 2      1948 movie     Superman                                  88 Sci-Fi      
 3      1948 movie     Superman                                 244 Action,Sci-

 4      1951 movie     Superman and the Mole Men                 58 Action,Adve

 5      1954 movie     Superman Flies Again                      77 Action,Fant

 6      1954 movie     Superman and the Jungle Devil             77 Action,Fant

 7      1954 movie     Superman in Exile                         77 Action,Fant

 8      1954 movie     Superman and Scotland Yard                90 Action,Fant

 9      1954 movie     Superman's Peril                          77 Action,Fant

10      1954 short     Stamp Day for Superman                    18 Action,Adve

# â„č more rows
tictoc::toc()

 

0.31 sec elapsed
tictoc::tic()
(superman <- lazy %>% collect()) # rĂ©cupĂ©ration dans R (l'en-tĂȘte diffĂšre)

 

# A tibble: 304 × 5
   startYear titleType originalTitle                 runtimeMinutes genres      
       <int> <chr>     <chr>                                  <dbl> <chr>       
 1      1941 short     Superman                                  10 Action,Adve

 2      1948 movie     Superman                                  88 Sci-Fi      
 3      1948 movie     Superman                                 244 Action,Sci-

 4      1951 movie     Superman and the Mole Men                 58 Action,Adve

 5      1954 movie     Superman Flies Again                      77 Action,Fant

 6      1954 movie     Superman and the Jungle Devil             77 Action,Fant

 7      1954 movie     Superman in Exile                         77 Action,Fant

 8      1954 movie     Superman and Scotland Yard                90 Action,Fant

 9      1954 movie     Superman's Peril                          77 Action,Fant

10      1954 short     Stamp Day for Superman                    18 Action,Adve

# â„č 294 more rows
tictoc::toc()

 

0.36 sec elapsed

Performances comparées des différentes approches

Sur les donnĂ©es IMDB, Ă  titre d’exemple, on va comparer les diffĂ©rentes configurations Ă©voquĂ©es ci-dessous, et des imports R classiques complĂ©tĂ©s par des requĂȘtes {dplyr}.

library(microbenchmark)
library(vroom)
library(dplyr)
library(stringr)
library(dbplyr)
library(arrow)
library(duckdb)

import_films_csv <- function(){
  vroom::vroom("c:/olivier/imdb/title.basics.tsv",
               delim = "\t",
               na = "\\N",
               col_types = cols(
                 'tconst'         = col_character(),
                 'titleType'      = col_character(),
                 'primaryTitle'   = col_character(),
                 'originalTitle'  = col_character(),
                 'isAdult'        = col_integer(),
                 'startYear'      = col_integer(),
                 'endYear'        = col_integer(),
                 'runtimeMinutes' = col_double(),
                 'genres'         = col_character()
               ))
}

requete <- function(tb){
  tb %>% 
    filter(str_detect(str_to_lower(originalTitle),"superman") 
           & ! str_detect(str_to_lower(titleType), "tv")) %>% 
    select(startYear, titleType, originalTitle, 
           runtimeMinutes, genres) %>% 
    arrange(startYear, titleType)
}


bd <- dbConnect(duckdb(
  dbdir = "fichier_stock.duckdb",
  config = list(
    temp_directory = "fichier_temp.duckdb",
    threads = "4", 
    memory_limit = "4GB")
))

microbenchmark(
  "import CSV + dplyr"={
    films <- import_films_csv()
    superman <- films %>% requete()
    rm(films, superman)
    gc()
  },
  "import Parquet + dplyr"={
    films <- arrow::read_parquet("c:/olivier/imdb/films/films.parquet")
    superman <- films %>% requete()
    rm(films, superman)
    gc()
  },
  "DuckDB sur import CSV"={
    films <- import_films_csv()
    duckdb_register(bd, "tb_films", films)
    superman <- tbl(bd, "tb_films") %>% 
      requete() %>% 
      collect()
    duckdb_unregister(bd, "tb_films")
    rm(films, superman)
    gc()
  },
  "DuckDB sur import Parquet"={
    films <- arrow::read_parquet("c:/olivier/imdb/films/films.parquet")
    duckdb_register(bd, "tb_films", films)
    superman <- tbl(bd, "tb_films") %>% 
      requete() %>% 
      collect()
    duckdb_unregister(bd, "tb_films")
    rm(films, superman)
    gc()
  },
  "DuckDB direct sur CSV"={
    dbExecute(bd,
              "CREATE TABLE tb_films AS
          SELECT *
          FROM read_csv('C:/olivier/imdb/title.basics.tsv',
                        delim='\t',
                        nullstr='\\N',
                        header=true,
                        columns={
                          'tconst'         : 'VARCHAR',
                          'titleType'      : 'VARCHAR',
                          'primaryTitle'   : 'VARCHAR',
                          'originalTitle'  : 'VARCHAR',
                          'isAdult'        : 'SMALLINT',
                          'startYear'      : 'INTEGER',
                          'endYear'        : 'INTEGER',
                          'runtimeMinutes' : 'FLOAT',
                          'genres'         : 'VARCHAR'
                        }
          )")
    superman <- tbl(bd, "tb_films") %>% 
      requete() %>% 
      collect()
    rm(films, superman)
    dbExecute(bd, "DROP TABLE IF EXISTS tb_films")
    gc()
  },
  "DuckDB direct sur Parquet"={
    pqt <- arrow::open_dataset("c:/olivier/imdb/films")
    duckdb_register_arrow(bd, "tb_films", pqt)
    superman <- tbl(bd, "tb_films") %>% 
      requete() %>% 
      collect()
    duckdb_unregister_arrow(bd, "tb_films")
    dbExecute(bd, "DROP VIEW IF EXISTS tb_films")
    rm(superman, pqt)
    gc()
  },
  "Arrow direct sur Parquet"={
    pqt <- arrow::open_dataset("c:/olivier/imdb/films")
    superman <- pqt %>% 
      requete() %>% 
      collect()
    rm(superman, pqt)
    gc()
  },
  times=50
)
dbDisconnect(bd)

 

Unit: seconds

expr min lq mean median uq max
import CSV + dplyr 14.30 15.58 17.88 16.64 18.29 64.40
import Parquet + dplyr 15.18 16.46 17.89 17.84 19.15 21.31
DuckDB sur import CSV 18.40 20.50 24.96 25.43 26.76 39.59
DuckDB sur import Parquet 15.13 16.43 19.90 20.05 21.25 49.23
DuckDB direct sur CSV 9.46 12.74 14.79 14.22 16.93 26.75
DuckDB direct sur Parquet 0.49 0.53 0.73 0.65 0.82 1.89
Arrow direct sur Parquet 2.65 2.84 3.28 2.89 3.29 13.41

On voit que le constat est sans appel : les phases d’import dans R, mĂȘme d’un fichier Parquet, sont coĂ»teuses en temps (et en RAM). Faire travailler DuckDB au plus prĂšs des donnĂ©es source (CSV ou Parquet) est intĂ©ressant sur tous les plans. Et si votre source est un CSV, la conversion en Parquet, Ă  n’effectuer qu’une fois, gĂ©nĂ©rera un temps de traitement largement amorti par la suite !

A titre de comparaison j’ai Ă©galement inclus le travail par {arrow} et {dplyr} sur des donnĂ©es Parquet non importĂ©es, sans utiliser DuckDB. On voit que l’ajout de DuckDB accĂ©lĂšre les traitements Ă©galement dans cette configuration.

Remarque :

Volontairement je n’ai pas proposĂ© de code {data.table} pour les solutions en “pur R”. Principalement parce qu’au-delĂ  de la vitesse d’exĂ©cution, l’atout majeur de DuckDB reste l’absence d’import dans l’Environnement, et donc l’impact limitĂ© sur la RAM d’un serveur. LĂ -dessus {data.table} ne propose pas vraiment de solution satisfaisante.

Configuration de R pour le code de cet article

Matériel :

  • PC Dell Precision 3571 de fin 2022 sous Windows 10
  • 64 Go de RAM
  • Processeur Inter i7 2,4 GHz Ă  14 coeurs
sessionInfo()

 

R version 4.5.3 (2026-03-11 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 10 x64 (build 19045)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=French_France.utf8  LC_CTYPE=French_France.utf8   
[3] LC_MONETARY=French_France.utf8 LC_NUMERIC=C                  
[5] LC_TIME=French_France.utf8    

time zone: Europe/Paris
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] stringr_1.6.0      dbplyr_2.5.2       dplyr_1.2.1        nycflights13_1.0.2
[5] duckdb_1.4.4       DBI_1.3.0         

loaded via a namespace (and not attached):
 [1] bit_4.6.0         jsonlite_2.0.0    compiler_4.5.3    tidyselect_1.2.1 
 [5] blob_1.2.4        assertthat_0.2.1  arrow_23.0.1.1    yaml_2.3.12      
 [9] fastmap_1.2.0     R6_2.6.1          generics_0.1.4    knitr_1.51       
[13] htmlwidgets_1.6.4 tibble_3.3.0      pillar_1.11.0     tzdb_0.5.0       
[17] rlang_1.2.0       utf8_1.2.6        stringi_1.8.7     xfun_0.59        
[21] bit64_4.6.0-1     otel_0.2.0        cli_3.6.5         withr_3.0.3      
[25] magrittr_2.0.3    tictoc_1.2.1      digest_0.6.37     rstudioapi_0.19.0
[29] lifecycle_1.0.5   vctrs_0.7.3       evaluate_1.0.5    glue_1.8.1       
[33] rmarkdown_2.29    purrr_1.2.2       tools_4.5.3       pkgconfig_2.0.3  
[37] htmltools_0.5.8.1
0 found this helpful