Library / SDK
tidyverse/dbplyr avatar
tidyverse/dbplyr

dbplyr turns dplyr verbs into SQL without loading the table

Database (DBI) backend for dplyr

518 stars195 forksRNOASSERTION

At a glance

What is it?
dbplyr is the database backend for dplyr: it translates a pipeline of dplyr calls into SQL and sends it to a DBI connection, so an R analyst can work against a remote table as if it were a local data frame. The translation is lazy, the generated query is inspectable with show_query(), and the hard part of using it is knowing where the translation stops being exact.
Who is it for?
dbplyr fits an R team that wants one analysis codebase to run against a local data frame in development and a warehouse table in production, and it does not fit work that needs window functions, common table expressions or hand tuned query plans.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 128 days ago.
What is it written in?
Mainly R, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A backend that writes the SQL for you

dbplyr is described as the database backend for dplyr, and the one line summary of what it does is that it lets you use remote database tables as if they are in-memory data frames by automatically converting dplyr code into SQL.

That is the entire idea, and it is worth being precise about why it is useful. A typical R analysis is a chain of verbs: group the rows, summarise, filter, join, arrange. Written against a local data frame, each step touches memory. Written against a database with dbplyr, the same chain is translated into a single SQL statement that the database plans and executes, and the intermediate rows never cross the network unless you ask for them.

The audience is the R analyst or statistician who has been handed a warehouse or a production database and still wants to work in the tidyverse grammar. The alternative to dbplyr is not SQL in the abstract, it is pulling the table into R with dbGetQuery or a read.csv and then analysing it locally, which works until the table is too large or too slow to move.

The project also points at its own documentation for the reasoning rather than repeating it. The README sends you to the vignette named sql for why you might use dbplyr instead of writing SQL, and to two further vignettes, translation-verb and translation-function, for the details of how the translation works. That second pair is the important one, and it is the subject of a section below.

Laziness is what keeps the data in the database

The mechanism that matters is that dplyr calls against a database are evaluated lazily. The README states that all dplyr calls are evaluated lazily, generating SQL that is only sent to the database when you request the data.

The example in the README makes the sequence visible.

r
# lazily generates query
summary <- mtcars2 |> 
  group_by(cyl) |> 
  summarise(mpg = mean(mpg, na.rm = TRUE)) |> 
  arrange(desc(mpg))

# see query
summary |> show_query()

Nothing has been sent to the database at that point. The pipeline has been turned into a description of a query, and show_query() prints what that description looks like once it is rendered as SQL.

code
#> <SQL>
#> SELECT `cyl`, AVG(`mpg`) AS `mpg`
#> FROM `mtcars`
#> GROUP BY `cyl`
#> ORDER BY `mpg` DESC

Only when you collect the result does the statement go to the database and the rows come back, as a tibble.

r
# execute query and retrieve results
summary |> collect()

This has a consequence that shows up in the printed output of a lazy table. When you print a remote table, the header shows the number of columns and a row count that is not yet known, because the count is part of the query that has not run. The README output for mtcars2 shows the query as two question marks followed by eleven columns, along with the database it is pointed at. That question mark is the honest answer at that moment, and it is the first thing that tells you the data is still on the server side.

You do not load dbplyr, and DBI owns the connection

Two details in the README save a lot of confusion for newcomers.

The first is that you do not actually need to load dbplyr with library(dbplyr), because dplyr loads it for you when it sees you working with a database. So a script that uses dbplyr contains no dbplyr call at all, which is confusing only if you expect to see the package named in the code.

The second is that database connections are coordinated by the DBI package, and the README points at the DBI site for that. The example creates an in-memory SQLite connection and copies a dataset into it.

r
library(dplyr, warn.conflicts = FALSE)

con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
copy_to(con, mtcars)

The important structural point is that dbplyr does not have a connection concept of its own. It sits on top of DBI, so any database DBI can talk to is available, and the database-specific details are handled by the DBI driver rather than by dbplyr. A table is then retrieved with tbl(), which the README ties to the help page for tbl_dbi, and printing it just retrieves the first few rows.

The in-memory SQLite database in the example is a testing convenience rather than a recommendation for production use, but it is a genuinely useful one, because it lets you develop a pipeline on a handful of rows and then point the same code at the real table. That is the workflow the package is built for: the data frame in development, the database in production, the same verbs in both.

Installing from CRAN, from tidyverse, or from GitHub

There are three documented ways to get dbplyr, and they differ in what else you get.

r
# The easiest way to get dbplyr is to install the whole tidyverse:
install.packages("tidyverse")

# Alternatively, install just dbplyr:
install.packages("dbplyr")

# Or the development version from GitHub:
# install.packages("pak")
pak::pak("tidyverse/dbplyr")

The first installs the whole tidyverse, which is the path the README calls the easiest. The second installs only dbplyr, which is what you want if you already have dplyr and the other packages you need. The third uses pak to install the development version straight from GitHub, and the commented line shows that pak itself has to be installed first.

One note on where the package lives. The README is generated from an Rmd file, and the badge points at the CRAN page for the package, so dbplyr is published on CRAN and installs with the ordinary install.packages call rather than from a GitHub remote.

The consequence of dbplyr being a CRAN package is the upgrade question. dbplyr is a backend for dplyr, and dplyr is a dependency of a large amount of R code in the wild, so a dbplyr release that changes the generated SQL can change results in code that never mentioned dbplyr. The repository layout shows how the project guards against that: there is a revdep/ directory, which is where reverse dependency checks live, and the package is checked against its downstream dependents before a release goes out.

The repository shows a package with a maintenance discipline

The top level of the repository is a standard R package, and the extra entries are the ones that tell you how the project is run.

The core is DESCRIPTION, NAMESPACE, R/, man/, inst/, tests/ and vignettes/, with NEWS.md holding the changelog and cran-comments.md the notes that go to CRAN with a submission. dbplyr.Rproj is the RStudio project file, and _pkgdown.yml and pkgdown/ are the documentation site configuration, which is why the package has its own site at dbplyr.tidyverse.org.

Then there are the files that a maintainer uses rather than a user. air.toml is the configuration for the formatter the project uses, .covrignore and codecov.yml configure test coverage measurement, .Rbuildignore keeps files out of the built package, and .devcontainer/, .vscode/ and .claude/ are the editor and agent environments the contributors work in. AGENTS.md at the root is the instruction file for those tools, and the README points readers there for detailed information rather than documenting everything itself.

The research/ directory is the most interesting of the set for a user, because it holds the exploratory work behind the translation rules, and the vignettes/ directory holds the two translation guides the README links to, one per verb and one per function.

On upkeep, the picture is straightforward. The repository is not archived, the last push was on 2026-06-17, and that day also carried the v2.6.0 release, after v2.5.2 from 2026-05-14 and v2.5.1 from 2025-09-10. The licence field on the repository is recorded as a custom licence that GitHub cannot classify, with both a LICENSE and a LICENSE.md file at the root, and the README notes that the project is released with a Contributor Code of Conduct.

Where the translation stops being exact

The limitation is intrinsic to the idea, and the project is honest enough to point at it with two vignettes dedicated to the details of SQL translation.

Every dplyr verb has to be expressed in SQL, and every function you call inside summarise or mutate has to be translated too. Where the two languages agree, the translation is invisible. Where they disagree, you get something that is nearly what you wrote, and the difference between nearly and exactly is where results change. Rounding, string handling, type coercion, integer division, the treatment of missing values: each of these has a database specific answer, and the translation has to pick one. The existence of vignettes named translation-verb and translation-function is a good sign about how seriously that is taken.

The second limitation is expressiveness. dbplyr generates the SQL, so the query shape is the one the translator produces. When you need a common table expression, a window function, a recursive query, a lateral join or a hand written hint, you are outside the vocabulary of the pipeline, and the usual escape is to drop to a lower level, which means writing the SQL yourself and losing the single codebase property.

The third is that laziness cuts both ways. A pipeline that is never collected costs nothing, but a pipeline that is collected too early, for example to peek at an intermediate result, will pull rows over the network and can quietly change the performance profile you were trying to protect.

The real alternative is to write the SQL. A DBI connection gives you dbGetQuery and dbSendQuery, and if you use them, you keep the database as the place where the work happens, with the full expressiveness of the engine and no translation layer between you and it. The difference in approach is ownership of the query. dbplyr is the better answer when the analysis is exploratory, when the same verbs need to run against both a local frame and a remote table, and when the reviewer understands dplyr but not your SQL. Hand written SQL is the better answer when the query is the product, when it needs to be tuned, reviewed and explained on its own terms, and when the translation rules would be an obstacle rather than a convenience.

Editorial conclusion

dbplyr fits an R team that wants one analysis codebase to run against a local data frame in development and a warehouse table in production, and it does not fit work that needs window functions, common table expressions or hand tuned query plans. Before you commit a pipeline to it, run show_query() on every stage and read the SQL it produced, because the translation is close but not identical to the dplyr semantics, and check the vignettes on translation that the README points you to. The last push was on 2026-06-17, the newest release is v2.6.0 from the same day, and the package ships under a licence the repository records as custom.

Frequently asked questions

How do I install dbplyr?

The README gives three options: install.packages("tidyverse") for the whole collection, install.packages("dbplyr") for just this package, or pak::pak("tidyverse/dbplyr") for the development version from GitHub, which requires pak to be installed first.

Do I need to call library(dbplyr) in my script?

No. The README says you do not actually need to load dbplyr with library(dbplyr), because dplyr loads it for you when it sees you working with a database. Database connections themselves are coordinated by the DBI package.

How do I see the SQL that dbplyr generates?

Call show_query() on the lazy result, as the README example does with summary |> show_query(). The generated statement is printed as SQL, and the query is only sent to the database when you collect the result.

When should I not use dbplyr?

When the query needs common table expressions, window functions or other constructs the translation does not produce, and when the SQL itself is the deliverable that has to be tuned and reviewed. In that case writing the query directly with DBI gives you the full expressiveness of the engine.

Is dbplyr still being updated?

The repository is not archived and the last push was on 2026-06-17, the same day as the v2.6.0 release, which followed v2.5.2 from 2026-05-14 and v2.5.1 from 2025-09-10. The package also carries a revdep directory, where reverse dependency checks are run before a release.

Official sources

  1. Issues
  2. Project website
  3. README
  4. Releases
  5. tidyverse/dbplyr on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tidyverse-dbplyr.svg)](https://hysenlabs.com/projects/tidyverse-dbplyr)

Was this useful?