---
title: Databases
author: David Kane and Mann Talati
tutorial:
  id: databases
output:
  learnr::tutorial:
    progressive: yes
    allow_skip: yes
runtime: shiny_prerendered
description: 'Tutorial for Chapter 21: Databases'
---

```{r setup, include = FALSE}
library(learnr)
library(tutorial.helpers)

library(tidyverse)
library(nycflights13)
library(DBI)
library(dbplyr)
library(RSQLite)
library(RPostgres)

knitr::opts_chunk$set(echo = FALSE)
options(tutorial.exercise.timelimit = 60, 
        tutorial.storage = "local") 

con <- dbConnect(RSQLite::SQLite(), dbname = "myDB.sqlite") 
dbWriteTable(con, "mpg", ggplot2::mpg, overwrite = TRUE)
dbWriteTable(con, "diamonds", ggplot2::diamonds, overwrite = TRUE)
mpg_sql <- "
  SELECT manufacturer, model, year, displ, class 
  FROM mpg 
  WHERE displ > 1.8
"
mpg_db <- tbl(con, "mpg")
diamonds_db <- tbl(con, "diamonds")
dbplyr::copy_nycflights13(con)
flights_db <- tbl(con, "flights")
planes_db <- tbl(con, "planes")

summarize_query <- function(df, ...) {
  df |> 
    summarize(...) |> 
    show_query()
}
mutate_query <- function(df, ...) {
  df |> 
    mutate(..., .keep = "none") |> 
    show_query()
}
```

```{r copy-code-chunk, child = system.file("child_documents/copy_button.Rmd", package = "tutorial.helpers")}
```

```{r info-section, child = system.file("child_documents/info_section.Rmd", package = "tutorial.helpers")}
```




## Introduction
### 

*This is a ROUGH, UNFINISHED, draft.*

This tutorial covers [Chapter 21: Databases](https://r4ds.hadley.nz/databases.html) from [*R for Data Science (2e)*](https://r4ds.hadley.nz/) by Hadley Wickham, Mine Çetinkaya-Rundel, and Garrett Grolemund. You will learn how to use [**DBI**](https://dbi.r-dbi.org/) and the [**dbplyr**](https://dbplyr.tidyverse.org/) packages to connect to databases and execute SQL.

## Connecting to a database
###

To first start working a database we need to connect to one. You’ll always use `DBI` (database interface) because it provides a set of generic functions that connect to the database, upload data, run SQL queries, etc. You’ll also use a package tailored for the `DBMS` you’re connecting to. This package translates the generic `DBI` commands into the specifics needed for a given `DBMS`. There’s usually one package for each `DBMS`, e.g. `RPostgres` for PostgreSQL and `RMariaDB` for MySQL.

### Exercise 1

Install and run the `library()` on `tidyverse`, `DBI`, and `tibble`.

```{r connecting-to-a-data-1, exercise = TRUE}

```

```{r connecting-to-a-data-1-hint, eval = FALSE}
library(...)
...(DBI)
library(...)
```

###

At the simplest level, you can think about a database as a collection of data frames, called tables in database terminology. Like a data frame, a database table is a collection of named columns, where every value in the column is the same type. Databases are run by database management systems (DBMS’s for short), which come in three basic forms: Client-server, Cloud, and In-process.

### Exercise 2

Set `dbConnect(RSQLite::SQLite())` to the `con` variable.

```{r connecting-to-a-data-2, exercise = TRUE}

```

```{r connecting-to-a-data-2-hint, eval = FALSE}
... <- dbConnect(RSQLite::SQLite()) 
```

###

`RSQLite` is a high-performance database that’s designed very much for the needs of a data scientist. We use it here because it’s very easy to get started with, but it’s also capable of handling gigabytes of data with great speed. 

### Exercise 3

Copy the code from above. Now within the `con` variable, add `dbname` and set it to `"myDB.sqlite"`.

```{r connecting-to-a-data-3, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r connecting-to-a-data-3-hint, eval = FALSE}
... <- dbConnect(RSQLite::SQLite(), dbname = "...") 
```

###

We want to use a **Client-server** DBMS’s because they run on a powerful central server, which you connect from your computer (the client). They are great for sharing data with multiple people in an organization. Popular client-server DBMS’s include `PostgreSQL`, `MariaDB`, `SQL Server`, and `Oracle`.

### Exercise 4

Let's write the `mpg` data to the database we just created. Use `dbWriteTable()` and within it put the arguments `con`, `"mpg"`, and `ggplot2::mpg`. This should not result in a tibble.

```{r connecting-to-a-data-4, exercise = TRUE}

```

```{r connecting-to-a-data-4-hint, eval = FALSE}
dbWriteTable(..., "mpg", ...)
```

###

When we use `dbWriteTable()` we need to supply three arguments: a database connection, the name of the table to create in the database, and a data frame of data.

### Exercise 5

Let's write the `diamonds` data to the database we just created. Use `dbWriteTable()` and within it put the arguments `con`, `"diamonds"`, and `ggplot2::diamonds`. This should not result in a tibble.

```{r connecting-to-a-data-5, exercise = TRUE}

```

```{r connecting-to-a-data-5-hint, eval = FALSE}
...(con, "...", ggplot2::diamonds)
```

###

We use the packages tailored for the **DBMS** we’re connecting to. This package translates the generic DBI commands into the specifics needed for a given **DBMS**. There’s usually one package for each **DBMS**, e.g. `RPostgres` for PostgreSQL and `RMariaDB` for MySQL.

### Exercise 6

Run `dbListTables()` on `con`.

```{r connecting-to-a-data-6, exercise = TRUE}

```

```{r connecting-to-a-data-6-hint, eval = FALSE}
dbListTables(...)
```

###

You can check that the data is loaded correctly by using the `dbListTables()` function that lists all tables in the database.

### Exercise 7

Let's use another DBI function: `dbReadTable()`. Start a pipe with `con` and use `dbReadTable()` on `"mpg"`.

```{r connecting-to-a-data-7, exercise = TRUE}

```

```{r connecting-to-a-data-7-hint, eval = FALSE}
con |>
  dbReadTable(...)
```

###

`dbReadTable()` returns a `data.frame`. 

### Exercise 8

Continue the pipe with `as_tibble()` which will return our data in a nice tibble format.

```{r connecting-to-a-data-8, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r connecting-to-a-data-8-hint, eval = FALSE}
con |>
  dbReadTable("mpg")|>
  ...
```

###

To convert data into a tibble we use `as_tibble()` which allows it to print nicely.

### Exercise 9

Now let's use `SQL` to select specific variables from the database. Create a `mpg_sql` variable and set it equal to 
`"SELECT manufacturer, model, year, displ, class FROM mpg WHERE displ > 1.8"`

```{r connecting-to-a-data-9, exercise = TRUE}
```

```{r connecting-to-a-data-9-hint, eval = FALSE}
... <- "
  SELECT manufacturer, model, year, displ, class 
  FROM mpg 
  WHERE displ > 1.8
"
```

###

With the above variable, we want to select 5 specific variables, from the `mpg` dataset, with only values with `displ > 1.8`.

### Exercise 10

Type `as_tibble()`. Within `as_tibble()`, add`dbGetQuery()` with the arguments `con` and `mpg_sql`

```{r connecting-to-a-data-10, exercise = TRUE}

```

```{r connecting-to-a-data-10-hint, eval = FALSE}
as_tibble(dbGetQuery(...))
```

###

You can use `dbGetQuery()` to get the results of running a query on the database with `as_tibble()` we get the results in tibble format.

## **dbplyr** basics
###

Now that we’ve connected to a database and loaded up some data, we can start to learn about **dbplyr**. **dbplyr** is a dplyr backend, which means that you keep writing dplyr code but the backend executes it differently. In this, **dbplyr** translates to SQL; other backends include **dtplyr** which translates to **data.table**, and **multidplyr** which executes your code on multiple cores.

### Exercise 1

Create a variable `mpg_db` and set that to `tbl()` on the two arguments: `con` and `"mpg"`.

```{r dbplyr-basics-1, exercise = TRUE}

```

```{r dbplyr-basics-1-hint, eval = FALSE}
mpg_db <- tbl(...)
```

###

We use `tbl()` To create an object that represents a database table.

### Exercise 2

Start a new pipe with `mpg_db`. Pipe that into `filter()`. Within `filter()`, add the argument `displ > 1.8`.

```{r dbplyr-basics-2, exercise = TRUE}

```

```{r dbplyr-basics-2-hint, eval = FALSE}
mpg_db |>
  filter(...)
```

###

Similar to using `dbGetQuery()`, we can use the **dbplyr** package to select the same variables from the database that we created. This object is called *lazy*; when you use **dplyr** verbs on it, **dplyr** doesn’t do any work: it just records the sequence of operations that you want to perform and only performs them when needed.

### Exercise 3

Select the `manufacturer`, `model`, `displ`, and `year` through `drv` variables.

```{r dbplyr-basics-3, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r dbplyr-basics-3-hint, eval = FALSE}
mpg_db |>
  filter(displ > 1.8)|>
  select(..., year:drv)
```

###

This represents a database query because it prints the DBMS name at the top, and while it tells you the number of columns, it typically doesn’t know the number of rows. This is because finding the total number of rows usually requires executing the complete query, something we’re trying to avoid.

### Exercise 4

Continue the pipe by adding `show_query()`.

```{r dbplyr-basics-4, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r dbplyr-basics-4-hint, eval = FALSE}
mpg_db |>
  filter(displ > 1.8)|>
  select(manufacturer, model, displ, year:drv)|>
  ...
```

###

You can see the `SQL` code generated by the dplyr function `show_query()`. If you know `dplyr`, this is a great way to learn `SQL`! Write some **dplyr** code, get **dbplyr** to translate it to `SQL`, and then try to figure out how the two languages match up.

### Exercise 5

Take the pipe from above and remove `show_query()` and instead add `collect()` to the pipe.

```{r dbplyr-basics-5, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r dbplyr-basics-5-hint, eval = FALSE}
mpg_db |>
  filter(displ > 1.8)|>
  select(manufacturer, model, displ, year:drv)|>
  ...
```

###

To get all the data back into R, you call `collect()`. Behind the scenes, this generates the `SQL`, calls `dbGetQuery()` to get the data, then turns the result into a tibble.

## SQL basics
###

The rest of the chapter will teach you a little `SQL` through the lens of **dbplyr**. It’s a rather non-traditional introduction to `SQL` but we hope it will get you quickly up to speed with the basics. Luckily, if you understand **dplyr** you’re in a great place to quickly pick up `SQL` because so many of the concepts are the same.

### Exercise 1

Run `dbplyr::copy_nycflights13()` with `con` as the argument.

```{r sql-1, exercise = TRUE}

```

```{r sql-1-hint, eval = FALSE}
dbplyr::copy_nycflights13(...)
```

###

Here we are exploring the relationship between **dplyr** and `SQL` using the `nycflights13` package: flights and planes.

### Exercise 2

Type `flights_db` set it to `tbl()`. Within `tbl()` use the `con`, `"flights"` arguments. On the next line type `planes_db` and set it to `tbl()`. Within `tbl()` use the `con` and `"planes"` arguments.

```{r sql-2, exercise = TRUE}

```

```{r sql-2-hint, eval = FALSE}
flights_db <- tbl(..., ...)
... <- ...(con, "planes")
```

###

These datasets are easy to get into our learning database because **dbplyr** comes with a function that copies the tables from `nycflights13` to our database.

### Exercise 3

Start a pipe with `planes_db` and pipe it into `filter()`. Within `filter()`, add `manufacturer == "AIRBUS INDUSTRIE"` to only include the rows where the `manufacturer` is `"AIRBUS INDUSTRIE"`.

```{r sql-3, exercise = TRUE}

```

```{r sql-3-hint, eval = FALSE}
... |> 
  filter(... == "AIRBUS INDUSTRIE")
```

###

The top-level components of `SQL` are called **statements**. Common statements include `CREATE` for defining new tables, `INSERT` for adding data, and `SELECT` for retrieving data.

### Exercise 4

Continue the pipe with `arrange()`. Within `arrange()`, add `year`.

```{r sql-4, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-4-hint, eval = FALSE}
planes_db |> 
  filter(manufacturer == "AIRBUS INDUSTRIE")|> 
  arrange(...)
```

###

We like using `SELECT` statements, also called **queries**, because they are almost exclusively what you’ll use as a data scientist.

### Exercise 5

Continue the pipe with `show_query()`.

```{r sql-5, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-5-hint, eval = FALSE}
planes_db |> 
  filter(manufacturer == "AIRBUS INDUSTRIE")|> 
  arrange(year)|>
  ...()
```

###

A query is made up of **clauses**. There are five important clauses: `SELECT`, `FROM`, `WHERE`, `ORDER BY`, and `GROUP BY`. Every query must have the `SELECT` and `FROM` clauses and the simplest query is `SELECT * FROM` table, which selects all columns from the specified table.

## SQL with `SELECT`, `FROM`, & `GROUP BY`
###

In `SQL`, order matters: you must always write the clauses in the order `SELECT`, `FROM`, `WHERE`, `GROUP BY`, `ORDER BY`. Confusingly, this order doesn’t match how the clauses actually evaluated which is first `FROM`, then `WHERE`, `GROUP BY`, `SELECT`, and `ORDER BY`.

### Exercise 1

Let's use the `flights_db` variable from above. Create a pipe with `flights_db`. Pipe it into the `select()` function. Within `select()`, add the arguments `carrier`, `tailnum`, `origin`, `distance`, and `year`. Make sure to separate these arguments with commas.

```{r sql-select-from-grou-1, exercise = TRUE}

```

```{r sql-select-from-grou-1-hint, eval = FALSE}
flights_db |>
  select(carrier, ..., origin, ..., year)
```

###

`select()` has very direct translations to `SELECT` as they just affect where a column appears (if at all) along with its name.

### Exercise 2

Continue the pipe with `rename()`. Within `rename()` set `year_built` to `year` using a `=`

```{r sql-select-from-grou-2, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-select-from-grou-2-hint, eval = FALSE}
flights_db |>
  select(carrier, tailnum, origin, distance, year)|>
  rename(year_built = ...)
```

###

The `SELECT` clause is the workhorse of queries and performs the same job as `rename()`.

### Exercise 3

Continue the pipe with `show_query()`.

```{r sql-select-from-grou-3, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-select-from-grou-3-hint, eval = FALSE}
flights_db |>
  select(carrier, tailnum, origin, distance, year)|>
  rename(year_built = year)
  show_query()
```

###

This example also shows you how `SQL` does renaming. In `SQL` terminology renaming is called **aliasing** and is done with `AS`.

### Exercise 4

Start a new pipe with `mpg`. Pipe it into `summarize()` and set `avg_hwy` to the `mean()` of `hwy` and `na.rm = TRUE` using the code `avg_hwy = mean(hwy, na.rm= TRUE)`.

```{r sql-select-from-grou-4, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-select-from-grou-4-hint, eval = FALSE}
flights_db |>
  select(carrier, tailnum, origin, distance, year)|>
  rename(year_built = year)
  show_query()|>
  summarize(... = mean(..., na.rm = TRUE))
```

###

`group_by()` is translated to the `GROUP BY` clause, however, we like to use `.by` because `group_by()` changes our original data, and `summarize()` is translated to the `SELECT` clause.

### Exercise 5

Within `summarize()`, add on `.by = displ`.

```{r sql-select-from-grou-5, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-select-from-grou-5-hint, eval = FALSE}
flights_db |>
  select(carrier, tailnum, origin, distance, year)|>
  rename(year_built = year)
  show_query()|>
  summarize(avg_hwy = mean(hwy, na.rm = TRUE),
            .by = ...)
```

## SQL with `WHERE`, `ORDER BY`, & `Subqueries`
###

`SQL` is able to use three more important functions: `WHERE`, `ORDER BY`, and `Subqueries`. `WHERE` is used when we filter data. `ORDER BY` is used when we arrange our data. `Subqueries` are used when we mutate and select our data.

### Exercise 1

Start a pipe with `flights_db`. Pipe it into `filter()`. Within `filter()`, add `dest = "IAH"` and `dest = "HOU` separated with a `|`.

```{r sql-with-where-order-1, exercise = TRUE}

```

```{r sql-with-where-order-1-hint, eval = FALSE}
... |> 
  ...(dest == "..." | ... == "HOU")
```

###

In `SQL`, `|` becomes `OR` and `&` becomes `AND`. `SQL` uses `=` for comparison, not `==`. `SQL` doesn’t have assignment, so there’s no potential for confusion there. Also, `SQL` uses only `''` for strings, not `""`. In `SQL`, `""` is used to identify variables, like R’s ````.

### Exercise 2

Continue the pipe with `filter()` with the arguments `arr_delay > 0` and `arr_delay < 20`. Make sure to separate these arguments with commas.

```{r sql-with-where-order-2, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-with-where-order-2-hint, eval = FALSE}
flights_db|>
filter(dest == "IAH"| dest == "HOU")|>
filter(... > 0 , ... < 20)
```

### Exercise 3

Finish the pipe by adding `show_query()`.

```{r sql-with-where-order-3, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-with-where-order-3-hint, eval = FALSE}
flights_db|>
filter(dest == "IAH"| dest == "HOU")|>
filter(arr_delay > 0, arr_delay < 20)|>
...()
```

### Exercise 4

Let's start a new pipe with `flights_db`. Let's `filter()` our data to `!is.na(dep_delay)`.

```{r sql-with-where-order-4, exercise = TRUE}

```

```{r sql-with-where-order-4-hint, eval = FALSE}
... |> 
  filter(!is.na(...))
```

###

In general, you can work with `NULL`s using the functions you’d use for `NA`s in R.

### Exercise 5

Finish the pipe by adding `show_query()`.

```{r sql-with-where-order-5, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-with-where-order-5-hint, eval = FALSE}
flights_db|> 
  filter(!is.na(dep_delay))|>
  ...()
```

###

This `SQL` query illustrates one of the drawbacks of **dbplyr**: while the `SQL` is correct, it isn’t as simple as you might write by hand. In this case, you could drop the parentheses and use a special operator that’s easier to read.

### Exercise 6

Let's start another new pipe with `planes_db`. Pipe it into `arrange()` and within `arrange()` add the arguments `year`, `engines`, `seats`, and `desc(speed)`.

```{r sql-with-where-order-6, exercise = TRUE}

```

```{r sql-with-where-order-6-hint, eval = FALSE}
... |> 
  ...(year, engines, seats, desc(speed))
```

###

Ordering rows involves a straightforward translation from `arrange()` to the `ORDER BY` clause.

### Exercise 7

Finish the pipe by adding `show_query()`.

```{r sql-with-where-order-7, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-with-where-order-7-hint, eval = FALSE}
planes_db|>
arrange(year, engines, desc(seats), desc(speed))|>
...()
```

###

Notice how `desc()` is translated to `DESC`: this is one of the many **dplyr** functions whose name was directly inspired by `SQL`.

### Exercise 8

Let's start another new pipe with `flights_db`. Pipe it into `mutate()`and within `mutate()`, set `year1` to `year + 1`.

```{r sql-with-where-order-8, exercise = TRUE}

```

```{r sql-with-where-order-8-hint, eval = FALSE}
... |> 
  ...(year1 = year + 1)
```

###

Sometimes it’s not possible to translate a **dplyr** pipeline into a single `SELECT` statement and you need to use a **subquery**. A **subquery** is just a query used as a data source in the `FROM` clause, instead of the usual table.

### Exercise 9

Continue the pipe with `filter()`. Within `filter()` set `year1` equal to `2014`.

```{r sql-with-where-order-9, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-with-where-order-9-hint, eval = FALSE}
flights_db|>
mutate(year1 = year + 1)|>
  filter(year1 == ...)
```

###

When we use `filter()`, **dbplyr** uses subqueries to work around limitations of `SQL`. Remember, even though `WHERE` is written after `SELECT`, it’s evaluated before it, so we need a **subquery** in this example

### Exercise 10

Finish the pipe by adding `show_query()`.

```{r sql-with-where-order-10, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-with-where-order-10-hint, eval = FALSE}
flights_db|>
mutate(year1 = year + 1)|>
filter(year1 == 2014)|>
show_query()
```

###

Sometimes **dbplyr** will create a subquery where it’s not needed because it doesn’t yet know how to optimize that translation. As **dbplyr** improves over time, these cases will get rarer but will probably never go away

## SQL with `Joins` & Other Verbs
###

You’re likely to need many joins when working with data from a database. That’s because database tables are often stored in a highly normalized form, where each “fact” is stored in a single place and to keep a complete dataset for analysis you need to navigate a complex network of tables connected by primary and foreign keys.

### Exercise 1

Start a pipe with `flights_db`. Pipe it into `left_join()` and add `planes|> rename(year_built = year)`

```{r sql-with-joins--othe-1, exercise = TRUE}

```

```{r sql-with-joins--othe-1-hint, eval = FALSE}
... |> 
  ...(planes_db |> rename(year_built = year)
```

### Exercise 2

Within `left_join` add `by` and set it to `"tailnum"`.

```{r sql-with-joins--othe-2, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-with-joins--othe-2-hint, eval = FALSE}
flights_db|>
left_join(planes_db|>
          rename(year_built = year),
          by = "..."
```

###

**dplyr**’s names for these functions are so closely connected to `SQL` that you can easily guess the equivalent `SQL` for `inner_join()`, `right_join()`, and `full_join()`.

### Exercise 3

Finish the pipe by adding `show_query()`.

```{r sql-with-joins--othe-3, exercise = TRUE}

```

<button onclick = "transfer_code(this)">Copy previous code</button>

```{r sql-with-joins--othe-3-hint, eval = FALSE}
flights_db|>
left_join(planes_db|>
          rename(year_built = year),
          by = "tailnum"|>
  show_query()
```

###

The main thing to notice here is the syntax: `SQL` joins use sub-clauses of the `FROM` clause to bring in additional tables, using `ON` to define how the tables are related.

###

**dbplyr** also translates other verbs like `distinct()`, `slice_*()`, and `intersect()`, and a growing selection of **tidyr** functions like `pivot_longer()` and `pivot_wider()`. Visit [**dbplyr**](https://dbplyr.tidyverse.org/reference/) to view other verbs.

## Function Translations
###

This section of the chapter is too difficult to capture within the structure of a tutorial.



## Cleaning Up
###

This section cleans up the databases, with which you were working above, from your system.

### Exercise 1

Our first step is to disconnect the database that we have created. Run the following code chunk below.

```{r cleaning-up-1, exercise = TRUE}
dbDisconnect(con, shutdown = TRUE)
```

### Exercise 2

Finally let's remove the `myDB.sqlite` from your computer. Find the location on your computer where the r4ds.tutorial is stored. Add that location to the beginning of the following code below and run the code chunk. 

```{r cleaning-up-2, exercise = TRUE}
file.remove("myDB.sqlite")
```

## Summary
### 

This tutorial covered [Chapter 21: Databases](https://r4ds.hadley.nz/databases.html) from [*R for Data Science (2e)*](https://r4ds.hadley.nz/) by Hadley Wickham, Mine Çetinkaya-Rundel, and Garrett Grolemund. You learned how to use [**DBI**](https://dbi.r-dbi.org/) and used the [**dbplyr**](https://dbplyr.tidyverse.org/) packages to connect to databases and execute SQL. To check out further reading take a look at [**Writing SQL with dbplyr**](https://dbplyr.tidyverse.org/articles/sql.html), [**Practical SQL: A Beginner's Guide to Storytelling with Data**](https://www.practicalsql.com/), and [**SQL For Data Scientists**](https://sqlfordatascientists.com/).


```{r download-answers, child = system.file("child_documents/download_answers.Rmd", package = "tutorial.helpers")}
```
