| name | dm-relational |
| description | Relational data modeling with {dm} package - create, visualize, and manipulate multi-table data models with primary/foreign keys. Use when mentions "dm package", "pacote dm", "relational data", "dados relacionais", "primary key", "chave primária", "foreign key", "chave estrangeira", "data model", "modelo de dados", "multi-table", "múltiplas tabelas", "tabelas relacionadas", "relate tables", "relacionar tabelas", "database schema", "esquema de banco", "esquema de dados", "dm_from_data_frames", "dm_add_pk", "dm_add_fk", "dm_draw", "dm_flatten", "dm_filter", "dm_zoom_to", "visualize schema", "visualizar esquema", "create data model", "criar modelo", "model relational data", "modelar dados relacionais", "work with related data", "trabalhar com dados relacionados", "multiple related tables", "várias tabelas relacionadas", or working with related data frames, relational databases, or referential integrity in R. |
| version | 1.1.0 |
| user-invocable | false |
| allowed-tools | Read, Grep, Glob |
Relational Data Modeling with {dm}
Best practices for working with the {dm} package for relational data structures in R
Core Principles
- Think relationally - Model data with multiple connected tables, not single wide tables
- Define keys explicitly - Always specify primary and foreign keys to establish relationships
- Visualize early - Use
dm_draw() frequently to understand your data model structure
- Validate constraints - Check data integrity with
dm_examine_constraints() before analysis
- Leverage cascading operations - Let dm propagate filters and joins through relationships
What is dm?
The dm package bridges individual data frames and relational databases, enabling you to:
- Work with multiple related tables as a single coherent object
- Automatically track relationships via primary and foreign keys
- Scale from in-memory data frames to billion-row database tables
- Perform intelligent joins that leverage existing relationships
- Maintain data integrity through constraint validation
Philosophy
"Use it for data analysis today. Build data models tomorrow. Deploy the data models to your organization's RDBMS the day after."
The dm package serves as a bridge in data pipelines - start with local data frames for prototyping, evolve into structured models, and ultimately deploy to production databases. It functions as a named list of tables that works seamlessly with dplyr verbs while adding relational model features.
Creating dm Objects
From Data Frames
library(dm)
my_dm <- dm(
customers,
orders,
products
)
my_dm <- my_dm |>
dm_add_pk(customers, customer_id) |>
dm_add_pk(orders, order_id) |>
dm_add_pk(products, product_id)
my_dm <- my_dm |>
dm_add_fk(orders, customer_id, customers) |>
dm_add_fk(orders, product_id, products)
my_dm |> dm_draw()
From Databases
con <- DBI::dbConnect(RPostgres::Postgres(), ...)
my_dm <- dm_from_con(con)
my_dm <- dm_from_con(con, learn_keys = FALSE)
my_dm <- dm(
customers = tbl(con, "customers"),
orders = tbl(con, "orders")
)
my_dm <- my_dm |>
dm_add_pk(customers, customer_id) |>
dm_add_fk(orders, customer_id, customers)
Discovering Keys
my_dm |> dm_enum_pk_candidates(orders)
orders |> check_key(order_id)
my_dm |> dm_enum_fk_candidates(orders, customers)
my_dm |> check_subset(orders, customer_id, customers, customer_id)
Key Management
Primary Keys
my_dm <- my_dm |> dm_add_pk(table_name, key_column)
my_dm <- my_dm |> dm_add_pk(table_name, c(col1, col2))
my_dm |> dm_has_pk(table_name)
my_dm |> dm_get_pk(table_name)
my_dm |> dm_get_all_pks()
my_dm <- my_dm |> dm_rm_pk(table_name)
Unique Keys (UK)
Unique keys differ from primary keys:
- A table can have only one PK but unlimited UKs
- PKs support autoincrement; UKs do not
- When copying to database, PKs are set by default; UKs are ignored
- UKs are useful for delta load processes and documenting additional uniqueness constraints
my_dm <- my_dm |> dm_add_uk(table_name, unique_column)
my_dm <- my_dm |>
dm_add_uk(users, email) |>
dm_add_uk(users, username)
my_dm |> dm_get_all_uks()
my_dm <- my_dm |> dm_rm_uk(table_name, unique_column)
Foreign Keys
my_dm <- my_dm |>
dm_add_fk(
child_table,
child_column,
parent_table
)
my_dm <- my_dm |>
dm_add_fk(
child_table,
c(col1, col2),
parent_table
)
my_dm |> dm_has_fk(child_table, parent_table)
my_dm |> dm_get_all_fks()
my_dm <- my_dm |> dm_rm_fk(child_table, parent_table)
Visualization
my_dm |> dm_draw()
my_dm |> dm_draw(view_type = "keys_only")
my_dm |> dm_draw(view_type = "all")
my_dm |> dm_draw(view_type = "title_only")
my_dm <- my_dm |>
dm_set_colors(
maroon4 = flights,
orange = starts_with("air"),
lightblue = customers
)
my_dm |> dm_draw()
my_dm |> dm_get_colors()
dm_get_available_colors()
my_dm |> dm_gui()
Filtering with Cascading
Key Feature: Filters automatically propagate through foreign key relationships
flights_dm |>
dm_filter(airports, name == "John F Kennedy Intl") |>
dm_apply_filters()
flights_dm |>
dm_filter(carriers, name == "Delta Air Lines Inc.") |>
dm_filter(airports, city == "New York") |>
dm_apply_filters()
How Cascading Works
Filtering uses successive semi-joins along relationship paths:
"filtering semi-joins are successively performed along the paths from each of the filtered tables to the requested table, each join reducing the left-hand side tables of the joins to only those of their rows with key values that have corresponding values in key columns of the right-hand side tables"
For database-backed dm objects, filters generate optimized SQL with nested WHERE EXISTS clauses.
Important: The foreign key graph must be cycle-free for filtering to work.
Joining and Flattening
Flatten to Single Table
flights_dm |>
dm_flatten_to_tbl(flights, airlines, .join = left_join)
flights_dm |>
dm_flatten_to_tbl(.start = flights, .recursive = TRUE)
flights_dm |>
dm_flatten_to_tbl(.start = flights, .recursive = FALSE)
flights_dm |>
dm_flatten_to_tbl(flights, planes, .join = inner_join)
flights_dm |>
dm_flatten_to_tbl(flights, weather, .join = anti_join)
Advantages of dm Joins
- Automatically uses foreign key columns (no need to specify
by =)
- Handles column name conflicts with automatic renaming
- Cleaner code than manual dplyr joins
- Maintains referential integrity
Zoom Workflow
Use zoom to focus on one table while preserving relationships
zoomed_dm <- my_dm |>
dm_zoom_to(orders)
zoomed_dm <- zoomed_dm |>
mutate(total_price = quantity * unit_price) |>
filter(status == "completed")
my_dm <- zoomed_dm |> dm_update_zoomed()
my_dm <- zoomed_dm |> dm_insert_zoomed("completed_orders")
my_dm <- zoomed_dm |> dm_discard_zoomed()
When to Use Zoom
- Adding computed columns to tables
- Creating surrogate keys
- Building summary/aggregate tables
- Enriching tables with joins
- Resolving cyclic relationships
Supported Operations in Zoom
- Data transformation:
mutate(), transmute(), select(), relocate(), rename(), filter(), arrange(), slice(), distinct()
- Grouping:
summarise(), group_by(), ungroup()
- Joins:
left_join(), inner_join(), full_join(), right_join(), semi_join(), anti_join()
- tidyr:
unite(), separate()
Important: Key Tracking
Keys are tracked by column names - if you rename a key column, the relationship may be lost and need manual re-establishment with dm_add_pk() or dm_add_fk().
Database Operations
Copy to Database
con <- DBI::dbConnect(...)
copy_dm_to(con, my_dm, temporary = FALSE)
copy_dm_to(
con,
my_dm,
table_names = ~ paste0("project_", .x)
)
copy_dm_to(con, my_dm, temporary = TRUE)
Materialize Computed Tables
my_dm |>
dm_zoom_to(orders) |>
filter(year == 2023) |>
compute() |>
dm_insert_zoomed("orders_2023")
Collect from Database
local_dm <- my_dm |> collect()
orders_local <- my_dm |> pull_tbl(orders) |> collect()
Row Operations (Insert/Update/Delete)
All dm_rows_*() functions follow the same workflow:
- Create a changeset dm with rows to modify
- Copy changeset to same database as destination
- Simulate first with
in_place = FALSE (default)
- Execute with
in_place = TRUE to persist changes
dm_rows_insert(
target_dm,
changeset_dm,
in_place = FALSE
)
dm_rows_update(
target_dm,
changeset_dm,
in_place = FALSE
)
dm_rows_delete(
target_dm,
changeset_dm,
in_place = FALSE
)
Important: You are responsible for setting database transactions to ensure integrity across multiple tables.
Validation and Constraints
my_dm |> dm_examine_constraints()
my_dm |> dm_examine_cardinalities()
check_cardinality_0_n(parent_table, parent_col, child_table, child_col)
check_cardinality_1_n(parent_table, parent_col, child_table, child_col)
check_cardinality_0_1(parent_table, parent_col, child_table, child_col)
check_cardinality_1_1(parent_table, parent_col, child_table, child_col)
examine_cardinality(parent_table, parent_col, child_table, child_col)
Table Management
my_dm <- my_dm |> dm_add_tbl(new_table_df)
my_dm <- my_dm |> dm_rm_tbl(table_name)
my_dm <- my_dm |> dm_rename_tbl(new_name = old_name)
my_dm <- my_dm |> dm_select_tbl(customers, orders)
customers_tbl <- my_dm |> pull_tbl(customers)
keyed_tbl <- my_dm |> pull_tbl(customers, keyed = TRUE)
my_dm <- my_dm |>
dm_mutate_tbl(orders = orders |> filter(year >= 2020))
my_dm |> dm_nrow()
my_dm |> dm_get_tables()
my_dm |> dm_get_con()
dm_deconstruct(my_dm)
Column Operations
my_dm <- my_dm |>
dm_select(orders, order_id, customer_id, total) |>
dm_select(customers, customer_id, name)
my_dm <- my_dm |>
dm_rename(orders, order_date = date)
my_dm <- my_dm |>
dm_set_table_description(
orders = "Customer orders with line items",
customers = "Customer master data"
)
Normalization
decomposed <- orders |>
decompose_table(
new_id_column,
customer_name,
customer_email
)
reunited <- reunite_parent_child(
child_table,
parent_table,
id_column
)
reunited <- reunite_parent_child_from_list(decomposed_list)
Best Practices
Model Design
- Start simple - Begin with core tables and relationships
- Normalize appropriately - Avoid redundant data, but don't over-normalize
- Document relationships - Use
dm_draw() as living documentation
- Check cardinalities - Understand if relationships are 1:1, 1:many, many:many
Development Workflow
my_dm <- dm(...)
my_dm <- my_dm |>
dm_add_pk(...) |>
dm_add_fk(...)
my_dm |> dm_draw()
my_dm |> dm_examine_constraints()
my_dm |>
dm_filter(...) |>
dm_flatten_to_tbl(...)
Database Connection Management
Important: Database connections don't serialize. Always wrap dm creation in functions:
get_my_dm <- function() {
con <- DBI::dbConnect(...)
dm_from_con(con)
}
my_dm <- get_my_dm()
my_dm <- dm_from_con(con)
saveRDS(my_dm, "model.rds")
Performance Tips
- Use
compute() for expensive intermediate results in database queries
- Check
dm_nrow() before calling collect() on large tables
- Filter early and often before flattening
- Use lazy evaluation - only materialize when necessary
- For large databases, work with filtered subsets
Memory Management
my_dm |> dm_nrow()
small_dm <- my_dm |>
dm_filter(orders, year == 2023) |>
dm_apply_filters()
small_dm |> collect()
Common Patterns
Pattern 1: Load, Validate, Visualize
my_dm <- dm_from_con(con)
constraints <- my_dm |> dm_examine_constraints()
if (any(!constraints$is_valid)) {
warning("Constraint violations detected!")
print(constraints |> filter(!is_valid))
}
my_dm |> dm_draw()
Pattern 2: Build from Scratch
my_dm <- dm(
customers = customers_df,
orders = orders_df,
products = products_df
)
my_dm <- my_dm |>
dm_add_pk(customers, customer_id) |>
dm_add_pk(orders, order_id) |>
dm_add_pk(products, product_id) |>
dm_add_fk(orders, customer_id, customers) |>
dm_add_fk(orders, product_id, products)
my_dm |> dm_examine_constraints()
Pattern 3: Filter and Flatten
filtered_dm <- my_dm |>
dm_filter(customers, country == "USA") |>
dm_filter(orders, year == 2023) |>
dm_apply_filters()
analysis_tbl <- filtered_dm |>
dm_flatten_to_tbl(orders, .recursive = TRUE)
Pattern 4: Zoom for Derived Tables
my_dm <- my_dm |>
dm_zoom_to(orders) |>
group_by(customer_id) |>
summarise(
total_orders = n(),
total_spent = sum(amount)
) |>
ungroup() |>
dm_insert_zoomed("customer_summary")
my_dm <- my_dm |>
dm_add_pk(customer_summary, customer_id) |>
dm_add_fk(customer_summary, customer_id, customers)
Pattern 5: Database Deployment
local_dm <- dm(table1, table2, table3) |>
dm_add_pk(...) |>
dm_add_fk(...)
local_dm |> dm_examine_constraints()
con <- DBI::dbConnect(...)
copy_dm_to(
con,
local_dm,
temporary = FALSE,
table_names = ~ paste0("prod_", .x)
)
Antipatterns to Avoid
❌ Don't: Skip Key Definition
my_dm <- dm(customers, orders)
my_dm <- dm(customers, orders) |>
dm_add_pk(customers, customer_id) |>
dm_add_pk(orders, order_id) |>
dm_add_fk(orders, customer_id, customers)
❌ Don't: Forget to Validate
my_dm <- my_dm |> dm_add_pk(orders, order_id)
if (orders |> check_key(order_id)) {
my_dm <- my_dm |> dm_add_pk(orders, order_id)
}
❌ Don't: Flatten Everything Immediately
wide_table <- my_dm |> dm_flatten_to_tbl(orders, .recursive = TRUE)
analysis_tbl <- my_dm |>
dm_filter(orders, year == 2023) |>
dm_flatten_to_tbl(orders, customers)
❌ Don't: Collect Large Database Tables
my_dm <- dm_from_con(con) |> collect()
my_dm <- dm_from_con(con) |>
dm_filter(orders, year == 2023) |>
dm_apply_filters()
small_tbl <- my_dm |> pull_tbl(orders) |> collect()
❌ Don't: Ignore Cyclic Relationships
my_dm <- my_dm |>
dm_add_fk(table_a, id_b, table_b) |>
dm_add_fk(table_b, id_a, table_a)
my_dm <- my_dm |>
dm_add_tbl(table_b_copy = table_b) |>
dm_add_fk(table_a, id_b, table_b) |>
dm_add_fk(table_b_copy, id_a, table_a)
Integration with tidyverse
The dm package works seamlessly with tidyverse:
library(dplyr)
library(tidyr)
library(ggplot2)
plot_data <- my_dm |>
dm_filter(orders, year >= 2020) |>
dm_flatten_to_tbl(orders, customers, products) |>
collect()
plot_data |>
ggplot(aes(x = order_date, y = amount)) +
geom_line()
result <- my_dm |>
dm_zoom_to(orders) |>
filter(status == "completed") |>
dm_update_zoomed() |>
dm_flatten_to_tbl(orders, customers)
Sample Data
dm_nycflights13()
dm_financial()
dm_pixarfilms()
flights_dm <- dm_nycflights13()
flights_dm |> dm_draw()
Code Generation and Utilities
my_dm |> dm_paste()
my_dm |> dm_ptype()
my_dm |> dm_sql()
my_dm |> dm_ddl_pre()
my_dm |> dm_dml_load()
Schema Management (Database)
db_schema_list(con)
db_schema_create(con, "my_schema")
db_schema_drop(con, "my_schema")
Keyed Tables Workflow (Experimental)
An alternative to zoom for working with individual tables:
customers <- my_dm |> pull_tbl(customers, keyed = TRUE)
orders <- my_dm |> pull_tbl(orders, keyed = TRUE)
customers_updated <- customers |>
mutate(full_name = paste(first_name, last_name))
orders_filtered <- orders |>
filter(year >= 2023)
enriched <- orders_filtered |>
left_join(customers_updated)
my_dm <- dm(
customers = customers_updated,
orders = orders_filtered
)
Resources
Quick Reference
| Task | Function |
|---|
| Create dm | dm(), dm_from_con() |
| Add keys | dm_add_pk(), dm_add_fk(), dm_add_uk() |
| Visualize | dm_draw(), dm_gui() |
| Validate | dm_examine_constraints() |
| Filter | dm_filter() |
| Flatten | dm_flatten_to_tbl() |
| Zoom | dm_zoom_to(), dm_insert_zoomed() |
| Copy to DB | copy_dm_to() |
| Modify rows | dm_rows_insert(), dm_rows_update() |
| Collect | collect() |
| Get table | pull_tbl() |
| Generate code | dm_paste(), dm_deconstruct() |
Use this skill when working with multiple related data frames, connecting to databases with foreign keys, or building relational data pipelines in R.