Databases and Big Files
In lessons 1 to 3 you opened files: a .csv, an Excel workbook, an SPSS file, even JSON straight off a web API. Every one of them shared a quiet assumption: the whole thing fits in R's memory, so you read it all in and work on it.
Maria's corner bakery has grown into a small chain of shops. Two years of every till receipt no longer fit in one emailed spreadsheet: they live in a database. Her analytics provider drops a monthly Parquet file. And an old flour supplier still sends a giant text export whose accented names arrive scrambled. None of these wants to be read whole.
This lesson teaches the one idea that handles all of them: bring your question to the data, instead of dragging all the data into R. By the end you will be able to:
- Connect to a database from R and load a table into it
- Send a question in SQL and get back only the small answer
- Query a database with the dplyr verbs you already know, and pull the result with
collect() - Read only the columns you need from a file too big to open, and fix a garbled encoding
Prerequisites: lessons 1 to 3 (you know a tibble, that columns have a type, read_csv(), and library()). No SQL is assumed; every term is defined as it appears. The map below is the whole lesson in one picture.
A database holds the data; you send it questions
A database is a program built to store tables and answer questions about them, even when those tables hold millions of rows. Think of it as the bakery's stockroom: you do not carry the whole stockroom to the front counter, you send back a note asking for "two trays of croissants" and only those come out.
To talk to one from R you open a connection: a live link to the database that you send questions down and get answers back through. R uses one consistent toolkit for this called DBI (the DataBase Interface), so the same handful of functions work whether the database is SQLite, Postgres, or, here, duckdb, a fast database that needs no separate server and runs right inside your R session. That makes it perfect for learning, and genuinely useful for big local files.
We will stand up a tiny sales table so every step is real. In a true project this table would already hold years of data; the few rows here behave exactly the same way.
dbConnect() opens the link and dbDisconnect(con, shutdown = TRUE) closes it when you are done. Everything between is the same no matter which database sits on the other end: that is the whole point of DBI.Ask in SQL, get back only the answer
The language databases speak is SQL (Structured Query Language). A query is one SQL sentence describing the answer you want; dbGetQuery() sends it down the connection and hands back the result as an ordinary data frame.
Read this query like a sentence: SELECT the columns I want, FROM the table, GROUP BY item so each item is summed on its own, ORDER BY revenue from high to low. SUM(qty) adds the quantities and AS units names that new column.
The sales table had six rows; the answer has three. That gap is the entire lesson in miniature.
Where does the work happen?
Maria's real sales table has two million rows. She runs dbGetQuery(con, "SELECT item, SUM(qty) AS units FROM sales GROUP BY item"). Where does the adding-up happen, and what comes back to R?
Query with dplyr, not just SQL
You do not have to write SQL by hand. The dbplyr package lets you query a database using the dplyr verbs from earlier in your R journey: filter(), group_by(), summarise(), arrange(). You point tbl() at a table to get a lazy reference (a stand-in that has run nothing yet), pipe your verbs onto it, and dbplyr quietly translates the whole chain into SQL.
show_query() lets you see that translation:
That is the very query you wrote by hand a moment ago, generated from dplyr. Nothing has actually run yet, though: q is still just a plan. To execute it in the database and pull the result into R as a real tibble, you call collect().
collect(). You can stack as many verbs as you like, see the SQL, and refine it, all without moving a single row. collect() is the moment the question finally goes to the data and the answer comes back.Pull the answer into R
The connection con and its sales table are still open from the steps above. This pipeline counts croissants sold across all shops, but it stops at a lazy plan: it never runs. Add the one verb that executes it in the database and pulls the result back, then check it.
Show answer
library(dplyr)
library(dbplyr)
croissants <- tbl(con, "sales") |>
filter(item == "croissant") |>
summarise(units = sum(qty)) |>
collect()
croissants
#> # A tibble: 1 x 1
#> units
#> <dbl>
#> 1 23Read only what you need
Not all big data lives in a database. Sometimes it is just an enormous flat file, more rows or columns than you want to pull into memory. The same principle rescues you: read only the part you need.
read_csv() takes a col_select argument that reads a chosen set of columns and skips the rest entirely. Maria's export has seven columns, but to total sales she needs only two. The widget shows the move: a wide table narrowed to just item and qty.
Here it is for real. We write a small file, then read back only the two columns that matter:
When you just want to see a file's shape before committing to it, n_max reads only the first few rows:
For data that is genuinely huge, there is a file format built for exactly this: Parquet. A CSV stores data row by row, so reading two columns still means scanning every row. Parquet is columnar: each column is stored separately and compressed, so reading 2 columns out of 40 touches only those 2. The arrow package reads it, and can even query a folder of Parquet files on disk without loading it whole. (Parquet needs the arrow library, which runs on your own machine rather than here, so this block is for reference.)
library(arrow)
library(dplyr)
# Read only the columns you need from a columnar Parquet file:
trimmed <- read_parquet("sales.parquet", col_select = c("item", "qty"))
# Bigger than memory? Point at a folder of Parquet files and query it on disk,
# pulling only the small summary back into R:
open_dataset("sales/") |>
group_by(shop) |>
summarise(revenue = sum(qty * price)) |>
collect()
Fix a garbled encoding
One last way a file fights back: the text comes out scrambled. A computer stores text as bytes, and an encoding is the codebook that says which byte means which character. Modern files use UTF-8; older systems often used Latin-1, where each accented character is a single byte. Read a Latin-1 file as if it were UTF-8 and the accented bytes turn to nonsense.
Maria's old supplier is named "Müller". Watch what happens when its Latin-1 bytes are read with the wrong codebook, then the right one:
The \xfc is R showing you a raw byte it could not make sense of: 0xfc is "u with an umlaut" in Latin-1, but it is not valid UTF-8, so the character is lost. Tell readr the file's real encoding with locale() and it decodes correctly:
locale(encoding = ...) to match the file.Bring your question to the data
Every part of this lesson is the same instinct: do not haul everything into R, send your question to where the data already is and take back only the answer. Which tool you reach for depends on where the data lives and what is in your way.
| Your situation | The move | Why it works |
|---|---|---|
| Data already lives in a database | Query it with DBI or dbplyr, collect() only the result |
The engine does the heavy work; little crosses into R |
| One huge flat file, you need a few columns | read_csv(col_select = ...), or a columnar Parquet file |
You read only the bytes you actually need |
| File is bigger than memory | Load it into duckdb, or open_dataset() the Parquet, and query on disk |
The whole thing is never held in R at once |
| Text comes back garbled | Set locale(encoding = ...) to the file's real encoding |
The bytes decode into the right characters |
The dataset changed from a friendly CSV to a database, a Parquet file, and a stubborn encoding, but the skill did not: read what you need, where it lives, in the right shape.
Pick the right plan
A colleague sends a 50-million-row sales file. You need total revenue per shop. The file also has 40 columns you do not need, and the shop names contain accented characters that arrived garbled. What is the soundest plan?
References
A few authoritative places to take this further:
- R for Data Science (2e), Databases - the canonical free walkthrough of
DBIanddbplyr, withtbl(),collect()and SQL translation. - DBI package home (r-dbi) - the universal database interface used here:
dbConnect,dbGetQuery,dbWriteTableand friends. - dbplyr (tidyverse) - how dplyr verbs become SQL, what is lazy, and when to
collect(). - Arrow for R - reading Parquet and querying bigger-than-memory datasets on disk with
read_parquetandopen_dataset. - readr: locales and encodings - how to set a file's encoding (and date, time and number formats) when the defaults are wrong.
Lesson 4 complete
You can now reach data that will not fit in a single read_csv(). You connected to a database with DBI and duckdb, asked questions in SQL with dbGetQuery(), and queried it with the same dplyr verbs you already knew, pulling results back with collect(). You read only the columns you needed from a wide file, met Parquet as the columnar format built for big data, and fixed a garbled name by setting its encoding.
The thread through all of it: bring your question to the data, and take back only the answer.
Next, Lesson 5: Saving and Exporting Data. Once you have the result you wanted, you need to write it back out, as a CSV, an R data file, or an Excel workbook, and choose the right format for who reads it next. That closes the loop on getting data in and out of R.