An R script that reads a 4 GB CSV, groups by three columns and writes a summary should not take longer than your coffee break — but the same job can run in seconds or in minutes depending on which data library you load. That difference is not academic. It decides whether an analyst iterates five times an hour or twice a day, and it decides whether a laptop with 16 GB of RAM can hold the working set at all.
Three packages cover almost all of modern R data manipulation: data.table, dplyr and tidytable. They share a language, they do not share a philosophy, and picking the wrong one for a codebase costs months of refactoring later. Here is how they compare in 2026 — with syntax you can paste, memory behaviour you can measure, and the migration traps that actually bite.
TL;DR — Quick Verdict
Pick data.table when the data is large, the job is repeated, or memory is tight. Its DT[i, j, by] syntax is terse, it modifies data in place, and it is the only one of the three with built-in multi-threading.
Pick dplyr when the codebase is shared and readability wins. It is the lingua franca of R, it has the largest ecosystem of compatible packages, and it talks to databases through dbplyr — something the other two cannot do at all.
Pick tidytable when you want dplyr’s verbs but data.table’s engine and no dependency on tibble internals. Just know it returns its own class, so “drop-in” is approximately true rather than literally true.
The 2026 Status Check
Live GitHub figures, pulled 2026-10-15:
| data.table | dplyr | tidytable | |
|---|---|---|---|
| Repo | Rdatatable/data.table | tidyverse/dplyr | markfairbanks/tidytable |
| Stars | 3,927 | 5,073 | 475 |
| Last commit | 2026-10-14 | 2026-06-02 | 2026-07-23 |
| Install | install.packages("data.table") | install.packages("dplyr") | install.packages("tidytable") |
| Core syntax | DT[i, j, by] | verbs + pipe | verbs + pipe |
| Engine | C, in-place, multi-threaded | C++ via vctrs, copy-on-modify | data.table underneath |
| Returns | data.table (a data.frame) | tibble | tidytable |
| Third-party deps | none | tibble, vctrs, tidyselect, generics | data.table, vctrs, tidyselect, rlang |
| Multi-threaded grouping | Yes | No | Yes (inherited) |
| Modifies input by reference | Yes (:=) | No | No |
| Database backend | No | Yes (dbplyr) | No |
Two rows matter more than the star counts. Reference semantics means data.table can update a column without copying the whole table — fast, memory-efficient, and a genuine footgun in functions. Database backends are dplyr-only: if your data lives in Postgres or a warehouse, dplyr pushes the computation down, while data.table must pull everything into memory first.
Decision Matrix: Pick in 10 Seconds
| Your situation | Use | Why |
|---|---|---|
| 10M+ rows, grouped aggregation, repeated runs | data.table | In-place ops plus multi-threading avoid copies |
| Shared analysis code read by many people | dplyr | Verb-based syntax is self-documenting |
| Data sits in a SQL database or warehouse | dplyr | dbplyr translates verbs into SQL |
| Tidy syntax but data.table performance | tidytable | Same verbs, data.table engine, no tibble |
| Memory-constrained machine, wide tables | data.table | := avoids duplicating columns |
| Strict data.frame contract with libraries | dplyr or data.table | tidytable returns its own class by default |
| Time-series with rolling windows | data.table | roll, frollmean, non-equi joins built in |
| You are learning R in 2026 | dplyr first | Transferable vocabulary, gentler errors |
data.table — Maximum Speed, Minimum Abstraction
data.table compresses query logic into a single bracket. The mnemonic is FROM[WHERE, SELECT, GROUP BY], and the README states it exactly that way:
| |
Reading data is where the package earns its reputation. fread is a parallel CSV parser that guesses types and separators:
| |
:= is the pivot: it adds or replaces columns by reference, so DT itself changes. That is why data.table handles wide tables on modest hardware — you are not allocating a second copy on every transformation.
![data.table R package logo The data.table logo, the package built around DT[i, j, by] syntax](/img/screenshots/r-datatable-logo.jpg)
dplyr — The Grammar Everyone Reads
dplyr’s design bet was that a small vocabulary of verbs beats a compact but cryptic operator. The code reads like a sentence, and that is precisely why it dominates collaborative R:
| |
Two details are easy to miss. .by replaces group_by() for one-shot aggregations and avoids leaving grouped state behind on the object. And the native pipe |> (R 4.1+) means you no longer need magrittr for new code — the tidyverse still exports %>%, but mixing both in one file is a readability tax.
The real differentiator is untouched by any benchmark table: dbplyr. The same verbs can target a remote database, and dplyr compiles them to SQL instead of downloading rows:
| |

dplyr’s cost is memory and speed. Copy-on-modify means each mutate on a large frame can duplicate data, and grouping is single-threaded. On multi-gigabyte frames that shows up as the classic “cannot allocate vector of size” error.
tidytable — Tidy Syntax on the data.table Engine
tidytable exists because plenty of teams want both halves: dplyr’s verbs and data.table’s speed. It replicates tidyverse syntax and dispatches to data.table underneath:
| |
Because the engine is data.table, grouped aggregation gets threading and in-place execution — but with copy semantics on the surface, so you do not inherit :=’s spooky action at a distance. Objects created with tidytable() or returned from verbs are of class tidytable, which behaves like a data.frame in most contexts but is not one. Code that calls is.data.frame() or hands results to packages with strict data.frame expectations needs as.data.frame().
That class boundary is the honest caveat: tidytable is a syntax-compatible reimplementation, not a linker-level drop-in. Not every dplyr or tidyr function is implemented, so check the package’s function reference before migrating a large codebase, and expect the odd behaviour difference rather than assuming parity.

Performance and Memory: What Actually Shows Up
Rather than repeat second-hand numbers, measure on your own data. This benchmark is short enough to run in any session:
| |
Three patterns hold across hardware and package versions:
- Grouped aggregation: data.table and tidytable typically finish in a fraction of dplyr’s time on multi-million-row frames, because grouping is multi-threaded and nothing is copied.
- Column updates:
DT[, new := f(old)]reuses existing memory, while a dplyrmutateon a 50-column frame can duplicate the object. - CSV import:
freadis the fastest of the three for large flat files; baseread.csvis not close, and dplyr’sreadrsits between them.
Two fairness notes. First, dplyr’s optimisations (vctrs-backed mutate, summarise(.by)) have improved materially, so old benchmark blog posts understate its current speed. Second, reading a Parquet or Arrow file into memory is often a bigger win than any package swap — if your bottleneck is I/O, fix that first. Our Python dataframe comparison of Polars, Vaex and data.table-style libraries covers that side of the problem.
Pitfalls and Migration Notes
Reference semantics will surprise someone. A function that does DT[, x := x * 2] modifies the caller’s object. Inside package code or a function, either document it loudly or start with DT <- copy(DT). Silent mutation of shared data is the most common bug reported by newcomers to data.table.
Programmatic column names need the parentheses. In a loop or function, DT[, col := 1] creates a literal column named col. Use DT[, (colname) := 1] and DT[colname == ..target]. This differs from dplyr, where {{ }} and .data[[ ]] handle indirection — a rewrite that is mechanical but must be done deliberately.
Grouped state lingers in dplyr. group_by() persists on the object, so a later summarise() silently regroups. Prefer .by for single aggregations and call ungroup() when you do use group_by().
setkey() sorts your data in place. That is useful for non-equi and rolling joins, but it is not free and not silent — if row order matters downstream, sort explicitly at the end.
Third-party package compatibility is a real migration cost. Many R packages accept data.frame or tibble. data.table inherits from data.frame and tidytable mostly behaves like one, but strict consumers can still surprise you. If your pipeline feeds a modelling or reporting layer, keep the conversion at the boundary.
For teams running R in production, the deployment layer matters as much as the library choice — our Shiny Server vs ShinyProxy vs Posit Connect deployment guide covers the hosting tradeoffs. And if your workload is trial data with regulatory expectations, the open-source clinical biostatistics stack around gtsummary, finalfit and arsenal shows why auditable output sometimes outranks raw speed.
FAQ
Is data.table faster than dplyr in 2026? For grouped aggregation, column updates and large CSV imports, generally yes — data.table is multi-threaded, avoids copies and parses CSV in parallel. The honest answer is that dplyr narrowed the gap with vctrs-backed operations, so run the bundled benchmark on your own data before rewriting a working pipeline.
Can I use dplyr syntax on a data.table? Yes, via the dtplyr package: it accepts dplyr verbs and translates them into data.table operations, so you get familiar syntax with much of the speed. It will not match hand-written data.table exactly, and some verbs are unsupported or slower than the native equivalent.
Is tidytable a drop-in replacement for dplyr?
Almost. The verb names and pipeline shape match, and the engine is data.table, but objects are class tidytable rather than tibble or data.frame, and not every dplyr or tidyr function is implemented. Treat it as a compatible reimplementation and check the function reference before migrating.
Does data.table support multi-threading, and can I control it?
Yes. Many operations — fread, grouping, joins, sorting — are multi-threaded by default. Control it with setDTthreads(4) or setDTthreads(0) to use all cores, and remember that some environments restrict thread counts, so pin the value explicitly in production scripts.
Which should a beginner learn first? Learn dplyr first: the vocabulary transfers to other languages, the errors are more legible, and the same verbs work against databases. Add data.table when you hit a size or runtime wall — at that point its syntax has a concrete motivation rather than being trivia.
Can data.table query a database directly? No. data.table works on in-memory data, so a database workflow means reading the table in full (or pre-aggregating in SQL yourself). If your data lives in Postgres, a warehouse or a lakehouse, dplyr plus dbplyr is the practical choice.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com