<?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: Navas Herbert</title>
    <description>The latest articles on DEV Community by Navas Herbert (@navas_herbert).</description>
    <link>https://dev.to/navas_herbert</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3934612%2F33241999-7c90-49d0-b4fe-b19aee0ff483.jpg</url>
      <title>DEV Community: Navas Herbert</title>
      <link>https://dev.to/navas_herbert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/navas_herbert"/>
    <language>en</language>
    <item>
      <title>I Taught SQL to Complete Beginners: Here's What Actually Happened</title>
      <dc:creator>Navas Herbert</dc:creator>
      <pubDate>Sat, 16 May 2026 10:49:20 +0000</pubDate>
      <link>https://dev.to/navas_herbert/i-taught-sql-to-complete-beginners-heres-what-actually-happened-4h84</link>
      <guid>https://dev.to/navas_herbert/i-taught-sql-to-complete-beginners-heres-what-actually-happened-4h84</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Ffxuh99x6lbimziumstrk.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.amazonaws.com%2Fuploads%2Farticles%2Ffxuh99x6lbimziumstrk.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Let me set the scene.&lt;/p&gt;

&lt;p&gt;A room full of people who had never written a single line of SQL in their lives. Some came from accounting. Some from operations. One person told me they still used Excel to track hospital records manually. And they were all looking at me like I was about to teach them magic.&lt;/p&gt;

&lt;p&gt;Honestly? I kind of was.&lt;/p&gt;

&lt;p&gt;This is my retrospective on our SQL journey - what we covered, what broke people's brains (in a good way), and the moments I realised teaching this stuff is genuinely one of the most fun things I do.&lt;/p&gt;


&lt;h2&gt;
  
  
  Where We Started: "What Even Is a Database?"
&lt;/h2&gt;

&lt;p&gt;Before I could show anyone a &lt;code&gt;SELECT&lt;/code&gt; statement, I had to answer the most fundamental question first: &lt;em&gt;why does this exist?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My go-to analogy: a database is like a very organised filing cabinet. Each drawer is a &lt;strong&gt;table&lt;/strong&gt;. Each folder inside the drawer is a &lt;strong&gt;row&lt;/strong&gt;. Each label on the folder is a &lt;strong&gt;column&lt;/strong&gt;. SQL is just the language you use to ask the filing cabinet questions.&lt;/p&gt;

&lt;p&gt;Once that clicked, the first query felt almost anticlimactic - in a good way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;students&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;grade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Wait… that's it?" Yes. That's it. The magic is in building from there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 1: The Foundation (And the First Confused Faces)
&lt;/h2&gt;

&lt;p&gt;We started with the basics - &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;. We used a school dataset called &lt;strong&gt;Nairobi Academy&lt;/strong&gt; (fictional Nairobi school - felt very local and relatable) and a hospital dataset called &lt;strong&gt;City Hospital&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The moment that got everyone: understanding the difference between &lt;code&gt;WHERE&lt;/code&gt; and &lt;code&gt;HAVING&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- WHERE filters rows BEFORE grouping&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_staff&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Active'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- HAVING filters AFTER grouping&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_staff&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used the analogy of a matatu route: &lt;code&gt;WHERE&lt;/code&gt; is the conductor deciding who even boards the matatu. &lt;code&gt;HAVING&lt;/code&gt; is checking at the destination who actually made it the whole way. That one landed really well.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Thing That Broke Everyone (In the Best Way): JOINs
&lt;/h2&gt;

&lt;p&gt;I won't lie - JOINs are where beginner SQL classes either soar or crash. I've seen it go both ways.&lt;/p&gt;

&lt;p&gt;We covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;INNER JOIN&lt;/strong&gt; - only rows that match in both tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LEFT JOIN&lt;/strong&gt; - all rows from the left, matched where possible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RIGHT JOIN&lt;/strong&gt; - all rows from the right&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SELF JOIN&lt;/strong&gt; - a table joining itself (yes, this is a thing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LEFT JOIN + IS NULL&lt;/strong&gt; - finding orphaned records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The self-join is always the one that causes dramatic pauses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- "Who is each employee's manager?" - same table, different roles&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manager_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;"Wait… it's joining to itself?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yes. Yes it is. Welcome to SQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Window Functions: When SQL Starts Feeling Like a Superpower
&lt;/h2&gt;

&lt;p&gt;This is the week I could see people's eyes light up.&lt;/p&gt;

&lt;p&gt;Window functions let you do calculations &lt;em&gt;across&lt;/em&gt; rows without collapsing them into a group. We covered &lt;code&gt;ROW_NUMBER&lt;/code&gt;, &lt;code&gt;RANK&lt;/code&gt;, &lt;code&gt;DENSE_RANK&lt;/code&gt;, &lt;code&gt;NTILE&lt;/code&gt;, running averages with &lt;code&gt;AVG OVER&lt;/code&gt;, and time-travel queries with &lt;code&gt;LAG&lt;/code&gt; and &lt;code&gt;LEAD&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Rank students by score within each class&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;student_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;class_rank&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;LAG&lt;/code&gt; - the one that made the data analysts in the room go quiet for a moment before saying &lt;em&gt;"oh wow"&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Compare this month's sales to last month's&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;previous_month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_sales&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;LAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;difference&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;monthly_sales&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No subquery. No join. Just clean, readable SQL. This is the moment people stop thinking of SQL as "just for pulling data" and start seeing it as an analytical tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  CTEs: Writing SQL Like a Human Being
&lt;/h2&gt;

&lt;p&gt;Before CTEs, students were writing nested subqueries that looked like abstract art. Beautiful in theory, impossible to read three days later.&lt;/p&gt;

&lt;p&gt;Common Table Expressions (&lt;code&gt;WITH ... AS&lt;/code&gt;) changed everything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find students who scored above the class average&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;class_averages&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_score&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;student_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;class_averages&lt;/span&gt; &lt;span class="n"&gt;ca&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;avg_score&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule I drilled in: &lt;strong&gt;write your CTEs like you're telling a story.&lt;/strong&gt; First set up your averages, then filter against them. Each CTE is a chapter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Data Cleaning: Where Reality Hits
&lt;/h2&gt;

&lt;p&gt;Every SQL class eventually gets to the uncomfortable truth: real data is messy. Dirty, inconsistent, full of rogue spaces and names typed in ALL CAPS at 4pm on a Friday.&lt;/p&gt;

&lt;p&gt;We built a dedicated dirty dataset and went to town with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Standardise names that arrived inconsistently&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; 
    &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;INITCAP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TRIM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;last_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;INITCAP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TRIM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;-- Strip non-numeric characters from phone numbers&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;contacts&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGEXP_REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[^0-9]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'g'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Fill in missing values rather than leaving NULLs&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Unassigned'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Golden rule we repeated every cleaning session: &lt;strong&gt;always run a &lt;code&gt;SELECT&lt;/code&gt; first before an &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt;.&lt;/strong&gt; See exactly what you're about to change. Only then pull the trigger.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Capstone: Tembo Hotel
&lt;/h2&gt;

&lt;p&gt;The final project was set in &lt;strong&gt;Tembo Hotel&lt;/strong&gt; - a fictional Nairobi hotel with ~285 rows of genuinely messy booking data. Students worked in groups to clean it, analyse it, and answer real business questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which room types generate the most revenue?&lt;/li&gt;
&lt;li&gt;Which months have the highest no-show rates?&lt;/li&gt;
&lt;li&gt;Who are our repeat guests?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watching groups go from "this data is a disaster" to producing clean queries with CTEs and window functions across a few sessions was something else.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned Teaching This
&lt;/h2&gt;

&lt;p&gt;A few honest takeaways from being on the teaching side:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Analogies are not optional.&lt;/strong&gt; Every concept needs a real-world anchor before any code appears. If you skip the analogy and go straight to syntax, you lose half the room.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Local context matters more than I expected.&lt;/strong&gt; Using Nairobi names, M-Pesa examples, matatu analogies - students were visibly more engaged when the data felt like &lt;em&gt;their&lt;/em&gt; world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. PostgreSQL is the right call for beginners.&lt;/strong&gt; Clean syntax, no shortcuts, no &lt;code&gt;backtick&lt;/code&gt; weirdness. If you learn PostgreSQL first, everything else makes sense later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The moment someone writes their first CTE that actually works, they look different.&lt;/strong&gt; Slightly smug. Entirely justified.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;SQL was the foundation. Now we've moved into &lt;strong&gt;Python&lt;/strong&gt; - same students, same energy, new language. We're building toward a full data programme where they'll split into Data Science and Data Engineering tracks.&lt;/p&gt;

&lt;p&gt;I'll be writing about that journey too. Follow along if you're building something similar, learning from scratch yourself, or just enjoy watching beginners turn into dangerous analysts one query at a time.&lt;/p&gt;

&lt;p&gt;See you next week. &lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a data engineer in Nairobi. I write weekly about what I'm teaching, what's working, and what surprised me in the classroom. If you're a fellow trainer or a self-taught analyst, let's talk in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>beginners</category>
      <category>teaching</category>
    </item>
    <item>
      <title>Making Africa Energy Data Accessible to LLMs - a Practical Guide</title>
      <dc:creator>Navas Herbert</dc:creator>
      <pubDate>Sat, 16 May 2026 09:57:57 +0000</pubDate>
      <link>https://dev.to/navas_herbert/making-africa-energy-data-accessible-to-llms-a-practical-guide-4p14</link>
      <guid>https://dev.to/navas_herbert/making-africa-energy-data-accessible-to-llms-a-practical-guide-4p14</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;As an AI enthusiast and trainer, I believe high-quality open data is essential for building useful language models and agents. This project - the "Africa Energy Data MCP" - wraps a public Africa Energy Data API and exposes it as MCP tools so LLMs (Claude, Cursor, etc.) can call them directly.&lt;/p&gt;

&lt;p&gt;In this short guide you'll find a clear, step-by-step walkthrough for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up the project locally&lt;/li&gt;
&lt;li&gt;Understanding the architecture&lt;/li&gt;
&lt;li&gt;Connecting an MCP client (Claude / Cursor)&lt;/li&gt;
&lt;li&gt;Running and testing the tools&lt;/li&gt;
&lt;li&gt;Publishing options (brief)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Special thanks to my friend Denzel (dkkinyua) who developed the underlying API powering this project - check out his work here: &lt;a href="https://github.com/dkkinyua/AfricaEnergyDataAPI/tree/main" rel="noopener noreferrer"&gt;https://github.com/dkkinyua/AfricaEnergyDataAPI/tree/main&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;LLMs become far more useful when they can call tools that fetch real data. This project makes historical electricity, energy and economic indicators for 54 African countries (2000–2022) trivially available to agents, with caching, retries and validation built in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick highlights
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tools: &lt;code&gt;get_electricity_data&lt;/code&gt;, &lt;code&gt;get_energy_data&lt;/code&gt;, &lt;code&gt;get_economic_data&lt;/code&gt;, &lt;code&gt;check_api_health&lt;/code&gt;, &lt;code&gt;list_countries&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tech: Python 3.11, &lt;code&gt;httpx&lt;/code&gt;, &lt;code&gt;mcp&lt;/code&gt; server, simple in-memory cache, structured logging&lt;/li&gt;
&lt;li&gt;Containerization: &lt;code&gt;Dockerfile&lt;/code&gt; + GitHub Actions example included&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 - Get the code
&lt;/h2&gt;

&lt;p&gt;Clone the repo and enter the project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Navashub/africa_energy_mcp.git
&lt;span class="nb"&gt;cd &lt;/span&gt;africa_energy_mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 - Install dependencies
&lt;/h2&gt;

&lt;p&gt;Create a virtualenv and install requirements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="c"&gt;# Windows&lt;/span&gt;
venv&lt;span class="se"&gt;\S&lt;/span&gt;cripts&lt;span class="se"&gt;\a&lt;/span&gt;ctivate
&lt;span class="c"&gt;# macOS / Linux&lt;/span&gt;
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 - Add your API key
&lt;/h2&gt;

&lt;p&gt;Copy the example env file and paste your RapidAPI key from the API provider (Denzel's API or the RapidAPI listing):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# edit .env and replace AFRICA_ENERGY_API_KEY with your key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important: never commit &lt;code&gt;.env&lt;/code&gt; or your API key to GitHub. The repo includes a &lt;code&gt;.gitignore&lt;/code&gt; already.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 - Run the MCP server
&lt;/h2&gt;

&lt;p&gt;Start the server locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;py server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see structured logs on stderr indicating the MCP server initialized and registered tools. Leave this running — it listens on stdio for MCP clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 - Connect an MCP client (Claude / Cursor)
&lt;/h2&gt;

&lt;p&gt;For local testing, point your MCP-capable client at the &lt;code&gt;server.py&lt;/code&gt; process. Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Desktop (Windows) — edit &lt;code&gt;%APPDATA%\Claude\claude_desktop_config.json&lt;/code&gt; and add:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"africa-energy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"D:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;mcp_projects&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;africa_energy_mcp&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;server.py"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Cursor — add the equivalent &lt;code&gt;mcp.servers&lt;/code&gt; entry in Cursor settings pointing to your local &lt;code&gt;server.py&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6 - Test useful queries
&lt;/h2&gt;

&lt;p&gt;Start with discovery commands so you confirm connectivity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What countries are supported by the Africa Energy Data tool?" — should return the 54-country list (tool &lt;code&gt;list_countries&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;"Is the Africa Energy Data API online?" — &lt;code&gt;check_api_health&lt;/code&gt; tool should return a health JSON.&lt;/li&gt;
&lt;li&gt;Example: "Get electricity data for Kenya in 2022." — will call &lt;code&gt;get_electricity_data&lt;/code&gt; and return JSON results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an endpoint returns a 500, that indicates the remote API backend failed — the error is on the API provider side. The MCP server will surface a clear error message in that case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture (brief)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;server.py&lt;/code&gt; - MCP server that declares tools and routes calls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;handlers.py&lt;/code&gt; - Input validation and mapping of tool arguments to API calls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api_client.py&lt;/code&gt; - &lt;code&gt;httpx.AsyncClient&lt;/code&gt; wrapper with retries, 30s timeout, and a 10-minute in-memory cache&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools.py&lt;/code&gt; - Schemas for each MCP tool&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config.py&lt;/code&gt; - Environment and constants (including the hardcoded list of 54 countries)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Containerization &amp;amp; CI
&lt;/h2&gt;

&lt;p&gt;If you want remote hosting, the repo includes a &lt;code&gt;Dockerfile&lt;/code&gt; and an example GitHub Actions workflow that builds and pushes a container to GHCR. Publishing requires a registry token — we added a workflow that prefers a &lt;code&gt;GHCR_TOKEN&lt;/code&gt; secret or falls back to the repo &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; when allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;Huge thanks to Denzel (dkkinyua) for building the underlying Africa Energy Data API — please check his repository: &lt;a href="https://github.com/dkkinyua/AfricaEnergyDataAPI/tree/main" rel="noopener noreferrer"&gt;https://github.com/dkkinyua/AfricaEnergyDataAPI/tree/main&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Shout-out: This project was adapted and wrapped into an MCP server by an AI enthusiast and trainer (that’s me — Navashub) to make the data accessible to LLMs for education, research and production usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best practices &amp;amp; tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep your API keys secret - use &lt;code&gt;.env&lt;/code&gt; locally and repository secrets for CI.&lt;/li&gt;
&lt;li&gt;Test with &lt;code&gt;list_countries&lt;/code&gt; and &lt;code&gt;check_api_health&lt;/code&gt; before running large queries.&lt;/li&gt;
&lt;li&gt;Increase &lt;code&gt;REQUEST_TIMEOUT&lt;/code&gt; in &lt;code&gt;config.py&lt;/code&gt; only if you know the backend requires more time.&lt;/li&gt;
&lt;li&gt;For production hosting, use the Docker image and provide secrets through the host or the platform's secret mechanism.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Making structured, historical energy data available to LLMs unlocks better automated analysis, answers, and decision support. This project is a small but practical bridge - thanks to Denzel for the API and to you for exploring it.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>africa</category>
      <category>energy</category>
      <category>python</category>
    </item>
  </channel>
</rss>
