<?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: Lucy Anyango</title>
    <description>The latest articles on DEV Community by Lucy Anyango (@lucy_anyango_d6aa9788c23a).</description>
    <link>https://dev.to/lucy_anyango_d6aa9788c23a</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%2F4071786%2F97f99e5c-739a-48e9-a3b0-a6ae5aea3c7d.jpg</url>
      <title>DEV Community: Lucy Anyango</title>
      <link>https://dev.to/lucy_anyango_d6aa9788c23a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucy_anyango_d6aa9788c23a"/>
    <language>en</language>
    <item>
      <title>Window Functions, Joins, CTEs and Subqueries Using a Hospital Database</title>
      <dc:creator>Lucy Anyango</dc:creator>
      <pubDate>Sun, 18 Oct 2026 11:05:50 +0000</pubDate>
      <link>https://dev.to/lucy_anyango_d6aa9788c23a/window-functions-joins-ctes-and-subqueries-using-a-hospital-database-49b9</link>
      <guid>https://dev.to/lucy_anyango_d6aa9788c23a/window-functions-joins-ctes-and-subqueries-using-a-hospital-database-49b9</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;SQL allows us to store, retrieve, organize and analyze data in relational databases. In a hospital, SQL can help management understand patient visits, identify doctors handling the most cases, compare hospital bills, track admissions and investigate patterns in diagnoses and this article uses the PostgreSQL hospital database, which contains tables such as patients, doctors, department, diagnosis, procedures, wards and visits. The visits table is especially important because it records hospital visits and connects other tables through IDs. For example, visits.patient_id references patients.patient_id, and visits.doctor_id references doctors.doctor_id.&lt;br&gt;
Before running the examples:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What are window functions?&lt;/strong&gt;&lt;br&gt;
A window function performs a calculation across a set of related rows while keeping the individual rows in the result. For example, a hospital manager can see every visit alongside the total bill for that visit’s admission type. A GROUP BY query would combine visits into groups, but a window function can calculate a group total while still displaying each visit.&lt;br&gt;
Window functions are useful for ranking doctors or visits, comparing bills across rows, calculating running totals, and finding group averages, counts, minimums and maximums.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OVER(), PARTITION BY, ORDER BY and window frames&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Over()&lt;/em&gt; Defines the windows over which a function operates&lt;br&gt;
&lt;em&gt;Partition by&lt;/em&gt; Divides rows into independent groups without collapsing them.&lt;br&gt;
&lt;em&gt;Order by&lt;/em&gt; Defines the order in which rows are ranked.&lt;br&gt;
&lt;em&gt;Window frame&lt;/em&gt; Defines the particular rows in a partition used for a calculation relative to the current row.&lt;/p&gt;

&lt;p&gt;Example Calculate a total for each group while retaining individual rows.&lt;br&gt;
Compare billing across outpatient, inpatient and emergency visit&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Select &lt;br&gt;
Visit_id,&lt;br&gt;
Visit_date,&lt;br&gt;
Admission_type,&lt;br&gt;
Total_bill_kes,&lt;br&gt;
Sum(total_bill_kes) over |(partition by admission_type) as total_bill_by_admission_type&lt;br&gt;
From visits&lt;br&gt;
Order by visit_id;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each visit remains in the output. All outpatient visits show the same outpatient total; inpatient and emergency visits show their own group totals. PARTITION BY separates the groups, while the final ORDER BY sorts the output.&lt;/p&gt;

&lt;p&gt;A window frame determines exactly which rows inside the ordered window are included. For a running total, use a frame from the first row through the current row.&lt;/p&gt;

&lt;p&gt;Example Calculate a cumulative sum in a deterministic order.&lt;br&gt;
Hospital scenario: Track how recorded billing accumulates from one visit to the next.&lt;br&gt;
&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    visit_date,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    SUM(total_bill_kes) OVER (&lt;br&gt;
        ORDER BY visit_date, visit_id&lt;br&gt;
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;br&gt;
    ) AS running_total_kes&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The total begins at KES 3,500 for visit 1001, reaches KES 6,000 after visit 1002, and KES 34,000 after visit 1003. The frame includes all rows from the beginning through the current row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ranking window functions&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;ROW_NUMBER()&lt;/strong&gt;&lt;br&gt;
A unique sequential number to every row in the window.&lt;br&gt;
Hospital scenario: List each doctor’s visits from the highest bill to the lowest.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    doctor_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    ROW_NUMBER() OVER (&lt;br&gt;
        PARTITION BY doctor_id&lt;br&gt;
        ORDER BY total_bill_kes DESC, visit_id&lt;br&gt;
    ) AS bill_position&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each doctor has a sequence beginning at 1. Doctor 101’s visit 1017, costing KES 82,000, ranks first among that doctor’s visits. The visit_id tie-breaker makes ordering predictable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RANK()&lt;/strong&gt;&lt;br&gt;
Assign the same rank to tied values and leave gaps after ties.&lt;br&gt;
Rank visits by total bill.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    RANK() OVER (&lt;br&gt;
        ORDER BY total_bill_kes DESC&lt;br&gt;
    ) AS bill_rank&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Visit 1017, costing KES 82,000, receives rank 1; visit 1010, costing KES 76,000, receives rank 2. If two visits tie for first, both get rank 1 and the next rank is 3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DENSE_RANK()&lt;/strong&gt;&lt;br&gt;
Assign the same rank to ties without leaving gaps.&lt;br&gt;
Rank distinct bill amounts without skipping rank numbers after ties.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    DENSE_RANK() OVER (&lt;br&gt;
        ORDER BY total_bill_kes DESC&lt;br&gt;
    ) AS bill_rank&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The highest bill receives rank 1. The next distinct bill receives rank 2, even if several visits share rank 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NTILE()&lt;/strong&gt;&lt;br&gt;
Divide ordered rows into a specified number of approximately equal-sized groups.&lt;br&gt;
Divide the 30 visits into four groups by bill amount.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    NTILE(4) OVER (&lt;br&gt;
        ORDER BY total_bill_kes DESC, visit_id&lt;br&gt;
    ) AS bill_quartile&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every visit is assigned to group 1, 2, 3 or 4. Group 1 contains the highest-billed visits and group 4 the lowest. Because 30 is not divisible by four, the groups will not all have the same size. NTILE creates groups, not statistical quartile boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigation window functions&lt;br&gt;
LAG()&lt;/strong&gt;&lt;br&gt;
Retrieve a value from a preceding row without joining the table to itself.&lt;br&gt;
Compare each visit’s bill with the previous visit’s bill in the ordered data.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    visit_date,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    LAG(total_bill_kes) OVER (&lt;br&gt;
        ORDER BY visit_date, visit_id&lt;br&gt;
    ) AS previous_visit_bill&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Visit 1001 has NULL because it is first. Visit 1002 shows KES 3,500, the bill for visit 1001. LAG returns NULL when no preceding row exists unless a default is supplied.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LEAD()&lt;/strong&gt;&lt;br&gt;
Retrieve a value from a subsequent row in the ordered result.&lt;br&gt;
Display the next visit’s bill beside the current visit.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    visit_date,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    LEAD(total_bill_kes) OVER (&lt;br&gt;
        ORDER BY visit_date, visit_id&lt;br&gt;
    ) AS next_visit_bill&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Visit 1001 shows KES 2,500 as the next bill because visit 1002 follows it. The final row has NULL because there is no subsequent row. LAG looks backward; LEAD looks forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FIRST_VALUE()&lt;/strong&gt;&lt;br&gt;
Return the first value in the specified window frame.&lt;br&gt;
Show each visit alongside the highest bill recorded for that visit’s doctor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    doctor_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    FIRST_VALUE(total_bill_kes) OVER (&lt;br&gt;
        PARTITION BY doctor_id&lt;br&gt;
        ORDER BY total_bill_kes DESC, visit_id&lt;br&gt;
    ) AS highest_bill_for_doctor&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every visit handled by doctor 101 displays KES 82,000, the highest bill for that doctor. Other doctors display their own highest-bill values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LAST_VALUE()&lt;/strong&gt;&lt;br&gt;
Return the last value in the specified window frame. With ORDER BY, the default frame commonly ends at the current row’s last peer rather than the whole partition.&lt;br&gt;
Show each doctor’s lowest bill alongside every visit handled by that doctor.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    doctor_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    LAST_VALUE(total_bill_kes) OVER (&lt;br&gt;
        PARTITION BY doctor_id&lt;br&gt;
        ORDER BY total_bill_kes, visit_id&lt;br&gt;
        ROWS BETWEEN UNBOUNDED PRECEDING&lt;br&gt;
                 AND UNBOUNDED FOLLOWING&lt;br&gt;
    ) AS lowest_bill_for_doctor&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;All visits handled by doctor 101 show KES 3,500 as that doctor’s lowest bill. The explicit frame makes PostgreSQL examine the whole partition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aggregate window functions&lt;br&gt;
SUM()&lt;/strong&gt;&lt;br&gt;
Add values across a window.&lt;br&gt;
Display each visit alongside total recorded hospital billing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    SUM(total_bill_kes) OVER () AS total_hospital_billing&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every visit shows its own bill and the same overall total of KES 577,000 across all 30 visits. The window calculation retains individual rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AVG()&lt;/strong&gt;&lt;br&gt;
Calculate the average value across a window.&lt;br&gt;
Compare each bill with the average bill for the same admission type.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    admission_type,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    AVG(total_bill_kes) OVER (&lt;br&gt;
        PARTITION BY admission_type&lt;br&gt;
    ) AS average_bill_by_admission&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each visit displays its bill and the average for its admission type. All inpatient visits share one inpatient average, for example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;COUNT()&lt;/strong&gt;&lt;br&gt;
Count rows or non-NULL values within a window.&lt;br&gt;
Show how many visit records each doctor handled.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    doctor_id,&lt;br&gt;
    COUNT(*) OVER (&lt;br&gt;
        PARTITION BY doctor_id&lt;br&gt;
    ) AS visits_handled_by_doctor&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each visit for doctors 101 and 102 displays 10; visits for doctors 103 and 104 display 5. This counts visit records, not distinct patients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MIN()&lt;/strong&gt;&lt;br&gt;
Find the minimum value within a window.&lt;br&gt;
Show the smallest bill recorded for each admission type.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    admission_type,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    MIN(total_bill_kes) OVER (&lt;br&gt;
        PARTITION BY admission_type&lt;br&gt;
    ) AS minimum_bill_for_admission&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every visit displays the minimum bill for its admission type. The minimum outpatient bill is KES 1,800, for visit 1014.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MAX()&lt;/strong&gt;&lt;br&gt;
Find the maximum value within a window.&lt;br&gt;
Show the largest bill recorded for each admission type.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    admission_type,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    MAX(total_bill_kes) OVER (&lt;br&gt;
        PARTITION BY admission_type&lt;br&gt;
    ) AS maximum_bill_for_admission&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every visit displays the maximum bill for its admission type. The maximum inpatient bill is KES 82,000 for visit 1017; the emergency visit is KES 22,000.&lt;/p&gt;

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

&lt;p&gt;*&lt;em&gt;What are SQL joins?&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
A join combines data from two or more tables using related columns or a join condition. In the hospital database, visits stores IDs rather than repeating every patient, doctor and department name. Joins retrieve those details together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;INNER JOIN&lt;/strong&gt;&lt;br&gt;
Return only rows that have matching records in both tables.&lt;br&gt;
Hospital scenario: Create a detailed report of each visit, its patient and the doctor responsible.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    v.visit_id,&lt;br&gt;
    p.patient_name,&lt;br&gt;
    d.doctor_name,&lt;br&gt;
    v.total_bill_kes&lt;br&gt;
FROM visits v&lt;br&gt;
INNER JOIN patients p&lt;br&gt;
    ON v.patient_id = p.patient_id&lt;br&gt;
INNER JOIN doctors d&lt;br&gt;
    ON v.doctor_id = d.doctor_id;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The report contains 30 rows because every visit references an existing patient and doctor. A patient with repeat visits appears multiple times. Use INNER JOIN when matching records are required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LEFT JOIN&lt;/strong&gt;&lt;br&gt;
Preserve every row from the left table and add matching right-table values; unmatched right-side values are NULL.&lt;br&gt;
Hospital scenario: List every doctor, including any who have no visits.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    d.doctor_id,&lt;br&gt;
    d.doctor_name,&lt;br&gt;
    v.visit_id&lt;br&gt;
FROM doctors d&lt;br&gt;
LEFT JOIN visits v&lt;br&gt;
    ON d.doctor_id = v.doctor_id&lt;br&gt;
ORDER BY d.doctor_id, v.visit_id;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;All four doctors appear. In the current dataset each doctor has visits; a doctor without visits would still appear with a NULL visit_id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RIGHT JOIN&lt;/strong&gt;&lt;br&gt;
Purpose: Preserve every row from the right table and add matching left-table values; unmatched left-side values are NULL.&lt;br&gt;
Hospital scenario: List every registered patient, including any who have never visited.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    v.visit_id,&lt;br&gt;
    v.total_bill_kes,&lt;br&gt;
    p.patient_id,&lt;br&gt;
    p.patient_name&lt;br&gt;
FROM visits v&lt;br&gt;
RIGHT JOIN patients p&lt;br&gt;
    ON v.patient_id = p.patient_id&lt;br&gt;
ORDER BY p.patient_id, v.visit_id;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;All 25 patients appear. Patients with several visits may appear on several rows; patients without visits would have NULL visit details. All supplied patients have at least one visit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FULL OUTER JOIN&lt;/strong&gt;&lt;br&gt;
Purpose: Return matches plus unmatched rows from both tables.&lt;br&gt;
Hospital scenario: Reconcile doctor records against visit records.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    d.doctor_id,&lt;br&gt;
    d.doctor_name,&lt;br&gt;
    v.visit_id&lt;br&gt;
FROM doctors d&lt;br&gt;
FULL OUTER JOIN visits v&lt;br&gt;
    ON d.doctor_id = v.doctor_id;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Matching doctor-visit combinations appear. Unmatched doctors would have NULL visit details, and visits without matching doctors would have NULL doctor details. The supplied foreign-key data has no such unmatched records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CROSS JOIN&lt;/strong&gt;&lt;br&gt;
Produce every possible combination of rows from two tables.&lt;br&gt;
Create a planning matrix of all doctors and all departments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    d.doctor_name,&lt;br&gt;
    dep.department_name&lt;br&gt;
FROM doctors d&lt;br&gt;
CROSS JOIN department dep;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Four doctors multiplied by four departments produces 16 rows. Every doctor is paired with every department, whether or not that doctor actually works there. Use CROSS JOIN only when all combinations are intended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SELF JOIN&lt;/strong&gt;&lt;br&gt;
Join a table to itself using aliases to compare different rows in that same table.&lt;br&gt;
Identify pairs of patients registered in the same county.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    p1.patient_name AS patient_one,&lt;br&gt;
    p2.patient_name AS patient_two,&lt;br&gt;
    p1.county&lt;br&gt;
FROM patients p1&lt;br&gt;
JOIN patients p2&lt;br&gt;
    ON p1.county = p2.county&lt;br&gt;
   AND p1.patient_id &amp;lt; p2.patient_id&lt;br&gt;
ORDER BY p1.county;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Patient pairs from the same county are listed. For example, Amina Otieno and Isaac Kamau are both registered in Nairobi. The ID condition avoids pairing a patient with themselves and avoids reversed duplicate pairs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Join comparison&lt;/strong&gt;&lt;br&gt;
Join          What it preserves       Common use&lt;br&gt;
INNER JOIN -Matching rows only- Visits with patient and doctor details&lt;br&gt;
LEFT JOIN  -Every left-side row All doctors, including those without visits&lt;br&gt;
RIGHT JOIN  -Every right-side row -All patients, including those without visits&lt;br&gt;
FULL OUTER JOIN  -All rows from both sides -Reconciliation and mismatch detection&lt;br&gt;
CROSS JOIN  -Every possible combination -Planning matrices and combinations&lt;br&gt;
SELF JOIN   -Related rows within the same table- Comparing patients in the same county&lt;/p&gt;

&lt;p&gt;A join does not automatically aggregate data. If a patient has five visits, joining that patient to visits produces five matching rows. Use aggregation when you need one summary row per patient or doctor.&lt;/p&gt;

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

&lt;p&gt;** What is a CTE?**&lt;br&gt;
A Common Table Expression (CTE) is a named result set defined using WITH. It can be referenced by the main SQL statement that follows it. CTEs break complicated SQL into smaller, more understandable steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic CTE&lt;/strong&gt;&lt;br&gt;
Define one temporary named result set for use within a single SQL statement.&lt;br&gt;
Compare total recorded billing associated with each doctor’s visits.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;WITH doctor_billing AS (&lt;br&gt;
    SELECT&lt;br&gt;
        doctor_id,&lt;br&gt;
        SUM(total_bill_kes) AS total_billing_kes&lt;br&gt;
    FROM visits&lt;br&gt;
    GROUP BY doctor_id&lt;br&gt;
)&lt;br&gt;
SELECT&lt;br&gt;
    d.doctor_name,&lt;br&gt;
    db.total_billing_kes&lt;br&gt;
FROM doctor_billing db&lt;br&gt;
JOIN doctors d&lt;br&gt;
    ON db.doctor_id = d.doctor_id&lt;br&gt;
ORDER BY db.total_billing_kes DESC;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One row per doctor shows the doctor’s name and the sum of bills for their visits, ordered from highest to lowest. The CTE aggregates visits; the main query joins in doctor names. The CTE does not create a permanent table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple CTEs&lt;/strong&gt;&lt;br&gt;
Define two or more named result sets that can feed into the main query.&lt;br&gt;
Compare doctors by total billing and number of visits.&lt;br&gt;
_&lt;br&gt;
WITH doctor_billing AS (&lt;br&gt;
    SELECT&lt;br&gt;
        doctor_id,&lt;br&gt;
        SUM(total_bill_kes) AS total_billing_kes&lt;br&gt;
    FROM visits&lt;br&gt;
    GROUP BY doctor_id&lt;br&gt;
),&lt;br&gt;
doctor_visit_counts AS (&lt;br&gt;
    SELECT&lt;br&gt;
        doctor_id,&lt;br&gt;
        COUNT(*) AS total_visits&lt;br&gt;
    FROM visits&lt;br&gt;
    GROUP BY doctor_id&lt;br&gt;
)&lt;br&gt;
SELECT&lt;br&gt;
    d.doctor_name,&lt;br&gt;
    db.total_billing_kes,&lt;br&gt;
    vc.total_visits&lt;br&gt;
FROM doctors d&lt;br&gt;
JOIN doctor_billing db&lt;br&gt;
    ON d.doctor_id = db.doctor_id&lt;br&gt;
JOIN doctor_visit_counts vc&lt;br&gt;
    ON d.doctor_id = vc.doctor_id&lt;br&gt;
ORDER BY db.total_billing_kes DESC;_&lt;/p&gt;

&lt;p&gt;One row per doctor displays name, total billing and visit count. One CTE calculates billing, another calculates visit counts, and the main query combines them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recursive CTE&lt;/strong&gt;&lt;br&gt;
 Repeatedly reference a CTE’s own result to process sequential or hierarchical data.&lt;br&gt;
Generate all dates in a five-day hospital reporting period, including dates without visits.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;WITH RECURSIVE reporting_dates AS (&lt;br&gt;
    SELECT DATE '2026-06-01' AS report_date&lt;br&gt;
    UNION ALL&lt;br&gt;
    SELECT report_date + 1&lt;br&gt;
    FROM reporting_dates&lt;br&gt;
    WHERE report_date &amp;lt; DATE '2026-06-05'&lt;br&gt;
)&lt;br&gt;
SELECT report_date&lt;br&gt;
FROM reporting_dates&lt;br&gt;
ORDER BY report_date;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Five dates are returned: 1–5 June 2026. The anchor query starts at 1 June; the recursive query adds one day each time; the WHERE condition stops after 5 June. Recursive CTEs are also useful for parent-child hierarchies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CTE with joins and aggregations&lt;/strong&gt;&lt;br&gt;
Aggregate data in a CTE, then join it to a descriptive table.&lt;br&gt;
Compare departments by visit volume, total billing and average bill.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;WITH department_summary AS (&lt;br&gt;
    SELECT&lt;br&gt;
        department_id,&lt;br&gt;
        COUNT(*) AS total_visits,&lt;br&gt;
        SUM(total_bill_kes) AS total_billing_kes,&lt;br&gt;
        AVG(total_bill_kes) AS average_bill_kes&lt;br&gt;
    FROM visits&lt;br&gt;
    GROUP BY department_id&lt;br&gt;
)&lt;br&gt;
SELECT&lt;br&gt;
    dep.department_name,&lt;br&gt;
    ds.total_visits,&lt;br&gt;
    ds.total_billing_kes,&lt;br&gt;
    ROUND(ds.average_bill_kes, 2) AS average_bill_kes&lt;br&gt;
FROM department_summary ds&lt;br&gt;
JOIN department dep&lt;br&gt;
    ON ds.department_id = dep.department_id&lt;br&gt;
ORDER BY ds.total_billing_kes DESC;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each department appears with visit count, total billing and average bill. The CTE aggregates the visits, and the join adds department names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CTEs versus subqueries&lt;/strong&gt;&lt;br&gt;
Feature CTE Subquery&lt;br&gt;
Definition  Uses WITH   Written inside another query&lt;br&gt;
Readability Often clearer for multi-step logic  Convenient for short calculations&lt;br&gt;
Reuse in one statement  Can be referenced multiple times, depending on structure    May need to be repeated&lt;br&gt;
Recursion   Supports WITH RECURSIVE Does not directly provide the same recursive CTE structure&lt;br&gt;
Performance Depends on query and PostgreSQL plan    Depends on query and PostgreSQL plan&lt;/p&gt;

&lt;p&gt;A CTE is not automatically faster than a subquery. PostgreSQL may inline or materialize a CTE depending on the query, version and optimization choices. Use CTEs primarily for clarity and inspect the execution plan when performance matters.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What is a subquery?&lt;/strong&gt;&lt;br&gt;
A subquery is a SQL query placed inside another query. It can return a single value, a set of values, a derived table, or a Boolean result used to filter data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scalar subquery&lt;/strong&gt;&lt;br&gt;
Return a single value (one row and one column) for use in an expression or comparison.&lt;br&gt;
Find visits with bills above the average bill across all visits.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    total_bill_kes&lt;br&gt;
FROM visits&lt;br&gt;
WHERE total_bill_kes &amp;gt; (&lt;br&gt;
    SELECT AVG(total_bill_kes)&lt;br&gt;
    FROM visits&lt;br&gt;
);&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Only visits with bills greater than the overall average are returned. The inner query calculates the average; the outer query compares every visit against it. A scalar subquery must return no more than one row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-correlated subquery&lt;/strong&gt;&lt;br&gt;
Run independently of the current outer row because it does not reference outer-query columns.&lt;br&gt;
Hospital scenario: Find visits involving patients registered in Kisumu County.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    patient_id,&lt;br&gt;
    total_bill_kes&lt;br&gt;
FROM visits&lt;br&gt;
WHERE patient_id IN (&lt;br&gt;
    SELECT patient_id&lt;br&gt;
    FROM patients&lt;br&gt;
    WHERE county = 'Kisumu'&lt;br&gt;
);&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Visits for patients whose county is exactly Kisumu are returned, including visits for Esther Achieng and Brian Omondi. The inner query identifies patient IDs and does not depend on the outer row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correlated subquery&lt;/strong&gt;&lt;br&gt;
Reference a column from the outer query, making the subquery depend logically on each outer row.&lt;br&gt;
Identify doctors who have handled more than five visits.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    d.doctor_id,&lt;br&gt;
    d.doctor_name&lt;br&gt;
FROM doctors d&lt;br&gt;
WHERE (&lt;br&gt;
    SELECT COUNT(*)&lt;br&gt;
    FROM visits v&lt;br&gt;
    WHERE v.doctor_id = d.doctor_id&lt;br&gt;
) &amp;gt; 5;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Doctors 101 and 102 qualify because each handled 10 visits. Doctors 103 and 104 handled five each and do not qualify. The inner query refers to d.doctor_id from the outer query. PostgreSQL may optimize execution rather than literally rerunning it for every row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nested subqueries&lt;/strong&gt;&lt;br&gt;
Place a subquery inside another subquery to answer a question in multiple stages.&lt;br&gt;
Find patients who have at least one visit with a bill above the overall average.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    patient_id,&lt;br&gt;
    patient_name,&lt;br&gt;
    county&lt;br&gt;
FROM patients&lt;br&gt;
WHERE patient_id IN (&lt;br&gt;
    SELECT patient_id&lt;br&gt;
    FROM visits&lt;br&gt;
    WHERE total_bill_kes &amp;gt; (&lt;br&gt;
        SELECT AVG(total_bill_kes)&lt;br&gt;
        FROM visits&lt;br&gt;
    )&lt;br&gt;
);&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The innermost query calculates the average bill; the middle query identifies patient IDs with an above-average visit; the outer query retrieves those patients. Each patient is returned once even if they have multiple qualifying visits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IN operator&lt;/strong&gt;&lt;br&gt;
Check whether a value matches any value in a list or subquery result.&lt;br&gt;
Retrieve visits associated with diagnoses categorized as chronic diseases.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    patient_id,&lt;br&gt;
    diagnosis_id,&lt;br&gt;
    total_bill_kes&lt;br&gt;
FROM visits&lt;br&gt;
WHERE diagnosis_id IN (&lt;br&gt;
    SELECT diagnosis_id&lt;br&gt;
    FROM diagnosis&lt;br&gt;
    WHERE diagnosis_category = 'Chronic Disease'&lt;br&gt;
);&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The result includes visits linked to diagnoses such as Hypertension, Diabetes and Heart Failure. The inner query returns diagnosis IDs in the exact category Chronic Disease.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EXISTS operator&lt;/strong&gt;&lt;br&gt;
Test whether a subquery returns at least one row.&lt;br&gt;
Find patients who have had an emergency visit.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    p.patient_id,&lt;br&gt;
    p.patient_name,&lt;br&gt;
    p.county&lt;br&gt;
FROM patients p&lt;br&gt;
WHERE EXISTS (&lt;br&gt;
    SELECT 1&lt;br&gt;
    FROM visits v&lt;br&gt;
    WHERE v.patient_id = p.patient_id&lt;br&gt;
      AND v.admission_type = 'Emergency'&lt;br&gt;
);&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The query returns patients with at least one emergency visit. In the supplied data, visit 1030 is an emergency visit involving patient 10, Peter Ouma. SELECT 1 is used because only existence matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ANY operator&lt;/strong&gt;&lt;br&gt;
Return true when a comparison succeeds for at least one value returned by a subquery.&lt;br&gt;
Find visits whose bills exceed at least one emergency-visit bill.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    total_bill_kes&lt;br&gt;
FROM visits&lt;br&gt;
WHERE total_bill_kes &amp;gt; ANY (&lt;br&gt;
    SELECT total_bill_kes&lt;br&gt;
    FROM visits&lt;br&gt;
    WHERE admission_type = 'Emergency'&lt;br&gt;
);&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There is one emergency visit costing KES 22,000, so the query returns visits costing more than KES 22,000. With multiple non-NULL emergency bills, &amp;gt; ANY means greater than at least one of them, effectively greater than the minimum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ALL operator&lt;/strong&gt;&lt;br&gt;
Return true only when a comparison succeeds for every value returned by a subquery.&lt;br&gt;
Find visits whose bills exceed every emergency-visit bill.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    total_bill_kes&lt;br&gt;
FROM visits&lt;br&gt;
WHERE total_bill_kes &amp;gt; ALL (&lt;br&gt;
    SELECT total_bill_kes&lt;br&gt;
    FROM visits&lt;br&gt;
    WHERE admission_type = 'Emergency'&lt;br&gt;
);&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With one emergency visit costing KES 22,000, the query returns visits costing more than KES 22,000. With multiple non-NULL emergency bills, &amp;gt; ALL means greater than the maximum.&lt;br&gt;
ANY versus ALL: &amp;gt; ANY means greater than at least one value; &amp;gt; ALL means greater than every value. Empty subqueries and NULL values have special SQL comparison semantics. In particular, NULL values can affect comparisons, and NOT IN requires care when the subquery can return NULL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subqueries in SELECT, FROM and WHERE&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Subquery in SELECT&lt;/strong&gt;&lt;br&gt;
Add a calculated scalar value to every output row.&lt;br&gt;
Hospital scenario: Display every visit’s bill alongside the hospital-wide average.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    (&lt;br&gt;
        SELECT AVG(total_bill_kes)&lt;br&gt;
        FROM visits&lt;br&gt;
    ) AS hospital_average_bill&lt;br&gt;
FROM visits;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every visit appears with its own bill and the same hospital-wide average. The subquery calculates the average independently of each visit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subquery in FROM&lt;/strong&gt;&lt;br&gt;
Create a derived table that the outer query can treat like a table.&lt;br&gt;
Find doctors whose total recorded billing exceeds KES 100,000.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    doctor_id,&lt;br&gt;
    total_billing_kes&lt;br&gt;
FROM (&lt;br&gt;
    SELECT&lt;br&gt;
        doctor_id,&lt;br&gt;
        SUM(total_bill_kes) AS total_billing_kes&lt;br&gt;
    FROM visits&lt;br&gt;
    GROUP BY doctor_id&lt;br&gt;
) AS doctor_summary&lt;br&gt;
WHERE total_billing_kes &amp;gt; 100000&lt;br&gt;
ORDER BY total_billing_kes DESC;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The inner query calculates each doctor’s total bills. The outer query filters totals above KES 100,000 and sorts them from highest to lowest. The derived table needs an alias in PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subquery in WHERE&lt;/strong&gt;&lt;br&gt;
Filter records according to a calculated value or set of values.&lt;br&gt;
Find visits whose bills exceed the overall average.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    patient_id,&lt;br&gt;
    total_bill_kes&lt;br&gt;
FROM visits&lt;br&gt;
WHERE total_bill_kes &amp;gt; (&lt;br&gt;
    SELECT AVG(total_bill_kes)&lt;br&gt;
    FROM visits&lt;br&gt;
);&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Only visits with bills above the average are returned. Use this form when the records you need depend on a separate calculation or list of values.&lt;/p&gt;

&lt;p&gt;Clause  Purpose Hospital application&lt;br&gt;
SELECT  Adds a calculated value to the output   Show each bill beside the hospital average&lt;br&gt;
FROM    Creates a derived table for further analysis    Calculate and filter total billing by doctor&lt;br&gt;
WHERE   Filters rows using a subquery result    Find visits above average billing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
Window functions, joins, CTEs and subqueries are fundamental SQL techniques for analyzing relational data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Window functions calculate rankings, comparisons and summaries while preserving individual rows.&lt;/li&gt;
&lt;li&gt;Joins combine related information stored across tables.&lt;/li&gt;
&lt;li&gt;CTEs organize complex queries into understandable steps, while recursive CTEs support sequential and hierarchical analysis.&lt;/li&gt;
&lt;li&gt;Subqueries allow one query to depend on another query’s results.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Data Relationships, Data Modelling Schemas, and Joins in Power BI.</title>
      <dc:creator>Lucy Anyango</dc:creator>
      <pubDate>Sat, 12 Sep 2026 22:14:18 +0000</pubDate>
      <link>https://dev.to/lucy_anyango_d6aa9788c23a/data-relationships-data-modelling-schemas-and-joins-in-power-bi-1hkp</link>
      <guid>https://dev.to/lucy_anyango_d6aa9788c23a/data-relationships-data-modelling-schemas-and-joins-in-power-bi-1hkp</guid>
      <description>&lt;h2&gt;
  
  
  Data Modelling in Power BI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Data Modelling Means in Power BI
&lt;/h3&gt;

&lt;p&gt;Data modelling is the process of defining relationships between tables and organising data structures to support analysis. It determines how data is stored, related, and queried.&lt;/p&gt;

&lt;p&gt;The core responsibilities of a data model include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Establishing relationships between tables&lt;/strong&gt;: Connecting tables through one-to-many, many-to-many, or one-to-one relationships to enable cross-table filtering and aggregation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enabling effective analysis&lt;/strong&gt;: Allowing users to slice data by different attributes (date, geography, category)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improving performance and efficiency&lt;/strong&gt;: Reducing data redundancy, minimising memory footprint, and accelerating report loading&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supporting reusability and maintainability&lt;/strong&gt;: Building scalable structures so measures and calculations can be reused across reports&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why a Well-Designed Data Model Matters
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Impact on DAX calculations&lt;/strong&gt;: In a star schema, DAX measures are simpler and more reliable because dimension tables and fact tables have clearly separated responsibilities — dimensions filter, facts aggregate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact on performance&lt;/strong&gt;: Power BI's storage engine and query engine are optimised for star schemas. The simpler the relationships (typically one-to-many from dimension to fact), the more efficient filter propagation and query optimization become.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact on scalability&lt;/strong&gt;: As data volumes grow and multiple business subjects or reusable dimensions are needed, flat tables quickly become a bottleneck, while star schemas maintain stable performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact on maintainability&lt;/strong&gt;: A clear model structure reduces the risk of modelling errors, enables non-technical users to understand the model, and allows them to build their own reports.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison of Three Data Modelling Approaches
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Flat Table
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Definition&lt;/strong&gt;: A flat table is a fully denormalized structure where all data is stored in a single table. Each row contains both fact measures and all related descriptive attributes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table structure&lt;/strong&gt;: One wide table containing both transactional data (e.g., sales amount, quantity) and descriptive data (e.g., product name, category, customer city). No relationships, no separate dimension or fact tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simplest structure, easy to understand&lt;/li&gt;
&lt;li&gt;Fast to build for small, simple datasets&lt;/li&gt;
&lt;li&gt;No relationships to manage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Severe data redundancy&lt;/strong&gt;: Descriptive information such as product category is repeated in every row&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance problems&lt;/strong&gt;: As data volume grows, storage bloats and queries slow down&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Difficult maintenance&lt;/strong&gt;: Updating a product category requires modifying all related rows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complicated DAX&lt;/strong&gt;: Cannot clearly separate filter context from aggregation logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not scalable&lt;/strong&gt;: Cannot be reused across multiple business subjects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When appropriate&lt;/strong&gt;: Extremely small and simple datasets, limited dimensions, no need to reuse dimensions across multiple fact tables, and low performance/scalability requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implications for Power BI performance and complexity&lt;/strong&gt;: Not recommended for any scaled analytics. A fully denormalised structure cannot fully leverage Power BI's relationship engine, resulting in inefficient queries. Model complexity appears low on the surface (no relationships), but the performance cost of data bloat far exceeds the overhead of managing relationships.&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%2Fxocw2235xmzjvmgdv902.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%2Fxocw2235xmzjvmgdv902.png" alt=" " width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Star Schema
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Definition&lt;/strong&gt;: A star schema is a mature dimensional modelling approach that requires the modeller to classify tables into two categories: &lt;strong&gt;dimension tables&lt;/strong&gt; and &lt;strong&gt;fact tables&lt;/strong&gt;. The fact table sits at the centre, with dimension tables connected directly to it, forming a star pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table structure&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dimension tables&lt;/strong&gt;: Describe business entities (product, customer, date, geography) and contain unique key columns plus descriptive columns. Relatively few rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fact tables&lt;/strong&gt;: Store measurable business events (sales orders, inventory balances) and contain dimension key columns plus numeric measure columns. Can be very large and continuously growing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimal performance&lt;/strong&gt;: Simple one-to-many relationships make filter propagation efficient, and the query engine optimises best&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplified DAX&lt;/strong&gt;: Dimension and fact responsibilities are separated, making measure logic clear&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to understand&lt;/strong&gt;: Intuitive structure that both business users and AI tools can grasp easily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Highly scalable&lt;/strong&gt;: Supports adding new dimensions without affecting existing structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High compression efficiency&lt;/strong&gt;: VertiPaq's columnar compression works best with star schemas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dimension tables may have some redundancy (e.g., category names repeated across product rows)&lt;/li&gt;
&lt;li&gt;Highly complex hierarchies may require additional management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When appropriate&lt;/strong&gt;: Almost all Power BI production models. When supporting multiple business subjects, reusable dimensions, large data volumes, or a good DAX experience is needed, the star schema is the first choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implications for Power BI performance and complexity&lt;/strong&gt;: The star schema is Power BI's &lt;strong&gt;recommended design pattern&lt;/strong&gt;. Relationships are simple (typically single-direction one-to-many), filter paths are clear, and both the VertiPaq storage and query engines are optimised for this structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;_&lt;/strong&gt;Diagram**:&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%2F86ld7z4i7g82r2ad7vua.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%2F86ld7z4i7g82r2ad7vua.png" alt=" " width="799" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Snowflake Schema
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Definition&lt;/strong&gt;: A snowflake schema is an extension of the star schema in which dimension tables are further normalised and split into multiple related tables. Hierarchies (e.g., product → subcategory → category) are broken into separate tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table structure&lt;/strong&gt;: The fact table remains at the centre, but dimensions no longer connect directly. For example, the product dimension points to a subcategory table, which points to a category table. Queries must traverse multiple relationship layers to reach the top-level descriptive attributes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduced data redundancy&lt;/strong&gt;: Category names are stored once rather than repeated in every product row&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suited to already-normalised source systems&lt;/strong&gt;: If the data warehouse is already snowflaked, it can be used directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More flexible hierarchy management&lt;/strong&gt;: Simpler updates when hierarchy attributes change independently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Increased number of relationships&lt;/strong&gt;: Filtering must traverse multiple relationship chains, increasing query complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Degraded performance&lt;/strong&gt;: More joins mean the engine processes more tables, and queries are typically slower than with star schemas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complicated DAX&lt;/strong&gt;: Requires understanding filter propagation across multiple relationship layers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harder to understand&lt;/strong&gt;: Business users struggle to intuitively find the attributes they need&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When appropriate&lt;/strong&gt;: When source data is already normalised, when dimensions are extremely large with complex hierarchies, or when attributes change frequently and need independent management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implications for Power BI performance and complexity&lt;/strong&gt;: Snowflake schemas are &lt;strong&gt;generally not recommended&lt;/strong&gt; in Power BI. Although Power BI supports snowflake structures, the extra joins degrade query performance and increase model complexity. Best practice is to denormalise (flatten) snowflake dimensions in Power Query, converting them into single dimension tables within a star schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagram references&lt;/strong&gt;:&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%2Fbzg7jp4e7rblloxk8ram.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%2Fbzg7jp4e7rblloxk8ram.png" alt=" " width="799" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fact Tables and Dimension Tables
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Difference Between a Fact Table and a Dimension Table
&lt;/h3&gt;

&lt;p&gt;A fact table stores the measurable, numeric events of a business — the transactions, orders, sales, or activities that occur. A dimension table stores the descriptive context that gives those events meaning — who, what, where, when, and how.&lt;/p&gt;

&lt;p&gt;In short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fact table&lt;/strong&gt; = what happened, how much, how many&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dimension table&lt;/strong&gt; = who, what, where, when, why&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What Information Is Normally Stored in a Fact Table
&lt;/h3&gt;

&lt;p&gt;A fact table normally stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Foreign keys&lt;/strong&gt; that link to dimension tables (e.g., CustomerID, ProductID, DateID)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numeric measures&lt;/strong&gt; that represent business events (e.g., SalesAmount, Quantity, Discount, Cost)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Degenerate dimensions&lt;/strong&gt; in some cases (e.g., OrderNumber stored directly in the fact table)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction-level or aggregated-level records&lt;/strong&gt; depending on the grain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fact tables are typically narrow in columns but very tall in rows. They grow continuously as new transactions occur.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Information Is Normally Stored in a Dimension Table
&lt;/h3&gt;

&lt;p&gt;A dimension table normally stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A primary key&lt;/strong&gt; that uniquely identifies each row (e.g., CustomerID, ProductID, DateID)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptive attributes&lt;/strong&gt; used for filtering, grouping, and labelling (e.g., CustomerName, City, Country, ProductName, Category, Year, Month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hierarchical attributes&lt;/strong&gt; that support drill-down (e.g., Country → Region → City, Category → SubCategory → Product)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static or slowly changing data&lt;/strong&gt; that describes business entities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dimension tables are typically wide in columns but short in rows. They change infrequently compared to fact tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measures/Numeric Business Events Versus Descriptive Attributes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Measures (fact table)&lt;/strong&gt;: Numeric values that can be aggregated — summed, averaged, counted, or compared. Examples: SalesAmount, Quantity, Revenue, Profit, Discount, UnitPrice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Descriptive attributes (dimension table)&lt;/strong&gt;: Text or categorical values that provide context. Examples: CustomerName, ProductCategory, OrderDate, ShipCity, EmployeeName.&lt;/p&gt;

&lt;p&gt;A measure answers "how much?" A descriptive attribute answers "which one?" or "what kind?"&lt;/p&gt;

&lt;h3&gt;
  
  
  The Concept of the Grain/Granularity of a Fact Table
&lt;/h3&gt;

&lt;p&gt;The grain of a fact table defines the level of detail at which each row is recorded. It answers the question: "What does one row in this fact table represent?"&lt;/p&gt;

&lt;p&gt;Examples of grain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One row per sales transaction line (finest grain)&lt;/li&gt;
&lt;li&gt;One row per order (coarser grain)&lt;/li&gt;
&lt;li&gt;One row per day per product per store (aggregated grain)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choosing the correct grain is critical because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It determines what questions the model can answer&lt;/li&gt;
&lt;li&gt;It affects storage size and query performance&lt;/li&gt;
&lt;li&gt;It must be consistent across all rows in the fact table&lt;/li&gt;
&lt;li&gt;Mixing grains in one table causes incorrect aggregations and double-counting&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FactSales&lt;/strong&gt;: One row per sales transaction line, containing SalesAmount, Quantity, Discount, and foreign keys to DimCustomer, DimProduct, DimDate, DimLocation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FactOrders&lt;/strong&gt;: One row per order, containing OrderQuantity, OrderAmount, and keys to DimCustomer, DimDate, DimProduct&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FactTransactions&lt;/strong&gt;: One row per financial transaction, containing Debit, Credit, Balance, and keys to DimAccount, DimDate, DimCustomer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FactInventory&lt;/strong&gt;: One row per product per store per day, containing QuantityOnHand, InventoryValue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FactShipments&lt;/strong&gt;: One row per shipment, containing FreightCost, QuantityShipped, and keys to DimCarrier, DimDate, DimLocation&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DimCustomer&lt;/strong&gt;: CustomerID (PK), CustomerName, City, Country, Segment, JoinDate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DimProduct&lt;/strong&gt;: ProductID (PK), ProductName, Category, SubCategory, UnitPrice, Color&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DimDate&lt;/strong&gt;: DateID (PK), FullDate, Year, Quarter, Month, Week, DayOfWeek, IsWeekend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DimLocation&lt;/strong&gt;: LocationID (PK), City, State, Country, Region, PostalCode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DimEmployee&lt;/strong&gt;: EmployeeID (PK), EmployeeName, Department, Role, HireDate&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Relationships and Joins Between Tables
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Relationship Types
&lt;/h3&gt;

&lt;p&gt;In a Power BI data model, &lt;strong&gt;relationships&lt;/strong&gt; are the mechanism that connects tables and enables filter propagation. Relationships define table types through &lt;strong&gt;cardinality&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One-to-many&lt;/strong&gt;: The most common relationship type. A unique key in the dimension table (the "one" side) corresponds to multiple records in the fact table (the "many" side). Filters flow from dimension to fact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Many-to-many&lt;/strong&gt;: Used when neither table has unique values, requiring a &lt;strong&gt;bridge table&lt;/strong&gt; to resolve. The bridge table contains unique combinations of keys from both sides and establishes one-to-many relationships with each.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;One-to-one&lt;/strong&gt;: Less common; both tables have unique keys.&lt;/p&gt;
&lt;h2&gt;
  
  
  One-to-Many (1:*)
&lt;/h2&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: A unique value in the "one" side (dimension table) relates to multiple occurrences of that value in the "many" side (fact table). The filter flows from the one side to the many side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example&lt;/strong&gt;: DimCustomer has one row per customer (CustomerID is unique). FactSales has many rows per customer (CustomerID repeats for every sale). One customer → many sales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it should be used&lt;/strong&gt;: This is the default and most common relationship type. It should be used whenever a dimension table connects to a fact table. It is the backbone of the star schema.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: A single row in one table relates to exactly one row in another table. Both columns contain unique values. Filter propagation can flow in either direction, but only one direction is active at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example&lt;/strong&gt;: DimEmployee and DimEmployeeDetails — each employee has exactly one detail record (e.g., social security number, bank account). EmployeeID is unique in both tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it should be used&lt;/strong&gt;: Rarely. It is used when two tables contain different attributes of the same entity and must be kept separate for security or organisational reasons. In most cases, the two tables should be merged into one dimension table.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: Multiple rows in one table relate to multiple rows in another table. Neither side contains unique values. A bridge table (also called a junction table) is required to resolve the relationship into two one-to-many relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example&lt;/strong&gt;: Students and Courses. One student can enroll in many courses. One course can have many students. A bridge table called Enrollment contains StudentID and CourseID combinations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it should be used&lt;/strong&gt;: When there is a genuine many-to-many business relationship that cannot be resolved by adding attributes to either side. It is also used when connecting a fact table to another fact table at a different grain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Importance of Primary Keys, Foreign Keys, Unique Values, Cardinality, Referential Integrity, and Active/Inactive Relationships
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Primary Keys&lt;/strong&gt;: A primary key is a column (or combination of columns) that uniquely identifies each row in a table. In DimCustomer, CustomerID is the primary key — no two customers share the same ID. Primary keys must contain unique values and cannot be null.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Foreign Keys&lt;/strong&gt;: A foreign key is a column in one table that refers to the primary key in another table. In FactSales, CustomerID is a foreign key — it points to the CustomerID in DimCustomer. Foreign keys can contain duplicate values because many sales can belong to the same customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unique Values&lt;/strong&gt;: The "one" side of a one-to-many relationship must contain unique values. If CustomerID appears twice in DimCustomer, the relationship cannot be created or will produce incorrect results. The "many" side does not require unique values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cardinality&lt;/strong&gt;: Cardinality defines how many rows in one table relate to rows in another. The three types are one-to-many, one-to-one, and many-to-many. Choosing the correct cardinality is essential for accurate filter propagation and DAX results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referential Integrity&lt;/strong&gt;: Referential integrity means that every foreign key value in the fact table has a matching primary key value in the dimension table. If FactSales contains CustomerID 999 but DimCustomer has no CustomerID 999, the sale is an orphan record. Power BI handles this by creating a blank row in the dimension for unmatched keys, but this can hide data quality problems. Referential integrity should be enforced at the source whenever possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active and Inactive Relationships&lt;/strong&gt;: A table can have multiple relationships to another table, but only one can be active at a time. The active relationship is used by default in DAX calculations. Inactive relationships must be explicitly activated using the USERELATIONSHIP function in DAX. This is common with role-playing dimensions — for example, a DimDate table connected to FactSales by OrderDate (active), ShipDate (inactive), and DeliveryDate (inactive).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: CustomerID contains unique values in DimCustomer because each customer appears once. CustomerID appears multiple times in FactSales because one customer can place many orders. The primary key enforces uniqueness in the dimension. The foreign key allows repetition in the fact. The one-to-many relationship between them enables filtering: selecting a customer in DimCustomer filters all their sales in FactSales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filter Direction
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Filter Direction Means
&lt;/h3&gt;

&lt;p&gt;Filter direction determines the path along which a filter travels between two related tables. In Power BI, filter direction can be:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single&lt;/strong&gt;: The filter flows from the "one" side to the "many" side only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both&lt;/strong&gt;: The filter flows in both directions between the two tables.&lt;br&gt;
The default and recommended direction is &lt;strong&gt;single-direction&lt;/strong&gt;: from dimension table to fact table. This ensures predictable filter propagation and avoids ambiguous results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bidirectional filtering&lt;/strong&gt; should be used cautiously and only when there is a clear requirement, as it can lead to unexpected filter propagation and circular relationships.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example Dimensions
&lt;/h3&gt;

&lt;p&gt;A role-playing dimension is a dimension table that relates to the same fact table multiple times in different roles. The classic example is a date dimension: the same date table can filter a sales fact by &lt;strong&gt;order date&lt;/strong&gt;, &lt;strong&gt;ship date&lt;/strong&gt;, and &lt;strong&gt;delivery date&lt;/strong&gt; separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Bidirectional Filtering Is
&lt;/h2&gt;

&lt;p&gt;Bidirectional filtering is a relationship setting in Power BI where filters propagate in both directions between two tables. Instead of the default single-direction flow (from the "one" side to the "many" side), a bidirectional relationship allows filters to travel from the "many" side back to the "one" side as well.&lt;/p&gt;

&lt;p&gt;In Power BI Model View, this is configured by setting the &lt;strong&gt;Cross filter direction&lt;/strong&gt; to &lt;strong&gt;Both&lt;/strong&gt; on a relationship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Should Be Used Carefully
&lt;/h2&gt;

&lt;p&gt;Bidirectional filtering is powerful but dangerous. It should be used carefully because it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Introduces ambiguity into filter propagation&lt;/li&gt;
&lt;li&gt;Can produce unexpected and incorrect results&lt;/li&gt;
&lt;li&gt;Creates unnecessary model complexity&lt;/li&gt;
&lt;li&gt;Degrades query performance&lt;/li&gt;
&lt;li&gt;Makes the model harder to debug and maintain&lt;/li&gt;
&lt;li&gt;Can create circular relationships that Power BI cannot resolve&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Problem 1: Ambiguous Filter Paths
&lt;/h2&gt;

&lt;p&gt;An ambiguous filter path occurs when there is more than one route for a filter to travel between two tables. When multiple paths exist, Power BI cannot determine which path to use, and the result becomes unpredictable or incorrect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: Unnecessary Model Complexity
&lt;/h2&gt;

&lt;p&gt;Every bidirectional relationship adds a layer of complexity. The more bidirectional relationships there are, the harder it becomes to predict filter propagation, understand measure results, identify bugs, and document the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: Performance Degradation
&lt;/h2&gt;

&lt;p&gt;Bidirectional filtering increases the number of filter operations the engine must perform. Each bidirectional relationship forces the query engine to evaluate filter propagation in both directions, which increases query execution time, consumes more memory, slows down report rendering, and reduces the effectiveness of VertiPaq compression optimisations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 4: Circular Relationships
&lt;/h2&gt;

&lt;p&gt;Bidirectional filtering can create circular relationships. A circular relationship occurs when a filter can travel from Table A to Table B and back to Table A through a different path. Power BI cannot resolve circular relationships and will either disable one of the relationships, return an error, or produce incorrect results silently.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What a Join Is
&lt;/h2&gt;

&lt;p&gt;A join is an operation that combines two tables into one by matching rows based on a common column (a key). In Power Query, this is done using &lt;strong&gt;Merge Queries&lt;/strong&gt;, which lets you select two tables, choose the matching columns, and pick a join type.&lt;/p&gt;

&lt;p&gt;The result is a single table containing columns from both source tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample Tables Used in All Examples
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Customers&lt;/strong&gt;&lt;br&gt;
CustomerID  CustomerName&lt;br&gt;
1           Alice&lt;br&gt;
2           Brian&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orders&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OrderID CustomerID Amount&lt;br&gt;
101    1       500&lt;br&gt;
102    1       300&lt;/p&gt;

&lt;p&gt;Note: CustomerID 3 and 4 have no orders. CustomerID 5 has an order but no customer.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: Returns all rows from the left table and matching rows from the right table. Unmatched rows from the left keep their data; right-side columns become null.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Records retained&lt;/strong&gt;: All left table rows + matching right table rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example&lt;/strong&gt;: Keep all customers, attach their orders where available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected output&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;CustomerID  CustomerName    OrderID Amount&lt;br&gt;
1              Alice    101 500&lt;br&gt;
1              Alice    102 300&lt;br&gt;
2              Brian    103 700&lt;br&gt;
3              Carol    null    null&lt;br&gt;
4              David    null    null&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: Returns all rows from the right table and matching rows from the left table. Unmatched rows from the right keep their data; left-side columns become null.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Records retained&lt;/strong&gt;: All right table rows + matching left table rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example&lt;/strong&gt;: Keep all orders, attach customer details where available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected output&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CustomerID  CustomerName    OrderID Amount&lt;br&gt;
1               Alice   101 500&lt;br&gt;
1               Alice   102 300&lt;br&gt;
2               Brian   103 700&lt;br&gt;
null                null    104 200&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: Returns all rows from both tables. Matches are combined; unmatched rows from either side get nulls in the opposite columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Records retained&lt;/strong&gt;: All rows from both tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example&lt;/strong&gt;: See every customer and every order, whether or not they match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected output&lt;/strong&gt;:&lt;br&gt;
CustomerID  CustomerName    OrderID Amount&lt;br&gt;
1             Alice          101     500&lt;br&gt;
1             Alice           102    300&lt;br&gt;
2             Brian           100    700&lt;br&gt;
3             Carol           null   null&lt;br&gt;
4             David           null   null&lt;br&gt;
null              null             104   200&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: Returns only rows where the key exists in both tables. Unmatched rows from either side are discarded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Records retained&lt;/strong&gt;: Only matching rows from both tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example&lt;/strong&gt;: Show only customers who have placed orders, and only orders that belong to a known customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected output&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;CustomerID  CustomerName    OrderID Amount&lt;br&gt;
1             Alice         101 500&lt;br&gt;
1             Alice         102 300&lt;br&gt;
2             Brian         103 700&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: Returns only rows from the left table that have no match in the right table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Records retained&lt;/strong&gt;: Left table rows with no match in the right table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example&lt;/strong&gt;: Find customers who have never placed an order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected output&lt;/strong&gt;:&lt;br&gt;
CustomerID  CustomerName&lt;br&gt;
3           Carol&lt;br&gt;
4           David&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: Returns only rows from the right table that have no match in the left table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Records retained&lt;/strong&gt;: Right table rows with no match in the left table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical example&lt;/strong&gt;: Find orders that do not belong to any known customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected output&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;OrderID CustomerID  Amount&lt;br&gt;
104   5         200&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Power Query joins and Power BI relationships are two different operations that happen at two different stages of the Power BI workflow. They are often confused because both "connect" tables — but they do so in fundamentally different ways.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Aspect | Power Query Merge (Join) | Power BI Relationship |&lt;/li&gt;
&lt;li&gt; Stage | Data preparation (before loading) | Data modelling (after loading) |&lt;/li&gt;
&lt;li&gt; Effect | Physically combines columns into one table | Logically connects separate tables |&lt;/li&gt;
&lt;li&gt; Storage | One wide table stored in the model | Separate tables stored in the model |&lt;/li&gt;
&lt;li&gt; Filtering | Not applicable — data is already combined | Enables filter propagation between tables |&lt;/li&gt;
&lt;li&gt; Reversible | Yes, by editing the query steps | Yes, by deleting the relationship |&lt;/li&gt;
&lt;li&gt; Performance | Increases table width and size | Keeps model compact and optimised |&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Does a Power Query Merge Physically Combine Columns/Data from Tables?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yes.&lt;/strong&gt; A merge in Power Query physically combines columns from two tables into a single table. The result is one wider table that contains columns from both source tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;:&lt;br&gt;
&lt;strong&gt;Customers&lt;/strong&gt;&lt;br&gt;
Customers   CustomerName&lt;br&gt;
1           Alice&lt;br&gt;
2           Brian&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orders&lt;/strong&gt;&lt;br&gt;
OrderID CustomerID  Amount&lt;br&gt;
101  1         500&lt;br&gt;
103  2         700&lt;/p&gt;

&lt;p&gt;After a &lt;strong&gt;Left Outer Merge&lt;/strong&gt; on CustomerID:&lt;br&gt;
CustomerID  CustomerName    OrderID Amount&lt;br&gt;
1            Alice           101    500&lt;br&gt;
2            Brian           102    700&lt;/p&gt;

&lt;p&gt;The result is a &lt;strong&gt;single physical table&lt;/strong&gt; with all columns. CustomerName is now repeated for every order. The original two tables no longer exist separately in the model — only the merged result is loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Does Creating a Relationship Combine the Tables?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No.&lt;/strong&gt; A relationship does &lt;strong&gt;not&lt;/strong&gt; combine tables. It keeps them separate and creates a logical link between them based on a common key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: Using the same Customers and Orders tables, if you load them separately and create a relationship on CustomerID:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customers table stays as:&lt;/strong&gt;&lt;br&gt;
CustomerID   CustomerName&lt;br&gt;&lt;br&gt;
1            Alice&lt;br&gt;&lt;br&gt;
2            Brian  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orders table stays as:&lt;/strong&gt;&lt;br&gt;
OrderID CustomerID  Amount&lt;br&gt;
101  1         500&lt;br&gt;
102  2         700&lt;/p&gt;

&lt;p&gt;They remain two separate tables. The relationship allows filtering across them, but no columns are physically merged.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. At What Stage of the Power BI Workflow Does Each Operation Occur?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Power Query Merge&lt;/strong&gt;: Occurs during the &lt;strong&gt;data preparation stage&lt;/strong&gt;, before data is loaded into the Power BI data model. It is part of the ETL (Extract, Transform, Load) process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Power BI Relationship&lt;/strong&gt;: Occurs during the &lt;strong&gt;data modelling stage&lt;/strong&gt;, after data is loaded into the model. It is created in Model View or automatically detected by Power BI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;:&lt;br&gt;
Step 1: Get Data&lt;br&gt;
↓&lt;br&gt;
Step 2: Power Query (Transform + Merge) ← Power Query joins happen here&lt;br&gt;
↓&lt;br&gt;
Step 3: Load to Data Model&lt;br&gt;
↓&lt;br&gt;
Step 4: Create Relationships in Model View ← Relationships happen here&lt;br&gt;
↓&lt;br&gt;
Step 5: Build Reports and Visuals&lt;/p&gt;

&lt;h2&gt;
  
  
  4. When Would You Choose a Merge Instead of a Relationship?
&lt;/h2&gt;

&lt;p&gt;Use a &lt;strong&gt;merge&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need columns from both tables in a &lt;strong&gt;single flat output&lt;/strong&gt;, such as for export or a calculated column.&lt;/li&gt;
&lt;li&gt;You need to &lt;strong&gt;filter rows&lt;/strong&gt; based on existence in another table (e.g., anti joins to find unmatched records).&lt;/li&gt;
&lt;li&gt;You need to &lt;strong&gt;replace missing values&lt;/strong&gt; with values from another table (e.g., lookup).&lt;/li&gt;
&lt;li&gt;You are preparing data for a system that does not support relationships.&lt;/li&gt;
&lt;li&gt;You need to &lt;strong&gt;combine tables at the same grain&lt;/strong&gt; and the result is intended to be one table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: Finding customers who have never placed an order using a Left Anti Join. This is a Power Query operation because it filters rows, not a modelling operation.&lt;/p&gt;

&lt;p&gt;Use a &lt;strong&gt;relationship&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are building a &lt;strong&gt;star schema&lt;/strong&gt; for reporting and analytics.&lt;/li&gt;
&lt;li&gt;You need &lt;strong&gt;filter propagation&lt;/strong&gt; from dimensions to facts.&lt;/li&gt;
&lt;li&gt;You want to keep the model &lt;strong&gt;compact and performant&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You need to write &lt;strong&gt;DAX measures&lt;/strong&gt; that aggregate across tables.&lt;/li&gt;
&lt;li&gt;You want to avoid data redundancy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: Connecting DimCustomer to FactSales for slicing sales by customer attributes. This is a relationship because the tables serve different roles (dimension vs fact) and should remain separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. How Can Excessive Merging Affect the Structure of a Data Model?
&lt;/h2&gt;

&lt;p&gt;Excessive merging produces a &lt;strong&gt;wide, flat table&lt;/strong&gt; with many repeated values. This leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data redundancy&lt;/strong&gt;: Descriptive attributes repeat in every row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Increased storage size&lt;/strong&gt;: More columns and more rows consume more memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poor compression&lt;/strong&gt;: VertiPaq compresses poorly when the same text repeats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slower queries&lt;/strong&gt;: Larger tables take longer to scan and aggregate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss of relationships&lt;/strong&gt;: Once merged, you cannot create relationships between the original tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Difficult maintenance&lt;/strong&gt;: Updating a dimension attribute requires refreshing the entire merged table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No reusability&lt;/strong&gt;: The merged table serves one purpose only; it cannot be reused for other analyses.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Why Might Keeping Fact and Dimension Tables Separate Be Preferable in a BI Model?
&lt;/h2&gt;

&lt;p&gt;Keeping fact and dimension tables separate is the foundation of the &lt;strong&gt;star schema&lt;/strong&gt;, which is Power BI's recommended design pattern. Benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Simple one-to-many relationships enable efficient filter propagation and query optimisation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression&lt;/strong&gt;: Dimension tables store unique values once, improving VertiPaq compression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusability&lt;/strong&gt;: The same dimension can serve multiple fact tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: New dimensions can be added without restructuring the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability&lt;/strong&gt;: Updating a dimension attribute requires refreshing only one small table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clarity&lt;/strong&gt;: Business users can easily understand the model structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DAX simplicity&lt;/strong&gt;: Measures are simpler when dimensions filter and facts aggregate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt;: Users can slice and dice data by any dimension attribute without pre-defining combinations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion and Recommendation
&lt;/h1&gt;

&lt;p&gt;For a typical business intelligence project, I would use a &lt;strong&gt;star schema&lt;/strong&gt; with &lt;strong&gt;one-to-many, single-direction relationships&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Justification
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query and report performance&lt;/strong&gt;: One-to-many relationships between dimensions and facts give the fastest filter propagation and query execution. VertiPaq is optimised for this structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DAX simplicity&lt;/strong&gt;: Measures are simple — dimensions filter, facts aggregate. No complex filter traversal is needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model readability&lt;/strong&gt;: A fact table surrounded by dimensions is intuitive for both developers and business users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: New dimensions can be added without restructuring the model. Handles growing data volumes well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data redundancy&lt;/strong&gt;: Dimension attributes are stored once, not repeated in every fact row, keeping the model compact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability&lt;/strong&gt;: Updating a dimension attribute requires refreshing one small table, not the entire model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ease of creating reports&lt;/strong&gt;: Users can slice facts by any dimension attribute without pre-defined combinations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter propagation&lt;/strong&gt;: Single-direction filtering from dimension to fact is predictable, avoids ambiguity, and prevents circular relationships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model complexity&lt;/strong&gt;: Star schemas are simple — no multi-layer chains, no ambiguous paths, no unnecessary bidirectional filtering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Not the Others
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flat table&lt;/strong&gt;: Rejected — severe redundancy, poor performance, no relationship support, not scalable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake schema&lt;/strong&gt;: Rejected — extra joins slow queries, complicate DAX, increase model complexity, and are harder for users to understand. Dimensions should be flattened in Power Query.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Relationship Design I Would Implement
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-to-many (1:*)&lt;/strong&gt; between every dimension and the fact table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-direction filtering&lt;/strong&gt; from dimension to fact in all standard cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bidirectional filtering&lt;/strong&gt; only for bridge tables in genuine many-to-many scenarios, documented and tested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-to-one&lt;/strong&gt; avoided — merge tables instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many-to-many&lt;/strong&gt; resolved through a bridge table with two one-to-many relationships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-playing dimensions&lt;/strong&gt; handled with multiple relationships and "User relation" in DAX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Star schema as the target model&lt;/strong&gt;, with all snowflake dimensions flattened in Power Query before loading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This design delivers the best balance of performance, simplicity, scalability, and maintainability for a typical BI project.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>data</category>
      <category>database</category>
    </item>
    <item>
      <title>GIT WORKFLOW</title>
      <dc:creator>Lucy Anyango</dc:creator>
      <pubDate>Mon, 24 Aug 2026 20:31:46 +0000</pubDate>
      <link>https://dev.to/lucy_anyango_d6aa9788c23a/git-workflow-j0b</link>
      <guid>https://dev.to/lucy_anyango_d6aa9788c23a/git-workflow-j0b</guid>
      <description>&lt;p&gt;&lt;strong&gt;## What is Git?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Git is a version control system used to track changes in files and maintain a history of a project.&lt;/p&gt;

&lt;p&gt;It allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track changes&lt;/li&gt;
&lt;li&gt;Create versions of your work&lt;/li&gt;
&lt;li&gt;Work with branches&lt;/li&gt;
&lt;li&gt;Collaborate with others&lt;/li&gt;
&lt;li&gt;Return to previous versions&lt;/li&gt;
&lt;li&gt;Git vs GitHub&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Git runs on your computer and tracks your project locally.&lt;/p&gt;

&lt;p&gt;GitHub is an online platform where Git repositories can be stored, shared, and collaborated on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## The Basic Git Workflow&lt;/strong&gt;&lt;br&gt;
Important workflow to understand is:&lt;/p&gt;

&lt;p&gt;Working Directory&lt;br&gt;
       ↓&lt;br&gt;
    git add&lt;br&gt;
       ↓&lt;br&gt;
Staging Area&lt;br&gt;
       ↓&lt;br&gt;
  git commit&lt;br&gt;
       ↓&lt;br&gt;
Local Repository&lt;br&gt;
       ↓&lt;br&gt;
   git push&lt;br&gt;
       ↓&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## GitHub&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check your changes
git status&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Shows which files have been added, modified, or are waiting to be staged.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stage changes
git add .&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Moves your changes to the staging area.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Commit changes
git commit -m "Update project files"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Creates a saved version of the staged changes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push to GitHub
git push&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Uploads your committed changes to the GitHub repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## Creating a Repository&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a project folder and initialize Git:&lt;/p&gt;

&lt;p&gt;mkdir my-project&lt;br&gt;
cd my-project&lt;br&gt;
git init&lt;/p&gt;

&lt;p&gt;git init turns the folder into a Git repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## Connecting to GitHub&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Connect your local repository to GitHub:&lt;/p&gt;

&lt;p&gt;git remote add origin &lt;a href="mailto:git@github.com"&gt;git@github.com&lt;/a&gt;:username/repository.git&lt;/p&gt;

&lt;p&gt;Check the connection:&lt;/p&gt;

&lt;p&gt;git remote -v&lt;/p&gt;

&lt;p&gt;Then push the project:&lt;/p&gt;

&lt;p&gt;git branch -M main&lt;br&gt;
git push -u origin main&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;## Git Commands to Remember&lt;br&gt;
Command Purpose&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;git status    Check changes&lt;/li&gt;
&lt;li&gt;git add . Stage changes&lt;/li&gt;
&lt;li&gt;git commit -m "message"   Save changes&lt;/li&gt;
&lt;li&gt;git push  Upload to GitHub&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember this workflow:&lt;/p&gt;

&lt;p&gt;EDIT&lt;br&gt;
 ↓&lt;br&gt;
git status&lt;br&gt;
 ↓&lt;br&gt;
git add&lt;br&gt;
 ↓&lt;br&gt;
git commit&lt;br&gt;
 ↓&lt;br&gt;
git push&lt;br&gt;
 ↓&lt;br&gt;
GITHUB&lt;/p&gt;

&lt;p&gt;Once you understand this cycle, you have the best foundation for using Git and GitHub effectively.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>git</category>
      <category>github</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
