<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zena Muhoro</title>
    <description>The latest articles on DEV Community by Zena Muhoro (@zena_muhoro_).</description>
    <link>https://dev.to/zena_muhoro_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4071715%2Febf670d2-c19c-4756-b0c2-a01692421981.png</url>
      <title>DEV Community: Zena Muhoro</title>
      <link>https://dev.to/zena_muhoro_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zena_muhoro_"/>
    <language>en</language>
    <item>
      <title>Introduction to Databases and SQL with PostgreSQL: Window Functions, Joins, CTEs and Subqueries on a simple Hospital Database.</title>
      <dc:creator>Zena Muhoro</dc:creator>
      <pubDate>Sun, 18 Oct 2026 12:26:26 +0000</pubDate>
      <link>https://dev.to/zena_muhoro_/introduction-to-databases-and-sql-with-postgresql-window-functions-joins-ctes-and-subqueries-on-10cd</link>
      <guid>https://dev.to/zena_muhoro_/introduction-to-databases-and-sql-with-postgresql-window-functions-joins-ctes-and-subqueries-on-10cd</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Introduction&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Up to now in our SQL class, I could use &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt; and &lt;code&gt;ORDER BY&lt;/code&gt; to get simple answers out of a table. This assignment pushed me further. We had to research four topics and show how each one works on our hospital database:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Part 1: Window functions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 2: Joins&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 3: CTEs (Common Table Expressions)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 4: SQL Subqueries&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this article I explain each one the way I came to understand it. For every idea, I'll say what it does, why you would use it, show a query I ran on the hospital data, and explain what the result told me. I've also written down the mistakes I made along the way, because those taught me the most.&lt;/p&gt;

&lt;p&gt;I ran every query on my local PostgreSQL 13 on Windows, and the results shown are exactly what I got back.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;The hospital database I used&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The tables live in a schema (a folder for tables) called &lt;code&gt;morning&lt;/code&gt;. I ran this first, so I didn't have to type &lt;code&gt;morning.&lt;/code&gt; in front of every table name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;morning&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are seven tables. The main one is &lt;code&gt;visits&lt;/code&gt;, where each row is one visit to the hospital. The other six tables hold the details about each visit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;One row is...&lt;/th&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;visits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one visit&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;patients&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one patient&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;doctors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one doctor&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;department&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one department&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wards&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one ward&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;diagnosis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one diagnosis&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;procedures&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one procedure&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first thing I noticed is that &lt;code&gt;visits&lt;/code&gt; doesn't store any names. It only stores ID numbers like &lt;code&gt;patient_id&lt;/code&gt; and &lt;code&gt;doctor_id&lt;/code&gt;, which point to the other tables. It also stores facts about each visit: the date, the type of visit (Outpatient, Inpatient or Emergency), how many days the patient stayed, the bill in Kenya shillings (&lt;code&gt;total_bill_kes&lt;/code&gt;), the satisfaction score, and a few more.&lt;/p&gt;

&lt;p&gt;Before writing any queries, I looked through the data and wrote down a few facts that helped me read my results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There are four doctors, and each one works in one department:

&lt;ul&gt;
&lt;li&gt;Dr. Brian Mwangi (101): Cardiology (10)&lt;/li&gt;
&lt;li&gt;Dr. Sarah Wanjiku (102): General Medicine (20)&lt;/li&gt;
&lt;li&gt;Dr. Peter Otieno (103): Orthopedics (30)&lt;/li&gt;
&lt;li&gt;Dr. Faith Chebet (104): Pediatrics (40)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;All the visits happened between &lt;strong&gt;1 and 15 June 2026&lt;/strong&gt;, two visits a day.&lt;/li&gt;
&lt;li&gt;Five patients came twice: patients 1, 2, 5, 7 and 10.&lt;/li&gt;
&lt;li&gt;All 30 visits together were billed &lt;strong&gt;KES 488,000&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Assumptions I made&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I used the data exactly as it is in the script. I didn't add, remove or fix anything. Names are shown as they're saved, so one doctor appears as &lt;code&gt;DR.Brian Mwangi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;"Week 1" means 1 to 7 June. "Week 2" means 8 to 15 June.&lt;/li&gt;
&lt;li&gt;In this data, every patient, doctor and department has at least one visit. I found out this makes some joins look the same, so in a few places I asked a narrower question (like "Emergency visits only") to show the difference.&lt;/li&gt;
&lt;li&gt;The data has no empty (&lt;code&gt;NULL&lt;/code&gt;) IDs.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Part 1: Window Functions&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1.1 The problem window functions solve&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I already knew &lt;code&gt;GROUP BY&lt;/code&gt; from class. It combines many rows into &lt;strong&gt;one row per group&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;doctor_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;103&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; doctor_id | doctor_total
-----------+--------------
       103 |    105200.00
       104 |     74900.00
(2 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave me the totals, but I lost the individual visits. I wanted to see each visit with the doctor's total beside it, and with &lt;code&gt;GROUP BY&lt;/code&gt; alone I couldn't do that.&lt;/p&gt;

&lt;p&gt;That's exactly what a window function does. It does the maths for a group, but &lt;strong&gt;it doesn't combine the rows&lt;/strong&gt;. Every row remains.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;doctor_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;103&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | doctor_id | total_bill_kes | doctor_total
----------+-----------+----------------+--------------
     1003 |       103 |       28000.00 |    105200.00
     1008 |       103 |        6500.00 |    105200.00
     1015 |       103 |       24000.00 |    105200.00
     1020 |       103 |        6500.00 |    105200.00
     1023 |       103 |        7200.00 |    105200.00
     1027 |       103 |       33000.00 |    105200.00
     1004 |       104 |       42000.00 |     74900.00
     1007 |       104 |        5500.00 |     74900.00
     1014 |       104 |        1800.00 |     74900.00
     1019 |       104 |        5200.00 |     74900.00
     1024 |       104 |       18500.00 |     74900.00
     1028 |       104 |        1900.00 |     74900.00
(12 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The totals are the same as before, but all 12 visits are still there.&lt;/p&gt;

&lt;p&gt;We learn that &lt;code&gt;GROUP BY&lt;/code&gt; gives &lt;strong&gt;one row per group&lt;/strong&gt;. A window function gives &lt;strong&gt;every row, plus something about its group&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1.2 How a window function is written: OVER, PARTITION BY, ORDER BY and window frames&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I learnt that a window function always has the word &lt;code&gt;OVER&lt;/code&gt; after it. The brackets describe the "window", which is just the set of rows the function looks at.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;function_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt;   &lt;span class="c1"&gt;-- split rows into groups (optional)&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt;       &lt;span class="c1"&gt;-- sort rows inside each group (optional)&lt;/span&gt;
    &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;      &lt;span class="c1"&gt;-- the frame: which nearby rows to use (optional)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what I learnt about each part.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;OVER()&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Empty brackets mean "look at &lt;strong&gt;all&lt;/strong&gt; the rows". I used this to put the hospital's grand total next to every visit and work out each visit's share of the money:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hospital_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;percent_of_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | total_bill_kes | hospital_total | percent_of_total
----------+----------------+----------------+------------------
     1017 |       82000.00 |      488000.00 |             16.8
     1010 |       76000.00 |      488000.00 |             15.6
     1004 |       42000.00 |      488000.00 |              8.6
     1016 |       36000.00 |      488000.00 |              7.4
     1027 |       33000.00 |      488000.00 |              6.8
(5 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output told me that one visit (1017, a heart failure patient) brought in almost 17% of all the money in these two weeks. I wouldn't have spotted that from the raw table.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;PARTITION BY&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;PARTITION BY&lt;/code&gt; splits the rows into groups, and the function starts again for each group. It's like &lt;code&gt;GROUP BY&lt;/code&gt;, except the rows aren't combined. I used &lt;code&gt;PARTITION BY doctor_id&lt;/code&gt; above to get each doctor's total.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;ORDER BY (inside OVER)&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;This sorts the rows &lt;strong&gt;before&lt;/strong&gt; the function does its work. It matters whenever order matters: numbering rows 1, 2, 3, looking at the previous row, or keeping a running total.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ORDER BY&lt;/code&gt; inside &lt;code&gt;OVER (...)&lt;/code&gt; is not the same as the &lt;code&gt;ORDER BY&lt;/code&gt; at the end of the query. The one inside changes how the answer is worked out. The one at the end only changes how the rows are shown.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Window frames&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;A frame says &lt;strong&gt;which rows near the current row&lt;/strong&gt; to include. These are the ones I used:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frame&lt;/th&gt;
&lt;th&gt;In plain words&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;From the very first row up to this one (a running total)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROWS BETWEEN 2 PRECEDING AND CURRENT ROW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This row and the two before it (a 3-row average)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every row in the group&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Something I noticed is that when I wrote a running total with &lt;code&gt;ORDER BY&lt;/code&gt; but no frame, the numbers looked wrong. I found out that without a frame, PostgreSQL quietly uses &lt;code&gt;RANGE&lt;/code&gt; instead of &lt;code&gt;ROWS&lt;/code&gt;, and &lt;code&gt;RANGE&lt;/code&gt; treats rows with the &lt;strong&gt;same value&lt;/strong&gt; as one block. Our hospital has two visits every day, so this is what happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;running_default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;
                                 &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CURRENT&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;running_rows&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | visit_date | total_bill_kes | running_default | running_rows
----------+------------+----------------+-----------------+--------------
     1001 | 2026-06-01 |        3500.00 |         6000.00 |      3500.00
     1002 | 2026-06-01 |        2500.00 |         6000.00 |      6000.00
     1003 | 2026-06-02 |       28000.00 |        76000.00 |     34000.00
     1004 | 2026-06-02 |       42000.00 |        76000.00 |     76000.00
     1005 | 2026-06-03 |        4000.00 |        83000.00 |     80000.00
     1006 | 2026-06-03 |        3000.00 |        83000.00 |     83000.00
(6 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;running_default&lt;/code&gt; adds &lt;strong&gt;both visits from the same day together&lt;/strong&gt;. That's why both 1 June rows say 6,000.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;running_rows&lt;/code&gt; adds &lt;strong&gt;one visit at a time&lt;/strong&gt;: 3,500, then 6,000, then 34,000, and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end I understood that neither one is wrong. The first answers "how much by the end of this day?" The second answers "how much up to this visit?"&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1.3 Ranking functions: ROW_NUMBER, RANK, DENSE_RANK and NTILE&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;ROW_NUMBER()&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;This function numbers the rows 1, 2, 3... Every row gets its own number, even if two rows look the same.&lt;/p&gt;

&lt;p&gt;It can be used to count things in order, like a patient's 1st visit, 2nd visit, and so on.&lt;/p&gt;

&lt;p&gt;In my practice, I numbered each returning patient's visits by date.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;visit_number&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; patient_id | visit_id | visit_date | visit_number
------------+----------+------------+--------------
          1 |     1001 | 2026-06-01 |            1
          1 |     1011 | 2026-06-06 |            2
          2 |     1002 | 2026-06-01 |            1
          2 |     1021 | 2026-06-11 |            2
          5 |     1005 | 2026-06-03 |            1
          5 |     1025 | 2026-06-13 |            2
          7 |     1007 | 2026-06-04 |            1
          7 |     1028 | 2026-06-14 |            2
         10 |     1010 | 2026-06-05 |            1
         10 |     1030 | 2026-06-15 |            2
(10 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed that because of &lt;code&gt;PARTITION BY patient_id&lt;/code&gt;, the count starts again at 1 for every patient. Any row marked 2 is a return visit.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;RANK() and DENSE_RANK()&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;These functions give each row a position, like runners in a race. If two rows tie, they get the &lt;strong&gt;same&lt;/strong&gt; position. The difference is what comes &lt;strong&gt;next&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RANK()&lt;/code&gt; &lt;strong&gt;skips&lt;/strong&gt; a number after a tie: 1, 1, 3&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DENSE_RANK()&lt;/code&gt; &lt;strong&gt;doesn't skip&lt;/strong&gt;: 1, 1, 2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my practice, I ranked the visits by patient satisfaction score, highest first. Some visits have the same score, so I put all three functions side by side to see how they differ.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patient_satisfaction_score&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_satisfaction_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;row_num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_satisfaction_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;DENSE_RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_satisfaction_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dense_rank&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | score | row_num | rank | dense_rank
----------+-------+---------+------+------------
     1011 |   4.8 |       1 |    1 |          1
     1028 |   4.8 |       2 |    1 |          1
     1005 |   4.7 |       3 |    3 |          2
     1019 |   4.7 |       4 |    3 |          2
     1007 |   4.6 |       5 |    5 |          3
     1015 |   4.6 |       6 |    5 |          3
     1025 |   4.6 |       7 |    5 |          3
     1001 |   4.5 |       8 |    8 |          4
     1013 |   4.5 |       9 |    8 |          4
(9 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the output, visits 1011 and 1028 both scored 4.8, so &lt;code&gt;RANK&lt;/code&gt; and &lt;code&gt;DENSE_RANK&lt;/code&gt; both put them in 1st place.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The next score (4.7) is &lt;strong&gt;3rd&lt;/strong&gt; with &lt;code&gt;RANK&lt;/code&gt;, because two visits already took 1st and 2nd. With &lt;code&gt;DENSE_RANK&lt;/code&gt; it's &lt;strong&gt;2nd&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ROW_NUMBER&lt;/code&gt; ignores ties completely and just counts 1 to 9.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I first ran this, tied visits came out of &lt;code&gt;ROW_NUMBER&lt;/code&gt; in a different order each time. I learnt that if rows tie, PostgreSQL can number them in any order. Adding &lt;code&gt;visit_id&lt;/code&gt; to its sort made the result the same every time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Do ties share a number?&lt;/th&gt;
&lt;th&gt;Does it skip after a tie?&lt;/th&gt;
&lt;th&gt;I'd use it when I want...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROW_NUMBER()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;exactly one row per number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RANK()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;race-style positions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DENSE_RANK()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;the "top 3 scores", even if more than 3 rows have them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;NTILE(n)&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;NTILE(n)&lt;/code&gt; splits the rows into &lt;code&gt;n&lt;/code&gt; groups of about the same size and says which group each row is in.&lt;/p&gt;

&lt;p&gt;It is used to sort things into bands, like cheap, middle and expensive.&lt;/p&gt;

&lt;p&gt;In practice, I split the 9 inpatient visits into 3 cost groups.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;NTILE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cost_group&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;admission_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Inpatient'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | total_bill_kes | cost_group
----------+----------------+------------
     1024 |       18500.00 |          1
     1015 |       24000.00 |          1
     1003 |       28000.00 |          1
     1026 |       29000.00 |          2
     1027 |       33000.00 |          2
     1016 |       36000.00 |          2
     1004 |       42000.00 |          3
     1010 |       76000.00 |          3
     1017 |       82000.00 |          3
(9 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As per the output, group 1 holds the three cheapest stays and group 3 the three most expensive. I also read that if the rows don't split evenly, the first groups just get one extra row each.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1.4 Looking at other rows: LAG, LEAD, FIRST_VALUE and LAST_VALUE&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;LAG() and LEAD()&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;LAG&lt;/code&gt; looks back at the &lt;strong&gt;previous&lt;/strong&gt; row. &lt;code&gt;LEAD&lt;/code&gt; looks ahead at the &lt;strong&gt;next&lt;/strong&gt; row.&lt;/p&gt;

&lt;p&gt;They are used to compare a row with the one before or after it. For example, "How many days since this patient was last here?"&lt;/p&gt;

&lt;p&gt;In my practice, for each returning patient, I showed their previous visit, their next visit, and the number of days in between.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;previous_visit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;LEAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;next_visit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;visit_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;days_since_last&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; patient_id | visit_id | visit_date | previous_visit | next_visit | days_since_last
------------+----------+------------+----------------+------------+-----------------
          1 |     1001 | 2026-06-01 | NULL           | 2026-06-06 |            NULL
          1 |     1011 | 2026-06-06 | 2026-06-01     | NULL       |               5
          2 |     1002 | 2026-06-01 | NULL           | 2026-06-11 |            NULL
          2 |     1021 | 2026-06-11 | 2026-06-01     | NULL       |              10
          5 |     1005 | 2026-06-03 | NULL           | 2026-06-13 |            NULL
          5 |     1025 | 2026-06-13 | 2026-06-03     | NULL       |              10
          7 |     1007 | 2026-06-04 | NULL           | 2026-06-14 |            NULL
          7 |     1028 | 2026-06-14 | 2026-06-04     | NULL       |              10
         10 |     1010 | 2026-06-05 | NULL           | 2026-06-15 |            NULL
         10 |     1030 | 2026-06-15 | 2026-06-05     | NULL       |              10
(10 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output showed that patient 1 came back after 5 days, and everyone else came back after 10. A first visit has nothing before it, so &lt;code&gt;LAG&lt;/code&gt; shows &lt;code&gt;NULL&lt;/code&gt;. A last visit has nothing after it, so &lt;code&gt;LEAD&lt;/code&gt; shows &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;FIRST_VALUE() and LAST_VALUE()&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;FIRST_VALUE&lt;/code&gt; takes the value from the &lt;strong&gt;first&lt;/strong&gt; row in the window. &lt;code&gt;LAST_VALUE&lt;/code&gt; takes it from the &lt;strong&gt;last&lt;/strong&gt; row.&lt;/p&gt;

&lt;p&gt;Using our hospital data, for each of Dr. Faith Chebet's visits (doctor 104), I showed the bill from her first visit and from her last visit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;FIRST_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_bill&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;LAST_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_bill_default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;LAST_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;
                                         &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt;
                                                  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;UNBOUNDED&lt;/span&gt; &lt;span class="k"&gt;FOLLOWING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_bill_full&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;104&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt; &lt;span class="k"&gt;visit&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;id&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;visit&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;date&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;total&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;bill&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;kes&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;first&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;bill&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;bill&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;bill&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;full&lt;/span&gt;
&lt;span class="err"&gt;----------+------------+----------------+------------+-------------------+----------------&lt;/span&gt;
     &lt;span class="mf"&gt;1004&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="ld"&gt;2026-06-02&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;       &lt;span class="mf"&gt;42000.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;   &lt;span class="mf"&gt;42000.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;42000.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;        &lt;span class="mf"&gt;1900.00&lt;/span&gt;
     &lt;span class="mf"&gt;1007&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="ld"&gt;2026-06-04&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;        &lt;span class="mf"&gt;5500.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;   &lt;span class="mf"&gt;42000.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;           &lt;span class="mf"&gt;5500.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;        &lt;span class="mf"&gt;1900.00&lt;/span&gt;
     &lt;span class="mf"&gt;1014&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="ld"&gt;2026-06-07&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;        &lt;span class="mf"&gt;1800.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;   &lt;span class="mf"&gt;42000.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;           &lt;span class="mf"&gt;1800.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;        &lt;span class="mf"&gt;1900.00&lt;/span&gt;
     &lt;span class="mf"&gt;1019&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="ld"&gt;2026-06-10&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;        &lt;span class="mf"&gt;5200.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;   &lt;span class="mf"&gt;42000.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;           &lt;span class="mf"&gt;5200.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;        &lt;span class="mf"&gt;1900.00&lt;/span&gt;
     &lt;span class="mf"&gt;1024&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="ld"&gt;2026-06-12&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;       &lt;span class="mf"&gt;18500.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;   &lt;span class="mf"&gt;42000.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;18500.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;        &lt;span class="mf"&gt;1900.00&lt;/span&gt;
     &lt;span class="mf"&gt;1028&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="ld"&gt;2026-06-14&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;        &lt;span class="mf"&gt;1900.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;   &lt;span class="mf"&gt;42000.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;           &lt;span class="mf"&gt;1900.00&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;        &lt;span class="mf"&gt;1900.00&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;6&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The output showed that &lt;code&gt;first_bill&lt;/code&gt; is 42,000 on every row. That's her first visit (1004), so that part worked straight away.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_bill_default&lt;/code&gt; just copied each row's own bill. The reason is the same hidden default frame from earlier. It stops at the &lt;strong&gt;current row&lt;/strong&gt;, so from each row's point of view, the "last" row is itself.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_bill_full&lt;/code&gt; tells it to look at &lt;strong&gt;all&lt;/strong&gt; the rows, and then it correctly shows 1,900, her last visit (1028).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson learnt: whenever I use &lt;code&gt;LAST_VALUE&lt;/code&gt;, I add &lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1.5 Aggregate window functions: SUM, AVG, COUNT, MIN and MAX&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I knew these five from the &lt;code&gt;GROUP BY&lt;/code&gt; chapter of our notes. What I learnt is that putting &lt;code&gt;OVER (...)&lt;/code&gt; after them turns them into window functions with the same answers, but every row stays.&lt;/p&gt;

&lt;p&gt;I put each Orthopedics visit (department 30) next to its department's count, total, average, lowest and highest bill.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dept_visits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dept_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dept_avg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dept_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dept_max&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | total_bill_kes | dept_visits | dept_total | dept_avg | dept_min | dept_max
----------+----------------+-------------+------------+----------+----------+----------
     1003 |       28000.00 |           6 |  105200.00 | 17533.33 |  6500.00 | 33000.00
     1008 |        6500.00 |           6 |  105200.00 | 17533.33 |  6500.00 | 33000.00
     1015 |       24000.00 |           6 |  105200.00 | 17533.33 |  6500.00 | 33000.00
     1020 |        6500.00 |           6 |  105200.00 | 17533.33 |  6500.00 | 33000.00
     1023 |        7200.00 |           6 |  105200.00 | 17533.33 |  6500.00 | 33000.00
     1027 |       33000.00 |           6 |  105200.00 | 17533.33 |  6500.00 | 33000.00
(6 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results show the average Orthopedics bill is KES 17,533.33. With that number on every row, we see that visits 1003, 1015 and 1027 are above average and the other three are below.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;A moving average&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Adding &lt;code&gt;ORDER BY&lt;/code&gt; and a frame turns &lt;code&gt;AVG&lt;/code&gt; into a &lt;strong&gt;moving average&lt;/strong&gt;, which is an average that slides along the rows. This one averages each visit with the two before it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;
                                       &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;PRECEDING&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;CURRENT&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;moving_avg_3&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;104&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | visit_date | total_bill_kes | moving_avg_3
----------+------------+----------------+--------------
     1004 | 2026-06-02 |       42000.00 |     42000.00
     1007 | 2026-06-04 |        5500.00 |     23750.00
     1014 | 2026-06-07 |        1800.00 |     16433.33
     1019 | 2026-06-10 |        5200.00 |      4166.67
     1024 | 2026-06-12 |       18500.00 |      8500.00
     1028 | 2026-06-14 |        1900.00 |      8533.33
(6 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows that the first row has nothing before it, so its average is just itself. The second row averages two visits: (42,000 + 5,500) ÷ 2 = 23,750. From the third row on, it always uses three. I could see how a moving average evens out a one-off big bill like that 42,000.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1.6 A mistake I made: putting a window function in WHERE&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I wanted the most expensive visit in each department, so my first try was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;bill_rank&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL gave me an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;window&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;are&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it failed:&lt;/strong&gt; this is where the "order of execution" chapter in our notes finally clicked for me. PostgreSQL runs a query in this order: &lt;code&gt;FROM&lt;/code&gt; → &lt;code&gt;WHERE&lt;/code&gt; → &lt;code&gt;GROUP BY&lt;/code&gt; → &lt;code&gt;HAVING&lt;/code&gt; → &lt;code&gt;SELECT&lt;/code&gt; → &lt;code&gt;ORDER BY&lt;/code&gt; → &lt;code&gt;LIMIT&lt;/code&gt;. Window functions are worked out in the &lt;code&gt;SELECT&lt;/code&gt; step, which comes &lt;strong&gt;after&lt;/strong&gt; &lt;code&gt;WHERE&lt;/code&gt;. So when &lt;code&gt;WHERE&lt;/code&gt; runs, the rank doesn't exist yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I fixed it:&lt;/strong&gt; the trick is to do it in two steps. First I worked out the rank inside a CTE (I explain CTEs in Part 3), and then I filtered on that rank in the main query. By the time the main query's &lt;code&gt;WHERE&lt;/code&gt; runs, the rank already exists, so it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;bill_rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bill_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | department_id | total_bill_kes
----------+---------------+----------------
     1017 |            10 |       82000.00
     1016 |            20 |       36000.00
     1027 |            30 |       33000.00
     1004 |            40 |       42000.00
(4 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I get the most expensive visit in each of the four departments, which is what I wanted in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Part 2: Joins&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;As I mentioned earlier, the &lt;code&gt;visits&lt;/code&gt; table only has numbers like &lt;code&gt;patient_id = 3&lt;/code&gt; and &lt;code&gt;doctor_id = 103&lt;/code&gt;. The names live in the &lt;code&gt;patients&lt;/code&gt; and &lt;code&gt;doctors&lt;/code&gt; tables. I learnt that storing each thing only once is good, but it means that to make a report people can read, I have to stitch the tables back together. That's what a join does.&lt;/p&gt;

&lt;p&gt;Every join needs an &lt;code&gt;ON&lt;/code&gt; part that says how the rows match up. Usually it's "this ID equals that ID".&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2.1 INNER JOIN&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; keeps only the rows that have matching values in &lt;strong&gt;both&lt;/strong&gt; tables. We use it when we only care about rows that exist on both sides.&lt;/p&gt;

&lt;p&gt;I showed each visit with the patient's name and the doctor's name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;patients&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;doctors&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id |  patient_name  |    doctor_name    | total_bill_kes
----------+----------------+-------------------+----------------
     1001 | Amina Otieno   | DR.Brian Mwangi   |        3500.00
     1002 | John Kariuki   | Dr. Sarah Wanjiku |        2500.00
     1003 | Grace Naliaka  | Dr. Peter Otieno  |       28000.00
     1004 | David Mutua    | Dr. Faith Chebet  |       42000.00
     1005 | Esther Achieng | DR.Brian Mwangi   |        4000.00
(5 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;v&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt; and &lt;code&gt;d&lt;/code&gt; are nicknames for the tables, so I don't have to type the full names every time. &lt;code&gt;v.patient_id&lt;/code&gt; just means "the &lt;code&gt;patient_id&lt;/code&gt; column from &lt;code&gt;visits&lt;/code&gt;". I also learnt you can join as many tables as you need, one &lt;code&gt;JOIN&lt;/code&gt; at a time.&lt;/p&gt;

&lt;p&gt;Joins work with &lt;code&gt;GROUP BY&lt;/code&gt; too. Here's how much each department billed, using the real department names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dep&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department_name&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; department_name  | visits | total_billed
------------------+--------+--------------
 Cardiology       |      9 |    210100.00
 Orthopedics      |      6 |    105200.00
 General Medicine |      9 |     97800.00
 Pediatrics       |      6 |     74900.00
(4 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows that Cardiology and General Medicine each saw 9 visits, but Cardiology made more than twice as much money.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2.2 LEFT JOIN&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;LEFT JOIN&lt;/code&gt; keeps &lt;strong&gt;every&lt;/strong&gt; row from the left table (the one after &lt;code&gt;FROM&lt;/code&gt;), and adds matching rows from the right table. Where there's no match, the right side is &lt;code&gt;NULL&lt;/code&gt; (empty).&lt;/p&gt;

&lt;p&gt;We use it when we want to keep everything from one table, even rows with no match. For example, "all doctors, even the ones who had no emergency cases".&lt;/p&gt;

&lt;p&gt;At first I tried a plain &lt;code&gt;LEFT JOIN&lt;/code&gt; from doctors to visits, and I got the same 30 rows as an &lt;code&gt;INNER JOIN&lt;/code&gt;. That confused me until I realised every doctor in our data has visits, so there was nothing unmatched to show. The difference only appears when something has no match. So I decided to ask: &lt;strong&gt;which doctors handled an Emergency visit?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admission_type&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;doctors&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
       &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admission_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Emergency'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    doctor_name    | visit_id | admission_type
-------------------+----------+----------------
 DR.Brian Mwangi   |     1030 | Emergency
 Dr. Sarah Wanjiku |     NULL | NULL
 Dr. Peter Otieno  |     NULL | NULL
 Dr. Faith Chebet  |     NULL | NULL
(4 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result shows all four doctors are listed, but only Dr. Brian Mwangi had an emergency. The &lt;code&gt;NULL&lt;/code&gt;s for the other three just mean "no emergency visit found".&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;A mistake I learnt from: filtering a LEFT JOIN in WHERE&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Before I got the query above right, I had put the &lt;code&gt;Emergency&lt;/code&gt; check in &lt;code&gt;WHERE&lt;/code&gt; instead of in &lt;code&gt;ON&lt;/code&gt;, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admission_type&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;doctors&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admission_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Emergency'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   doctor_name   | visit_id | admission_type
-----------------+----------+----------------
 DR.Brian Mwangi |     1030 | Emergency
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of the doctors disappeared. It took me a while to see why, and again the answer was the order SQL runs in. &lt;code&gt;WHERE&lt;/code&gt; runs &lt;strong&gt;after&lt;/strong&gt; the join, so it looks at every row and throws out anything that isn't 'Emergency'. That includes the empty rows the &lt;code&gt;LEFT JOIN&lt;/code&gt; was keeping for the other three doctors. So without meaning to, I had turned my &lt;code&gt;LEFT JOIN&lt;/code&gt; into an &lt;code&gt;INNER JOIN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What I took away from this: with a &lt;code&gt;LEFT JOIN&lt;/code&gt;, if I'm filtering the right-hand table and still want to keep the rows with no match, the condition goes in &lt;code&gt;ON&lt;/code&gt;, not &lt;code&gt;WHERE&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2.3 RIGHT JOIN&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;RIGHT JOIN&lt;/code&gt; mirrors the &lt;code&gt;LEFT JOIN&lt;/code&gt;. It keeps &lt;strong&gt;every&lt;/strong&gt; row from the right table (the one after &lt;code&gt;JOIN&lt;/code&gt;), and adds matching rows from the left table. It is used when we want to keep every row from one table, even rows with no match. The only difference is which side that table sits on.&lt;/p&gt;

&lt;p&gt;To really see this, I asked the exact same question as in my &lt;code&gt;LEFT JOIN&lt;/code&gt; example: &lt;strong&gt;which doctors handled an Emergency visit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This time I started &lt;code&gt;FROM visits&lt;/code&gt; and put &lt;code&gt;doctors&lt;/code&gt; on the &lt;strong&gt;right&lt;/strong&gt;, after &lt;code&gt;RIGHT JOIN&lt;/code&gt;. Because &lt;code&gt;doctors&lt;/code&gt; is now the right-hand table, &lt;code&gt;RIGHT JOIN&lt;/code&gt; keeps every doctor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admission_type&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;RIGHT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;doctors&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admission_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Emergency'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    doctor_name    | visit_id | admission_type
-------------------+----------+----------------
 DR.Brian Mwangi   |     1030 | Emergency
 Dr. Sarah Wanjiku |     NULL | NULL
 Dr. Peter Otieno  |     NULL | NULL
 Dr. Faith Chebet  |     NULL | NULL
(4 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is exactly the same as the &lt;code&gt;LEFT JOIN&lt;/code&gt;. &lt;code&gt;A LEFT JOIN B&lt;/code&gt; is the same as &lt;code&gt;B RIGHT JOIN A&lt;/code&gt;. From my reading, most people stick to &lt;code&gt;LEFT JOIN&lt;/code&gt; because "start with this table, then add that one" is easier to follow. &lt;code&gt;RIGHT JOIN&lt;/code&gt; is useful when adding one more table to the end of a long query and you want to keep all of its rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2.4 FULL OUTER JOIN&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FULL OUTER JOIN&lt;/code&gt; keeps &lt;strong&gt;every&lt;/strong&gt; row from &lt;strong&gt;both&lt;/strong&gt; tables. Rows that match are joined. Rows that don't match get &lt;code&gt;NULL&lt;/code&gt; on the side that's missing.&lt;/p&gt;

&lt;p&gt;We use it to compare two lists and see what's in one, what's in the other, and what's in both.&lt;/p&gt;

&lt;p&gt;I compared the patients who came in Week 1 (1–7 June) with those who came in Week 2 (8–15 June). Each list is a small query in brackets, called a subquery, which I explain in Part 4.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;w1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;week1_patient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;w2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;week2_patient&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-07'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;w1&lt;/span&gt;
&lt;span class="k"&gt;FULL&lt;/span&gt; &lt;span class="k"&gt;OUTER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt;
     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;visit_date&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-08'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-15'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;w2&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;w1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; week1_patient | week2_patient
---------------+---------------
             1 |          NULL
             2 |             2
             3 |          NULL
             4 |          NULL
             5 |             5
             6 |          NULL
             7 |             7
             8 |          NULL
             9 |          NULL
            10 |            10
            11 |          NULL
            12 |          NULL
            13 |          NULL
          NULL |            14
          NULL |            15
          NULL |            16
          NULL |            17
          NULL |            18
          NULL |            19
          NULL |            20
          NULL |            21
          NULL |            22
          NULL |            23
          NULL |            24
          NULL |            25
(25 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4&lt;/strong&gt; patients came in both weeks (2, 5, 7 and 10), so both columns are filled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9&lt;/strong&gt; patients came only in Week 1. Patient 1 came twice, but both visits were in Week 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12&lt;/strong&gt; patients came only in Week 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An &lt;code&gt;INNER JOIN&lt;/code&gt; would only show the 4 who came both weeks. A &lt;code&gt;LEFT JOIN&lt;/code&gt; would only show the 13 from Week 1. The &lt;code&gt;FULL OUTER JOIN&lt;/code&gt; shows all 25.&lt;/p&gt;

&lt;p&gt;(&lt;code&gt;COALESCE(a, b)&lt;/code&gt; means "use &lt;code&gt;a&lt;/code&gt;, but if it's empty, use &lt;code&gt;b&lt;/code&gt;". I used it from Chapter 19 of our notes to keep the list sorted by patient number.)&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2.5 CROSS JOIN&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CROSS JOIN&lt;/code&gt; pairs &lt;strong&gt;every&lt;/strong&gt; row from one table with &lt;strong&gt;every&lt;/strong&gt; row from the other. There's no &lt;code&gt;ON&lt;/code&gt;. 4 rows × 4 rows = 16 rows.&lt;/p&gt;

&lt;p&gt;We use it when we want every possible pair, like a blank duty rota listing every doctor against every ward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ward_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;doctors&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;wards&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ward_id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    doctor_name    |     ward_name
-------------------+-------------------
 DR.Brian Mwangi   | Cardiac Care Unit
 DR.Brian Mwangi   | General Ward
 DR.Brian Mwangi   | Surgical Ward
 DR.Brian Mwangi   | Childrens Ward
 Dr. Sarah Wanjiku | Cardiac Care Unit
 Dr. Sarah Wanjiku | General Ward
(6 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the &lt;code&gt;LIMIT&lt;/code&gt; there are &lt;strong&gt;16&lt;/strong&gt; rows: 4 doctors × 4 wards.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2.6 SELF JOIN&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;SELF JOIN&lt;/code&gt; joins a table &lt;strong&gt;to itself&lt;/strong&gt;. I was surprised there's no special keyword for it. You just use the same table twice with two different nicknames. It's used to compare rows in a table with other rows in the &lt;strong&gt;same&lt;/strong&gt; table.&lt;/p&gt;

&lt;p&gt;We try to answer the question: which patients come from the same county?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;patient_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;patient_2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;county&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;patients&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;patients&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;county&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;county&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;county&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   patient_1    |  patient_2   | county
----------------+--------------+---------
 Esther Achieng | Brian Omondi | Kisumu
 Amina Otieno   | Isaac Kamau  | Nairobi
(2 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;p1.patient_id &amp;lt; p2.patient_id&lt;/code&gt; part took me a moment to understand. Without it, every patient gets matched with themselves, and every pair shows up twice (Esther–Brian &lt;em&gt;and&lt;/em&gt; Brian–Esther).&lt;/p&gt;

&lt;p&gt;I also tried to match each returning patient's first visit with their later one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_visit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;later_visit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;later_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_date&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; patient_id | first_visit | first_date | later_visit | later_date
------------+-------------+------------+-------------+------------
          1 |        1001 | 2026-06-01 |        1011 | 2026-06-06
          2 |        1002 | 2026-06-01 |        1021 | 2026-06-11
          5 |        1005 | 2026-06-03 |        1025 | 2026-06-13
          7 |        1007 | 2026-06-04 |        1028 | 2026-06-14
         10 |        1010 | 2026-06-05 |        1030 | 2026-06-15
(5 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From my research, self joins are also how you'd show "who is whose boss" if a table had a column like &lt;code&gt;supervisor_id&lt;/code&gt;. Our &lt;code&gt;doctors&lt;/code&gt; table doesn't have one, so I couldn't show that here.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2.7 How the joins compare&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;After trying all six, it helped me to put them next to each other. The main difference between them is simply &lt;strong&gt;which rows they keep&lt;/strong&gt; when there's no match:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Join&lt;/th&gt;
&lt;th&gt;Rows kept from the left table&lt;/th&gt;
&lt;th&gt;Rows kept from the right table&lt;/th&gt;
&lt;th&gt;When I'd use it&lt;/th&gt;
&lt;th&gt;My example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INNER JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only matches&lt;/td&gt;
&lt;td&gt;only matches&lt;/td&gt;
&lt;td&gt;I only want rows that exist on both sides&lt;/td&gt;
&lt;td&gt;Visits with patient and doctor names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LEFT JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;only matches&lt;/td&gt;
&lt;td&gt;I want everything from the first table, matched or not&lt;/td&gt;
&lt;td&gt;All doctors, with any emergency visits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RIGHT JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only matches&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;Same as LEFT, but the table I want to keep is on the right&lt;/td&gt;
&lt;td&gt;Same question, tables swapped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FULL OUTER JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;I want to compare two lists and see what's missing on either side&lt;/td&gt;
&lt;td&gt;Week 1 patients vs Week 2 patients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CROSS JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;every pair&lt;/td&gt;
&lt;td&gt;every pair&lt;/td&gt;
&lt;td&gt;I want every possible combination&lt;/td&gt;
&lt;td&gt;Every doctor with every ward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELF JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the table joined to itself&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;I want to compare rows within one table&lt;/td&gt;
&lt;td&gt;Patients from the same county&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Part 3: CTEs (Common Table Expressions)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Our notes (Chapter 14) introduced CTEs briefly. A CTE lets you give a name to a query and use that name like a table inside a bigger query. It starts with the word &lt;code&gt;WITH&lt;/code&gt;, and it only lasts while that one query runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;name_of_cte&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;name_of_cte&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it helps:&lt;/strong&gt; my queries were getting long and hard to follow. A CTE breaks one big query into small, named steps that read from top to bottom.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3.1 A basic CTE&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A basic CTE using the question: which departments billed more than KES 100,000?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;department_totals&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;department_totals&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; department_id | total_billed
---------------+--------------
            10 |    210100.00
            30 |    105200.00
(2 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result shows that Cardiology (10) and Orthopedics (30) each billed more than KES 100,000.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The CTE &lt;code&gt;department_totals&lt;/code&gt; adds up the bills for each department. That gives four rows.&lt;/li&gt;
&lt;li&gt;The main query reads from &lt;code&gt;department_totals&lt;/code&gt; like any other table and keeps only the totals over 100,000.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3.2 Multiple CTEs&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I learnt you can write several CTEs in one &lt;code&gt;WITH&lt;/code&gt;, with commas between them, and a later CTE can use an earlier one. For example: which doctors billed more than the average doctor?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;doctor_totals&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;average_total&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_billed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_billed&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;doctor_totals&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_billed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;avg_billed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_billed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;doctor_totals&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;average_total&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_billed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;avg_billed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; doctor_id | total_billed | avg_billed
-----------+--------------+------------
       101 |    210100.00 |  122000.00
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;We see that &lt;code&gt;doctor_totals&lt;/code&gt; works out each doctor's total, giving 4 rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;average_total&lt;/code&gt; uses those 4 totals to get the average, KES 122,000. That's just 1 row.&lt;/li&gt;
&lt;li&gt;The main query puts them together. A &lt;code&gt;CROSS JOIN&lt;/code&gt; works here because &lt;code&gt;average_total&lt;/code&gt; has only one row, so it simply adds the average to every doctor's row. Then it keeps only the doctors above it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The output shows that only Dr. Brian Mwangi (101) billed more than the average doctor.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3.3 CTEs with joins and aggregations&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We use the example: who are the top five patients by money spent, and where are they from?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;patient_spend&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_spent&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;county&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_spent&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;patient_spend&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;patients&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patient_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_spent&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    patient_name    |   county    | visits | total_spent
--------------------+-------------+--------+-------------
 Peter Ouma         | Siaya       |      2 |    98000.00
 Hellen Jepchirchir | Bomet       |      1 |    82000.00
 David Mutua        | Machakos    |      1 |    42000.00
 Francis Njuguna    | Laikipia    |      1 |    36000.00
 Ruth Nekesa        | Trans Nzoia |      1 |    33000.00
(5 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We see that the CTE does the adding up (the aggregation), and the main query adds the names (the join). Peter Ouma spent the most, KES 98,000 over two visits: a heart failure stay, and later an emergency visit for chest pain.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3.4 Recursive CTEs&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A recursive CTE is one that uses itself. It builds a list one step at a time, a bit like counting on your fingers. It has two parts, joined by &lt;code&gt;UNION ALL&lt;/code&gt;: &lt;strong&gt;a starting point&lt;/strong&gt;, and &lt;strong&gt;a next step&lt;/strong&gt; that takes the last row and makes the next one. It keeps going until a condition tells it to stop.&lt;/p&gt;

&lt;p&gt;I started with the simplest example I could think of, counting from 1 to 5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;                          &lt;span class="c1"&gt;-- start at 1&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;  &lt;span class="c1"&gt;-- add 1 until we reach 5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; n
---
 1
 2
 3
 4
 5
(5 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the hospital data, I built a calendar for Week 1 and counted the visits on each day. The reason for a calendar is that a normal &lt;code&gt;GROUP BY visit_date&lt;/code&gt; can only show days that &lt;strong&gt;had&lt;/strong&gt; visits. A calendar lists &lt;strong&gt;every&lt;/strong&gt; day, so a quiet day would show 0 instead of just being missing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-01'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-07'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;billed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    day     | visits |  billed
------------+--------+----------
 2026-06-01 |      2 |  6000.00
 2026-06-02 |      2 | 70000.00
 2026-06-03 |      2 |  7000.00
 2026-06-04 |      2 | 12000.00
 2026-06-05 |      2 | 91000.00
 2026-06-06 |      2 |  6500.00
 2026-06-07 |      2 |  8800.00
(7 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows that the hospital had exactly 2 visits every day, and 5 June brought in the most money (KES 91,000), mostly from one heart failure patient. I was pleased that this one query uses three things I learnt in this assignment: a recursive CTE, a &lt;code&gt;LEFT JOIN&lt;/code&gt; and a total.&lt;/p&gt;

&lt;p&gt;From my research, recursive CTEs are also the usual way to follow a chain, like "this doctor reports to that doctor, who reports to..." Our data doesn't have that kind of column, so I used a calendar instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3.5 CTEs vs subqueries&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A subquery (Part 4) is a query written &lt;strong&gt;inside&lt;/strong&gt; another query. While I was learning about CTEs, I kept wondering how they were different from subqueries. So I took my first CTE from 3.1 and rewrote it as a subquery to compare them side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
      &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;department_totals&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_billed&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; department_id | total_billed
---------------+--------------
            10 |    210100.00
            30 |    105200.00
(2 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It gave exactly the same answer. The difference isn't in the result. It's in how the query is written and how easy it is to read. With the CTE, I name the step first and then use it, so it reads from top to bottom. With the subquery, the step is tucked inside the main query, so I have to read from the inside out.&lt;/p&gt;

&lt;p&gt;This is how I'd sum up the differences I found:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CTE&lt;/th&gt;
&lt;th&gt;Subquery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where you write it&lt;/td&gt;
&lt;td&gt;At the top, before the main query&lt;/td&gt;
&lt;td&gt;Inside the main query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does it have a name?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Only when it's in &lt;code&gt;FROM&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can you use it twice in the same query?&lt;/td&gt;
&lt;td&gt;Yes, just use the name again&lt;/td&gt;
&lt;td&gt;No, you'd have to type it out again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can it repeat itself (recursive)?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Easy to read with many steps?&lt;/td&gt;
&lt;td&gt;Yes, top to bottom&lt;/td&gt;
&lt;td&gt;Harder, with brackets inside brackets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Step-by-step logic and reusing results&lt;/td&gt;
&lt;td&gt;Short, one-off checks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I also read that in PostgreSQL 12 and newer, a simple CTE and the same subquery run about as fast as each other. So my takeaway is to pick whichever one is &lt;strong&gt;easier to read&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Part 4: SQL Subqueries&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A subquery is a &lt;code&gt;SELECT&lt;/code&gt; inside another query, wrapped in brackets. PostgreSQL answers the small question first, then uses that answer in the big one. You can put a subquery in &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;SELECT&lt;/code&gt; or &lt;code&gt;FROM&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4.1 Scalar (single-value) subqueries&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This is a subquery that gives back &lt;strong&gt;just one value&lt;/strong&gt;: one row, one column. "Scalar" turned out to be just the fancy name for that. It can be used to compare against a number I'd otherwise have to look up first, like an average.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Scalar subquery in WHERE&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;For example, to answer the question: which visits cost more than the average visit?&lt;/p&gt;

&lt;p&gt;First I checked the average on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        avg
--------------------
 16266.666666666667
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of copying that number into another query by hand, I let a subquery work it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | patient_id | total_bill_kes
----------+------------+----------------
     1017 |         16 |       82000.00
     1010 |         10 |       76000.00
     1004 |          4 |       42000.00
     1016 |         15 |       36000.00
     1027 |         24 |       33000.00
     1026 |         23 |       29000.00
     1003 |          3 |       28000.00
     1015 |         14 |       24000.00
     1030 |         10 |       22000.00
     1024 |         22 |       18500.00
(10 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows that 10 of the 30 visits cost more than the average (KES 16,266.67). I also realised that if new visits are added, the query still works, because it works out the average fresh every time.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Scalar subquery in SELECT&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;To answer the question: what share of the hospital's money did each department bring in?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;department_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;percent_of_hospital&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; department_id | department_total | percent_of_hospital
---------------+------------------+---------------------
            10 |        210100.00 |                43.1
            20 |         97800.00 |                20.0
            30 |        105200.00 |                21.6
            40 |         74900.00 |                15.3
(4 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows that Cardiology (10) brought in 43% of all the money. The subquery gives the hospital total (488,000), and each department's total is divided by it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4.2 Non-correlated vs correlated subqueries&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This was the difference that took me longest to get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;non-correlated&lt;/strong&gt; subquery &lt;strong&gt;stands alone&lt;/strong&gt;. You could copy it out and run it by itself, and PostgreSQL only runs it once. Both examples above are like this.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;correlated&lt;/strong&gt; subquery &lt;strong&gt;borrows a value from the outer query&lt;/strong&gt;, so it can't run by itself. It's worked out again &lt;strong&gt;for each row&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example (correlated), to answer the question: which visits cost more than the average &lt;strong&gt;for their own department&lt;/strong&gt;? I thought it was fairer to compare a Cardiology visit with other Cardiology visits, not with the whole hospital.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                          &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v2&lt;/span&gt;
                          &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | department_id | total_bill_kes
----------+---------------+----------------
     1017 |            10 |       82000.00
     1010 |            10 |       76000.00
     1016 |            20 |       36000.00
     1026 |            20 |       29000.00
     1009 |            20 |       15000.00
     1027 |            30 |       33000.00
     1003 |            30 |       28000.00
     1015 |            30 |       24000.00
     1004 |            40 |       42000.00
     1024 |            40 |       18500.00
(10 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For reference, the department averages are: Cardiology 23,344.44, General Medicine 10,866.67, Orthopedics 17,533.33 and Pediatrics 12,483.33.&lt;/p&gt;

&lt;p&gt;The output shows that visit 1009 (General Medicine, KES 15,000) is &lt;strong&gt;below&lt;/strong&gt; the hospital average, so my earlier query skipped it. But it's &lt;strong&gt;above&lt;/strong&gt; the General Medicine average, so this query includes it. The part that makes it correlated is &lt;code&gt;WHERE v2.department_id = v.department_id&lt;/code&gt;: the inside query borrows &lt;code&gt;v&lt;/code&gt; from the outside query.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Non-correlated&lt;/th&gt;
&lt;th&gt;Correlated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Borrows from the outer query?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can you run it by itself?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How often does it run?&lt;/td&gt;
&lt;td&gt;Once&lt;/td&gt;
&lt;td&gt;Once per outer row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My example&lt;/td&gt;
&lt;td&gt;Above the &lt;strong&gt;hospital&lt;/strong&gt; average&lt;/td&gt;
&lt;td&gt;Above &lt;strong&gt;their department's&lt;/strong&gt; average&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4.3 Nested subqueries&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;nested&lt;/strong&gt; subquery is a subquery &lt;strong&gt;inside another subquery&lt;/strong&gt;. PostgreSQL starts with the innermost one and works its way out.&lt;/p&gt;

&lt;p&gt;My example: which patients saw the doctor who billed the most?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;patient_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;patients&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
            &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;
            &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
            &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;patient_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    patient_name
--------------------
 Amina Otieno
 Catherine Musyoka
 Esther Achieng
 Hellen Jepchirchir
 John Kariuki
 Peter Ouma
(6 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How I read it, from the inside out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The innermost query finds the doctor who billed the most: &lt;strong&gt;101&lt;/strong&gt;, Dr. Brian Mwangi (KES 210,100).&lt;/li&gt;
&lt;li&gt;The middle query finds the patients he saw: 1, 2, 5, 10, 12 and 16.&lt;/li&gt;
&lt;li&gt;The outer query swaps those numbers for names.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4.4 Subqueries with IN&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;IN (subquery)&lt;/code&gt; keeps a row if its value is &lt;strong&gt;on the list&lt;/strong&gt; the subquery gives back.&lt;/p&gt;

&lt;p&gt;My example: which visits were for a chronic (long-term) disease?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diagnosis_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;diagnosis_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;diagnosis_id&lt;/span&gt;
                       &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;diagnosis&lt;/span&gt;
                       &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;diagnosis_category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Chronic Disease'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt; &lt;span class="k"&gt;visit&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;id&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;patient&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;id&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;diagnosis&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;id&lt;/span&gt;
&lt;span class="err"&gt;----------+------------+--------------&lt;/span&gt;
     &lt;span class="mf"&gt;1001&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;1&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;501&lt;/span&gt;
     &lt;span class="mf"&gt;1005&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;5&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;505&lt;/span&gt;
     &lt;span class="mf"&gt;1010&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;         &lt;span class="mf"&gt;10&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;510&lt;/span&gt;
     &lt;span class="mf"&gt;1011&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;1&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;505&lt;/span&gt;
     &lt;span class="mf"&gt;1013&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;         &lt;span class="mf"&gt;12&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;501&lt;/span&gt;
     &lt;span class="mf"&gt;1017&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;         &lt;span class="mf"&gt;16&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;510&lt;/span&gt;
     &lt;span class="mf"&gt;1021&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;2&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;501&lt;/span&gt;
     &lt;span class="mf"&gt;1025&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;5&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;          &lt;span class="mf"&gt;501&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;8&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Findings: the subquery gives back three chronic diseases (501 Hypertension, 505 Diabetes and 510 Heart Failure), and 8 visits were for one of them.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;A trap I read about: NOT IN with an empty value&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;NOT IN&lt;/code&gt; keeps rows that are &lt;strong&gt;not&lt;/strong&gt; on the list. But I learnt that if the list has even one &lt;code&gt;NULL&lt;/code&gt; in it, &lt;code&gt;NOT IN&lt;/code&gt; gives back &lt;strong&gt;nothing at all&lt;/strong&gt;. Our data has no &lt;code&gt;NULL&lt;/code&gt; IDs, so I tested it with a tiny list typed by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;patients_found&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;patients&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; patients_found
----------------
             23
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;patients_found&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;patients&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;patient_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; patients_found
----------------
              0
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our notes taught us that &lt;code&gt;NULL&lt;/code&gt; means "we don't know". PostgreSQL can't be sure patient 3 isn't the unknown value, so it plays it safe and keeps nothing. If a list might contain a &lt;code&gt;NULL&lt;/code&gt;, &lt;code&gt;NOT EXISTS&lt;/code&gt; (next) is the safer choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4.5 Subqueries with EXISTS&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;EXISTS&lt;/code&gt; asks a yes-or-no question: "Is there &lt;strong&gt;at least one&lt;/strong&gt; matching row?" It doesn't care what's in the row, only whether there is one. That's why people usually write &lt;code&gt;SELECT 1&lt;/code&gt; inside it.&lt;/p&gt;

&lt;p&gt;For example, which doctors had at least one emergency visit, and which had none?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;doctors&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
              &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
              &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;
                &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admission_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Emergency'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   doctor_name
-----------------
 DR.Brian Mwangi
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;doctors&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
                  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctor_id&lt;/span&gt;
                    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admission_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Emergency'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    doctor_name
-------------------
 Dr. Sarah Wanjiku
 Dr. Peter Otieno
 Dr. Faith Chebet
(3 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output gives the same answer as my &lt;code&gt;LEFT JOIN&lt;/code&gt; example in Part 2, just asked as a yes-or-no question for each doctor. I also noticed the inside query borrows &lt;code&gt;d.doctor_id&lt;/code&gt; from the outside, so &lt;code&gt;EXISTS&lt;/code&gt; subqueries are nearly always correlated.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;IN&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;EXISTS&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The question it asks&lt;/td&gt;
&lt;td&gt;"Is this value on the list?"&lt;/td&gt;
&lt;td&gt;"Is there at least one matching row?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the subquery gives back&lt;/td&gt;
&lt;td&gt;A list of values&lt;/td&gt;
&lt;td&gt;Rows (only whether there are any matters)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safe with &lt;code&gt;NULL&lt;/code&gt; when you use NOT?&lt;/td&gt;
&lt;td&gt;No, &lt;code&gt;NOT IN&lt;/code&gt; breaks&lt;/td&gt;
&lt;td&gt;Yes, &lt;code&gt;NOT EXISTS&lt;/code&gt; is safe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4.6 Subqueries with ANY and ALL&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ANY&lt;/code&gt; and &lt;code&gt;ALL&lt;/code&gt; compare one value with &lt;strong&gt;a whole list&lt;/strong&gt;, using &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;=&lt;/code&gt; and so on. This is how I made sense of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;gt; ANY (list)&lt;/code&gt; means "bigger than &lt;strong&gt;at least one&lt;/strong&gt; of them", so bigger than the &lt;strong&gt;smallest&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;gt; ALL (list)&lt;/code&gt; means "bigger than &lt;strong&gt;every one&lt;/strong&gt; of them", so bigger than the &lt;strong&gt;biggest&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My example: I compared Cardiology visits (department 10) with Orthopedics bills (department 30). The six Orthopedics bills are 6,500, 6,500, 7,200, 24,000, 28,000 and 33,000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ANY:&lt;/strong&gt; which Cardiology visits cost more than at least one Orthopedics visit?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;
                            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
                            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | total_bill_kes
----------+----------------
     1013 |        7000.00
     1025 |        7600.00
     1030 |       22000.00
     1010 |       76000.00
     1017 |       82000.00
(5 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ALL:&lt;/strong&gt; which Cardiology visits cost more than every Orthopedics visit?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visit_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;
                            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
                            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; visit_id | total_bill_kes
----------+----------------
     1010 |       76000.00
     1017 |       82000.00
(2 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;gt; ANY&lt;/code&gt; kept the 5 Cardiology visits over 6,500, the cheapest Orthopedics bill.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;gt; ALL&lt;/code&gt; kept only the 2 over 33,000, the most expensive Orthopedics bill.&lt;/li&gt;
&lt;li&gt;I also found out that &lt;code&gt;= ANY (...)&lt;/code&gt; means exactly the same as &lt;code&gt;IN (...)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4.7 Subqueries in FROM&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A subquery in &lt;code&gt;FROM&lt;/code&gt; acts like a temporary table. I learnt that it &lt;strong&gt;must&lt;/strong&gt; have a nickname. Here I called it &lt;code&gt;totals&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, what's the average amount billed per doctor? That takes two steps: first each doctor's total, then the average of those totals.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doctor_total&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_per_doctor&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bill_kes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;doctor_total&lt;/span&gt;
      &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;visits&lt;/span&gt;
      &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;doctor_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; avg_per_doctor
----------------
      122000.00
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows that the four doctor totals are 210,100, 97,800, 105,200 and 74,900, and their average is KES 122,000. That's the same number I got with two CTEs in Part 3, which was a nice way to check my work. You can't write &lt;code&gt;AVG(SUM(...))&lt;/code&gt; in one go, so the subquery handles the first step.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4.8 Where a subquery can go: SELECT, FROM and WHERE&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What it needs to give back&lt;/th&gt;
&lt;th&gt;My example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHERE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One value (for &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;=&lt;/code&gt;...), a list (for &lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;ANY&lt;/code&gt;, &lt;code&gt;ALL&lt;/code&gt;), or rows (for &lt;code&gt;EXISTS&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Visits above the average bill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One value for each row&lt;/td&gt;
&lt;td&gt;Each department's share of the total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FROM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A table of rows, with a nickname&lt;/td&gt;
&lt;td&gt;The average of the doctor totals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What I learnt overall&lt;/strong&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;In one line&lt;/th&gt;
&lt;th&gt;A question it helped me answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Window functions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Do maths across a group &lt;strong&gt;without losing any rows&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;"Rank visits by satisfaction", "days since the last visit"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Joins&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Combine tables&lt;/strong&gt; using matching IDs&lt;/td&gt;
&lt;td&gt;"Show each visit with the patient's and doctor's names"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CTEs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Name the steps&lt;/strong&gt; of a query so it reads top to bottom&lt;/td&gt;
&lt;td&gt;"Which doctors billed more than the average doctor?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subqueries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;A small question inside a bigger one&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Which visits cost more than their department's average?"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>beginners</category>
      <category>database</category>
    </item>
    <item>
      <title>Excel Dashboards: Product Analysis for Jumia Kenya</title>
      <dc:creator>Zena Muhoro</dc:creator>
      <pubDate>Sat, 05 Sep 2026 18:09:55 +0000</pubDate>
      <link>https://dev.to/zena_muhoro_/excel-dashboards-product-analysis-for-jumia-kenya-cjk</link>
      <guid>https://dev.to/zena_muhoro_/excel-dashboards-product-analysis-for-jumia-kenya-cjk</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;E-commerce platforms like Jumia collect large amounts of data daily on prices, discounts, reviews, and ratings. However, this data is only useful when it is broken down and analyzed to identify trends and insights that can help improve pricing, promotions, and customer engagement.&lt;/p&gt;

&lt;p&gt;For this project, I analyzed a dataset of 112 products listed on &lt;strong&gt;Jumia&lt;/strong&gt;, to understand how pricing and discounting relate to customer reviews and ratings. The goal was to move from a raw dataset to a clean, interactive Excel dashboard that surfaces real business insights to be used by the company's managers and directors to make desions.&lt;/p&gt;

&lt;p&gt;This article walks you through my process: cleaning the data, enriching it with calculated fields, running the analysis, building the dashboard, and the insights that came out of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6z7639ajio0pyukqe8z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6z7639ajio0pyukqe8z.png" alt=" " width="800" height="632"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Dataset Description
&lt;/h2&gt;

&lt;p&gt;The dataset contained six fields per product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product&lt;/strong&gt; — the product name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current Price&lt;/strong&gt; — the current selling price, in KSh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old Price&lt;/strong&gt; — the price before any discount&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discount&lt;/strong&gt; — the discount percentage applied&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; — the number of customer reviews&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rating&lt;/strong&gt; — the average customer rating, out of 5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At a glance, the raw data had a few problems typical of scraped or exported e-commerce data: prices stored as text with currency symbols and commas, ratings embedded in strings like &lt;code&gt;"4.5 out of 5"&lt;/code&gt; instead of numbers, and some review counts stored as negative values. None of this was usable for analysis until it was cleaned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Cleaning and Preparation
&lt;/h2&gt;

&lt;p&gt;Before any analysis could happen, I worked through a standard data-cleaning checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Numeric conversion&lt;/strong&gt; — stripped &lt;code&gt;KSh&lt;/code&gt;, commas, and other non-numeric characters from &lt;code&gt;Current Price&lt;/code&gt; and &lt;code&gt;Old Price&lt;/code&gt;, converting both to clean numeric columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rating extraction&lt;/strong&gt; — parsed the numeric rating out of the &lt;code&gt;"X out of 5"&lt;/code&gt; text format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixing sign errors&lt;/strong&gt; — corrected review counts that had been stored as negative numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing values&lt;/strong&gt; — checked every column for gaps. Notably, &lt;strong&gt;55 of the 112 products (about 49%) had no rating at all&lt;/strong&gt;. Rather than dropping these rows (which would have thrown away real pricing and discount data), I labeled them &lt;code&gt;"Not Provided"&lt;/code&gt; in a rating category column so they could be handled explicitly instead of silently distorting averages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicates and inconsistencies&lt;/strong&gt; — scanned for duplicate or near-duplicate product entries and inconsistent formatting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F803nptxstjfc880rf653.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F803nptxstjfc880rf653.png" alt=" " width="799" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enrichment: New Calculated Columns
&lt;/h3&gt;

&lt;p&gt;Once the base data was clean, I added several derived columns to make deeper analysis possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discount Amount&lt;/strong&gt; = &lt;code&gt;Old Price − Current Price&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rating Category&lt;/strong&gt;: Poor (below 3), Average (3–4), Excellent (above 4.5) &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Formula used:&lt;br&gt;
=IF(ISBLANK(I13),"",IF(I13&amp;lt;3,"Poor",IF(I13&amp;gt;=4.5,"Excellent","Average")))&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discount Category&lt;/strong&gt;: Low (below 20%), Medium (20–40%), High (above 40%)&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Formula used:&lt;br&gt;
=IF(F2&amp;lt;20%,"Low Discount",IF(F2&amp;lt;=40%,"Medium Discount", "High Discount"))&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Price Category&lt;/strong&gt;: Low, Medium, High — based on price quartiles (roughly KSh 493 and KSh 1,670 as breakpoints)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Formula used: &lt;br&gt;
=IF([@[Current Price]]="","Missing",IF([@[Current Price]]&amp;lt;Data_Analysis!$B$21,"Low Price",IF([@[Current Price]]&amp;lt;Data_Analysis!$B$22,"Medium Price","High Price")))&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Excel Techniques and Analysis
&lt;/h2&gt;

&lt;p&gt;With clean, enriched data in place, I moved to analysis using core Excel tools: formulas (&lt;code&gt;AVERAGE&lt;/code&gt;, &lt;code&gt;MAX&lt;/code&gt;, &lt;code&gt;MIN&lt;/code&gt;, &lt;code&gt;CORREL&lt;/code&gt;, &lt;code&gt;COUNTIFS&lt;/code&gt;), PivotTables, and Pivot Charts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Descriptive statistics&lt;/strong&gt; across the 112 products:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total products&lt;/td&gt;
&lt;td&gt;112&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total reviews&lt;/td&gt;
&lt;td&gt;723&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average current price&lt;/td&gt;
&lt;td&gt;KSh 1,186.89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average old price&lt;/td&gt;
&lt;td&gt;KSh 1,811.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average discount&lt;/td&gt;
&lt;td&gt;36.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average rating (where available)&lt;/td&gt;
&lt;td&gt;3.89 / 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Most expensive product&lt;/td&gt;
&lt;td&gt;32PCS Portable Cordless Drill Set — KSh 3,750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Least expensive product&lt;/td&gt;
&lt;td&gt;3PCS Single Head Knitting Crochet Sweater Needle Set — KSh 38&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Correlation analysis&lt;/strong&gt; using &lt;code&gt;CORREL()&lt;/code&gt; to test three relationships:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Relationship&lt;/th&gt;
&lt;th&gt;Correlation&lt;/th&gt;
&lt;th&gt;Reading&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Discount % vs. Reviews&lt;/td&gt;
&lt;td&gt;−0.14&lt;/td&gt;
&lt;td&gt;No relationship — discounts don't drive more reviews&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rating vs. Reviews&lt;/td&gt;
&lt;td&gt;+0.06&lt;/td&gt;
&lt;td&gt;Essentially no relationship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price vs. Rating&lt;/td&gt;
&lt;td&gt;+0.11&lt;/td&gt;
&lt;td&gt;Weak — expensive products aren't rated meaningfully higher&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These weak-to-negligible correlations were one of the more interesting findings of the project — the intuitive assumption that "bigger discount = more engagement" simply didn't hold up in this dataset.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv98gwwzchxgyfx3w3r5g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv98gwwzchxgyfx3w3r5g.png" alt=" " width="799" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Category breakdowns&lt;/strong&gt; (via PivotTables):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rating: 23 Excellent, 22 Average, 12 Poor, 55 Not Provided&lt;/li&gt;
&lt;li&gt;Discount: 62 High, 32 Medium, 18 Low&lt;/li&gt;
&lt;li&gt;Price: 88 High, 7 Medium, 17 Low&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Seller-performance flags&lt;/strong&gt; — I built rule-based flags with &lt;code&gt;COUNTIFS&lt;/code&gt; to segment products by behavior pattern:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strong customer engagement&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;Reviews ≥ 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High discount + low engagement&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;Discount &amp;gt; 40% AND Reviews &amp;lt; 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many reviews + average rating&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Reviews ≥ 8 AND Rating 3.0–4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High discount + low rating&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Discount &amp;gt; 40% AND Rating &amp;lt; 3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Dashboard Creation Process
&lt;/h2&gt;

&lt;p&gt;The final step was pulling everything into a single interactive dashboard sheet. I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;KPI cards&lt;/strong&gt; for total products, average price, average discount, average rating, and total reviews&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PivotCharts&lt;/strong&gt; for top 10 products by rating, by reviews, and by discount&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scatter/column charts&lt;/strong&gt; to visualize the discount-vs-review, rating-vs-review, and price-vs-rating relationships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pie and column charts&lt;/strong&gt; for the rating, discount, and price category breakdowns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional formatting&lt;/strong&gt; to visually flag high-discount/low-rating and other risk products directly in the tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slicers&lt;/strong&gt; connected to all PivotTables/charts for Rating Category, Discount Category, and Price Category, so the whole dashboard can be filtered interactively&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;(Screenshot: final dashboard with slicers)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Insights and Business Recommendations
&lt;/h2&gt;

&lt;p&gt;A few findings stood out as genuinely useful for decision-making, not just descriptive trivia:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discounting alone doesn't drive engagement.&lt;/strong&gt; Nearly half the catalog (50 of 112 products) pairs a discount above 40% with fewer than 8 reviews. Sellers relying on discount depth to generate interest aren't seeing it pay off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Popularity ≠ quality.&lt;/strong&gt; The single most-reviewed product (a cordless vacuum cleaner, 69 reviews) has a "Poor" rating of 2.8/5 — visibility without customer satisfaction is a churn risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price doesn't buy trust.&lt;/strong&gt; The weak price–rating correlation shows customers aren't rating expensive items more favorably just because they cost more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rating data has a real gap.&lt;/strong&gt; With 49% of products unrated, both buyers and analysts are working with an incomplete picture of product quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High discount + low rating (10 products) is a quality issue, not a pricing issue&lt;/strong&gt; — these listings are already discounted heavily and still underperforming, so deeper discounts are unlikely to fix the underlying problem.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Recommendations for Jumia sellers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't lean on discount depth as an engagement strategy — invest in better listings (photos, descriptions, search visibility) instead.&lt;/li&gt;
&lt;li&gt;Encourage post-purchase reviews, especially for high-selling but unrated products, to close the 49% rating gap.&lt;/li&gt;
&lt;li&gt;Treat high-review/low-rating products as a quality-control priority, not a marketing one.&lt;/li&gt;
&lt;li&gt;Be skeptical of "perfect" 5.0 ratings built on only 1–2 reviews — they're not statistically meaningful yet.&lt;/li&gt;
&lt;li&gt;Escalate the 10 "high discount + low rating" products for product-level review rather than further discounting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This project reinforced something that comes up in almost every real-world dataset: the obvious assumption (discounts drive engagement, price signals quality) often doesn't survive contact with the actual numbers. Cleaning the data properly — especially handling the missing ratings honestly instead of hiding them — was what made the correlation analysis trustworthy enough to act on.&lt;/p&gt;

&lt;p&gt;The full workbook, including the raw data, cleaned data, analysis sheets, and interactive dashboard, is available in the GitHub repository linked below.&lt;/p&gt;

&lt;p&gt;GitHub repo: &lt;a href="https://github.com/zenamuhoro-dot/jumia-product-performance/tree/main" rel="noopener noreferrer"&gt;https://github.com/zenamuhoro-dot/jumia-product-performance/tree/main&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>datascience</category>
      <category>analytics</category>
      <category>microsoft</category>
    </item>
    <item>
      <title># My First Week Learning Git</title>
      <dc:creator>Zena Muhoro</dc:creator>
      <pubDate>Sun, 23 Aug 2026 07:35:00 +0000</pubDate>
      <link>https://dev.to/zena_muhoro_/-my-first-week-learning-git-3n1c</link>
      <guid>https://dev.to/zena_muhoro_/-my-first-week-learning-git-3n1c</guid>
      <description>&lt;p&gt;This week, I was introduced to Git and GitHub as part of my Data Science learning journey.&lt;/p&gt;

&lt;p&gt;At first, I found Git a little confusing. There were several commands to learn, and I wasn't sure how they all fitted together.&lt;/p&gt;

&lt;p&gt;After practising, I started to understand that Git is mainly about &lt;strong&gt;keeping track of changes to my work&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The basic process I learned was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Working Directory → Staging → Commit → Push&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Working Directory
&lt;/h2&gt;

&lt;p&gt;The working directory is basically where I do my work on my computer.&lt;/p&gt;

&lt;p&gt;When I create or edit a file, the changes are made here first.&lt;/p&gt;

&lt;p&gt;One of the first commands I learned was:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>beginners</category>
      <category>git</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
