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
dbdirservira Ă stocker des tables créées au cours de la session DuckDB (principalement via la syntaxe SQLCREATE 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Ă©etempfile()car la taille de ces fichiers est souvent limitĂ©e. - đLe fichier de lâoption
temp_directoryservira 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Ă©etempfile()car la taille de ces fichiers est souvent limitĂ©e. - đ€Lâoption
threadsindique 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_limitindique 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 :
arrow::open_datasetqui 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.duckdb::duckdb_register_arrowest une variante duduckdb_registervu 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.dplyr::tbldĂ©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 pardbdir.
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