Skip to content

Commit ff1edc6

Browse files
committed
week 3 ready
1 parent 5651fa9 commit ff1edc6

3 files changed

Lines changed: 135 additions & 89 deletions

File tree

slides/lesson2_slides.html

Lines changed: 15 additions & 9 deletions
Large diffs are not rendered by default.

slides/lesson2_slides.qmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ SELECT person.person_id, procedure_occurrence.procedure_occurrence_id
8484

8585
3. Then, we `SELECT` for the columns we want to keep: `person.person_id, procedure_occurrence.procedure_occurrence_id`
8686

87-
## Table References
87+
## Table Alias
8888

8989
We can short-hand the table names via the `AS` statement:
9090

week3.qmd

Lines changed: 119 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ con <- DBI::dbConnect(duckdb::duckdb(),
1818

1919
## `GROUP BY`
2020

21-
Say we want to count, calculate totals, or averages for a particular column by a particular grouping variable. We can use a `SELECT/GROUP BY` pattern to do this.
21+
Say we want to count, calculate totals, or averages for a particular column by a particular grouping variable. For example, suppose we want to group `gender_source_value` column in the `person` table and count the number of `person_id`s for each value of `gender_source_value`. We can use a `SELECT/GROUP BY` pattern to do this.
2222

2323
There are some requirements to using `SELECT`/`GROUP BY`:
2424

@@ -34,6 +34,67 @@ SELECT gender_source_value, COUNT(person_id) AS person_count
3434
GROUP BY gender_source_value
3535
```
3636

37+
Notice that we use the `AS` alias to rename `COUNT(person_id)` to `person_count` in the column name.
38+
39+
We summarize our column in other ways besides `COUNT`:
40+
41+
- `MEAN`
42+
43+
- `MIN`
44+
45+
- `MAX`
46+
47+
- `MEDIAN`
48+
49+
For example, we can look at the minimum `year_of_birth` for each gender in the `person` table:
50+
51+
```{sql}
52+
#| connection: "con"
53+
54+
SELECT gender_source_value, MIN(year_of_birth)
55+
FROM person
56+
GROUP BY gender_source_value
57+
```
58+
59+
### Check on Learning
60+
61+
If we look at `concept` table, we notice that there are groups of concepts organized by the `domain_id` column:
62+
63+
```{sql connection}
64+
#| connection: "con"
65+
66+
SELECT concept_id, concept_name, domain_id, vocabulary_id
67+
FROM concept
68+
LIMIT 10
69+
```
70+
71+
`COUNT` the number of `concept_id`s grouped by `domain_id` in the `concept` table:
72+
73+
```{sql}
74+
#| connection: "con"
75+
#| eval: false
76+
SELECT domain_id, COUNT(------) AS count_domain
77+
FROM concept
78+
GROUP BY -------
79+
ORDER BY count_domain DESC
80+
```
81+
82+
You can also group by multiple variables. What happens if you group by `domain_id` *and* `vocabulary_id`?
83+
84+
## GROUP BY with JOINs
85+
86+
Recall that table `procedure_occurrence` records the procedures of each person. Suppose that we do a `GROUP BY` on each `procedure_concept_id` and count the number of `person_id`s to understand how many people were treated for each procedure:
87+
88+
```{sql}
89+
#| connection: "con"
90+
SELECT procedure_concept_id, COUNT(person_id) AS person_count
91+
FROM procedure_occurrence
92+
GROUP BY procedure_concept_id
93+
ORDER BY person_count DESC
94+
```
95+
96+
We wish we know what the `procedure_concept_id` referred to. We need to join it with `concept` table.
97+
3798
Here, we're combining `SELECT`/`GROUP_BY` with an `INNER JOIN`:
3899

39100
```{sql}
@@ -46,7 +107,7 @@ SELECT c.concept_name AS procedure, COUNT(person_id) AS person_count
46107
ORDER BY person_count DESC
47108
```
48109

49-
We can group by multiple variables. Here is a triple join where we are counting by both `gender_source_value` and `concept_name`:
110+
Even more complicated: We can group by multiple variables. Here is a triple join where we are counting by both `gender_source_value` and `concept_name`:
50111

51112
```{sql}
52113
#| connection: "con"
@@ -60,19 +121,6 @@ SELECT c.concept_name AS procedure, p.gender_source_value, COUNT(p.person_id) AS
60121
ORDER BY person_count DESC
61122
```
62123

63-
### Check on Learning
64-
65-
`COUNT` the number of `concept_id`s grouped by `domain_id` in the `concept` table:
66-
67-
```{sql}
68-
#| connection: "con"
69-
#| eval: false
70-
SELECT domain_id, COUNT(------) AS count_domain
71-
FROM concept
72-
GROUP BY -------
73-
ORDER BY count_domain DESC
74-
```
75-
76124
## `HAVING`
77125

78126
We can filter by these aggregate variables. But we can't use them in a `WHERE` clause. There is an additional clause `HAVING`:
@@ -88,9 +136,21 @@ SELECT c.concept_name AS procedure, COUNT(person_id) AS person_count
88136
ORDER BY person_count DESC
89137
```
90138

91-
Why can't we use `WHERE`? `WHERE` is actually evaluated before `SELECT`/`GROUP_BY`, so it has no idea that the aggregated variables exist. Remember [SQL clause priorities?](https://intro-sql-fh.netlify.app/concepts.html#what-is-sql). `WHERE` is priority 2, and `GROUP BY`/`HAVING` are priorities 3 and 4.
139+
Why can't we use `WHERE`?
140+
141+
Well, it turns out that SQL clauses have different priorities, which tells the engine how to order the clauses to execute as your queries become bigger. The `WHERE` clause has *higher priority* than the `GROUP BY` clause, which means if you had written `WHERE person_count > 500`, it would be evaluated before `GROUP BY`, thus it has no idea `person_count` exists and throws an error. Here is the full list of SQL clause priorities:
142+
143+
| Priority | Clause | Purpose |
144+
|-----------------|-----------------|--------------------------------------|
145+
| 1 | `FROM` | Choose tables to query and specify how to `JOIN` them together |
146+
| 2 | `WHERE` | Filter tables based on criteria |
147+
| 3 | `GROUP BY` | Aggregates the Data |
148+
| 4 | `HAVING` | Filters Aggregated Data |
149+
| 5 | `SELECT` | Selects columns in table and calculate new columns |
150+
| 6 | `ORDER BY` | Sorts by a database field |
151+
| 7 | `LIMIT` | Limits the number of records returned |
92152

93-
In general, you need to put `WHERE` before `GROUP BY`/`HAVING`. Your SQL statement will not work if you put `WHERE` after `GROUP BY` / `HAVING`.
153+
In general, you need to put `WHERE` to do any filtering before running `GROUP BY`. Then, after the data is grouped and aggregrated, you can do additional filtereing on the aggregated data via `HAVING`. Your SQL statement will not work if you put `WHERE` after `GROUP BY` / `HAVING`.
94154

95155
Here is an example of using both `WHERE` and `HAVING`:
96156

@@ -104,21 +164,11 @@ SELECT domain_id, COUNT(concept_id) AS count_domain
104164
ORDER BY count_domain DESC
105165
```
106166

107-
```{r}
108-
sql_statement <- "EXPLAIN SELECT domain_id, COUNT(concept_id) AS count_domain
109-
FROM concept
110-
WHERE domain_id != 'Drug'
111-
GROUP BY domain_id
112-
HAVING count_domain > 40
113-
ORDER BY count_domain DESC"
114-
115-
DBI::dbGetQuery(con, sql_statement)
116-
```
117-
118-
Here's what happens when you put `WHERE` after `GROUP BY`/`HAVING`:
167+
Here's what happens when you put `WHERE` after `GROUP BY`/`HAVING`. Can you fix it?
119168

120169
```{sql}
121170
#| connection: "con"
171+
#| eval: false
122172
SELECT domain_id, COUNT(concept_id) AS count_domain
123173
FROM concept
124174
GROUP BY domain_id
@@ -141,16 +191,30 @@ SELECT c.concept_name AS procedure, COUNT(person_id) AS person_count
141191
ORDER BY person_count DESC
142192
```
143193

144-
We can group by `year` by first extracting it from `po.procedure_datetime` and using an alias `year`:
194+
### Check on learning
195+
196+
Suppose we were given this join, with the column `year` extracted from `procedure_datatime`.
145197

146198
```{sql}
147199
#| connection: "con"
148-
SELECT date_part('YEAR', po.procedure_datetime) AS year, COUNT(po.person_id) AS procedure_count
200+
SELECT date_part('YEAR', po.procedure_datetime) AS year, person_id, procedure_occurrence_id
149201
FROM procedure_occurrence AS po
150202
INNER JOIN concept AS c
151203
ON po.procedure_concept_id = c.concept_id
152-
GROUP BY year
153-
ORDER BY procedure_count DESC
204+
205+
```
206+
207+
Build on top of this query: Group by `year`, and then aggregate by the `COUNT` of `person_id`. Finally, filter it so that the `year` is higher than 1990. Should you be using `WHERE` or `HAVING`?
208+
209+
```{sql}
210+
#| connection: "con"
211+
#| eval: false
212+
SELECT date_part('YEAR', po.procedure_datetime) AS year, person_id
213+
FROM procedure_occurrence AS po
214+
INNER JOIN concept AS c
215+
ON po.procedure_concept_id = c.concept_id
216+
217+
154218
```
155219

156220
## `IN`/`LIKE`
@@ -184,21 +248,32 @@ SELECT concept_name, domain_id
184248
WHERE domain_id LIKE 'Dru%'
185249
```
186250

251+
You can find more informaiton about pattern matching [here](https://duckdb.org/docs/stable/sql/functions/pattern_matching).
252+
187253
## Creating Temporary Tables
188254

189-
Temporary tables can be very useful when you are trying to merge on a list of concepts, or for storing intermediate results.
255+
Temporary tables can be very useful for storing intermediate results.
190256

191257
Temporary tables only last for the session - they disappear after you disconnect, so don't use them for permanent storage.
192258

193-
Here is the csv (comma separated value) file that we're going to load in:
259+
You can use `CREATE OR REPLACE TEMPORARY TABLE` clause, followed by your temporary table name and `AS`, and then give a query of choice:
260+
261+
```{sql}
262+
#| connection: "con"
263+
CREATE OR REPLACE TEMPORARY TABLE temp_person AS
264+
SELECT person_id, gender_source_value
265+
FROM person
266+
```
267+
268+
You can also use `CREATE TEMPORARY TABLE` clause, but it will give you an error if the temporary table has been created already.
269+
270+
We can also load in a spreadsheet as a temporary table. Suppose we want to load in the following:
194271

195272
```{r}
196273
read_csv("data/temp_cost.csv")
197274
198275
```
199276

200-
We use `CREATE TEMP TABLE` to create a temp table. We will need to specify the data types of the columns before we can add data to it. We are using `CREATE OR REPLACE` in the below chunk to prevent errors when we run it, just in case we have run it before.
201-
202277
Then we can use `COPY` from DuckDB to load it in:
203278

204279
```{sql}
@@ -226,11 +301,6 @@ Now our table exists in our database, and we can work with it.
226301
SELECT * FROM cost
227302
```
228303

229-
```{sql}
230-
#| connection: "con"
231-
DESCRIBE cost
232-
```
233-
234304
Now we can merge our temporary `cost` table with `procedure_occurrence` and calculate the sum cost per year:
235305

236306
```{sql}
@@ -243,21 +313,7 @@ SELECT date_part('YEAR', po.procedure_datetime) AS year, SUM(cost) AS sum_cost_m
243313
ORDER BY year DESC
244314
```
245315

246-
We'll talk much more about subqueries and Views next time, which are another options to split queries up.
247-
248-
### Check on Learning
249-
250-
Modify the query below to calculate average cost per month using `AVG(cost)` named as `average_monthly_cost`:
251-
252-
```{sql}
253-
#| connection: "con"
254-
SELECT date_part('YEAR', po.procedure_datetime) AS year, SUM(cost)
255-
FROM procedure_occurrence AS po
256-
INNER JOIN cost AS c
257-
ON po.procedure_concept_id = c.procedure_concept_id
258-
GROUP BY year
259-
ORDER BY year DESC
260-
```
316+
We'll talk more about Subqueries and Views next time, which are another options to split queries up.
261317

262318
## Data Integrity
263319

@@ -281,33 +337,17 @@ Finally, the design of the tables and what information they contain, and how the
281337
Database design can be difficult because:
282338

283339
1. You need to understand the requirements of the data and how it is collected
284-
285-
```{=html}
286-
<!-- -->
287-
```
288-
a. For example, when is procedure information collected?
289-
b. Do patients have multiple procedures? (Cardinality)
290-
291-
```{=html}
292-
<!-- -->
293-
```
340+
- For example, when is procedure information collected?
341+
- Do patients have multiple procedures? (Cardinality)
294342
2. You need to group like data with like (normalization)
295-
296-
```{=html}
297-
<!-- -->
298-
```
299-
a. Data that is dependent on a primary key should stay together
300-
b. For example, `person` should contain information of a patient such as demographics, but not individual `procedure_concept_ids`.
301-
302-
```{=html}
303-
<!-- -->
304-
```
343+
- Data that is dependent on a primary key should stay together
344+
- For example, `person` should contain information of a patient such as demographics, but not individual `procedure_concept_ids`.
305345
3. You need to have an automated process to add data to the database (Extract Transfer Load, or ETL).
306346
4. Search processes must be optimized for common operations (indexing)
307347

308-
Of this, steps 1 and 2 are the most difficult and take the most time. They require the designer to interview users of the data and those who collect the data to reflect the *business processes*. These two steps are called the **Data Modeling** steps.
348+
Of this, steps 1 and 2 are the most difficult and take the most time. They require the designer to interview users of the data and those who collect the data to reflect the business processes. These two steps are called the **Data Modeling** steps.
309349

310-
These processes are essential if you are designing a **transactional database** that is collecting data from multiple sources (such as clinicians at time of care) and is updated multiple times a second. For example, bank databases have a rigorous design.
350+
These processes are essential if you are designing a **transactional database** that is collecting data from multiple sources (such as clinicians at time of care) and is updated multiple times a second.
311351

312352
If you want to read more about the data model we're using, I've written up a short bit here: [OMOP Data Model](miscellaneous.html#the-omop-data-model).
313353

0 commit comments

Comments
 (0)