<?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: Naomi Kwamboka Onderi</title>
    <description>The latest articles on DEV Community by Naomi Kwamboka Onderi (@naomi_kwamboka1001).</description>
    <link>https://dev.to/naomi_kwamboka1001</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%2F4071765%2F6f008c64-670b-4a56-970c-410e14d15957.png</url>
      <title>DEV Community: Naomi Kwamboka Onderi</title>
      <link>https://dev.to/naomi_kwamboka1001</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naomi_kwamboka1001"/>
    <language>en</language>
    <item>
      <title>Joins In SQL</title>
      <dc:creator>Naomi Kwamboka Onderi</dc:creator>
      <pubDate>Sat, 17 Oct 2026 23:38:41 +0000</pubDate>
      <link>https://dev.to/naomi_kwamboka1001/joins-in-sql-36ln</link>
      <guid>https://dev.to/naomi_kwamboka1001/joins-in-sql-36ln</guid>
      <description>&lt;h2&gt;
  
  
  Introduction-SQL window functions
&lt;/h2&gt;

&lt;p&gt;A window function calculates a value for each row using a set of related rows, called the window, and it keeps every input row. &lt;code&gt;GROUP BY&lt;/code&gt; is the other aggregate tool: it collapses each group into one row. Use a window when the hospital still needs the visit, and also needs a rank, a running total, or a comparison with the previous visit.&lt;/p&gt;

&lt;p&gt;OVER(), PARTITION BY, ORDER BY, and window frames&lt;/p&gt;

&lt;p&gt;-OVER() is what makes an aggregate or ranking function a window function. Everything inside    OVER() defines which rows are visible to the current row.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PARTITION BY&lt;code&gt;splits the result into independent groups.&lt;/code&gt;PARTITION BY department_id&lt;code&gt;restarts the calculation for each department. With no&lt;/code&gt;PARTITION BY`, the window is the whole result.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY&lt;/code&gt; inside &lt;code&gt;OVER()&lt;/code&gt; puts rows in a sequence inside each partition. Ranking functions need it. Aggregates use it to define a running calculation.&lt;/li&gt;
&lt;li&gt;A frame is the slice of the partition that the current row can see. It is written as &lt;code&gt;ROWS&lt;/code&gt; or &lt;code&gt;RANGE&lt;/code&gt; plus a start and an end, for example &lt;code&gt;UNBOUNDED PRECEDING&lt;/code&gt;, &lt;code&gt;CURRENT ROW&lt;/code&gt;, and &lt;code&gt;UNBOUNDED FOLLOWING&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two defaults matter in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An aggregate with an &lt;code&gt;ORDER BY&lt;/code&gt; and no frame uses &lt;code&gt;RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;/code&gt;. That is a running total from the first row through the current row.&lt;/li&gt;
&lt;li&gt;An aggregate with no &lt;code&gt;ORDER BY&lt;/code&gt; uses the whole partition. Each row in the department then sees the same department total.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ROWS&lt;/code&gt; counts physical rows. &lt;code&gt;RANGE&lt;/code&gt; also pulls in peer rows that share the same &lt;code&gt;ORDER BY&lt;/code&gt; value. Two Cardiology visits both billed at KES 30,000 are peers if the window is ordered only by &lt;code&gt;total_bill_kes&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    department_id,&lt;br&gt;
    visit_date,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    SUM(total_bill_kes) OVER (&lt;br&gt;
        PARTITION BY department_id&lt;br&gt;
        ORDER BY visit_date&lt;br&gt;
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;br&gt;
    ) AS running_department_bill&lt;br&gt;
FROM visits_fact;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This lists every visit and adds a running bill within its department, in date order. Cardiology comes out as 45,000, then 57,000, then 87,000, then 117,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ranking functions
&lt;/h2&gt;

&lt;p&gt;These four answer “where does this visit sit relative to the others?” They need &lt;code&gt;ORDER BY&lt;/code&gt; inside &lt;code&gt;OVER()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ROW_NUMBER()&lt;/code&gt;&lt;/strong&gt; assigns a unique sequence 1, 2, 3… inside the partition. Ties still get different numbers. The extra sort key decides which tied row comes first. Use it to keep one row per patient, such as the latest visit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;RANK()&lt;/code&gt;&lt;/strong&gt; gives tied rows the same rank and then skips. Two visits tied for 3rd both get 3, and the next visit gets 5. Use it for published league tables, where a shared place should open a gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;DENSE_RANK()&lt;/code&gt;&lt;/strong&gt; gives tied rows the same rank and does not skip. The next visit gets 4. Use it when the next band must stay consecutive, such as fee bands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;NTILE(n)&lt;/code&gt;&lt;/strong&gt; splits the ordered partition into &lt;code&gt;n&lt;/code&gt; buckets that are as equal as possible. Extra rows go into the earlier buckets. It splits rows, so two visits with the same bill can fall into different buckets. Use it to flag the top quarter of bills for audit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    ROW_NUMBER() OVER (ORDER BY total_bill_kes DESC, visit_id) AS row_num,&lt;br&gt;
    RANK()       OVER (ORDER BY total_bill_kes DESC)             AS rank_bill,&lt;br&gt;
    DENSE_RANK() OVER (ORDER BY total_bill_kes DESC)             AS dense_rank_bill,&lt;br&gt;
    NTILE(4)     OVER (ORDER BY total_bill_kes DESC, visit_id)   AS bill_quartile&lt;br&gt;
FROM visits_fact;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;br&gt;
&lt;code&gt;ROW_NUMBER()&lt;/code&gt; uses &lt;code&gt;visit_id&lt;/code&gt; as a tie-break so the two KES 30,000 visits (106 and 109) have a stable order. &lt;code&gt;RANK()&lt;/code&gt; and &lt;code&gt;DENSE_RANK()&lt;/code&gt; are ordered only by bill, so those two visits share a rank.&lt;br&gt;
| visit_id | bill | row_num | rank | dense_rank | quartile |&lt;br&gt;
|---|---:|---:|---:|---:|---:|&lt;br&gt;
| 105 | 60,000 | 1 | 1 | 1 | 1 |&lt;br&gt;
| 101 | 45,000 | 2 | 2 | 2 | 1 |&lt;br&gt;
| 106 | 30,000 | 3 | 3 | 3 | 1 |&lt;br&gt;
| 109 | 30,000 | 4 | 3 | 3 | 2 |&lt;br&gt;
| 107 | 18,000 | 5 | 5 | 4 | 2 |&lt;br&gt;
| 104 | 15,000 | 6 | 6 | 5 | 3 |&lt;br&gt;
| 102 | 12,000 | 7 | 7 | 6 | 3 |&lt;br&gt;
| 108 | 9,000 | 8 | 8 | 7 | 4 |&lt;br&gt;
| 103 | 8,000 | 9 | 9 | 8 | 4 |&lt;/p&gt;

&lt;p&gt;Visit 109 shows the difference: &lt;code&gt;RANK()&lt;/code&gt; jumps from 3 to 5, &lt;code&gt;DENSE_RANK()&lt;/code&gt; continues at 4, and &lt;code&gt;NTILE(4)&lt;/code&gt; places 106 in quartile 1 and 109 in quartile 2 even though the bill is the same. Nine rows into four tiles gives sizes 3, 2, 2, 2.&lt;/p&gt;

&lt;p&gt;Within a department, the same pattern marks the single most expensive visit:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT visit_id, department_id, total_bill_kes&lt;br&gt;
FROM (&lt;br&gt;
    SELECT&lt;br&gt;
        visit_id,&lt;br&gt;
        department_id,&lt;br&gt;
        total_bill_kes,&lt;br&gt;
        ROW_NUMBER() OVER (&lt;br&gt;
            PARTITION BY department_id&lt;br&gt;
            ORDER BY total_bill_kes DESC, visit_id&lt;br&gt;
        ) AS rn&lt;br&gt;
    FROM visits_fact&lt;br&gt;
) ranked&lt;br&gt;
WHERE rn = 1;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Expected rows: visit 101 for Cardiology (45,000), 104 for General Medicine (15,000), 105 for Orthopedics (60,000), and 107 for Pediatrics (18,000).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LAG()&lt;/code&gt; and &lt;code&gt;LEAD()&lt;/code&gt;&lt;br&gt;
&lt;code&gt;LAG(column, offset, default)&lt;/code&gt; reads a value from an earlier row in the ordered window. &lt;code&gt;LEAD()&lt;/code&gt; reads a later row. The offset defaults to 1. The default value is returned when that row does not exist; otherwise the result is &lt;code&gt;NULL&lt;/code&gt;.&lt;br&gt;
Finance uses this to see whether a return visit cost more than the previous one for the same patient.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    patient_id,&lt;br&gt;
    visit_date,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    LAG(total_bill_kes) OVER (&lt;br&gt;
        PARTITION BY patient_id&lt;br&gt;
        ORDER BY visit_date&lt;br&gt;
    ) AS previous_bill,&lt;br&gt;
    total_bill_kes - LAG(total_bill_kes) OVER (&lt;br&gt;
        PARTITION BY patient_id&lt;br&gt;
        ORDER BY visit_date&lt;br&gt;
    ) AS change_from_previous,&lt;br&gt;
    LEAD(visit_date) OVER (&lt;br&gt;
        PARTITION BY patient_id&lt;br&gt;
        ORDER BY visit_date&lt;br&gt;
    ) AS next_visit_date&lt;br&gt;
FROM visits_fact;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Patient 1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;visit_date&lt;/th&gt;
&lt;th&gt;bill&lt;/th&gt;
&lt;th&gt;previous_bill&lt;/th&gt;
&lt;th&gt;change&lt;/th&gt;
&lt;th&gt;next_visit_date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-01-05&lt;/td&gt;
&lt;td&gt;45,000&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;2026-01-12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-01-12&lt;/td&gt;
&lt;td&gt;12,000&lt;/td&gt;
&lt;td&gt;45,000&lt;/td&gt;
&lt;td&gt;−33,000&lt;/td&gt;
&lt;td&gt;2026-02-02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-02-02&lt;/td&gt;
&lt;td&gt;30,000&lt;/td&gt;
&lt;td&gt;12,000&lt;/td&gt;
&lt;td&gt;+18,000&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first visit has no previous bill. The last visit has no next date. Patient 2’s second visit rises by 7,000 (8,000 to 15,000). Patient 4’s second visit falls by 9,000 (18,000 to 9,000).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;FIRST_VALUE()&lt;/code&gt; and &lt;code&gt;LAST_VALUE()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FIRST_VALUE()&lt;/code&gt; returns the first value in the ordered frame. &lt;code&gt;LAST_VALUE()&lt;/code&gt; returns the last value in the frame. Both depend on the frame. With the default frame, &lt;code&gt;LAST_VALUE()&lt;/code&gt; stops at the current row, so it repeats the current visit and does not mean “the patient’s latest visit.” Extend the frame to the end of the partition when the latest value is what you want.&lt;br&gt;
&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    patient_id,&lt;br&gt;
    visit_id,&lt;br&gt;
    visit_date,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    FIRST_VALUE(total_bill_kes) OVER (&lt;br&gt;
        PARTITION BY patient_id&lt;br&gt;
        ORDER BY visit_date&lt;br&gt;
    ) AS first_visit_bill,&lt;br&gt;
    LAST_VALUE(total_bill_kes) OVER (&lt;br&gt;
        PARTITION BY patient_id&lt;br&gt;
        ORDER BY visit_date&lt;br&gt;
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING&lt;br&gt;
    ) AS last_visit_bill&lt;br&gt;
FROM visits_fact;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Every row for a patient shows that patient’s first and last bill. Patient 1 shows 45,000 and 30,000 on all three rows. Patient 2 shows 8,000 and 15,000. Patient 3 shows 60,000 and 30,000. Patient 4 shows 18,000 and 9,000. Drop the frame on &lt;code&gt;LAST_VALUE()&lt;/code&gt; and visit 101 would show 45,000 as its “last” bill, because the frame would end on that row.&lt;/p&gt;

&lt;h3&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;, and &lt;code&gt;MAX()&lt;/code&gt; as window functions
&lt;/h3&gt;

&lt;p&gt;These are the same aggregates used with &lt;code&gt;GROUP BY&lt;/code&gt;. Written with &lt;code&gt;OVER()&lt;/code&gt;, they attach the aggregate back onto each visit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    visit_id,&lt;br&gt;
    department_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    SUM(total_bill_kes)   OVER (PARTITION BY department_id) AS department_bill,&lt;br&gt;
    AVG(total_bill_kes)   OVER (PARTITION BY department_id) AS department_avg,&lt;br&gt;
    COUNT(*)              OVER (PARTITION BY department_id) AS department_visits,&lt;br&gt;
    MIN(total_bill_kes)   OVER (PARTITION BY department_id) AS department_min,&lt;br&gt;
    MAX(total_bill_kes)   OVER (PARTITION BY department_id) AS department_max,&lt;br&gt;
    total_bill_kes - AVG(total_bill_kes) OVER (PARTITION BY department_id)&lt;br&gt;
        AS variance_from_department_avg&lt;br&gt;
FROM visits_fact;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;There is no &lt;code&gt;ORDER BY&lt;/code&gt;, so each function sees the whole department and repeats the same aggregate on every visit in that department.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;department&lt;/th&gt;
&lt;th&gt;visits&lt;/th&gt;
&lt;th&gt;sum&lt;/th&gt;
&lt;th&gt;avg&lt;/th&gt;
&lt;th&gt;min&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cardiology&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;117,000&lt;/td&gt;
&lt;td&gt;29,250&lt;/td&gt;
&lt;td&gt;12,000&lt;/td&gt;
&lt;td&gt;45,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;General Medicine&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;23,000&lt;/td&gt;
&lt;td&gt;11,500&lt;/td&gt;
&lt;td&gt;8,000&lt;/td&gt;
&lt;td&gt;15,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orthopedics&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;60,000&lt;/td&gt;
&lt;td&gt;60,000&lt;/td&gt;
&lt;td&gt;60,000&lt;/td&gt;
&lt;td&gt;60,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pediatrics&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;27,000&lt;/td&gt;
&lt;td&gt;13,500&lt;/td&gt;
&lt;td&gt;9,000&lt;/td&gt;
&lt;td&gt;18,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Visit 101 (45,000) is 15,750 above the Cardiology average. Visit 102 (12,000) is 17,250 below it. A &lt;code&gt;GROUP BY department_id&lt;/code&gt; query would return the four summary rows and drop the visit identity. The window keeps both.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;ORDER BY visit_date&lt;/code&gt; and a &lt;code&gt;ROWS&lt;/code&gt; frame when the figure must accumulate. A running average of length of stay in Cardiology, ordered by date, is 3.0 after visit 101, then 2.0 after 102 (&lt;code&gt;(3+1)/2&lt;/code&gt;), then 2.0 after 106 (&lt;code&gt;(3+1+2)/3&lt;/code&gt;), then 2.0 after 109 (&lt;code&gt;(3+1+2+2)/4&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  SQL joins
&lt;/h2&gt;

&lt;p&gt;A join combines rows from two tables by matching them. In this model the match is a key: &lt;code&gt;visits_fact.department_id = dim_departments.department_id&lt;/code&gt;, and the same pattern for patients and doctors. The join type decides what happens to rows that have no match.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;INNER JOIN&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; returns only rows that match on both sides. A visit whose department id exists, and a department that has at least one visit, both appear. Maternity and patient 5 disappear.&lt;br&gt;
&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    v.visit_id,&lt;br&gt;
    d.department_name,&lt;br&gt;
    v.total_bill_kes&lt;br&gt;
FROM visits_fact AS v&lt;br&gt;
INNER JOIN dim_departments AS d&lt;br&gt;
    ON v.department_id = d.department_id;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Nine rows, one per visit, with Cardiology, General Medicine, Orthopedics, or Pediatrics. Use this when an unmatched row would be an incomplete fact, such as a bill that must belong to a known department.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;LEFT JOIN&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LEFT JOIN&lt;/code&gt; keeps every row from the left table. Columns from the right table are &lt;code&gt;NULL&lt;/code&gt; when nothing matches. Put the table you must not lose on the left.&lt;br&gt;
&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    d.department_name,&lt;br&gt;
    v.visit_id,&lt;br&gt;
    v.total_bill_kes&lt;br&gt;
FROM dim_departments AS d&lt;br&gt;
LEFT JOIN visits_fact AS v&lt;br&gt;
    ON d.department_id = v.department_id;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The nine visits appear, plus one extra row: Maternity with a &lt;code&gt;NULL&lt;/code&gt; visit and a &lt;code&gt;NULL&lt;/code&gt; bill. Use this for “show every department, including those with no activity this period.” Putting &lt;code&gt;visits_fact&lt;/code&gt; on the left instead would keep every visit and drop Maternity, which is a different question.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;RIGHT JOIN&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;RIGHT JOIN&lt;/code&gt; keeps every row from the right table. It is the same match rule as &lt;code&gt;LEFT JOIN&lt;/code&gt; with the tables swapped. This query returns the same rows as the &lt;code&gt;LEFT JOIN&lt;/code&gt; above:&lt;br&gt;
&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    d.department_name,&lt;br&gt;
    v.visit_id,&lt;br&gt;
    v.total_bill_kes&lt;br&gt;
FROM visits_fact AS v&lt;br&gt;
RIGHT JOIN dim_departments AS d&lt;br&gt;
    ON v.department_id = d.department_id;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;br&gt;
&lt;code&gt;LEFT JOIN&lt;/code&gt; is the usual way to write this, because reading from the preserved table on the left is easier to follow. &lt;code&gt;RIGHT JOIN&lt;/code&gt; is the right tool when a query is already built from the fact table and you need to preserve the dimension without rewriting the &lt;code&gt;FROM&lt;/code&gt; clause.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;FULL OUTER JOIN&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FULL OUTER JOIN&lt;/code&gt; keeps unmatched rows from both tables. A patient with no visit and a visit with no patient would both appear. In this extract every visit has a patient, so the only extra row is patient 5.&lt;br&gt;
&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    p.patient_id,&lt;br&gt;
    p.county,&lt;br&gt;
    v.visit_id,&lt;br&gt;
    v.total_bill_kes&lt;br&gt;
FROM dim_patients AS p&lt;br&gt;
FULL OUTER JOIN visits_fact AS v&lt;br&gt;
    ON p.patient_id = v.patient_id;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Nine visit rows, plus patient 5 (Kiambu) with &lt;code&gt;NULL&lt;/code&gt; visit columns. Use it for a reconciliation: registered patients who never attended, and visit records that do not match a patient.&lt;/p&gt;

&lt;p&gt;MySQL writes the same result as a union of both outer joins:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT p.patient_id, p.county, v.visit_id, v.total_bill_kes&lt;br&gt;
FROM dim_patients AS p&lt;br&gt;
LEFT JOIN visits_fact AS v ON p.patient_id = v.patient_id&lt;br&gt;
UNION ALL&lt;br&gt;
SELECT p.patient_id, p.county, v.visit_id, v.total_bill_kes&lt;br&gt;
FROM dim_patients AS p&lt;br&gt;
RIGHT JOIN visits_fact AS v ON p.patient_id = v.patient_id&lt;br&gt;
WHERE p.patient_id IS NULL;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The second leg is empty on this extract, because no visit lacks a patient.&lt;/p&gt;

&lt;p&gt;## &lt;code&gt;CROSS JOIN&lt;/code&gt;&lt;br&gt;
&lt;code&gt;CROSS JOIN&lt;/code&gt; matches every row on the left with every row on the right. There is no &lt;code&gt;ON&lt;/code&gt;. Five departments and six doctors produce 30 rows. Use it only when the product is the real question, such as a blank roster of every doctor against every department before you mark who is actually assigned.&lt;br&gt;
&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    doc.doctor_name,&lt;br&gt;
    dep.department_name&lt;br&gt;
FROM dim_doctors AS doc&lt;br&gt;
CROSS JOIN dim_departments AS dep;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Crossing &lt;code&gt;visits_fact&lt;/code&gt; with &lt;code&gt;dim_departments&lt;/code&gt; would invent department combinations the patient never attended. That is the failure mode to avoid: a missing &lt;code&gt;ON&lt;/code&gt; clause in an old comma-style join behaves as a cross join.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;SELF JOIN&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A self join uses the same table twice, under two aliases, and relates rows to other rows in that table. Follow-up visits are a visit matched to an earlier visit for the same patient.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    earlier.patient_id,&lt;br&gt;
    earlier.visit_id  AS first_visit,&lt;br&gt;
    earlier.visit_date AS first_date,&lt;br&gt;
    later.visit_id    AS follow_up_visit,&lt;br&gt;
    later.visit_date  AS follow_up_date&lt;br&gt;
FROM visits_fact AS earlier&lt;br&gt;
INNER JOIN visits_fact AS later&lt;br&gt;
    ON earlier.patient_id = later.patient_id&lt;br&gt;
   AND earlier.visit_date &amp;lt; later.visit_date;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;patient&lt;/th&gt;
&lt;th&gt;first visit&lt;/th&gt;
&lt;th&gt;follow-up&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;101 on 5 Jan&lt;/td&gt;
&lt;td&gt;102 on 12 Jan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;101 on 5 Jan&lt;/td&gt;
&lt;td&gt;109 on 2 Feb&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;102 on 12 Jan&lt;/td&gt;
&lt;td&gt;109 on 2 Feb&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;103 on 6 Jan&lt;/td&gt;
&lt;td&gt;104 on 20 Jan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;105 on 8 Jan&lt;/td&gt;
&lt;td&gt;106 on 15 Jan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;107 on 9 Jan&lt;/td&gt;
&lt;td&gt;108 on 18 Jan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Patient 1 has three pairs because three visits can be ordered in three earlier/later combinations. The date condition stops a visit from matching itself and stops each pair from appearing twice.&lt;/p&gt;

&lt;p&gt;Which join answers which hospital question&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Join&lt;/th&gt;
&lt;th&gt;Rows that survive&lt;/th&gt;
&lt;th&gt;Hospital use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INNER JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Matches only&lt;/td&gt;
&lt;td&gt;Bills that must have a known department and doctor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LEFT JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All of the left table, plus matches&lt;/td&gt;
&lt;td&gt;Every department, including those with zero visits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RIGHT JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All of the right table, plus matches&lt;/td&gt;
&lt;td&gt;Same as &lt;code&gt;LEFT JOIN&lt;/code&gt;, with the preserved table written on the right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FULL OUTER JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Matches plus unmatched rows from both sides&lt;/td&gt;
&lt;td&gt;Patients with no visit and visits with no patient, together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CROSS JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every combination&lt;/td&gt;
&lt;td&gt;A blank coverage grid, not a list of real visits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELF JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rows related to other rows in the same table&lt;/td&gt;
&lt;td&gt;A visit compared with the same patient’s earlier visit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;## Common table expressions&lt;/p&gt;

&lt;p&gt;A CTE is a named result defined with &lt;code&gt;WITH&lt;/code&gt; and used by the main query that follows. It lasts for that one statement. It does not create a table. The reason to use one is to name an intermediate result, reuse it, and keep a multi-step hospital question readable.&lt;/p&gt;

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

&lt;p&gt;This names each department’s revenue, then keeps departments above the average department revenue.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
WITH department_revenue AS (&lt;br&gt;
    SELECT&lt;br&gt;
        d.department_id,&lt;br&gt;
        d.department_name,&lt;br&gt;
        SUM(v.total_bill_kes) AS revenue&lt;br&gt;
    FROM dim_departments AS d&lt;br&gt;
    INNER JOIN visits_fact AS v&lt;br&gt;
        ON v.department_id = d.department_id&lt;br&gt;
    GROUP BY d.department_id, d.department_name&lt;br&gt;
)&lt;br&gt;
SELECT *&lt;br&gt;
FROM department_revenue&lt;br&gt;
WHERE revenue &amp;gt; (SELECT AVG(revenue) FROM department_revenue);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The four department revenues are 117,000, 23,000, 60,000, and 27,000. Their average is 56,750. Cardiology (117,000) and Orthopedics (60,000) are returned. The same CTE is referenced twice: once as the source, once inside the average. A subquery would have to repeat the aggregation text.&lt;/p&gt;

&lt;p&gt;Multiple CTEs&lt;/p&gt;

&lt;p&gt;Separate steps by commas. Later CTEs may use earlier ones.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
WITH visit_detail AS (&lt;br&gt;
    SELECT&lt;br&gt;
        v.visit_id,&lt;br&gt;
        v.total_bill_kes,&lt;br&gt;
        p.county,&lt;br&gt;
        p.patient_gender,&lt;br&gt;
        d.department_name&lt;br&gt;
    FROM visits_fact AS v&lt;br&gt;
    INNER JOIN dim_patients AS p&lt;br&gt;
        ON p.patient_id = v.patient_id&lt;br&gt;
    INNER JOIN dim_departments AS d&lt;br&gt;
        ON d.department_id = v.department_id&lt;br&gt;
),&lt;br&gt;
county_summary AS (&lt;br&gt;
    SELECT&lt;br&gt;
        county,&lt;br&gt;
        COUNT(*) AS visits,&lt;br&gt;
        SUM(total_bill_kes) AS revenue&lt;br&gt;
    FROM visit_detail&lt;br&gt;
    GROUP BY county&lt;br&gt;
)&lt;br&gt;
SELECT&lt;br&gt;
    c.county,&lt;br&gt;
    c.visits,&lt;br&gt;
    c.revenue,&lt;br&gt;
    vd.department_name,&lt;br&gt;
    vd.total_bill_kes&lt;br&gt;
FROM county_summary AS c&lt;br&gt;
INNER JOIN visit_detail AS vd&lt;br&gt;
    ON vd.county = c.county&lt;br&gt;
ORDER BY c.revenue DESC, vd.visit_id;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;br&gt;
&lt;code&gt;visit_detail&lt;/code&gt; joins visits to patients and departments. &lt;code&gt;county_summary&lt;/code&gt; aggregates that result. The main query joins the summary back to the detail so each visit sits beside its county total.&lt;br&gt;
| county | visits | revenue | visit bills |&lt;br&gt;
|---|---:|---:|---|&lt;br&gt;
| Nairobi | 3 | 87,000 | 45,000, 12,000, 30,000 |&lt;br&gt;
| Mombasa | 2 | 90,000 | 60,000, 30,000 |&lt;br&gt;
| Kisumu | 2 | 23,000 | 8,000, 15,000 |&lt;br&gt;
| Nakuru | 2 | 27,000 | 18,000, 9,000 |&lt;/p&gt;

&lt;p&gt;Mombasa sorts first on revenue. Kiambu is absent because patient 5 never reaches &lt;code&gt;visit_detail&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A recursive CTE
&lt;/h2&gt;

&lt;p&gt;A recursive CTE has an anchor member, &lt;code&gt;UNION ALL&lt;/code&gt;, and a recursive member that reads the CTE itself. The anchor starts the set. Each pass adds the next level until no new rows appear. The Cardiology roster is a tree: Achieng, then Otieno, then Mwangi.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`sql&lt;br&gt;
WITH RECURSIVE reporting_line AS (&lt;br&gt;
    SELECT&lt;br&gt;
        doctor_id,&lt;br&gt;
        doctor_name,&lt;br&gt;
        reports_to_doctor_id,&lt;br&gt;
        0 AS level&lt;br&gt;
    FROM dim_doctors&lt;br&gt;
    WHERE doctor_name = 'Dr Achieng'&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UNION ALL

SELECT
    doc.doctor_id,
    doc.doctor_name,
    doc.reports_to_doctor_id,
    line.level + 1
FROM dim_doctors AS doc
INNER JOIN reporting_line AS line
    ON doc.reports_to_doctor_id = line.doctor_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;)&lt;br&gt;
SELECT *&lt;br&gt;
FROM reporting_line&lt;br&gt;
ORDER BY level;&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;doctor&lt;/th&gt;
&lt;th&gt;reports to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Dr Achieng&lt;/td&gt;
&lt;td&gt;nobody (anchor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Dr Otieno&lt;/td&gt;
&lt;td&gt;Dr Achieng&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Dr Mwangi&lt;/td&gt;
&lt;td&gt;Dr Otieno&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SQL Server writes &lt;code&gt;WITH reporting_line&lt;/code&gt; without the word &lt;code&gt;RECURSIVE&lt;/code&gt;. The query needs a condition that eventually returns no rows. A cycle in &lt;code&gt;reports_to_doctor_id&lt;/code&gt; would run until the engine’s recursion limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  How CTEs differ from subqueries
&lt;/h2&gt;

&lt;p&gt;Both can sit in &lt;code&gt;FROM&lt;/code&gt; and both can be joined and aggregated. The CTE name is the practical difference.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CTE is declared before the main query and can be joined more than once under one name. &lt;code&gt;department_revenue&lt;/code&gt; was used as the list and as the input to &lt;code&gt;AVG&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A subquery is written where it is used. The logic is local, and repeating it means copying the SQL.&lt;/li&gt;
&lt;li&gt;A recursive walk of the doctor tree can be expressed as a CTE. A subquery in &lt;code&gt;FROM&lt;/code&gt; cannot call itself.&lt;/li&gt;
&lt;li&gt;The optimizer may inline either form. Readability and reuse are the reasons to choose; one is not automatically faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a subquery for a short, single-use test. Use a CTE when the statement has several named steps, or when the step is recursive.&lt;/p&gt;




&lt;p&gt;## SQL subqueries&lt;/p&gt;

&lt;p&gt;A subquery is a query inside another query. Its place in the outer query, and whether it refers to the outer query, decide its type.&lt;/p&gt;

&lt;p&gt;Where a subquery can sit&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In &lt;code&gt;SELECT&lt;/code&gt;.&lt;/strong&gt; A scalar subquery returns one column and one row, and it is printed beside each outer row. This attaches the patient’s visit count to every visit:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    v.visit_id,&lt;br&gt;
    v.patient_id,&lt;br&gt;
    v.total_bill_kes,&lt;br&gt;
    (&lt;br&gt;
        SELECT COUNT(*)&lt;br&gt;
        FROM visits_fact AS v2&lt;br&gt;
        WHERE v2.patient_id = v.patient_id&lt;br&gt;
    ) AS visits_for_this_patient&lt;br&gt;
FROM visits_fact AS v;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Visits 101, 102, and 109 show 3. Visits 103 and 104 show 2. Visits 105 and 106 show 2. Visits 107 and 108 show 2. More than one row, or more than one column, in this position is an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In &lt;code&gt;FROM&lt;/code&gt;.&lt;/strong&gt; The inner query is a derived table and must have an alias. This builds department totals, then keeps those at or above KES 50,000:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    dept.department_name,&lt;br&gt;
    dept.revenue&lt;br&gt;
FROM (&lt;br&gt;
    SELECT&lt;br&gt;
        d.department_name,&lt;br&gt;
        SUM(v.total_bill_kes) AS revenue&lt;br&gt;
    FROM dim_departments AS d&lt;br&gt;
    INNER JOIN visits_fact AS v&lt;br&gt;
        ON v.department_id = d.department_id&lt;br&gt;
    GROUP BY d.department_name&lt;br&gt;
) AS dept&lt;br&gt;
WHERE dept.revenue &amp;gt;= 50000;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Cardiology (117,000) and Orthopedics (60,000). General Medicine and Pediatrics fall below the cut. Maternity never enters the inner join.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In &lt;code&gt;WHERE&lt;/code&gt;.&lt;/strong&gt; The subquery filters the outer row. The &lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;EXISTS&lt;/code&gt;, &lt;code&gt;ANY&lt;/code&gt;, and &lt;code&gt;ALL&lt;/code&gt; examples below all sit in &lt;code&gt;WHERE&lt;/code&gt;. A scalar comparison sits there too: &lt;code&gt;total_bill_kes &amp;gt; (SELECT AVG(total_bill_kes) FROM visits_fact)&lt;/code&gt;. The overall average is 227,000 / 9 = 25,222.22. Visits above it are 101 (45,000), 105 (60,000), 106 (30,000), and 109 (30,000).&lt;/p&gt;

&lt;p&gt;##Scalar subqueries&lt;/p&gt;

&lt;p&gt;A scalar subquery returns a single value. The average above is scalar and non-correlated: it does not mention the outer query, so the engine computes it once. A scalar subquery in &lt;code&gt;SELECT&lt;/code&gt; that filters on the outer row, such as the visit count, is correlated and runs per outer row.&lt;/p&gt;

&lt;p&gt;##Correlated and non-correlated subqueries&lt;/p&gt;

&lt;p&gt;A correlated subquery refers to a column from the outer query. It is re-evaluated for each outer row. This keeps visits that cost more than that same patient’s own average:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT&lt;br&gt;
    v.patient_id,&lt;br&gt;
    v.visit_id,&lt;br&gt;
    v.total_bill_kes&lt;br&gt;
FROM visits_fact AS v&lt;br&gt;
WHERE v.total_bill_kes &amp;gt; (&lt;br&gt;
    SELECT AVG(v2.total_bill_kes)&lt;br&gt;
    FROM visits_fact AS v2&lt;br&gt;
    WHERE v2.patient_id = v.patient_id&lt;br&gt;
);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;patient&lt;/th&gt;
&lt;th&gt;average&lt;/th&gt;
&lt;th&gt;visits above that average&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;29,000&lt;/td&gt;
&lt;td&gt;101 (45,000), 109 (30,000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;11,500&lt;/td&gt;
&lt;td&gt;104 (15,000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;45,000&lt;/td&gt;
&lt;td&gt;105 (60,000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;13,500&lt;/td&gt;
&lt;td&gt;107 (18,000)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;v2.patient_id = v.patient_id&lt;/code&gt; is the correlation. Visit 102 is under patient 1’s average, so it is excluded even though 12,000 would be above patient 2’s average.&lt;br&gt;
A non-correlated subquery can run on its own. This list of active department ids does not look at the outer row:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT department_name&lt;br&gt;
FROM dim_departments&lt;br&gt;
WHERE department_id IN (&lt;br&gt;
    SELECT DISTINCT department_id&lt;br&gt;
    FROM visits_fact&lt;br&gt;
);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Cardiology, General Medicine, Orthopedics, and Pediatrics. Maternity is excluded because its id is not in &lt;code&gt;visits_fact&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nested subqueries
&lt;/h2&gt;

&lt;p&gt;A nested subquery is a subquery inside another subquery. Here the innermost query finds departments whose average bill is above the hospital average (25,222.22). Those departments are Cardiology (29,250) and Orthopedics (60,000). The middle query turns those ids into doctor ids. The outer query prints the doctors.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT doctor_name&lt;br&gt;
FROM dim_doctors&lt;br&gt;
WHERE department_id IN (&lt;br&gt;
    SELECT department_id&lt;br&gt;
    FROM visits_fact&lt;br&gt;
    GROUP BY department_id&lt;br&gt;
    HAVING AVG(total_bill_kes) &amp;gt; (&lt;br&gt;
        SELECT AVG(total_bill_kes) FROM visits_fact&lt;br&gt;
    )&lt;br&gt;
);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Dr Achieng, Dr Otieno, Dr Mwangi, and Dr Kamau. Dr Wanjiku and Dr Njeri work in departments below the hospital average.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;EXISTS&lt;/code&gt;, &lt;code&gt;ANY&lt;/code&gt;, and &lt;code&gt;ALL&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;IN&lt;/code&gt;&lt;/strong&gt; keeps the outer row when its value equals any value in the subquery list. Patients who had a Cardiology visit:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT patient_id, county&lt;br&gt;
FROM dim_patients&lt;br&gt;
WHERE patient_id IN (&lt;br&gt;
    SELECT patient_id&lt;br&gt;
    FROM visits_fact&lt;br&gt;
    WHERE department_id = 1&lt;br&gt;
);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Patients 1 (Nairobi) and 3 (Mombasa). &lt;code&gt;NOT IN&lt;/code&gt; means “none of these values.” It becomes unsafe if the subquery can return &lt;code&gt;NULL&lt;/code&gt;, because a comparison with &lt;code&gt;NULL&lt;/code&gt; is unknown. Prefer &lt;code&gt;NOT EXISTS&lt;/code&gt; for the negative test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;EXISTS&lt;/code&gt;&lt;/strong&gt; keeps the outer row when the subquery returns at least one row. It does not care what the columns contain. Departments that raised at least one bill over KES 40,000:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT d.department_name&lt;br&gt;
FROM dim_departments AS d&lt;br&gt;
WHERE EXISTS (&lt;br&gt;
    SELECT 1&lt;br&gt;
    FROM visits_fact AS v&lt;br&gt;
    WHERE v.department_id = d.department_id&lt;br&gt;
      AND v.total_bill_kes &amp;gt; 40000&lt;br&gt;
);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Cardiology (visit 101, 45,000) and Orthopedics (visit 105, 60,000). This subquery is correlated through &lt;code&gt;d.department_id&lt;/code&gt;. &lt;code&gt;EXISTS&lt;/code&gt; is the usual choice for a yes-or-no check. &lt;code&gt;IN&lt;/code&gt; is the usual choice when you already have a list of keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ANY&lt;/code&gt;&lt;/strong&gt; compares the outer value with each subquery value and succeeds if the comparison is true for at least one of them. &lt;code&gt;&amp;gt; ANY (...)&lt;/code&gt; means “greater than the smallest value in the list.” Pediatrics bills are 18,000 and 9,000, so &lt;code&gt;&amp;gt; ANY&lt;/code&gt; of those bills means greater than 9,000.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT visit_id, total_bill_kes&lt;br&gt;
FROM visits_fact&lt;br&gt;
WHERE total_bill_kes &amp;gt; ANY (&lt;br&gt;
    SELECT total_bill_kes&lt;br&gt;
    FROM visits_fact&lt;br&gt;
    WHERE department_id = 4&lt;br&gt;
);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Every visit except 103 (8,000) and 108 (9,000). Visit 108 is not greater than either Pediatrics bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ALL&lt;/code&gt;&lt;/strong&gt; succeeds only when the comparison is true for every subquery value. &lt;code&gt;&amp;gt; ALL&lt;/code&gt; of the Pediatrics bills means greater than 18,000, the larger one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
SELECT visit_id, total_bill_kes&lt;br&gt;
FROM visits_fact&lt;br&gt;
WHERE total_bill_kes &amp;gt; ALL (&lt;br&gt;
    SELECT total_bill_kes&lt;br&gt;
    FROM visits_fact&lt;br&gt;
    WHERE department_id = 4&lt;br&gt;
);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Visits 101 (45,000), 105 (60,000), 106 (30,000), and 109 (30,000). Visit 107 equals 18,000, so it fails a strict &lt;code&gt;&amp;gt;&lt;/code&gt; test.&lt;br&gt;
&lt;code&gt;IN&lt;/code&gt; is the same test as &lt;code&gt;= ANY&lt;/code&gt;. &lt;code&gt;NOT IN&lt;/code&gt; matches &lt;code&gt;&amp;lt;&amp;gt; ALL&lt;/code&gt; only when the list contains no &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;##How the four tools fit one hospital question&lt;/p&gt;

&lt;p&gt;A monthly review can use all four in one statement: a CTE names the joined visit, a window ranks bills inside each department, and a subquery keeps departments that actually billed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;sql&lt;br&gt;
WITH billed_visits AS (&lt;br&gt;
    SELECT&lt;br&gt;
        v.visit_id,&lt;br&gt;
        v.visit_date,&lt;br&gt;
        v.total_bill_kes,&lt;br&gt;
        d.department_name,&lt;br&gt;
        doc.doctor_name&lt;br&gt;
    FROM visits_fact AS v&lt;br&gt;
    INNER JOIN dim_departments AS d&lt;br&gt;
        ON d.department_id = v.department_id&lt;br&gt;
    INNER JOIN dim_doctors AS doc&lt;br&gt;
        ON doc.doctor_id = v.doctor_id&lt;br&gt;
)&lt;br&gt;
SELECT&lt;br&gt;
    department_name,&lt;br&gt;
    doctor_name,&lt;br&gt;
    visit_id,&lt;br&gt;
    total_bill_kes,&lt;br&gt;
    RANK() OVER (&lt;br&gt;
        PARTITION BY department_name&lt;br&gt;
        ORDER BY total_bill_kes DESC&lt;br&gt;
    ) AS bill_rank_in_department&lt;br&gt;
FROM billed_visits&lt;br&gt;
WHERE department_name IN (&lt;br&gt;
    SELECT d.department_name&lt;br&gt;
    FROM dim_departments AS d&lt;br&gt;
    WHERE EXISTS (&lt;br&gt;
        SELECT 1&lt;br&gt;
        FROM visits_fact AS v&lt;br&gt;
        WHERE v.department_id = d.department_id&lt;br&gt;
    )&lt;br&gt;
);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The inner joins attach a department and a doctor to each real visit. &lt;code&gt;EXISTS&lt;/code&gt; limits the list to departments with activity, so Maternity stays out. &lt;code&gt;RANK()&lt;/code&gt; marks the most expensive visit in each department as 1. Cardiology’s rank 1 is visit 101. The two KES 30,000 Cardiology visits share rank 2, and visit 102 is rank 4.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Power BI Data Modelling, Relationships &amp; Joins</title>
      <dc:creator>Naomi Kwamboka Onderi</dc:creator>
      <pubDate>Sun, 13 Sep 2026 15:00:32 +0000</pubDate>
      <link>https://dev.to/naomi_kwamboka1001/power-bi-data-modelling-relationships-joins-3j85</link>
      <guid>https://dev.to/naomi_kwamboka1001/power-bi-data-modelling-relationships-joins-3j85</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This week we got started on learning Power BI its uses and how its applied in data analysis. When working with Power BI, the way data is organized and connected has a major impact on how easily reports can be created, how calculations work, and how well a Power BI solution performs.&lt;br&gt;
Data modelling in Power BI involves organizing data into tables and defining relationships between those tables so that the data can be analyzed effectively. A well-designed data model makes reports easier to understand, DAX calculations easier to write, and Power BI solutions easier to maintain and scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Modelling in Power BI
&lt;/h2&gt;

&lt;p&gt;Data modelling is the process of organizing data into structured tables and establishing relationships between those tables so that Power BI can understand how the data is connected.&lt;br&gt;
Instead of keeping all this information in one huge table, the data can be separated into different tables and connected using common fields.&lt;br&gt;
A well-designed data model is important because it can improve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model readability&lt;/li&gt;
&lt;li&gt;Maintainability&lt;/li&gt;
&lt;li&gt;Filter propagation&lt;/li&gt;
&lt;li&gt;Overall reporting experience&lt;/li&gt;
&lt;li&gt;DAX calculations&lt;/li&gt;
&lt;li&gt;Data organization&lt;/li&gt;
&lt;li&gt;Scalability&lt;/li&gt;
&lt;li&gt;Report performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are several ways data can be structured. The three common modelling approaches are &lt;strong&gt;flat tables&lt;/strong&gt;, &lt;strong&gt;star schemas&lt;/strong&gt;, and &lt;strong&gt;snowflake schemas&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flat Tables
&lt;/h2&gt;

&lt;p&gt;A flat table stores all the required information in a single table. For simple datasets, a flat table may be sufficient, but it is generally less suitable for large and complex business intelligence solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantages of a Flat Table
&lt;/h2&gt;

&lt;p&gt;A flat table is relatively easy to understand because there are no relationships between multiple tables.&lt;br&gt;
It can be appropriate when there are few repeated values, a quick analysis is required, the analysis is simple and dataset is small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disadvantages of a Flat Table
&lt;/h2&gt;

&lt;p&gt;The major problem is data redundancy which can result in repeated data, larger datasets, more difficult maintenance, potential inconsistencies and less flexible modelling.&lt;/p&gt;

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

&lt;p&gt;A star schema separates the data into a main fact table and several surrounding tables. The structure resembles a star, which is where the name comes from. The main fact table contains business events or transactions, while the dimension tables contain descriptive information about those events.&lt;br&gt;
It is particularly useful for business intelligence and reporting solutions where users need to analyze business events across different dimensions.&lt;br&gt;
For example, a company could use a star schema to analyze:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sales by product&lt;/li&gt;
&lt;li&gt;Sales by product category&lt;/li&gt;
&lt;li&gt;Sales by customer&lt;/li&gt;
&lt;li&gt;Sales by location&lt;/li&gt;
&lt;li&gt;Sales by month&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Advantages of a Star Schema:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It is generally well suited to Power BI's analytical model.&lt;/li&gt;
&lt;li&gt;It is relatively simple to understand.&lt;/li&gt;
&lt;li&gt;It reduces unnecessary data duplication.&lt;/li&gt;
&lt;li&gt;It supports efficient reporting and analysis.&lt;/li&gt;
&lt;li&gt;It makes DAX calculations easier to manage.&lt;/li&gt;
&lt;li&gt;It provides a clear structure for filter propagation.&lt;/li&gt;
&lt;li&gt;It is easier to maintain and extend.
## Disadvantages of the Star Schema
Incorrect relationships or poorly designed keys can result in incorrect calculations or unexpected filtering it also requires a proper understanding of relationships between tables for everything to be correct.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A snowflake schema is similar to a star schema, but dimension tables are further divided into additional related tables.&lt;br&gt;
Instead of storing all product-related information in one dimension table, information can be separated into multiple related tables.&lt;br&gt;
It may be appropriate when the underlying data contains complex hierarchical structures or when dimensions are large and naturally divided into related entities. However, unless it's necessary it's advised to keep the  dimensions simpler to make the Power BI model easier to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advantages of a Snowflake Schema
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Represent complex hierarchies.&lt;/li&gt;
&lt;li&gt;Organize highly structured source data.&lt;/li&gt;
&lt;li&gt;Useful when dimensions contain multiple levels.&lt;/li&gt;
&lt;li&gt;Reduce repeated information within dimensions.
##Disadvantages of a Snowflake Schema
It can be more difficult to navigate than a straightforward star schema because of the extra tables, extra relationships and more filter paths.
This makes it more complex.
It has a steeper learning curve.
The report development can be complicated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparing the Three Approaches
&lt;/h2&gt;

&lt;p&gt;The key difference is how the data is organized. A flat table keeps everything together, a star schema separates facts from dimensions, while a snowflake schema takes normalization further by splitting dimensions into additional related tables.&lt;br&gt;
The star schema provides a practical balance between performance, simplicity, and maintainability.&lt;br&gt;
Power BI models consists of tables, keys, relationships, and rules that determine how information flows between those tables.&lt;/p&gt;

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

&lt;p&gt;A &lt;strong&gt;fact table&lt;/strong&gt; stores measurable business events or transactions. It answers questions such as how many?, how much?and how many transactions.&lt;br&gt;
&lt;strong&gt;Dimension tables&lt;/strong&gt; contain descriptive information used to analyze the facts. It answers questions such as who?, when? and where?&lt;/p&gt;

&lt;h2&gt;
  
  
  Grain or Granularity
&lt;/h2&gt;

&lt;p&gt;The grain describes exactly what one row in the fact table represents. Clearly defining the grain is important because it prevents confusion and incorrect calculations. For example For example, a sales fact table should not have one row representing a whole order in some cases and one row representing an individual product in other cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relationships in Power BI
&lt;/h2&gt;

&lt;p&gt;A relationship connects two tables using a common column. Relationships are necessary because business data is often distributed across multiple tables. For example it shows how a customer relates to a product or different products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relationship Cardinality
&lt;/h2&gt;

&lt;p&gt;Cardinality describes how many records in one table can be related to records in another table. Power BI supports several relationship types such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-to-Many (1:*)&lt;/strong&gt;- This is normally the preferred relationship between a dimension and a fact table. For example, A customer can make many sales, but each sales record belongs to one customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-to-One (1:1)&lt;/strong&gt;- A one-to-one relationship means that each record in one table corresponds to only one record in the other table. For example, Every employee has their own unique employee details such as their email or ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many-to-Many (:)&lt;/strong&gt;- A many-to-many relationship occurs when multiple records in one table can relate to multiple records in another table for example, students can enroll in many courses, and each course can have many students.
Many-to-many relationships can be useful, but they should be handled carefully because they can create ambiguous filtering and unexpected results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Primary Keys and Foreign Keys
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;primary key&lt;/strong&gt; uniquely identifies a record in a table, for example employee ID, Student ID.&lt;br&gt;
A &lt;strong&gt;foreign key&lt;/strong&gt; is a column used to reference a record in another table for example, a customer ID column can be found in different tables since it's possible for customers to make multiple purchases. The relationship works because the dimension contains unique values while the fact table can contain repeated values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referential Integrity
&lt;/h2&gt;

&lt;p&gt;Referential integrity means that foreign-key values should correspond to valid records in the related dimension. If the column does not exist in the dimension, the model may produce unexpected results when filtering or calculating data.&lt;br&gt;
Ensuring that keys are clean and valid is therefore an important part of data preparation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Active and Inactive Relationships
&lt;/h2&gt;

&lt;p&gt;An active relationship is the default relationship used when filters travel between tables. Only one relationship between the same two tables can normally be active at a time. An inactive relationship can be used in specific DAX calculations when required.&lt;/p&gt;

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

&lt;p&gt;Relationships can determine how filters move between tables.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-Direction Filtering&lt;/strong&gt;- In a typical star schema, filtering usually flows from the dimension to the fact table. This is generally the preferred approach for a star schema because the filter path is clear and predictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bidirectional Filtering&lt;/strong&gt;- This is when filters flow in both directions. It is recommended to use the single-direction filtering unless there is a specific reason to use bidirectional filtering.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Power Query provides a feature called &lt;strong&gt;Merge Queries&lt;/strong&gt;, which allows tables to be joined during the data transformation stage therefore relationships in the Power BI model are not the same thing as joins in Power Query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;** Left Outer Join**- This keeps all records from the left table and matching records from the right table.&lt;/li&gt;
&lt;li&gt;** Right Outer Join**- This keeps all records from the right table and matching records from the left table.&lt;/li&gt;
&lt;li&gt;** Full Outer Join**- This keeps all records from both tables. &lt;code&gt;ALL LEFT records + ALL RIGHT records&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;** Inner Join**- This keeps only records that have a match in both tables. Inner joins are useful when we only want records that exist in both datasets.&lt;/li&gt;
&lt;li&gt;** Left Anti Join**- This returns records that exist in the left table but do not have a match in the right table. This can be useful for data quality checks and finding missing relationships. &lt;/li&gt;
&lt;li&gt;** Right Anti Join**- This returns records that exist in the right table but do not have a match in the left table.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A Power Query merge combines information from two tables during the data preparation/transformation stage. A Power Query merge can physically bring columns from one table into another table.&lt;br&gt;
A relationship in Power BI is created later in the data model. Instead of physically combining the tables, we keep them separate. This means creating a relationship in Power BI does not physically merge the tables together.&lt;br&gt;
The relationship tells Power BI how filters and calculations should move between the tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Should You Use a Merge Instead of a Relationship?
&lt;/h2&gt;

&lt;p&gt;A merge can be appropriate when information genuinely belongs together or when data transformation requires combining columns. However, excessive merging can create very large and complicated flat tables. For analytical reporting, keeping fact and dimension tables separate is often preferable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Keep Fact and Dimension Tables Separate?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reduced Redundancy&lt;/li&gt;
&lt;li&gt;Easier DAX-Measures can be written against a clear fact table while dimensions provide filtering context.&lt;/li&gt;
&lt;li&gt;Better Model Readability- A developer can easily understand which tables contain facts and which contain dimensions.&lt;/li&gt;
&lt;li&gt;Easier Reporting&lt;/li&gt;
&lt;li&gt;Better Scalability&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Data modelling is one of the foundations of building effective Power BI solutions. Creating attractive visualizations is only one part of the reporting process. If the underlying model is poorly designed, calculations can become difficult, filters can behave unexpectedly, and maintaining the report can become increasingly complicated. For a typical business intelligence project, I would recommend a star schema. A well-designed star schema provides a relatively simple model with clear relationships between fact and dimension tables. This can support efficient filtering and analytical queries. And use one-to-many relationships between dimensions and the fact table and single-direction filtering, where filters flow from the dimension to the fact table. I would avoid unnecessary bidirectional relationships because they can introduce ambiguous filter paths and make the model harder to understand. Many-to-many relationships would also only be used when the business requirement genuinely requires them. Where possible, a bridge table can be used to create a clearer model.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>data</category>
      <category>database</category>
      <category>learning</category>
    </item>
    <item>
      <title>Jumia Product Performance Analysis &amp; Pricing Dashboard</title>
      <dc:creator>Naomi Kwamboka Onderi</dc:creator>
      <pubDate>Mon, 07 Sep 2026 18:04:57 +0000</pubDate>
      <link>https://dev.to/naomi_kwamboka1001/jumia-product-performance-analysis-pricing-dashboard-3o95</link>
      <guid>https://dev.to/naomi_kwamboka1001/jumia-product-performance-analysis-pricing-dashboard-3o95</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As part of understanding how Microsoft Excel is used in E-commerce. I analyzed a dataset of products listed on Jumia to understand how product performance from pricing to discount affect sales and customer engagement. The first step was to clean and prepare the raw dataset. Then perform data analysis to create visualizations, and later developed an interactive dashboard that presented the findings.&lt;br&gt;
These analysis aim to provide useful insights that can support better pricing strategies, promotional decisions, and customer engagement for Jumia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning the Data
&lt;/h2&gt;

&lt;p&gt;Before cleaning I copied the worksheet to a new sheet for reference purposes or incase something goes wrong or I accidentally delete data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing Values&lt;/strong&gt;- I checked every column for empty cells. Which were plenty in the ratings column. I wrote&lt;code&gt;Not available&lt;/code&gt; in the blanks spaces for accurate output during analysis.&lt;/li&gt;
&lt;li&gt;** Duplicates**- Searched and removed duplicates and fixed other inconsistencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numeric transformation&lt;/strong&gt;-Fixed the review column data as it was stored as negative numbers. Also removed the Ksh and commas in the &lt;em&gt;Current price&lt;/em&gt; and &lt;em&gt;old price&lt;/em&gt; columns to numeric values. 
Lastly was removing the &lt;code&gt;out of 5&lt;/code&gt;in the ratings column.
After cleaning this is what was displayed:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmpgkt125l9xfltzxzo27.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%2Fmpgkt125l9xfltzxzo27.png" alt=" " width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The new calculated columns
&lt;/h2&gt;

&lt;p&gt;At this point I added the new columns which are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discount Amount&lt;/strong&gt;-Discount Amount = Old Price − Current Price.&lt;code&gt;=C2-B2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rating Category&lt;/strong&gt;- &lt;code&gt;=IF(G2="","",IF(G2&amp;lt;3,"Poor",IF(G2&amp;lt;=4,"Average",IF(G2&amp;lt;=4.5,"Good","Excellent"))))&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Discount Category&lt;/strong&gt;-&lt;code&gt;=IF(D2="","",IF(D2&amp;lt;20%,"Low Discount",IF(D2&amp;lt;=40%,"Medium Discount","High Discount")))&lt;/code&gt;&lt;br&gt;
This means below 20%-Low, 20%-40%=Medium and above 40% is high&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Price Category&lt;/strong&gt;- &lt;code&gt;=IF(B2&amp;lt;1000,"Low Price",IF(B2&amp;lt;=2000,"Medium Price","High Price"))&lt;/code&gt;&lt;br&gt;
This means that Below Kes 1000=Low price, Kes 1000-2000= Medium Price and above Kes 3000.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data Analysis
&lt;/h2&gt;

&lt;p&gt;For the KPIs  I used the following formulas&lt;/p&gt;

&lt;p&gt;Total Products-&lt;code&gt;=COUNTA('Excel_jumia_dataset'!A2:A113)&lt;/code&gt;&lt;br&gt;
Average Current Price-&lt;code&gt;=AVERAGE('Excel_jumia_dataset'!B2:B113)&lt;/code&gt;&lt;br&gt;
Average Old Price-&lt;code&gt;=AVERAGE('Excel_jumia_dataset'!C2:C113)&lt;/code&gt;&lt;br&gt;
Average Discount-&lt;code&gt;=AVERAGE('Excel_jumia_dataset'!E2:E113)&lt;/code&gt;&lt;br&gt;
Average Rating-&lt;code&gt;=AVERAGE('Excel_jumia_dataset'!I2:I113)&lt;/code&gt;&lt;br&gt;
Total Reviews-&lt;code&gt;=SUM('Excel_jumia_dataset'!H2:H113)&lt;/code&gt;&lt;br&gt;
Most Expensive Product-&lt;code&gt;=XLOOKUP(B9,'Excel_jumia_dataset'!B2:B113,'Excel_jumia_dataset'!A2:A113)&lt;/code&gt;&lt;br&gt;
Least Expensive Product-&lt;code&gt;=XLOOKUP(B11,'Excel_jumia_dataset'!B2:B113,'Excel_jumia_dataset'!A2:A113)&lt;/code&gt;&lt;br&gt;
This results to:&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%2Fuahdjzk7ilxywm4ecgsx.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%2Fuahdjzk7ilxywm4ecgsx.png" alt=" " width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Dashboard creation process
&lt;/h2&gt;

&lt;p&gt;Putting everything together to create a dashboard I used&lt;br&gt;
&lt;strong&gt;KPIs&lt;/strong&gt;-The screenshot above to be specificthat shows total products, average price, average discount, average rating, and total reviews.&lt;br&gt;
&lt;strong&gt;Pivot charts&lt;/strong&gt;- I created multiple pivot charts to showcase the top 10 products by discount, rating and reviews.&lt;br&gt;
&lt;strong&gt;Slicers&lt;/strong&gt;- To connect all PivotTables/charts for Rating Category, Discount Category, and Price Category, so the whole dashboard can be filtered interactively.&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%2Fb2mxr395sh14mnxmddd3.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%2Fb2mxr395sh14mnxmddd3.png" alt=" " width="800" height="314"&gt;&lt;/a&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%2Frysgdqsbhjti89402b2x.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%2Frysgdqsbhjti89402b2x.png" alt=" " width="799" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Correlation Analysis
&lt;/h2&gt;

&lt;p&gt;Used &lt;code&gt;CORREL()&lt;/code&gt; to test three relationships:&lt;br&gt;
&lt;strong&gt;Discount vs Reviews&lt;/strong&gt;--0.1368, This means that higher discounts don't influence higher customer engagement.&lt;br&gt;
&lt;strong&gt;Rating vs Reviews&lt;/strong&gt;-0.0572, So a product having a high rating doesn't necessarily mean it has lots of reviews.&lt;br&gt;
&lt;strong&gt;Current Price vs Rating&lt;/strong&gt;-0.1080, Expensive products are not clearly rated higher than cheaper products.&lt;br&gt;
&lt;strong&gt;Key Insights and Business Recommendations&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prioritize highly reviewed products when evaluating customer engagement because review volume varies substantially across products.&lt;/li&gt;
&lt;li&gt;Monitor products with low ratings and identify opportunities to improve product quality, descriptions, or customer experience.&lt;/li&gt;
&lt;li&gt;Focus promotional efforts on products where discounts can attract attention without compromising product quality.&lt;/li&gt;
&lt;li&gt;Use the dashboard's slicers to compare product performance across different price, discount, and rating categories.&lt;/li&gt;
&lt;li&gt;Investigate products with high discounts but poor ratings, as large discounts may not compensate for poor customer satisfaction.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The analysis showed that discounts, ratings, and customer reviews did not have strong relationships in this dataset. In particular, the weak correlation between discount percentage and reviews suggests that offering larger discounts does not necessarily result in higher customer engagement. The dashboard also made it easier to identify highly reviewed products, highly discounted products, and products that may require attention due to low ratings. The interactive dashboard brings these findings together through KPI cards, charts, PivotTables, and slicers, allowing users to explore product performance across different rating, discount, and price categories.&lt;br&gt;
Overall, this project strengthened my understanding of Excel for data cleaning, analysis, visualization, and dashboard development, while also demonstrating how data-driven insights can support better e-commerce business decisions. &lt;br&gt;
You can find the README.md and workbook that contains the clean dataset, pivot charts, pivot tables, Excel formulas, slicers and the dashboard on my GitHub link below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/naomionderi-codes/Jumia-Product-Performance-Dashboard" rel="noopener noreferrer"&gt;https://github.com/naomionderi-codes/Jumia-Product-Performance-Dashboard&lt;/a&gt;&lt;/p&gt;

</description>
      <category>analysis</category>
      <category>analytics</category>
      <category>data</category>
    </item>
    <item>
      <title>Getting Started with Excel for Data Analytics: From Basics to Data Cleaning</title>
      <dc:creator>Naomi Kwamboka Onderi</dc:creator>
      <pubDate>Sat, 29 Aug 2026 19:56:21 +0000</pubDate>
      <link>https://dev.to/naomi_kwamboka1001/getting-started-with-excel-for-data-analytics-from-basics-to-data-cleaning-4272</link>
      <guid>https://dev.to/naomi_kwamboka1001/getting-started-with-excel-for-data-analytics-from-basics-to-data-cleaning-4272</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Excel is a powerful tool used in data analytics, it is one of the most used tools for working with data. You can use Excel to collect, organize, clean, analyze, visualize, and present data without needing to write code. It is also the  easiest way you can  learn important analytical concepts while working with an actual spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Definition of Excel and Data Analytics
&lt;/h2&gt;

&lt;p&gt;Excel is a spreadsheet that allows you to store information in rows and columns.&lt;br&gt;
Data analytics refers to the process of examining data to discover useful information, patterns, and insights that can help with decision-making.&lt;br&gt;
For example a company like LC Waikiki has thousands of sales records. The raw data might contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer names&lt;/li&gt;
&lt;li&gt;Sales representatives&lt;/li&gt;
&lt;li&gt;Store Locations&lt;/li&gt;
&lt;li&gt;Product Names&lt;/li&gt;
&lt;li&gt;Sales etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding the Excel Interface
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Workbook&lt;/strong&gt;- This is basically an excel file.&lt;br&gt;
&lt;strong&gt;Worksheet&lt;/strong&gt;- This is an individual spreadsheet inside a workbook. The worksheet tabs are found at the bottom.&lt;br&gt;
For example:&lt;em&gt;Sheet1 | Sheet2 | Sheet3&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;Rows&lt;/strong&gt;- They run horizontally across the spreadsheet. They are identified by numbers.&lt;br&gt;
&lt;strong&gt;Columns&lt;/strong&gt;- They run vertically. They are identified by letters.&lt;br&gt;
&lt;strong&gt;Cell&lt;/strong&gt;- This is the intersection of a row and a column.&lt;br&gt;
&lt;strong&gt;Cell Range&lt;/strong&gt;- This is a group of cells. &lt;em&gt;A1:C10&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Example of a Worksheet
&lt;/h2&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%2F6a295av0ms0e3rva3ddi.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%2F6a295av0ms0e3rva3ddi.png" alt=" " width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Formatting data
&lt;/h2&gt;

&lt;p&gt;Formatting makes data easier to read. Correct formatting becomes especially important when analyzing dates and numerical values.&lt;br&gt;
You can use the following formatting options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Date&lt;/li&gt;
&lt;li&gt;Currency&lt;/li&gt;
&lt;li&gt;Time&lt;/li&gt;
&lt;li&gt;Percentage&lt;/li&gt;
&lt;li&gt;Number&lt;/li&gt;
&lt;li&gt;Fraction&lt;/li&gt;
&lt;li&gt;Text&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Basic Excel Formulas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SUM&lt;/strong&gt; adds numbers together. =SUM(F2:F6)&lt;br&gt;
&lt;strong&gt;AVERAGE&lt;/strong&gt; calculates the mean. =AVERAGE(F2:F6)&lt;br&gt;
&lt;strong&gt;COUNT&lt;/strong&gt; counts cells containing numbers. =COUNT(F2:F6)&lt;br&gt;
&lt;strong&gt;COUNTA&lt;/strong&gt; counts cells that are not empty. =COUNTA(B2:B6)&lt;br&gt;
&lt;strong&gt;MAX&lt;/strong&gt; returns the largest value. =MAX(F2:F6)&lt;br&gt;
&lt;strong&gt;MIN&lt;/strong&gt; returns the smallest value. =MIN(F2:F6)&lt;br&gt;
&lt;strong&gt;IF&lt;/strong&gt; function allows Excel to make decisions. =IF(F2&amp;gt;500,"High","Low") &lt;em&gt;This type of logic becomes very useful when analyzing large datasets.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sorting Data
&lt;/h2&gt;

&lt;p&gt;Sorting allows you to arrange your data according to a particular order. Like Largest → Smallest ,or alphabetically from A-Z,Z-A etc&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering Data
&lt;/h2&gt;

&lt;p&gt;Filtering allows you to display only the records that meet specific conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Data Cleaning?
&lt;/h2&gt;

&lt;p&gt;Data cleaning is the process of identifying and correcting problems in a dataset so that the data is accurate, consistent, complete, and suitable for analysis and decision-making.&lt;br&gt;
Some of the issues one can encounter in a large data set may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Invalid values&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inconsistent capitalization&lt;/strong&gt;- These can be standardized by using functions like =UPPER(A2) to change characters to uppercase, =LOWER(A2) to change characters to lowercase, =PROPER(A2) to change characters to proper capitalization.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Missing values&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Duplicate records&lt;/strong&gt;- This is when the same record appears more than once.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Spelling mistakes&lt;/strong&gt;- Find &amp;amp; Replace (ctrl+H) can be used to fix spelling mistakes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Incorrect dates&lt;/strong&gt;- You should make sure dates are stored consistently and recognized as actual dates rather than text.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Extra spaces&lt;/strong&gt;- Excel can treat them as different text values. The **TRIM **function helps remove the unnecessary spaces. =TRIM(A2)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wrong currencies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Numbers stored as text- =VALUE(A2) can be used to fix this. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before analyzing data, these issues should be addressed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use Find &amp;amp; Replace for Data Cleaning
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Find &amp;amp; Replace&lt;/strong&gt; is one of the simplest and most useful Excel cleaning tools. You can open it using Ctrl + H. It is useful in correcting abbreviations, unwanted characters, spelling errors, inconsistent labels and category names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Validation
&lt;/h2&gt;

&lt;p&gt;Data validation helps control what users can enter into a spreadsheet. This reduces the likelihood of users entering inconsistent values. This is particularly useful when creating spreadsheets that other people will use to enter data.&lt;/p&gt;

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

&lt;p&gt;When you receive a new dataset, carefully go through the entire worksheet before creating charts and calculating averages. Understand and inspect the dataset for irregularities and any inconsistencies. And after fixing the errors review the clean dataset again just incase. Learning Excel for data analytics is a gradual process and you don't need to learn every Excel function before you can start analyzing data. The skills you develop in Excel can be transferred to different areas of data analytics.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How To Upload Your First Project From a Local Folder to GitHub Using Git and SSH.</title>
      <dc:creator>Naomi Kwamboka Onderi</dc:creator>
      <pubDate>Sat, 22 Aug 2026 21:51:01 +0000</pubDate>
      <link>https://dev.to/naomi_kwamboka1001/how-to-upload-your-first-project-from-a-local-folder-to-github-using-git-and-ssh-500a</link>
      <guid>https://dev.to/naomi_kwamboka1001/how-to-upload-your-first-project-from-a-local-folder-to-github-using-git-and-ssh-500a</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you want to upload a project from your computer to your GitHub account, it can be done by simply using Git and SSH. This is one of the most important technical skills you can learn as a beginner because you will use this every time you want to create and or update an existing project. For a person using Windows OS, follow these simple steps as illustrated below. &lt;/p&gt;

&lt;h2&gt;
  
  
  1. Download Git and create a GitHub account.
&lt;/h2&gt;

&lt;p&gt;The GitHub account can be generated easily on the browser by using your email/username and a password. Git can be downloaded from Microsoft Store and installed in your computer.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Set up your Identity.
&lt;/h2&gt;

&lt;p&gt;Since it's your first time you are required to configure your name and email after installation.&lt;br&gt;
Run the following commands on Gitbash&lt;br&gt;
&lt;code&gt;git config --global user.name "Your Name"&lt;/code&gt; &lt;em&gt;Insert your name&lt;/em&gt;&lt;br&gt;
Then run&lt;br&gt;
&lt;code&gt;git config --global user.email "your-email@example.com"&lt;/code&gt;&lt;em&gt;Insert your email, the one used to create your GitHub account&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Check whether you already have an SSH key
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;code&gt;ls -al ~/.ssh&lt;/code&gt;&lt;br&gt;
You'll see &lt;br&gt;
&lt;code&gt;id_ed25519 &lt;br&gt;
id_ed25519.pub&lt;/code&gt;&lt;br&gt;
&lt;code&gt;id_ed25519&lt;/code&gt;refers to the private key and &lt;code&gt;id_ed25519.pub&lt;/code&gt; refers to the public key.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Generate a new SSH key
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;code&gt;ssh-keygen -t ed25519 -C "your-email@example.com"&lt;/code&gt;&lt;em&gt;Replace your email with your GitHub email&lt;/em&gt;&lt;br&gt;
You'll then see &lt;code&gt;Generating public/private ed25519 key pair.&lt;/code&gt;&lt;br&gt;
Press Enter since it's your first key.&lt;br&gt;
Next you'll see &lt;br&gt;
&lt;code&gt;Enter passphrase (empty for no passphrase):&lt;/code&gt;&lt;em&gt;Enter passphrase (password) for security&lt;/em&gt;&lt;br&gt;
You'll be asked to confirm your passphrase just enter it again.&lt;br&gt;
You'll see messages indicating the private and public keys are being generated.&lt;br&gt;
On Windows open PowerShell as an Administrator and run:&lt;br&gt;
&lt;code&gt;Get-Service -Name ssh-agent | Set-Service -StartupType Manual&lt;/code&gt;&lt;br&gt;
Then:&lt;br&gt;
&lt;code&gt;Start-Service ssh-agent&lt;/code&gt;&lt;br&gt;
Return to Gitbash and add your private SSH key:&lt;br&gt;
&lt;code&gt;ssh-add ~/.ssh/id_ed25519&lt;/code&gt;&lt;br&gt;
It should indicate that the Identity was added.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Add your public SSH key to Github
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;code&gt;cat ~/.ssh/id_ed25519.pub&lt;/code&gt;and press Enter. Copy the entire line that pops up upto the &lt;code&gt;.pub&lt;/code&gt;&lt;br&gt;
On GitHub open Settings → SSH and GPG keys then select New SSH Key give it a title and paste the public key then press add key.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Locate your project folder and adding it to GitHub
&lt;/h2&gt;

&lt;p&gt;Locate your project it may be in the documents folder on your desktop. Open Gitbash and navigate into the project folder&lt;br&gt;
&lt;code&gt;cd ~/Documents/Loan Report&lt;/code&gt;&lt;br&gt;
You can use &lt;code&gt;ls&lt;/code&gt; confirm&lt;br&gt;
Run:&lt;br&gt;
&lt;code&gt;git init -b main&lt;/code&gt; initializing an untracked local project.&lt;br&gt;
Then run:&lt;br&gt;
&lt;code&gt;git status&lt;/code&gt;  this shows the untracked files.&lt;br&gt;
Then run:&lt;br&gt;
&lt;code&gt;git add .&lt;/code&gt; this adds the file in the current  directory. Rerun &lt;code&gt;git status&lt;/code&gt; again.&lt;br&gt;
Then run:&lt;br&gt;
&lt;code&gt;git commit -m "Initial commit"&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Create a new Repository on GitHub
&lt;/h2&gt;

&lt;p&gt;Create a new repository with a new name preferably the same name as the file &lt;em&gt;Loan Report&lt;/em&gt; and choose whether it is public or private.&lt;br&gt;
After creating the repository, you will see the setup information and select and copy the SSH URL. Paste the link in Gitbash then press enter.&lt;br&gt;
Whatever comes up replace the name section with your GitHub username and  press enter.&lt;br&gt;
Run:&lt;br&gt;
&lt;code&gt;git remote -v&lt;/code&gt; this confirms that Git knows where the GitHub repository is&lt;br&gt;
Then run:&lt;br&gt;
&lt;code&gt;git push -u origin main&lt;/code&gt; This pushes a new repository to GitHub.&lt;br&gt;
Afterwards go to GitHub and confirm if the project is there.&lt;/p&gt;

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

&lt;p&gt;This is the basic Git and GitHub steps that you'll have to use repeatedly while working on different kind of projects and with time all of these will become easier.&lt;/p&gt;

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