install.packages("duckdb", repos="https://cloud.r-project.org")
21 Manipuler des données avec duckdb
21.1 Tâches concernées et recommandations
L’utilisateur souhaite manipuler des données structurées sous forme de data.frame
par le biais de l’écosystème duckdb
(sélectionner des variables, sélectionner des observations, créer des variables, joindre des tables).
Tâches concernées et recommandations
Pour des tables de données de taille petite et moyenne (inférieure à 1 Go ou moins d’un million d’observations), il est recommandé d’utiliser les packages
tibble
,dplyr
ettidyr
qui sont présentés dans la fiche Manipuler des données avec letidyverse
;Pour des tables de données de grande taille (plus de 1 Go en CSV, plus de 200 Mo en Parquet, ou plus d’un million d’observations), il est recommandé d’utiliser soit les packages
arrow
(voir la fiche Manipuler des données avecarrow
) etduckdb
qui fait l’objet de la présente fiche, soit le packagedata.table
qui fait l’objet de la fiche Manipuler des données avecdata.table
.Il est essentiel de travailler avec la dernière version d’
arrow
, deduckdb
et deR
car les packagesarrow
etduckdb
sont en cours de développement. Par ailleurs, les recommandations d’utilitR
peuvent évoluer en fonction du développement de ces packages.Si les données sont très volumineuses (plus de 5 Go en CSV, plus de 1 Go en Parquet ou plus de 5 millions d’observations), il est recommandé de manipuler les données avec
duckdb
(et avecarrow
) plutôt qu’avec letidyverse
. Il peut arriver que le volume de données soit tellement important qu’il ne soit pas possible de les traiter avecduckdb
etarrow
; il faut s’orienter vers des infrastructures big data permettant le calcul distribué et utiliser des logiciels adaptés (Spark
par exemple).
Apprendre à utiliser duckdb
n’est pas difficile, car la syntaxe utilisée est quasiment identique à celle du tidyverse
. Toutefois, une bonne compréhension du fonctionnement de R
et de duckdb
est nécessaire pour bien utiliser duckdb
sur des données volumineuses. Voici quelques conseils pour bien démarrer:
- Il est indispensable de lire la fiche Manipuler des données avec le
tidyverse
avant de lire la présente fiche. - Il est recommandé de lire les fiches Se connecter à une base de données et Manipuler des données avec
arrow
avant de lire la présente fiche. - Il est complètement normal de rencontrer des erreurs difficiles à comprendre lorsqu’on commence à utiliser
duckdb
, il ne faut donc pas se décourager. - Il ne faut pas hésiter à demander de l’aide à des collègues, ou à poser des questions sur les salons Tchap adaptés (le salon Langage
R
par exemple).
21.2 Présentation du package duckdb
et du projet associé
21.2.1 Qu’est-ce que duckdb
?
DuckDB
est un projet open-source (license MIT) qui propose un moteur SQL optimisé pour réaliser des travaux d’analyse statistique sur des bases de données :
- un moteur SQL rapide, capable d’utiliser des données au format
parquet
sans les charger complètement en mémoire, - un dialecte SQL enrichi avec des fonctions qui facilitent l’analyse de données,
- une installation et une utilisation faciles,
- un moteur portable, utilisable sous Windows, MacOS, Linux, et interfacé avec de nombreux langages de programmation (R, Python, Javascript, etc.).
Un point important à comprendre est que DuckDB
n’est pas un outil spécifique à R
: DuckDB
est un système de gestion de base de données (SGBD), similaire par exemple à une base PostgreSQL
. Cela a deux conséquencées:
- la base de données
DuckDB
a une existence propre sur le disque ou dans la mémoire, et on peut donc lui envoyer directement des requêtes SQL, sans passer parR
. - Il faut bien distinguer le projet
DuckDB
du packageR
duckdb
. Ce package propose simplement une interface avecR
parmi les autres interfaces existantes : Python, Java, Javascript, Julia, etc.
Toutefois, DuckDB
est très facile à utiliser avec R
, ce qui permet de bénéficier des optimisations inhérentes au langage SQL, à la fois en terme d’utilisation de la mémoire et de rapidité de calcul. C’est de plus un bon intermédiaire avant de passer à des infrastructures avancées telles que spark ou oracle.
21.2.2 À quoi sert le package duckdb
?
Du point de vue d’un statisticien utilisant R
, le package duckdb
permet de faire trois choses:
- Importer des données (exemples: fichiers CSV, fichiers Parquet);
- Manipuler des données avec la syntaxe
dplyr
, ou avec le langage SQL; - Écrire des données au format Parquet.
21.2.3 Quels sont les avantages de duckdb
?
- Disponibilité immédiate: on peut pré-visualiser les données ou le résultat d’un calcul sans l’exécuter totalement, sans attendre le chargement des données;
-
Performances élevées:
duckdb
est très rapide pour la manipulation de données tabulaires (nettement plus performant quedplyr
par exemple); -
Ne pas nécessairement charger les données en mémoire:
duckdb
permet également de travailler directement sur des fichiers du disque dur; -
Optimisations automatiques:
duckdb
sélectionne automatiquement les colonnes nécessaires, et ne lit que les lignes nécessaires. Cela permet d’accélérer les calculs et de réduire considérablement les besoins en mémoire, même lorsque les données sont volumineuses; -
Facilité d’apprentissage grâce aux approches
dplyr
et SQL:duckdb
peut être utilisé avec les verbes dedplyr
(select
,mutate
, etc.) et/ou avec le langage SQL. Par conséquent, il n’est pas nécessaire d’apprendre une nouvelle syntaxe pour utiliserduckdb
, on peut s’appuyer sur la ou les approches que l’on maîtrise déjà.
21.2.4 Quels sont les points d’attention à l’usage ?
-
Représentation des données en mémoire :
duckdb
est un moteur SQL. Les lignes n’ont pas d’ordre pré-défini, et le résultat d’un traitement peut être dans un ordre imprévisible. -
Traitement de données volumineuses:
duckdb
peut traiter de gros volumes de données, qu’elles soient en mémoire vive ou sur le disque dur. Lorsque les données sont en mémoire vive, les packagesduckdb
etarrow
peuvent être utilisés conjointement de façon très efficace: cela veut dire concrètement queduckdb
peut manipuler directement des données stockées dans un objetArrow Table
, sans avoir à convertir les données dans un autre format. Avec des données stockées sur le disque dur,duckdb
est capable de faire les traitements sur des données plus volumineuses que la mémoire vive (RAM). C’est un avantage majeur en comparaison aux autres approches possibles enR
(data.table
etdplyr
par exemple). Toutefois, il faut dans ce cas ajouter le temps de lecture des données au temps nécessaire pour le calcul. -
Évaluation différée:
duckdb
construit des requêtes SQL, qui sont exécutées uniquement lorsque le résultat est explicitement demandée, après optimisation des étapes intermédiaires, et peuvent être exécutées partiellement. La Section 21.5.2 présente en détail cette notion. -
Traduction en SQL:
duckdb
traduit automatiquement les instructionsdplyr
en requêtes SQL (de la même façon qu’arrow
traduit ces instructions en code C++). Il arrive toutefois que certaines fonctions dedplyr
n’aient pas d’équivalent direct enduckdb
et ne puissent être traduites automatiquement. Dans ce cas (qui est heureusement moins fréquent qu’avecarrow
), il faut parfois utiliser une fonction SQL directement ou trouver une solution pour contourner le problème. La Section 21.5.3 donne quelques trucs et astuces dans ce cas. -
Interopérabilité:
duckdb
est conçu pour être interopérable entre plusieurs langages de programmation tels queR
, Python, Java, C++, etc. Cela signifie que les données peuvent être échangées entre ces langages sans avoir besoin de convertir les données, d’où des gains importants de temps et de performance.
21.2.5 Quand utiliser duckdb
plutôt que arrow
?
Les packages duckdb
et arrow
ont des cas d’usage très similaires (voir la fiche Manipuler des données avec arrow
), mais on peut préférer l’un à l’autre selon les cas. On peut également les utiliser ensemble pour profiter de chacun de leurs avantages. Le tableau ci-dessous compare quelques cas d’usage de ces deux packages :
Je souhaite… | arrow | duckdb |
---|---|---|
Optimiser mes traitements pour des données volumineuses | ✔️ | ✔️ |
Travailler sur un fichier .parquet ou .csv sans le charger entièrement en mémoire | ✔️ | ✔️ |
Utiliser la syntaxe dplyr pour traiter mes données |
✔️ | ✔️ |
Utiliser du langage SQL pour traiter mes données | ❌ | ✔️ |
Joindre des tables très volumineuses (plus de 4 Go) | ❌ | ✔️ |
Utiliser des fonctions fenêtres (voir Section 21.6) | ❌ | ✔️ |
Utiliser des fonctions statistiques qui n’existent pas dans arrow (voir Section 21.6) | ❌ | ✔️ |
Écrire un fichier .parquet | ✔️ | ✔️ * |
* pour écrire un fichier .parquet avec le package duckdb
, il faut utiliser une instruction SQL (voir Section 21.5.4.2)
21.3 Installation de duckdb
Il suffit d’installer le package duckdb
, qui contient à la fois DuckDB
et une interface pour que R
puisse s’y connecter.
21.4 Utilisation de duckdb
Dans cette section, on présente l’utilisation basique de duckdb
. C’est très facile: il n’est pas nécessaire de connaître le langage SQL car il est possible d’utiliser duckdb
avec la syntaxe dplyr
.
21.4.1 Charger le package duckdb
Pour utiliser duckdb
, il faut commencer par charger le package. Cela charge automatiquement le package DBI
, qui permet de se connecter aux bases de données. Il est utile de charger également le package dplyr
afin de pouvoir requêter la base de données avec la syntaxe bien connue de dplyr
.
Le moteur duckdb
fonctionnant “en dehors” de R
, il détecte le nombre de processeurs et effectue les opérations en parallèle si possible.
21.4.2 Connexion à une base de données
duckdb
est une base de données distante et s’utilise comme telle: il faut ouvrir une connexion, puis “charger” les données dans la base de données pour les manipuler.
Comme beaucoup d’autres bases de données (distantes ou locales), on ouvre une connexion au moteur duckdb
avec une base de données en mémoire vive de la façon suivante :
Concrètement, cette commande crée une nouvelle base de données duckdb
dans la mémoire vive. Cette base de données ne contient aucune donnée lorsqu’elle est créée. L’objet conn_ddb
apparaît dans l’onglet Data
de l’environnement RStudio
, mais la liste des tables n’y est pas directement accessible. Pour plus d’informations, se reporter à la documentation du package DBI
.
À la fin du traitement ou du programme, on ferme la connexion avec le code ci-dessous. L’option shutdown
est importante : elle permet de fermer complètement la session duckdb
et de libérer la mémoire utilisée. Si on n’utilise pas cette option, il arrive souvent que des connexions à moitié ouvertes continuent à consommer des ressources, et il faut alors relancer la session R
.
DBI::dbDisconnect(conn_ddb, shutdown = TRUE)
Par défaut, duckdb
utilisera tous les cœurs disponibles. Si vous travaillez sur un serveur mutualisé, il est conseillé de limiter le nombre de cœurs utilisés par duckdb
afin de ne pas consommer toutes les ressources. Vous pouvez trouver plus d’information dans la section Configurer duckdb
.
Pour la suite, on supposera que la connexion à une base de données duckdb est ouverte.
21.4.3 Chargement des données
Une fois qu’on s’est connecté à une base de données duckDB, il faut charger des données dans cette base de données. Il y a deux façons de le faire:
- En établissant un lien entre la base de données duckDB et les objets de la session
R
; - En indiquant à
duckdb
l’emplacement des données sur le disque dur.
21.4.3.1 Chargement de données provenant de la session R
La fonction duckdb_register()
permet de charger dans duckdb
des données présentes dans la session R
. Cette méthode a l’avantage de ne pas recopier les données: elle se contente d’établir un lien logique entre la base de données duckdb
et un objet de la session R
. Voici un exemple avec la Base permanente des équipements: grâce à la fonction duckdb::duckdb_register()
, l’objet bpe_ens_2018
est référencé dans la base de données duckdb
sous le nom bpe_ens_2018_duckdb
.
# Charger la Base permanente des équipements 2018 dans la session R
bpe_ens_2018 <- doremifasolData::bpe_ens_2018 |> as_tibble()
# Etablir le lien logique entre la base de données duckdb et la table de données
conn_ddb %>% duckdb::duckdb_register(
name = "bpe_ens_2018_duckdb",
df = bpe_ens_2018)
Le code ci-dessous permet de vérifier que le chargement des données a bien fonctionné. La fonction tbl
permet d’accéder à un objet de la base de données par le nom (de la table), ou par du code SQL (utilisation un peu plus avancée). Par défaut, duckdb
affiche les 10 premières lignes du résultat, sans effectuer tout le calcul. C’est très pratique et très rapide !
# Source: table<bpe_ens_2018_duckdb> [?? x 7]
# Database: DuckDB v0.9.2 [unknown@Linux 6.5.0-1024-azure:R 4.3.2/:memory:]
REG DEP DEPCOM DCIRIS AN TYPEQU NB_EQUIP
<chr> <chr> <chr> <chr> <dbl> <chr> <dbl>
1 84 01 01001 01001 2018 A401 2
2 84 01 01001 01001 2018 A404 4
3 84 01 01001 01001 2018 A504 1
4 84 01 01001 01001 2018 A507 1
5 84 01 01001 01001 2018 B203 1
6 84 01 01001 01001 2018 C104 1
7 84 01 01001 01001 2018 D233 1
8 84 01 01001 01001 2018 F102 1
9 84 01 01001 01001 2018 F111 1
10 84 01 01001 01001 2018 F113 1
# ℹ more rows
21.4.3.2 Chargement de données stockées sur le disque dur
Pour l’exemple suivant, on sauvegarde les données bpe_ens_2018
au format Parquet.
bpe_ens_2018 |> arrow::write_dataset("bpe_ens_2018_dataset")
Il existe deux méthodes pour manipuler des données stockées en Parquet avec duckdb
sans avoir à les charger en mémoire: soit utiliser la fonction dplyr::tbl()
qui lit directement les fichiers Parquet avec duckdb
, soit utiliser la fonction arrow::open_dataset()
et créer un lien logique avec la fonction arrow::to_duckdb()
. Si la deuxième méthode est plus simple, surtout quand vous connaissez déjà arrow
, la première est systématiquement plus efficace et peut générer des gains de consommation mémoire et de temps de traitement conséquents. Il est donc conseillé de ne pas lire vos fichiers avec arrow::open_dataset
si vos traitements sont lourds (il ne faut pas hésiter à faire des tests).
La première approche repose uniquement sur duckdb
. Vous devez utilisez la fonction dplyr::tbl
:
ℹ Using `ident_q()` for a table identifier is intended as fallback in case of
bugs.
ℹ If you need it to work around a bug please open an issue
<https://github.com/tidyverse/dbplyr/issues>.
It looks like you tried to incorrectly use a table in a schema as source.
ℹ If you want to specify a schema use `in_schema()` or `in_catalog()`.
ℹ If your table actually contains "." in the name use `check_from = FALSE` to
silence this message.
This message is displayed once every 8 hours.
# Source: SQL [?? x 7]
# Database: DuckDB v0.9.2 [unknown@Linux 6.5.0-1024-azure:R 4.3.2/:memory:]
REG DEP DEPCOM DCIRIS AN TYPEQU NB_EQUIP
<chr> <chr> <chr> <chr> <dbl> <chr> <dbl>
1 84 01 01001 01001 2018 A401 2
2 84 01 01001 01001 2018 A404 4
3 84 01 01001 01001 2018 A504 1
4 84 01 01001 01001 2018 A507 1
5 84 01 01001 01001 2018 B203 1
6 84 01 01001 01001 2018 C104 1
7 84 01 01001 01001 2018 D233 1
8 84 01 01001 01001 2018 F102 1
9 84 01 01001 01001 2018 F111 1
10 84 01 01001 01001 2018 F113 1
# ℹ more rows
Quelques explications de cette commande:
- La fonction
read_parquet
est une fonction interne àduckdb
, elle ne doit surtout pas être confondue avec la fonctionread_parquet()
du packagearrow
. Remarque:duckdb
propose aussi des fonctions pour lire d’autres formats comme csv, json… -
**/*.parquet
est un motif qui indique que vous souhaitez lire, dans tous les sous-dossiers quelque soit le niveau (**
), l’ensemble des fichiers parquets (*.parquet
) qui s’y trouvent. C’est notamment utile pour lire des fichiers Parquet partitionnés. Quand vous n’avez pas besoin de passer d’arguments àread_parquet
, vous pouvez l’omettre :
It looks like you tried to incorrectly use a table in a schema as source.
ℹ If you want to specify a schema use `in_schema()` or `in_catalog()`.
ℹ If your table actually contains "." in the name use `check_from = FALSE` to
silence this message.
# Source: table<bpe_ens_2018_dataset/**/*.parquet> [?? x 7]
# Database: DuckDB v0.9.2 [unknown@Linux 6.5.0-1024-azure:R 4.3.2/:memory:]
REG DEP DEPCOM DCIRIS AN TYPEQU NB_EQUIP
<chr> <chr> <chr> <chr> <dbl> <chr> <dbl>
1 84 01 01001 01001 2018 A401 2
2 84 01 01001 01001 2018 A404 4
3 84 01 01001 01001 2018 A504 1
4 84 01 01001 01001 2018 A507 1
5 84 01 01001 01001 2018 B203 1
6 84 01 01001 01001 2018 C104 1
7 84 01 01001 01001 2018 D233 1
8 84 01 01001 01001 2018 F102 1
9 84 01 01001 01001 2018 F111 1
10 84 01 01001 01001 2018 F113 1
# ℹ more rows
La seconde approche consiste à passer par arrow
, puis à transmettre les données à duckdb
. Cette méthode utilise un objet intermédiaire de type Arrow Dataset (voir la fiche Manipuler des données avec arrow
):
# Créer une connexion au dataset Parquet
bpe_ens_2018_dataset <- arrow::open_dataset("bpe_ens_2018_dataset")
# Etablir le lien entre la base de données duckdb et le dataset Parquet
bpe_ens_2018_dataset %>% arrow::to_duckdb(conn_ddb)
# Source: table<arrow_001> [?? x 7]
# Database: DuckDB v0.9.2 [unknown@Linux 6.5.0-1024-azure:R 4.3.2/:memory:]
REG DEP DEPCOM DCIRIS AN TYPEQU NB_EQUIP
<chr> <chr> <chr> <chr> <dbl> <chr> <dbl>
1 84 01 01001 01001 2018 A401 2
2 84 01 01001 01001 2018 A404 4
3 84 01 01001 01001 2018 A504 1
4 84 01 01001 01001 2018 A507 1
5 84 01 01001 01001 2018 B203 1
6 84 01 01001 01001 2018 C104 1
7 84 01 01001 01001 2018 D233 1
8 84 01 01001 01001 2018 F102 1
9 84 01 01001 01001 2018 F111 1
10 84 01 01001 01001 2018 F113 1
# ℹ more rows
Ces deux approches ont un point commun important: elles établissent une connexion aux données contenues dans le dataset Parquet, mais elles ne chargent pas les données en mémoire (ni dans la mémoire de R
, ni dans celle de DuckDB
).
Pour plus de commodité, on sauvegarde l’instruction précédente dans la variable bpe_ens_2018_dataset
.
It looks like you tried to incorrectly use a table in a schema as source.
ℹ If you want to specify a schema use `in_schema()` or `in_catalog()`.
ℹ If your table actually contains "." in the name use `check_from = FALSE` to
silence this message.
21.4.4 Manipulation des données avec la syntaxe dplyr
Le package R
duckdb
a été écrit de façon à pouvoir manipuler les données avec la syntaxe de dplyr
(select
, filter
, mutate
, left_join
, etc.). duckdb
traduit le code R
, y compris certaines fonctions de stringr
et lubridate
en requête SQL. Cela s’avère très commode en pratique, car lorsqu’on sait utiliser dplyr
et le tidyverse
, on peut commencer à utiliser duckdb
sans avoir à apprendre une nouvelle syntaxe de manipulation de données.
Dans l’exemple suivant, on calcule le nombre d’équipements par région, à partir d’un tibble
et à partir d’une table duckdb
. La seule différence apparente entre les deux traitement est la présence de la fonction collect()
à la fin des instructions; cette fonction indique que l’on souhaite obtenir le résultat du traitement sous la forme d’un tibble
. La raison d’être de ce collect()
est expliquée plus loin, dans le paragraphe sur l’évaluation différée. Les résultats sont identiques, à l’exception de l’ordre des lignes. En effet, un moteur SQL ne respecte pas l’ordre par défaut, il faut le demander explicitement avec arrange
.
Manipulation d’un tibble
doremifasolData::bpe_ens_2018 |>
group_by(REG) |>
summarise(
NB_EQUIP_TOT = sum(NB_EQUIP)
)
# A tibble: 18 × 2
REG NB_EQUIP_TOT
<chr> <dbl>
1 01 23939
2 02 19068
3 03 7852
4 04 30767
5 06 7353
6 11 469181
7 24 81379
8 27 96309
9 28 107186
10 32 175859
11 44 181130
12 52 119689
13 53 111291
14 75 237008
15 76 256813
16 84 303775
17 93 254405
18 94 21778
Manipulation d’une table duckdb
Warning: Missing values are always removed in SQL aggregation functions.
Use `na.rm = TRUE` to silence this warning
This warning is displayed once every 8 hours.
# A tibble: 18 × 2
REG NB_EQUIP_TOT
<chr> <dbl>
1 84 303775
2 32 175859
3 76 256813
4 06 7353
5 24 81379
6 75 237008
7 52 119689
8 01 23939
9 94 21778
10 93 254405
11 11 469181
12 04 30767
13 02 19068
14 03 7852
15 28 107186
16 53 111291
17 44 181130
18 27 96309
On peut examiner la requête SQL construite par duckdb
avec la fonction show_query()
.
bpe_ens_2018_dataset |>
group_by(REG) |>
summarise(
NB_EQUIP_TOT = sum(NB_EQUIP)
) |>
show_query()
<SQL>
SELECT REG, SUM(NB_EQUIP) AS NB_EQUIP_TOT
FROM "bpe_ens_2018_dataset/*.parquet"
GROUP BY REG
Cette requête est envoyée au serveur SQL et exécutée de façon différente en fonction de la dernière instruction du traitement:
- si le traitement se termine par
collect()
: le calcul est exécuté en entier et le résultat est retourné sous la forme d’untibble
, - si le traitement se termine par
print(n=nb_lignes)
: le calcul est exécuté partiellement, et seules lesnb_lignes
demandées sont affichées. Cela permet de minimiser les ressources et la mémoire utilisées.
Ce point est important: en utilisant print()
, on peut prévisualiser le résultat d’une requête duckdb
de façon très rapide, sans exécuter tout le traitement. Il ne faut pas hésiter à s’en servir pour explorer les données et pour construire le traitement étape par étape, en ajustant en fonction des résultats.
21.4.5 Écriture au format Parquet
Pour écrire une table (ou le résultat de n’importe quelle requête) sur le disque au format Parquet, il est recommandé d’utiliser la librairie arrow
.
bpe_ens_2018_dataset %>%
arrow::to_arrow() %>% arrow::write_dataset("temp_dataset")
list.files("temp_dataset") # liste des fichiers du répertoire temp_dataset/
[1] "part-0.parquet"
Pour un usage basique en syntaxe dplyr
, passer par arrow
(au lieu de SQL) est plus facile à manipuler, notamment quand on souhaite ajouter des options telle que le partitionnement.
21.4.6 Erreurs courantes
Cette section présente quelques erreurs classiques.
21.4.6.1 On a éliminé des colonnes nécessaires
21.4.6.2 Convertir les types
Dans cet exemple, on veut multiplier un nombre par une indicatrice.
Error in `collect()`:
! Failed to collect lazy table.
Caused by error:
! rapi_prepare: Failed to prepare query SELECT DEP, SUM(NB_EQUIP * (TYPEQU = 'B203')) AS nb_boulangeries
FROM "bpe_ens_2018_dataset/*.parquet"
GROUP BY DEP
LIMIT 11
Error: Binder Error: No function matches the given name and argument types '*(DOUBLE, BOOLEAN)'. You might need to add explicit type casts.
Candidate functions:
*(TINYINT, TINYINT) -> TINYINT
*(SMALLINT, SMALLINT) -> SMALLINT
*(INTEGER, INTEGER) -> INTEGER
*(BIGINT, BIGINT) -> BIGINT
*(HUGEINT, HUGEINT) -> HUGEINT
*(FLOAT, FLOAT) -> FLOAT
*(DOUBLE, DOUBLE) -> DOUBLE
*(DECIMAL, DECIMAL) -> DECIMAL
*(UTINYINT, UTINYINT) -> UTINYINT
*(USMALLINT, USMALLINT) -> USMALLINT
*(UINTEGER, UINTEGER) -> UINTEGER
*(UBIGINT, UBIGINT) -> UBIGINT
*(INTERVAL, BIGINT) -> INTERVAL
*(BIGINT, INTERVAL) -> INTERVAL
LINE 1: SELECT DEP, SUM(NB_EQUIP * (TYPEQU = 'B203')) AS nb_boulangeries
^
Avec duckdb
, il faut transformer explicitement un booléen en nombre (entier ou flottant).
bpe_ens_2018_dataset %>%
summarise(nb_boulangeries = sum(NB_EQUIP * as.integer(TYPEQU == "B203")), .by = DEP)
# Source: SQL [?? x 2]
# Database: DuckDB v0.9.2 [unknown@Linux 6.5.0-1024-azure:R 4.3.2/:memory:]
DEP nb_boulangeries
<chr> <dbl>
1 13 1626
2 14 599
3 30 685
4 40 321
5 02 339
6 03 299
7 17 628
8 18 237
9 43 237
10 57 871
# ℹ more rows
21.5 Notions avancées / bien utiliser duckdb
21.5.1 Configurer duckdb
duckdb
propose de nombreux paramètres mais nous n’allons voir que les principaux. Vous pouvez vous reporter à la documentation officielle pour en apprendre davantage sur la configuration de duckdb
.
21.5.1.1 Configuration lors de l’initialisation
Pour configurer duckdb
lors de l’initialisation de la base de données (c’est-à-dire au moment où on utilise DBI::dbConnect(drv = duckdb::duckdb())
), on utilise les arguments du driver duckdb
.
Voici une description des principaux paramètres de configuration:
-
dbdir
: utiliser une base de données persistante. Par défaut,duckdb
crée une base de données dans la mémoire vive, qui est automatiquement détruite lorsque vous fermez la sessionR
ou la connexionduckdb
. Si vous mettez un chemin dans le paramètredbdir
,duckdb
créera une base de données sur disque que vous pourrez réouvrir à votre prochaine session.
Si vous utilisez principalement dplyr
, les bases de données en mémoire sont certainement suffisantes. En revanche, ce paramètre peut éventuellement vous être utile si vous utilisez du SQL, si vous créez des vues ou si vous utilisez dplyr::compute
.
threads
: limiter le nombre de threads utilisés parduckdb
. Pour simplifier, un thread est un processeur ou un morceau de processeur (l’unité électronique qui réalise les calculs). Par défaut,duckdb
utilise tous les processeurs disponibles, ce qui n’est pas forcément souhaitable pour plusieurs raisons :sur un serveur partagé, vos collègues seront gênés ;
il est conseillé de disposer de 5 à 10Go de mémoire par thread (5 pour des aggrégations, 10 pour des jointures) donc beaucoup de threads implique beaucoup de mémoire ;
avoir trop de threads peut être contre-productif.
Il n’existe pas de règle générale pour définir le nombre de threads, mais utiliser 4 à 8 threads (en respectant le ratio threads/mémoire ci-dessus) constitue un point de départ raisonnable. Au delà, les performances augmentent généralement peu pour une consommation mémoire plus importante.
memory_limit
: limiter la mémoire vive utilisée parduckdb
. Par défaut,duckdb
limite la mémoire à 80% de la mémoire disponible sur le serveur. Si vous avez une quantité limitée de mémoire, essayez plutôt de limiter le nombre de threads en respectant la règle de 5 à 10 Go par thread.temp_directory
: définir le dossier sur disque dans lequelduckdb
peut écrire des fichiers temporaires. Un avantage deduckdb
est qu’il sait “déborder” sur disque pour une grande partie de ces opérations. Cela signifie queduckdb
va écrire dans des fichiers temporaires sur le disque les données qu’il ne peut conserver en mémoire car il a atteint la limite de mémoire fixée. Le paramètretemp_directory
permet de choisir dans quel dossier ces fichiers temporaires seront écrits. Toutefois, il est généralement beaucoup plus efficace de diminuer le nombre de threads que de déborder sur disque mais dans le cas où vous avez besoin de “juste un peu plus” de mémoire cela peut se révéler utile. A noter que ce paramètre est automatiquement fixé si vous avez décidé d’utiliser une base persistante.preserve_insertion_order
: préserver l’ordre de lecture/écriture ou non.duckdb
peut consommer beaucoup de mémoire pour conserver l’ordre de lecture et d’écriture. Ce dernier paramètre permet d’autoriserduckdb
à ne pas préserver l’ordre des données à la lecture et à l’écriture des fichiers dans le cas où il n’y a pas de clauseORDER BY
/arrange
.
21.5.1.2 Fixer les paramètres après l’initialisation
Vous pouvez également changer les paramètres d’une base après son initialisation en utilisant la commande dbExecute
. Par exemple, pour fixer le nombre de threads à 4 :
dbExecute(conn_ddb, "SET threads = '4';")
21.5.2 L’évaluation différée avec duckdb
(lazy evaluation)
Il est vivement conseillé de lire la fiche Manipuler des données avec arrow
avant de lire cette section, en particulier la partie sur l’évaluation différée.
Quand on manipule des objets duckdb
, on construit des requêtes SQL. Le package duckdb
se contente de traduire le code dplyr
en SQL
sans l’exécuter (de la même façon que le package arrow
traduit du code dplyr
en instructions C++). On rappelle qu’il faut utiliser show_query()
pour visualiser la requête. La fonction print()
permet de pré-visualiser le résultat.
# Étape 1: compter les équipements
req_dep <-
bpe_ens_2018_dataset |>
group_by(DEP) |>
summarise(
NB_EQUIP_TOT = sum(NB_EQUIP)
)
req_dep |>
show_query()
<SQL>
SELECT DEP, SUM(NB_EQUIP) AS NB_EQUIP_TOT
FROM "bpe_ens_2018_dataset/*.parquet"
GROUP BY DEP
# Étape 2: filtrer sur le département
req_dep_filter <- req_dep |>
filter(DEP == "59")
req_dep_filter |>
show_query()
<SQL>
SELECT DEP, SUM(NB_EQUIP) AS NB_EQUIP_TOT
FROM "bpe_ens_2018_dataset/*.parquet"
GROUP BY DEP
HAVING (DEP = '59')
La fonction collect()
envoie à duckdb
l’instruction d’exécuter le calcul, et transmet les résultats à R
. Un point essentiel est qu’avant l’instruction collect()
, c’est le moteur SQL de duckdb
qui fait les calculs, tandis qu’après l’instruction collect()
, c’est le moteur de R
qui fait les calculs car on manipule un objet R
(tibble
) standard. Par conséquent, il faut faire le plus de calculs possibles avant collect()
pour bénéficier de la rapidité du moteur SQL !
req_dep_filter |> collect()
# A tibble: 1 × 2
DEP NB_EQUIP_TOT
<chr> <dbl>
1 59 76125
On pourrait penser que, lorsqu’on exécute l’ensemble de ce traitement, duckdb
se contente d’exécuter les instructions les unes après les autres: compter les équipements par département, puis conserver uniquement le département 59. Mais en réalité duckdb
fait beaucoup mieux que cela: duckdb
analyse la requête avant de l’exécuter, et optimise le traitement pour minimiser le travail. Dans le cas présent, duckdb
repère que la requête ne porte en fait que sur le département 59, et commence donc par filtrer les données sur le département avant de compter les équipements, de façon à ne conserver que le minimum de données nécessaires et à ne réaliser que le minimum de calculs. Ce type d’optimisation s’avère très utile quand les données à traiter sont très volumineuses.
Situation à éviter
La première étape de traitement est déclenchée par collect()
, la table intermédiaire res_etape1
est donc un tibble
. C’est le moteur d’exécution de dplyr
qui est utilisé pour manipuler res_etape1
lors de la seconde étape, ce qui dégrade fortement les performances sur données volumineuses.
Usage recommandé
La première étape construit une requête SQL, sans effectuer de calcul. La deuxième étape complète la requête sans effectuer de calcul. Ici, pas de fonction print()
, donc pas de calcul partiel. Le calcul n’est exécuté qu’au moment de la sauvegarde des résultats par DuckDB
, ce qui assure de bonnes performances notamment sur données volumineuses. Les données ne sont chargées dans la mémoire de R
à aucun moment.
Si vous ne savez plus si une table de données est une requête SQL ou un tibble
, il suffit d’exécuter print(votre_table)
ou class(votre_table)
.
21.5.3 Fonctions non traduites et/ou comment passer des paramètres ?
Il peut arriver que le package duckdb
ne parvienne pas à traduire votre code dplyr
en SQL, par exemple lorsque vous voulez utiliser une fonction R
dont duckdb
ne connaît pas la traduction SQL, ou lorsque vous voulez passer un paramètre à une fonction. Pour surmonter ce problème (heureusement peu fréquent), il faut mettre les mains dans le mécanisme de traduction vers SQL. Il y a deux points importants:
- Lorsque
duckdb
ne connaît pas la traduction SQL d’une fonctionR
est que la fonction inconnue est reprise directement dans le code SQL sans aucune modification. Voici un exemple, dans lequel on peut voir que la fonctionfonction_inexistante()
apparaît telle quelle dans le code SQL.
req <- bpe_ens_2018_dataset |>
mutate(test = fonction_inexistante(DEP)) |>
show_query()
<SQL>
SELECT "bpe_ens_2018_dataset/*.parquet".*, fonction_inexistante(DEP) AS test
FROM "bpe_ens_2018_dataset/*.parquet"
-
DuckDB
contient un grand nombre de fonctions optimisées (documentation ici), et il est possible de les utiliser directement dans du codeR
.
Ces deux points ensemble permettent de surmonter dans la plupart des cas le problème des fonctions R
inconnues de duckdb
: il suffit d’appeler la fonction de DuckDB
qui fait la même chose. Voici un exemple qui explique cela en détail dans le cas de la fonction R
as.Date()
. On commence par créer une petite table duckdb
contenant des dates sous forme de chaînes de caractères avec le format “DD/MM/YYYY”.
# Créer des dates sous forme de chaînes de caractères
dates <- tibble(
date_naissance = c("02/07/1980", "29/02/2004"),
date_deces = c("05/06/2001", "12/07/2023")
)
# Créer une connexion entre ces données et la base de données duckdb
conn_ddb %>% duckdb::duckdb_register(name = "dates_duckdb", df = dates, overwrite = TRUE)
Le package duckdb
dispose d’une traduction SQL de la fonction as.Date()
, mais cette traduction a deux limites: elle n’accepte que les données en format “YYYY-MM-DD”, et ne supporte pas l’argument format
qui permet de préciser que les données sont en format “DD/MM/YYYY”. Par conséquent, on rencontre une erreur si on essaie d’utiliser la fonction as.Date()
avec l’argument format
(car duckdb
ne sait pas gérer cet argument), et on rencontre une erreur si on essaie d’utiliser la fonction as.Date()
sans cet argument (car les données n’ont pas le bon format).
conn_ddb %>% tbl("dates_duckdb") %>%
mutate(date_naissance = as.Date(date_naissance, format = "%d/%m/%Y")) # erreur
Error in as.Date(date_naissance, format = "%d/%m/%Y"): unused argument (format = "%d/%m/%Y")
Error in `collect()`:
! Failed to collect lazy table.
Caused by error:
! rapi_execute: Failed to run query
Error: Conversion Error: date field value out of range: "02/07/1980", expected format is (YYYY-MM-DD)
On pourrait penser que ce problème est sérieux. En fait, la solution est très simple: il suffit d’utiliser la fonction strptime
du moteur SQL DuckDB
en indiquant le paramètre adéquat. Comme vous pouvez voir dans l’exemple suivant, on appelle cette fonction directement dans le code R
. Par ailleurs, cette façon d’utiliser les fonctions de DuckDB
dans du code R
permet de passer facilement un paramètre à une fonction (le format “%d/%m/%Y” dans le cas présent).
# Source: SQL [2 x 2]
# Database: DuckDB v0.9.2 [unknown@Linux 6.5.0-1024-azure:R 4.3.2/:memory:]
date_naissance date_deces
<dttm> <chr>
1 1980-07-02 00:00:00 05/06/2001
2 2004-02-29 00:00:00 12/07/2023
La logique présentée ici fonctionne également dans un cas plus avancé: l’utilisation d’une fonction sur plusieurs variables avec mutate_at
. L’exemple ci-dessous reprend l’exemple ci-dessus avec deux variables.
liste_variables <- c("date_naissance","date_deces")
conn_ddb %>% tbl("dates_duckdb") %>%
mutate_at(liste_variables, ~ strptime(.,"%d/%m/%Y"))
# Source: SQL [2 x 2]
# Database: DuckDB v0.9.2 [unknown@Linux 6.5.0-1024-azure:R 4.3.2/:memory:]
date_naissance date_deces
<dttm> <dttm>
1 1980-07-02 00:00:00 2001-06-05 00:00:00
2 2004-02-29 00:00:00 2023-07-12 00:00:00
21.5.4 Manipulation des données avec SQL
DuckDB
étant un moteur SQL à part entière, on peut interagir avec DuckDB
directement avec des requêtes SQL. Par exemple, en reprenant une table enregistrée plus haut avec la fonction duckdb::duckdb_register
:
DBI::dbGetQuery(conn_ddb, "SELECT * FROM bpe_ens_2018_duckdb") |> head()
REG DEP DEPCOM DCIRIS AN TYPEQU NB_EQUIP
1 84 01 01001 01001 2018 A401 2
2 84 01 01001 01001 2018 A404 4
3 84 01 01001 01001 2018 A504 1
4 84 01 01001 01001 2018 A507 1
5 84 01 01001 01001 2018 B203 1
6 84 01 01001 01001 2018 C104 1
Vous pouvez créer des vues ou des tables explicitement. La fonction dbExecute()
retourne le nombre de lignes modifiées, tandis que la fonction dbGetQuery
retourne le résultat sous la forme d’un tibble
. Si vous n’avez pas l’intention de conserver durablement une table intermédiaire, il est préférable de créer une vue (qui ne consomme pas de mémoire) plutôt qu’une table (qui consomme de la mémoire). On peut d’ailleurs noter que les fonctions read_parquet()
en SQL et duckdb_register
du package utilisent CREATE VIEW
implicitement.
# Créer une table dans la base de données DuckDB
DBI::dbExecute(conn_ddb, "
CREATE TABLE bpe_ens_2018_table AS
SELECT REG, SUM(NB_EQUIP) AS NB_EQUIP_TOT
FROM bpe_ens_2018_duckdb
GROUP BY REG") # Utilise de la mémoire
[1] 18
# Créer une vue dans la base de données DuckDB
DBI::dbExecute(conn_ddb, "
CREATE VIEW bpe_ens_2018_view AS
SELECT REG, SUM(NB_EQUIP) AS NB_EQUIP_TOT
FROM bpe_ens_2018_duckdb
GROUP BY REG") # n'utilise pas de mémoire
[1] 0
Vous pouvez ensuite requêter les objets créés dans la base SQL via dplyr
:
# Source: table<bpe_ens_2018_view> [?? x 2]
# Database: DuckDB v0.9.2 [unknown@Linux 6.5.0-1024-azure:R 4.3.2/:memory:]
REG NB_EQUIP_TOT
<chr> <dbl>
1 06 7353
2 76 256813
3 02 19068
4 03 7852
5 24 81379
6 84 303775
7 32 175859
8 01 23939
9 75 237008
10 94 21778
# ℹ more rows
Vous pouvez bien sûr lire des fichiers Parquet
, CSV
ou autres en utilisant les fonctions de duckdb :
DBI::dbGetQuery(conn_ddb, "SELECT * FROM read_parquet('bpe_ens_2018_dataset/**/*.parquet') LIMIT 5")
REG DEP DEPCOM DCIRIS AN TYPEQU NB_EQUIP
1 84 01 01001 01001 2018 A401 2
2 84 01 01001 01001 2018 A404 4
3 84 01 01001 01001 2018 A504 1
4 84 01 01001 01001 2018 A507 1
5 84 01 01001 01001 2018 B203 1
Le SQL de duckdb
est très proche de celui de PostgreSQL avec quelques évolutions très pertinentes.
21.5.4.1 Séparer vos traitements SQL en blocs
Si vos requêtes deviennent trop complexes et/ou longues, vous pouvez facilement les découper en créant des vues intermédiaires que vous réutiliserez plus tard :
# Créer une vue qui correspond à la première étape du traitement
dbExecute(conn_ddb, "CREATE OR REPLACE VIEW data1_nettoye AS SELECT ... FROM read_parquet('data1.parquet')")
# Créer une vue qui correspond à la deuxième étape du traitement
dbExecute(conn_ddb, "CREATE OR REPLACE VIEW data2_nettoye AS SELECT ... FROM read_parquet('data2.parquet')")
# Faire la dernière étape du traitement et récupérer les résultats dans un tibble
resultats <- dbGetQuery(conn_ddb, "SELECT * FROM data1_nettoye LEFT JOIN data2_nettoye ON data1.id = data2.id")
Et vous pouvez bien sûr créer des tables intermédiaires (temporaires ou non) à la place des vues (en utilisant CREATE TABLE
pluôt que CREATE VIEW
) pour éviter de les recalculer à chaque fois.
21.5.4.2 Écrire des fichiers
Vous pouvez exporter des données vers des fichiers en utilisant COPY ... TO ...
:
dbExecute(conn_ddb, "COPY (SELECT * FROM read_parquet('bpe_ens_2018_dataset/**/*.parquet'))
TO 'mon_dataset_parquet' (FORMAT PARQUET, PARTITION_BY (REG), OVERWRITE_OR_IGNORE 1)")
[1] 0
Si vous préférez utiliser les fonctions de arrow
, vous pouvez créez une vue et utiliser dbplr::tbl
avec arrow::write_dataset
:
21.5.5 Optimisations
Les opérations difficiles en SQL, longues, nécessitant beaucoup de mémoire, sont les fonctions dites “fenêtre”: jointures, GROUP BY
avec beaucoup de petits groupes, dédoublonnage, etc. On propose ici quelques techniques pour faire passer ces calculs difficiles.
21.5.5.1 Utilisation de la mémoire vive
Comme expliqué plus haut, les objets manipulés dans cette fiche sont des requêtes SQL, et ne nécessitent pas de mémoire vive. Les données déclarées par read_parquet
sont stockées sur le disque dur, lues à la demande, et “oubliées” à la fin du calcul. On retourne le résultat du calcul.
Pour les opérations compliquées, il peut être nécessaire de charger les données en mémoire pour effectuer le calcul, au risque de saturer la mémoire. Lorsque ce problème se pose, duckdb
renvoie un message du type:
Error: rapi_execute: Failed to run query
Error: Out of Memory Error: could not allocate block of size 262KB (99.7MB/100.0MB used)
Database is launched in in-memory mode and no temporary directory is specified.
Unused blocks cannot be offloaded to disk.
Launch the database with a persistent storage back-end
Or set PRAGMA temp_directory='/path/to/tmp.tmp'
Pour contourner le manque de mémoire vive, on propose les quatre techniques suivantes :
- diminuer le nombre de threads utilisés par
duckdb
, donc moins de besoins de mémoire (mais aussi moins de parallélisme):
<- dbConnect(duckdb(),
conn_ddb config=list("threads"="1")))
ou
dbExecute(conn_ddb, "SET threads = '1';")
- exécuter et sauvegarder les résultats au fur et à mesure. La commande
arrow::write_dataset
et la commande SQLCOPY request TO filename.parquet
savent le faire automatiquement, sans faire déborder la mémoire, pour certains calculs. - découper le calcul et sauvegarder une base intermédiaire (cf ci-dessous).
- adosser un fichier sur le disque dur à la base de données en mémoire au moment de la création de la connexion. Cela ralentit considérablement les calculs, et ne permet pas toujours d’obtenir un résultat.
L’interaction entre les différentes options de duckdb
est complexe et rendent difficile l’élaboration de recommandations claires. Nous mettrons à jour cette fiche quand des benchmarks plus poussés seront disponibles.
21.5.5.2 Sauvegarder des résultats intermédiaires
Dans plusieurs cas, vous pouvez vouloir passer par des résultats intermédiaires :
- Votre traitement est long et vous ne souhaitez pas le recalculer entièrement à chaque fois ;
- Certaines requêtes sont trop compliquées pour le moteur SQL et/ou pour la traduction automatique, vous devez le découper.
Vous avez plusieurs méthodes possibles :
-
arrow::write_dataset()
sait faire les calculs par morceaux automatiquement, et libère la mémoire au fur et à mesure.
conn_ddb %>% calcul1() %>%
arrow::to_arrow() %>%
arrow::write_dataset("base_intermediaire")
arrow::open_dataset("base_intermediaire") %>%
arrow::to_duckdb(conn_ddb) %>%
calcul2()
- Vous pouvez utiliser
dbplyr::compute()
pour créer une tableduckdb
stockée sur le disque (si vous avez préalablement créé une base sur disque) que vous pourrez directement utiliser par la suite dans une autre session :
La première méthode avec arrow
est généralement la plus rapide et la seconde avec dbplyr::compute
sur une table nommée est la plus efficace (de loin) en terme d’occupation mémoire.
A noter que vous pouvez également utiliser dbplyr::compute
pour créer une table temporaire duckdb
stockée en mémoire qui disparaitra à la fin de votre session :
21.5.5.3 Partitionner les données
- Pour exécuter une fonction fenêtre, il faut pouvoir localiser les données en mémoire avec un index.
- Les fichiers
parquet
ont un indexmin-max
: les fichiers sont structurés en blocs, et on indique le minimum et maximum des valeurs du bloc dans les métadonnées. Ceci permet de sauter la lecture d’un bloc si l’on s’intéresse à des valeurs en dehors de la plagemin-max
, parce que l’on filtre les données par exemple. - En SQL, on peut créer un index, mais il faut que les données soient en mémoire, ce qui peut s’avérer être incompatible avec de très grosses volumétries.
- Par contre, on peut partitionner les données, et le moteur SQL sait utiliser le partitionnement comme un index.
bpe_ens_2018_dataset %>%
arrow::to_arrow() %>%
arrow::write_dataset("bpe_ens_2018_dataset_parts", partitioning = "REG" )
list.files("bpe_ens_2018_dataset_parts") # on obtient un sous-répertoire par région
[1] "REG=01" "REG=02" "REG=03" "REG=04" "REG=06" "REG=11" "REG=24" "REG=27"
[9] "REG=28" "REG=32" "REG=44" "REG=52" "REG=53" "REG=75" "REG=76" "REG=84"
[17] "REG=93" "REG=94"
21.5.5.4 Exécuter les traitements par groupe explicitement
S’il faut absolument charger des données en mémoire, on peut découper le calcul pour ne charger qu’une partie des données. Par exemple, faire une jointure région par région au lieu de faire la jointure sur toute la base d’un coup. On peut utiliser le partitionnement pour sauvegarder les résultats partiels, et les ré-assembler ensuite.
groups <- bpe_ens_2018_dataset %>%
distinct(REG) %>%
pull() # liste des modalités de la variable REG
# Définir une fonction qui traite un morceau des données
# Puis exporte le résultats dans un Parquet partitionné
f <- function(x) {
bpe_ens_2018_dataset %>%
filter(REG == x) %>%
calcul_long() %>%
arrow::to_arrow() %>%
arrow::write_dataset("resultat", partitioning = "REG")
}
# Appliquer la fonction à chaque groupe
purrr::walk(f, groups)
21.6 Comparaison avec arrow
arrow
et duckdb
partagent de nombreux concepts. Voici quelques différences :
-
duckdb
comprend parfaitement SQL. Si vous savez utiliserPROC SQL
avec SAS, vous ne serez pas dépaysés. - Le projet
duckdb
est très récent. Il y a régulièrement des évolutions qui sont souvent des extensions ou des optimisations, et parfois la résolution de bugs.arrow
est un projet plus ancien et plus mature. - Certaines fonctions standards de
R
ne sont pas traduites, mais la situation est meilleure du côté deduckdb
que d’arrow
. Hormiswrite_dataset()
, la plupart des traitements peuvent être effectués en utilisant uniquementduckdb
, sans passer pararrow
. - Les conversions de type:
duckdb
est plus permissif quearrow
et fera plus facilement des conversions automatiques sans danger. - Les jointures de tables volumineuses:
arrow
ne parvient pas à joindre des tables de données très volumineuses; il est préférable d’utiliserduckdb
pour ce type d’opération. - Les réorganisations de données : les fonctions
pivot_wider
etpivot_longer
existent nativement dansduckdb
mais pas dansarrow
. - Les fonctions fenêtre (window functions):
arrow
ne permet pas d’ajouter directement à une table des informations issues d’une agrégation par groupe de la même table. Par exemple,arrow
ne peut pas ajouter directement à la base permanente des équipements une colonne égale au nombre total d’équipements du département. Le code fonctionne enduckdb
.
# arrow ne peut pas exécuter ceci
bpe_ens_2018_dataset |>
group_by(DEP) |>
mutate(NB_EQUIP_TOTAL_DEP = sum(NB_EQUIP)) |>
select(DEP, NB_EQUIP, NB_EQUIP_TOTAL_DEP)
# Source: SQL [?? x 3]
# Database: DuckDB v0.9.2 [unknown@Linux 6.5.0-1024-azure:R 4.3.2/:memory:]
# Groups: DEP
DEP NB_EQUIP NB_EQUIP_TOTAL_DEP
<chr> <dbl> <dbl>
1 09 2 7316
2 09 3 7316
3 09 5 7316
4 09 45 7316
5 09 1 7316
6 09 1 7316
7 09 1 7316
8 09 1 7316
9 09 3 7316
10 09 1 7316
# ℹ more rows
- les empilements de tables: il est facile d’empiler plusieurs
tibbles
avecdplyr
grâce à la fonctionbind_rows()
:bind_rows(table1, table2, table3, table4)
. En revanche, il n’existe pas à ce jour de fonction équivalente dansarrow
ou dansduckdb
: il faut empiler les tables deux à deux avec les fonctionsunion_all()
etunion()
. La différence entrearrow
etduckdb
est queduckdb
est plus souple et acceptera d’empiler des tables qui ne sont pas exactement compatibles (exemple: pas le même nombre de colonnes), tandis qu’arrow
exige que les deux tables soient parfaitement compatibles (il faut le même nombre de colonnes avec le même nom et le même type, ce qui n’est pas toujours le cas en pratique). Dans l’exemple suivant, on empile deux tables qui n’ont pas exactement le même nombre de colonnes: