<?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: Mark Maina</title>
    <description>The latest articles on DEV Community by Mark Maina (@mark_maina_a29c3c051c10aa).</description>
    <link>https://dev.to/mark_maina_a29c3c051c10aa</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%2F4070938%2F5194088e-8ab9-443d-8ec4-5d3de6856faf.png</url>
      <title>DEV Community: Mark Maina</title>
      <link>https://dev.to/mark_maina_a29c3c051c10aa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mark_maina_a29c3c051c10aa"/>
    <language>en</language>
    <item>
      <title>Advanced SQL in Healthcare Analytics: A Comprehensive Technical Guide Using Our Hospital Dataset</title>
      <dc:creator>Mark Maina</dc:creator>
      <pubDate>Sun, 18 Oct 2026 21:38:25 +0000</pubDate>
      <link>https://dev.to/mark_maina_a29c3c051c10aa/advanced-sql-in-healthcare-analytics-a-comprehensive-technical-guide-using-our-hospital-dataset-1h71</link>
      <guid>https://dev.to/mark_maina_a29c3c051c10aa/advanced-sql-in-healthcare-analytics-a-comprehensive-technical-guide-using-our-hospital-dataset-1h71</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In modern healthcare management, data is critical to improving patient outcomes, optimizing hospital operations, and managing financial performance. Using our hospital dataset—which encompasses tables for &lt;code&gt;patients&lt;/code&gt;, &lt;code&gt;doctors&lt;/code&gt;, &lt;code&gt;department&lt;/code&gt;, &lt;code&gt;diagnosis&lt;/code&gt;, &lt;code&gt;wards&lt;/code&gt;, &lt;code&gt;procedures&lt;/code&gt;, and &lt;code&gt;visits&lt;/code&gt;—this technical article explores advanced SQL concepts. We will cover &lt;strong&gt;Window Functions&lt;/strong&gt;, &lt;strong&gt;Joins&lt;/strong&gt;, &lt;strong&gt;Common Table Expressions (CTEs)&lt;/strong&gt;, and &lt;strong&gt;Subqueries&lt;/strong&gt;, demonstrating their practical applications through real-world hospital scenarios.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. SQL Window Functions
&lt;/h2&gt;

&lt;p&gt;Window functions perform calculations across a set of table rows that are related to the current row. Unlike aggregate functions used with &lt;code&gt;GROUP BY&lt;/code&gt;, window functions do not collapse rows into a single summary row. Instead, every individual row retains its identity while displaying the calculated window result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Anatomy of a Window Function
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OVER()&lt;/code&gt; Clause&lt;/strong&gt;: Defines the window or set of rows over which the function operates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PARTITION BY&lt;/code&gt;&lt;/strong&gt;: Divides the result set into partitions (groups) to which the window function is applied independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ORDER BY&lt;/code&gt;&lt;/strong&gt;: Determines the logical order of rows within each partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window Frames&lt;/strong&gt;: Subsets of rows within a partition (e.g., &lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;/code&gt;) that define precisely which rows feed into the calculation for the current row.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Types of Window Functions
&lt;/h3&gt;

&lt;h4&gt;
  
  
  A. Ranking and Distribution Functions
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ROW_NUMBER()&lt;/code&gt;&lt;/strong&gt;: Assigns a unique sequential integer to rows within a partition starting at 1, regardless of ties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RANK()&lt;/code&gt;&lt;/strong&gt;: Assigns a rank to each row within a partition, leaving gaps in ranking numbers when there are ties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DENSE_RANK()&lt;/code&gt;&lt;/strong&gt;: Assigns ranks without gaps; tied rows share the same rank, and the next rank is consecutive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NTILE(n)&lt;/code&gt;&lt;/strong&gt;: Distributes rows in an ordered partition into a specified number of roughly equal groups (&lt;code&gt;n&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hospital Scenario:&lt;/strong&gt; Ranking patient visits by their total bill within each department to identify high-cost medical interactions.&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;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;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;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;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;rank_val&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;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;dense_rank_val&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;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_tertile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;morning&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt;: This query evaluates every hospital visit partitioned by &lt;code&gt;department_id&lt;/code&gt;. It assigns a row number, rank, dense rank, and splits visits into three billing tiers (&lt;code&gt;NTILE(3)&lt;/code&gt;) ordered from highest to lowest bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expected Result&lt;/strong&gt;: Each department will list its visits sorted by bill amount. Ties in &lt;code&gt;total_bill_kes&lt;/code&gt; receive identical &lt;code&gt;RANK()&lt;/code&gt; and &lt;code&gt;DENSE_RANK()&lt;/code&gt; values, but &lt;code&gt;ROW_NUMBER()&lt;/code&gt; breaks ties arbitrarily to ensure uniqueness.&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  B. Value Functions
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LAG()&lt;/code&gt;&lt;/strong&gt;: Accesses data from a previous row at a specified offset within the partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LEAD()&lt;/code&gt;&lt;/strong&gt;: Accesses data from a subsequent row at a specified offset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FIRST_VALUE()&lt;/code&gt;&lt;/strong&gt;: Returns the value evaluated at the very first row of the window frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LAST_VALUE()&lt;/code&gt;&lt;/strong&gt;: Returns the value evaluated at the last row of the window frame.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hospital Scenario:&lt;/strong&gt; Tracking patient admission history by comparing a patient's current visit bill with their immediately preceding visit 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;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;total_bill_kes&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;total_bill_kes&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="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_bill&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;total_bill_kes&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="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_bill&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;morning&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt;: For every patient, &lt;code&gt;LAG&lt;/code&gt; retrieves the bill from their chronological previous visit, and &lt;code&gt;LEAD&lt;/code&gt; retrieves the bill from their next visit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expected Result&lt;/strong&gt;: Patients with multiple visits will show their historical billing trend side-by-side with current records, helping administrators monitor cost escalation per patient over time.&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  C. Aggregate Window Functions
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SUM()&lt;/code&gt; / &lt;code&gt;AVG()&lt;/code&gt; / &lt;code&gt;COUNT()&lt;/code&gt; / &lt;code&gt;MIN()&lt;/code&gt; / `MAX()&lt;/strong&gt;`: Compute running totals, moving averages, or running maximums without grouping away row-level details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hospital Scenario:&lt;/strong&gt; Calculating a running total of hospital revenue accumulated date 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;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="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;cumulative_revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;morning&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt;: The &lt;code&gt;SUM()&lt;/code&gt; function uses a framing clause (&lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;/code&gt;) to aggregate all bills from the earliest visit date up to the current row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expected Result&lt;/strong&gt;: A running balance of total hospital earnings streamed chronologically across all logged patient visits.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. SQL Joins
&lt;/h2&gt;

&lt;p&gt;SQL Joins combine columns from two or more tables based on a related column between them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overview of Join Types
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;INNER JOIN&lt;/code&gt;&lt;/strong&gt;: Returns only rows where there is a match in both tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LEFT JOIN&lt;/code&gt;&lt;/strong&gt;: Returns all rows from the left table, and matching rows from the right table (unmatched right columns return &lt;code&gt;NULL&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RIGHT JOIN&lt;/code&gt;&lt;/strong&gt;: Returns all rows from the right table, and matching rows from the left table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FULL OUTER JOIN&lt;/code&gt;&lt;/strong&gt;: Returns all rows when there is a match in either the left or right table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CROSS JOIN&lt;/code&gt;&lt;/strong&gt;: Produces the Cartesian product (every combination of rows) of both tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELF JOIN&lt;/code&gt;&lt;/strong&gt;: A table joined to itself, useful for hierarchical data or comparing rows within the same table.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Practical Hospital Comparison
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Inner Join vs. Left Join
&lt;/h4&gt;

&lt;p&gt;Suppose we want to list all doctors and their corresponding visits. An &lt;code&gt;INNER JOIN&lt;/code&gt; excludes doctors who have no recorded visits, whereas a &lt;code&gt;LEFT JOIN&lt;/code&gt; includes all doctors.&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="c1"&gt;-- INNER JOIN: Only doctors with recorded visits&lt;/span&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;total_bill_kes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;morning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctors&lt;/span&gt; &lt;span class="n"&gt;d&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;morning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visits&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="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- LEFT JOIN: All doctors, including those with zero visits&lt;/span&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;total_bill_kes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;morning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doctors&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;morning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visits&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="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt;: The &lt;code&gt;INNER JOIN&lt;/code&gt; filters out any doctor missing from the &lt;code&gt;visits&lt;/code&gt; table. The &lt;code&gt;LEFT JOIN&lt;/code&gt; preserves every doctor from the &lt;code&gt;doctors&lt;/code&gt; table, displaying &lt;code&gt;NULL&lt;/code&gt; for &lt;code&gt;visit_id&lt;/code&gt; and &lt;code&gt;total_bill_kes&lt;/code&gt; if a doctor has not handled any visits.&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Self Join Example
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Hospital Scenario:&lt;/strong&gt; Finding pairs of patients who share the same age and county of origin.&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_one&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_two&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;patient_age&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;morning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patients&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;morning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patients&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;patient_age&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;patient_age&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;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="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt;: The &lt;code&gt;patients&lt;/code&gt; table is joined to itself (&lt;code&gt;p1&lt;/code&gt; and &lt;code&gt;p2&lt;/code&gt;) to pair patients matching in age and county. The condition &lt;code&gt;p1.patient_id &amp;lt; p2.patient_id&lt;/code&gt; prevents duplicate inverse pairings and self-matching.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Common Table Expressions (CTEs)
&lt;/h2&gt;

&lt;p&gt;A Common Table Expression (CTE) is a temporary named result set defined within the execution scope of a single &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, or &lt;code&gt;DELETE&lt;/code&gt; statement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Types and Uses of CTEs
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Basic CTE&lt;/strong&gt;: Simplifies complex queries by breaking logic into readable named blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple CTEs&lt;/strong&gt;: Chaining several temporary result sets using a single &lt;code&gt;WITH&lt;/code&gt; clause separated by commas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive CTE&lt;/strong&gt;: A CTE that references itself, ideal for organizational hierarchies or sequence generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CTEs with Joins and Aggregations&lt;/strong&gt;: Pre-aggregating data before joining it back to dimension tables.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Practical Example: Multiple CTEs with Aggregations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Hospital Scenario:&lt;/strong&gt; Identifying departments whose average patient satisfaction score is above the overall hospital average.&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;DeptAvg&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;AVG&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_satisfaction&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;morning&lt;/span&gt;&lt;span class="p"&gt;.&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="n"&gt;OverallAvg&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;patient_satisfaction_score&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;overall_satisfaction&lt;/span&gt; 
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;morning&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="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;department_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;da&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;avg_satisfaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;oa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overall_satisfaction&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DeptAvg&lt;/span&gt; &lt;span class="n"&gt;da&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;OverallAvg&lt;/span&gt; &lt;span class="n"&gt;oa&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;morning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;department&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;da&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;d&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;da&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;avg_satisfaction&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;oa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overall_satisfaction&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;
&lt;strong&gt;Explanation&lt;/strong&gt;:&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DeptAvg&lt;/code&gt; calculates the mean satisfaction score per department.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OverallAvg&lt;/code&gt; computes the global hospital satisfaction average.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The main query joins these CTEs with the &lt;code&gt;department&lt;/code&gt; table to filter out departments performing above average.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expected Result&lt;/strong&gt;: A clean, readable list of high-performing departments that exceed the facility's baseline patient satisfaction mark.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  CTEs vs. Subqueries
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Readability&lt;/strong&gt;: CTEs improve query structure by separating sub-queries into named logical steps at the top of the statement. Subqueries nested deeply inside &lt;code&gt;FROM&lt;/code&gt; or &lt;code&gt;WHERE&lt;/code&gt; clauses can be hard to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusability&lt;/strong&gt;: A CTE can be referenced multiple times within the main query body, whereas standard inline subqueries cannot be reused without rewriting them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursion&lt;/strong&gt;: CTEs support recursive execution (&lt;code&gt;WITH RECURSIVE&lt;/code&gt;), which subqueries cannot do natively.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. SQL Subqueries
&lt;/h2&gt;

&lt;p&gt;A subquery is a query nested inside another SQL statement (such as &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, or &lt;code&gt;WHERE&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Types of Subqueries
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalar Subquery&lt;/strong&gt;: Returns exactly one row and one column (a single value).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-Correlated Subquery&lt;/strong&gt;: Independent of the outer query and can be executed entirely on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlated Subquery&lt;/strong&gt;: References columns from the outer query; it executes once for every row processed by the outer query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested Subquery&lt;/strong&gt;: A subquery inside another subquery.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Subqueries Across Clauses
&lt;/h3&gt;

&lt;h4&gt;
  
  
  A. Subquery in the &lt;code&gt;SELECT&lt;/code&gt; Clause (Scalar)
&lt;/h4&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="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;morning&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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;overall_avg_bill&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;morning&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt;: Computes the overall average hospital bill dynamically as a scalar value attached to every individual visit row for side-by-side comparison.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  B. Subquery in the &lt;code&gt;FROM&lt;/code&gt; Clause (Derived Table)
&lt;/h4&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;dept_summary&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;dept_summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_revenue&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_revenue&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;morning&lt;/span&gt;&lt;span class="p"&gt;.&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;dept_summary&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;dept_summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_revenue&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50000&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;
&lt;strong&gt;Explanation&lt;/strong&gt;: Uses an inline subquery in the &lt;code&gt;FROM&lt;/code&gt; clause as a temporary derived table to filter departments generating over KES 50,000 in total revenue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  C. Subquery in the &lt;code&gt;WHERE&lt;/code&gt; Clause (&lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;EXISTS&lt;/code&gt;, &lt;code&gt;ANY&lt;/code&gt;, &lt;code&gt;ALL&lt;/code&gt;)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;IN&lt;/code&gt;&lt;/strong&gt;: Matches values against a set returned by a subquery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXISTS&lt;/code&gt;&lt;/strong&gt;: Tests for the existence of any rows in a subquery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ANY&lt;/code&gt; / `ALL&lt;/strong&gt;`: Compares a value to a set of numeric values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hospital Scenario:&lt;/strong&gt; Finding patients who have visited for emergency care using an &lt;code&gt;EXISTS&lt;/code&gt; subquery.&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;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="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;morning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patients&lt;/span&gt; &lt;span class="n"&gt;p&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;morning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visits&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;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;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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation&lt;/strong&gt;: This correlated subquery checks the &lt;code&gt;visits&lt;/code&gt; table for each patient to see if an emergency admission exists. If true, the patient details are returned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expected Result&lt;/strong&gt;: A targeted list of patients who have experienced emergency admissions, useful for clinical review and emergency preparedness audits.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>Data Relationships, Data Modelling Schemas, and Joins in Power BI</title>
      <dc:creator>Mark Maina</dc:creator>
      <pubDate>Sun, 13 Sep 2026 12:13:01 +0000</pubDate>
      <link>https://dev.to/mark_maina_a29c3c051c10aa/data-relationships-data-modelling-schemas-and-joins-in-power-bi-1lo</link>
      <guid>https://dev.to/mark_maina_a29c3c051c10aa/data-relationships-data-modelling-schemas-and-joins-in-power-bi-1lo</guid>
      <description>&lt;p&gt;Introduction&lt;/p&gt;

&lt;p&gt;A successful Power BI solution depends on more than creating attractive charts and dashboards. The underlying data model determines how accurately data can be analysed, how easily DAX measures can be written, and how efficiently reports perform as the volume of data increases. Data modelling is therefore one of the most important stages in building a Power BI solution.&lt;/p&gt;

&lt;p&gt;A well-designed model organizes data into logical tables, defines relationships between those tables, and controls how filters move through the model. It also reduces unnecessary duplication and makes reports easier to understand and maintain.&lt;/p&gt;

&lt;p&gt;This article explains three common modelling approaches:Flat Table, Star Schema, and Snowflake Schema and examines fact and dimension tables, relationships, filter direction, Power Query joins, and the difference between merging tables and creating model relationships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flat Table
&lt;/h2&gt;

&lt;p&gt;A flat table stores most or all required information in one table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟩 SALES
┌─────────┬──────────┬─────────┬────────────┬────────────┐
│ OrderID │ Customer │ Product │ Date       │ SalesAmount│
├─────────┼──────────┼─────────┼────────────┼────────────┤
│ 1001    │ John     │ Laptop  │ 01-Jan-26  │ $1,200     │
│ 1002    │ Mary     │ Monitor │ 02-Jan-26  │ $500       │
│ 1003    │ John     │ Mouse   │ 03-Jan-26  │ $50        │
└─────────┴──────────┴─────────┴────────────┴────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The major advantage is simplicity. There are no relationships to configure, making the approach suitable for small datasets, prototypes, or simple reports.&lt;/p&gt;

&lt;p&gt;However, flat tables often contain substantial &lt;strong&gt;data redundancy&lt;/strong&gt;. Customer names, product descriptions, locations, and other attributes may be repeated across thousands or millions of transactions. This increases model size and makes changes harder to manage.&lt;/p&gt;

&lt;p&gt;For Power BI, a flat table can work well for small datasets, but it becomes less attractive as data volume and analytical requirements increase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Star Schema
&lt;/h2&gt;

&lt;p&gt;A star schema separates business events into a central &lt;strong&gt;fact table&lt;/strong&gt; and descriptive information into surrounding &lt;strong&gt;dimension tables&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         🟢 DimDate
                             │
                             │ 1:*
                             ▼
🔵 DimCustomer ── 1:* ── 🟣 FactSales ── *:1 ── 🟠 DimProduct
                             ▲
                             │
                             │ 1:*
                             │
                       🔴 DimLocation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fact table contains transactions, while dimensions describe those transactions. This is generally the preferred structure for Power BI because it provides clear filter paths, good performance, and straightforward DAX.&lt;/p&gt;

&lt;p&gt;Its main disadvantages are that it requires more modelling knowledge and relationships must be configured correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snowflake Schema
&lt;/h2&gt;

&lt;p&gt;A snowflake schema extends the star schema by further normalizing dimensions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟢 DimCountry
     │
     │ 1:*
     ▼
🔴 DimLocation ── 1:* ── 🟣 FactSales ── *:1 ── 🟠 DimProduct
                           ▲
                           │
                           │ *:1
                           │
                     🔵 DimCustomer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, instead of storing Country directly in &lt;code&gt;DimLocation&lt;/code&gt;, a separate &lt;code&gt;DimCountry&lt;/code&gt; table may contain country information.&lt;/p&gt;

&lt;p&gt;Snowflake schemas can reduce redundancy, especially in large hierarchical dimensions, but they introduce additional relationships and longer filter paths. Consequently, they can make DAX and troubleshooting more complicated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparison:&lt;/strong&gt; A flat table is simple but potentially redundant; a star schema balances simplicity and performance; a snowflake schema provides greater normalization but increases model complexity.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. Fact Tables and Dimension Tables
&lt;/h1&gt;

&lt;p&gt;The distinction between fact and dimension tables is fundamental to dimensional modelling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fact Tables
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;fact table&lt;/strong&gt; records measurable business events. It normally contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Foreign keys to dimensions&lt;/li&gt;
&lt;li&gt;Transaction or event identifiers&lt;/li&gt;
&lt;li&gt;Numeric values&lt;/li&gt;
&lt;li&gt;Quantities&lt;/li&gt;
&lt;li&gt;Costs&lt;/li&gt;
&lt;li&gt;Revenue&lt;/li&gt;
&lt;li&gt;Discounts&lt;/li&gt;
&lt;li&gt;Other measures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FactSales&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FactOrders&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FactTransactions&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important concept when designing a fact table is &lt;strong&gt;grain&lt;/strong&gt; or &lt;strong&gt;granularity&lt;/strong&gt;. Grain defines exactly what one row represents.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One row in &lt;code&gt;FactSales&lt;/code&gt; represents one product sold on one sales transaction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If this rule is unclear, calculations can become inconsistent because different rows may represent different levels of detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dimension Tables
&lt;/h3&gt;

&lt;p&gt;Dimension tables contain descriptive information used to categorize and filter facts.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DimCustomer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DimProduct&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DimDate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DimLocation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, &lt;code&gt;DimProduct&lt;/code&gt; might contain ProductID, ProductName, Category, Brand, and ProductGroup.&lt;/p&gt;

&lt;p&gt;A typical business model might therefore look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 🟢 DimDate
                     │
                     │
🔵 DimCustomer ── 🟣 FactSales ── 🟠 DimProduct
                     │
                     │
                 🔴 DimLocation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fact table answers questions such as &lt;strong&gt;"How much was sold?"&lt;/strong&gt;, while dimensions answer &lt;strong&gt;"Who bought it?", "What was sold?", "When?", and "Where?"&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Relationships in Power BI
&lt;/h1&gt;

&lt;p&gt;A relationship defines how two tables are connected using a common column. Relationships are necessary because business information is normally distributed across multiple tables.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;DimCustomer&lt;/code&gt; contains customer details while &lt;code&gt;FactSales&lt;/code&gt; contains transactions. &lt;code&gt;CustomerID&lt;/code&gt; connects the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  One-to-Many (1:*)
&lt;/h2&gt;

&lt;p&gt;This is the most common relationship in a Power BI star schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟢 DimCustomer                    🟣 FactSales
CustomerID (Unique)   1 ─────── * CustomerID (Repeated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One customer can have many sales transactions. Therefore, CustomerID occurs once in DimCustomer but can occur many times in FactSales.&lt;/p&gt;

&lt;p&gt;This should normally be the default relationship for dimension-to-fact modelling.&lt;/p&gt;

&lt;h2&gt;
  
  
  One-to-One (1:1)
&lt;/h2&gt;

&lt;p&gt;Each record in one table corresponds to exactly one record in another.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟢 CustomerProfile
CustomerID     1 ───── 1     🟣 CustomerAccount
CustomerID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may be appropriate when two tables contain different sets of attributes for exactly the same entities. However, it should not be used simply because two tables happen to contain unique IDs.&lt;/p&gt;

&lt;p&gt;If the tables represent the same business entity, combining them may sometimes be simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Many-to-Many (&lt;em&gt;:&lt;/em&gt;)
&lt;/h2&gt;

&lt;p&gt;Multiple records in one table can correspond to multiple records in another.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟠 Students       * ───── *       🟣 Courses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, one student can take many courses, while each course can contain many students.&lt;/p&gt;

&lt;p&gt;Many-to-many relationships can be valid, but they should be used carefully because they can create ambiguous filtering and unexpected aggregation results. A bridge table is often preferable for complex many-to-many scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keys and Referential Integrity
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;primary key&lt;/strong&gt; uniquely identifies a row in a table. A &lt;strong&gt;foreign key&lt;/strong&gt; references a key in another table.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DimCustomer
CustomerID
101
102
103
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;versus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FactSales
CustomerID
101
101
102
101
103
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DimCustomer[CustomerID]&lt;/code&gt; must be unique, while &lt;code&gt;FactSales[CustomerID]&lt;/code&gt; can repeat because the same customer can make multiple purchases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referential integrity&lt;/strong&gt; means foreign-key values should correctly correspond to records in the related dimension. Missing or unmatched keys can produce blank or unexpected results in reports.&lt;/p&gt;

&lt;p&gt;Power BI relationships can also be &lt;strong&gt;active&lt;/strong&gt; or &lt;strong&gt;inactive&lt;/strong&gt;. An active relationship is used automatically when filters propagate. An inactive relationship can be activated within specific DAX calculations using functions such as &lt;code&gt;USERELATIONSHIP&lt;/code&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. Filter Direction
&lt;/h1&gt;

&lt;p&gt;Filter direction determines how filtering moves between related tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single-Direction Filtering
&lt;/h3&gt;

&lt;p&gt;This is generally recommended for star schemas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟢 DimProduct ────────► 🟣 FactSales
       Filter              Results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose &lt;code&gt;DimProduct[Category] = "Electronics"&lt;/code&gt; is selected. The filter travels from &lt;code&gt;DimProduct&lt;/code&gt; to &lt;code&gt;FactSales&lt;/code&gt;, restricting sales to electronics products.&lt;/p&gt;

&lt;p&gt;This creates a predictable model with clear filter paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  Both/Bidirectional Filtering
&lt;/h3&gt;

&lt;p&gt;Bidirectional filtering allows filters to move in both directions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟢 DimProduct ◄──────► 🟣 FactSales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although useful in specific scenarios, it should not be enabled unnecessarily. With several related tables, bidirectional relationships can create &lt;strong&gt;ambiguous filter paths&lt;/strong&gt;, unexpected results, and additional model complexity.&lt;/p&gt;

&lt;p&gt;For most dimensional models, filtering from dimensions toward facts provides the clearest design.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Joins in Power Query
&lt;/h1&gt;

&lt;p&gt;A &lt;strong&gt;join&lt;/strong&gt; combines information from two tables based on matching columns. In Power Query, this operation is performed using &lt;strong&gt;Merge Queries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;h3&gt;
  
  
  Customers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;CustomerName&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;David&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Orders
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Customer 4 does not exist in the Customers table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inner Join
&lt;/h2&gt;

&lt;p&gt;Returns only matching records from both tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customers ∩ Orders
        🟢
      MATCHES
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected customers: &lt;strong&gt;John and Mary&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;CustomerName&lt;/th&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Left Outer Join
&lt;/h2&gt;

&lt;p&gt;Keeps &lt;strong&gt;all records from the left table&lt;/strong&gt; and matching records from the right table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟢 Customers  █████████████
🟣 Orders          ███████
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;David remains even though he has no order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right Outer Join
&lt;/h2&gt;

&lt;p&gt;Keeps &lt;strong&gt;all records from the right table&lt;/strong&gt; and matching records from the left table.&lt;/p&gt;

&lt;p&gt;Order 104 remains even though Customer 4 has no matching customer record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Outer Join
&lt;/h2&gt;

&lt;p&gt;Keeps &lt;strong&gt;all records from both tables&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟢 Customers  ████████
              ███████████  🟣 Orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result includes customers without orders and orders without matching customers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Left Anti Join
&lt;/h2&gt;

&lt;p&gt;Returns records from the left table that have &lt;strong&gt;no match&lt;/strong&gt; in the right table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customers − Orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;CustomerName&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;David&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is useful for identifying customers who have never placed an order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right Anti Join
&lt;/h2&gt;

&lt;p&gt;Returns records from the right table that have &lt;strong&gt;no match&lt;/strong&gt; in the left table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Orders − Customers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is particularly useful for data-quality checks because it identifies orders containing an invalid or missing customer reference.&lt;/p&gt;




&lt;h1&gt;
  
  
  6. Power Query Joins vs Power BI Relationships
&lt;/h1&gt;

&lt;p&gt;Although both concepts connect data, a Power Query merge and a Power BI relationship serve different purposes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Power Query Merge
&lt;/h3&gt;

&lt;p&gt;A merge happens during the &lt;strong&gt;data preparation/transformation stage&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customers + Orders
       ↓
 Power Query Merge
       ↓
Combined Table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result can physically bring columns from one table into another. For example, &lt;code&gt;CustomerName&lt;/code&gt; can be added to an Orders query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Power BI Relationship
&lt;/h3&gt;

&lt;p&gt;A relationship is created in the &lt;strong&gt;data model after data loading&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DimCustomer ───── 1:* ───── FactSales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tables remain separate. Power BI understands that the tables are connected and uses the relationship for filtering and calculations.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Merge combines data; relationships connect data logically.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A merge is appropriate when a column is genuinely required in the same query, when transforming source data, or when preparing a table for a specific analytical requirement.&lt;/p&gt;

&lt;p&gt;However, excessive merging can create very wide tables, duplicate descriptive information, increase redundancy, and make the model harder to maintain.&lt;/p&gt;

&lt;p&gt;Keeping fact and dimension tables separate is usually preferable in a BI model because each table has a clear responsibility and relationships allow the same dimension to be reused across multiple facts.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. Recommended Power BI Model
&lt;/h1&gt;

&lt;p&gt;For a typical business intelligence project, I would recommend a &lt;strong&gt;Star Schema&lt;/strong&gt; with a central fact table and several dimension tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         🟢 DimDate
                            │
                            │ 1:*
                            ▼
🔵 DimCustomer ── 1:* ── 🟣 FactSales ── *:1 ── 🟠 DimProduct
                            ▲
                            │
                            │ *:1
                            │
                       🔴 DimLocation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recommended relationship design would normally be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-to-many relationships&lt;/strong&gt; from dimensions to facts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-direction filtering&lt;/strong&gt; from dimensions toward fact tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique primary keys&lt;/strong&gt; in dimension tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foreign keys&lt;/strong&gt; in fact tables.&lt;/li&gt;
&lt;li&gt;Clearly defined fact-table grain.&lt;/li&gt;
&lt;li&gt;Separate fact and dimension tables rather than unnecessary merges.&lt;/li&gt;
&lt;li&gt;Inactive relationships only where there is a genuine business requirement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The star schema is preferable because it provides a strong balance between &lt;strong&gt;performance, simplicity, scalability, and maintainability&lt;/strong&gt;. It reduces redundancy compared with a flat table while avoiding much of the complexity introduced by a heavily normalized snowflake structure.&lt;/p&gt;

&lt;p&gt;It also makes DAX easier to understand. Measures such as:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Total Sales = SUM(FactSales[SalesAmount])&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;can naturally respond to filters from &lt;code&gt;DimCustomer&lt;/code&gt;, &lt;code&gt;DimProduct&lt;/code&gt;, &lt;code&gt;DimDate&lt;/code&gt;, or &lt;code&gt;DimLocation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From a reporting perspective, the model is intuitive: users select descriptive fields from dimensions and measures from the fact table. From a performance perspective, the model avoids unnecessary duplication and provides straightforward filter propagation. From a maintenance perspective, changes to customer, product, or location attributes can be managed in their respective dimensions rather than repeatedly across transaction records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Data modelling is the foundation of an effective Power BI solution. While flat tables can be useful for small and simple datasets, a &lt;strong&gt;star schema is generally the strongest choice for production BI models&lt;/strong&gt;. It separates measurable business events from descriptive attributes, creates predictable relationships, and supports efficient DAX calculations.&lt;/p&gt;

&lt;p&gt;Understanding relationship cardinality is equally important. One-to-many relationships should normally form the backbone of a dimensional model, while one-to-one and many-to-many relationships should be introduced only when the business structure justifies them. Single-direction filtering should generally be preferred because it creates predictable filter propagation.&lt;/p&gt;

&lt;p&gt;Power Query joins serve a different purpose from model relationships. Joins are primarily used during data preparation to combine or compare datasets, whereas relationships connect tables within the analytical model without physically combining them.&lt;/p&gt;

</description>
      <category>data</category>
      <category>schema</category>
    </item>
  </channel>
</rss>
