<?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: Charity Jelimo</title>
    <description>The latest articles on DEV Community by Charity Jelimo (@data_with_jelimo).</description>
    <link>https://dev.to/data_with_jelimo</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%2F3396373%2F192bdc45-0fb9-40d0-b855-52526df3afbe.jpg</url>
      <title>DEV Community: Charity Jelimo</title>
      <link>https://dev.to/data_with_jelimo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/data_with_jelimo"/>
    <language>en</language>
    <item>
      <title>Using dbt to Transform OpenSky Flight Data</title>
      <dc:creator>Charity Jelimo</dc:creator>
      <pubDate>Thu, 06 Aug 2026 15:09:58 +0000</pubDate>
      <link>https://dev.to/data_with_jelimo/using-dbt-to-transform-opensky-flight-data-2b51</link>
      <guid>https://dev.to/data_with_jelimo/using-dbt-to-transform-opensky-flight-data-2b51</guid>
      <description>&lt;h2&gt;
  
  
  Building an OpenSky Flight Data Pipeline with dbt Core
&lt;/h2&gt;

&lt;p&gt;Recently, I built an OpenSky Flight Data Pipeline that ingests aircraft state vectors into PostgreSQL and transforms them into analytics-ready datasets using &lt;strong&gt;dbt Core&lt;/strong&gt;. Along the way, I discovered that dbt isn't just about writing SQL, it brings software engineering practices like modularity, testing, documentation, and dependency management to analytics.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through how I used dbt, the concepts I learned, and how I organized my project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;The OpenSky Network provides aircraft state vectors containing information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ICAO24 identifier&lt;/li&gt;
&lt;li&gt;Callsign&lt;/li&gt;
&lt;li&gt;Origin country&lt;/li&gt;
&lt;li&gt;Latitude and longitude&lt;/li&gt;
&lt;li&gt;Speed&lt;/li&gt;
&lt;li&gt;Altitude&lt;/li&gt;
&lt;li&gt;Heading&lt;/li&gt;
&lt;li&gt;Vertical rate&lt;/li&gt;
&lt;li&gt;Flight status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although the data is rich, it isn't immediately suitable for analytics. It contains inconsistent formatting, duplicate records, missing values, and measurements that need conversion.&lt;/p&gt;

&lt;p&gt;Rather than writing one large SQL script, I wanted to build a modular transformation pipeline that was easy to maintain, test, and extend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is dbt?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;dbt (Data Build Tool)&lt;/strong&gt; is an open-source transformation framework that transforms data already stored in your data warehouse or database.&lt;/p&gt;

&lt;p&gt;Unlike ingestion tools, dbt &lt;strong&gt;does not move data&lt;/strong&gt;. Instead, it focuses on the &lt;strong&gt;Transform&lt;/strong&gt; stage of the ELT workflow.&lt;/p&gt;

&lt;p&gt;With dbt, you write SQL models while it handles the engineering around them, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running models in the correct order&lt;/li&gt;
&lt;li&gt;Managing dependencies&lt;/li&gt;
&lt;li&gt;Testing data quality&lt;/li&gt;
&lt;li&gt;Generating documentation&lt;/li&gt;
&lt;li&gt;Building lineage graphs&lt;/li&gt;
&lt;li&gt;Integrating with Git&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it as applying software engineering principles to SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use PostgreSQL?
&lt;/h2&gt;

&lt;p&gt;You can absolutely build transformation tables manually using SQL.&lt;/p&gt;

&lt;p&gt;The challenge comes when your project starts growing.&lt;/p&gt;

&lt;p&gt;Questions quickly arise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which table should run first?&lt;/li&gt;
&lt;li&gt;How do you avoid duplicating transformation logic?&lt;/li&gt;
&lt;li&gt;How do you document your models?&lt;/li&gt;
&lt;li&gt;How do you validate data quality?&lt;/li&gt;
&lt;li&gt;How do you understand model dependencies?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;dbt solves these problems by introducing modular models, dependency management, testing, and documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Architecture
&lt;/h2&gt;

&lt;p&gt;My transformation pipeline follows a layered architecture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bronze.all_state_vectors
            │
            ▼
stg_state_vectors
            │
            ▼
state_vectors
      ├───────────────┐
      │               │
      ▼               ▼
performance      location
      │               │
      ▼               ▼
status          latest
        │
        ▼
snapshot_summary
        │
        ▼
country_statistics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every downstream model builds upon another model using &lt;code&gt;ref()&lt;/code&gt;, allowing dbt to automatically determine the execution order.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Silver Layer
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;stg_state_vectors&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The staging model is responsible for cleaning the raw OpenSky data.&lt;/p&gt;

&lt;p&gt;It performs tasks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trimming text fields&lt;/li&gt;
&lt;li&gt;Removing duplicate rows&lt;/li&gt;
&lt;li&gt;Validating latitude and longitude values&lt;/li&gt;
&lt;li&gt;Standardizing country names&lt;/li&gt;
&lt;li&gt;Casting columns to appropriate data types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This model becomes the foundation for every downstream transformation.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;state_vectors&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;After cleaning the raw data, I created an enriched model containing derived metrics such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Speed in km/h&lt;/li&gt;
&lt;li&gt;Speed in knots&lt;/li&gt;
&lt;li&gt;Altitude in feet&lt;/li&gt;
&lt;li&gt;Flight status&lt;/li&gt;
&lt;li&gt;Aircraft movement classification&lt;/li&gt;
&lt;li&gt;Compass heading&lt;/li&gt;
&lt;li&gt;Snapshot date&lt;/li&gt;
&lt;li&gt;Snapshot hour&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than recalculating these metrics repeatedly across multiple queries, they're computed once and reused everywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Purpose-Built Models
&lt;/h2&gt;

&lt;p&gt;Instead of creating one massive analytics table, I broke the transformations into smaller, focused models.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;silver_aircraft_performance&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Speed, altitude, climb rate, heading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;silver_aircraft_location&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Latitude, longitude, timestamps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;silver_aircraft_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Operational state and movement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;silver_aircraft_latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Latest snapshot for each aircraft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;silver_snapshot_summary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fleet-level metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;country_statistics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Country-level aggregations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;airborne_flights&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Aircraft currently airborne&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;airport_surface_activity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Aircraft currently on the ground&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keeping each model focused makes them easier to understand, test, and maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  One of My Favourite Features: &lt;code&gt;ref()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of referencing tables directly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;state_vectors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;dbt encourages referencing models using &lt;code&gt;ref()&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="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'state_vectors'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;ref()&lt;/code&gt; has several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically creates model dependencies&lt;/li&gt;
&lt;li&gt;Executes models in the correct order&lt;/li&gt;
&lt;li&gt;Makes refactoring easier&lt;/li&gt;
&lt;li&gt;Builds lineage documentation automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As projects grow, this becomes incredibly valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with Sources
&lt;/h2&gt;

&lt;p&gt;The raw OpenSky table is defined as a &lt;strong&gt;source&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bronze&lt;/span&gt;
    &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bronze&lt;/span&gt;

    &lt;span class="na"&gt;tables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all_state_vectors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of hardcoding table names, models reference the source using:&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="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bronze'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'all_state_vectors'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using sources improves documentation, lineage, and maintainability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;One of the features I appreciated most was dbt's built-in testing.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;icao24&lt;/span&gt;
    &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flight_status&lt;/span&gt;
    &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;accepted_values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Airborne&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Ground&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbt &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;automatically validates these rules and reports any failures.&lt;/p&gt;

&lt;p&gt;Having tests alongside transformation logic makes it much easier to catch issues before downstream models are affected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation
&lt;/h2&gt;

&lt;p&gt;Generating documentation is incredibly simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbt docs generate
dbt docs serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;dbt creates an interactive documentation website containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Models&lt;/li&gt;
&lt;li&gt;Sources&lt;/li&gt;
&lt;li&gt;Column descriptions&lt;/li&gt;
&lt;li&gt;Tests&lt;/li&gt;
&lt;li&gt;Lineage graph&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of my favourite moments during this project was watching the lineage graph grow as I added more models.&lt;/p&gt;

&lt;p&gt;It provides a clear visual representation of how every transformation connects together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commands I Used
&lt;/h2&gt;

&lt;p&gt;Throughout the project, these were the commands I used most frequently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbt debug
dbt compile
dbt run
dbt &lt;span class="nb"&gt;test
&lt;/span&gt;dbt docs generate
dbt docs serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each command serves a different purpose, from validating configuration to building documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;This project completely changed how I think about SQL transformations.&lt;/p&gt;

&lt;p&gt;Instead of writing one massive SQL query, I built a collection of small, reusable models that are easier to understand, maintain, and test.&lt;/p&gt;

&lt;p&gt;My biggest takeaways were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build small, reusable models.&lt;/li&gt;
&lt;li&gt;Keep transformations modular.&lt;/li&gt;
&lt;li&gt;Document everything.&lt;/li&gt;
&lt;li&gt;Add tests early.&lt;/li&gt;
&lt;li&gt;Let dbt manage dependencies instead of doing it manually.&lt;/li&gt;
&lt;li&gt;Treat analytics projects like software engineering projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;The complete project is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/jelimo-charity/opensky-flight-data-pipeline/tree/main/dbt" rel="noopener noreferrer"&gt;https://github.com/jelimo-charity/opensky-flight-data-pipeline/tree/main/dbt&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Feel free to explore the models, documentation, and project structure.&lt;/p&gt;




&lt;p&gt;If you're already comfortable writing SQL and want to build more maintainable analytics pipelines, I highly recommend learning dbt.&lt;/p&gt;

&lt;p&gt;Beyond simplifying transformations, dbt encourages practices that make data projects easier to scale, collaborate on, and maintain over time. It bridges the gap between traditional SQL development and modern data engineering by making transformations modular, testable, and well documented.&lt;/p&gt;

&lt;p&gt;For me, this project wasn't just about transforming flight data—it was about learning a better way to build analytics pipelines.&lt;/p&gt;

</description>
      <category>data</category>
      <category>dbt</category>
      <category>sql</category>
    </item>
    <item>
      <title>Refreshing PostgreSQL Materialized Views Without Downtime</title>
      <dc:creator>Charity Jelimo</dc:creator>
      <pubDate>Thu, 14 May 2026 13:09:00 +0000</pubDate>
      <link>https://dev.to/data_with_jelimo/refreshing-postgresql-materialized-views-without-downtime-28n6</link>
      <guid>https://dev.to/data_with_jelimo/refreshing-postgresql-materialized-views-without-downtime-28n6</guid>
      <description>&lt;p&gt;Materialized views are one of PostgreSQL’s most useful features for analytics and reporting workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They solve a very common problem:&lt;/strong&gt; some queries are simply too expensive to run repeatedly in real time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Imagine a dashboard query that joins multiple large tables, aggregates millions of rows, and calculates metrics per customer. Running that query on every request can quickly become slow, expensive, and unpredictable under load.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A materialized view solves this by storing the query result physically on disk.&lt;/p&gt;

&lt;p&gt;Instead of recalculating the query every time, PostgreSQL precomputes the result once and serves future reads directly from the stored data.&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;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;company_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&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;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&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;company_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Querying the materialized view is fast because the expensive computation already happened earlier.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;But materialized views introduce a new problem:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the data becomes stale.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike normal views, materialized views do not automatically update when source tables change. You must refresh them manually.&lt;/p&gt;

&lt;p&gt;That is where things become complicated.&lt;/p&gt;

&lt;p&gt;Refreshing a materialized view sounds simple at first:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;REFRESH MATERIALIZED VIEW mv_ordersummary;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;But in production systems, especially multi-tenant systems with continuous traffic, refresh behavior becomes an operational challenge.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large refreshes can take minutes.&lt;/li&gt;
&lt;li&gt;Refresh operations acquire locks.&lt;/li&gt;
&lt;li&gt;Users continue reading during refreshes.&lt;/li&gt;
&lt;li&gt;One failed refresh should not impact other tenants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And at scale, dozens or hundreds of materialized views may refresh continuously.&lt;/p&gt;

&lt;p&gt;In our case, every tenant has its own set of materialized views:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mv_ordersummary_123
mv_ordersummary_456
mv_ordersummary_789
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Refreshing Materialized Views Is Harder Than It Looks
&lt;/h2&gt;

&lt;p&gt;The problem is not creating the materialized view.&lt;/p&gt;

&lt;p&gt;The problem is replacing old data with new data while traffic is actively reading from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A few realities make this difficult:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refreshes can take minutes on large datasets&lt;/li&gt;
&lt;li&gt;PostgreSQL refresh operations acquire locks&lt;/li&gt;
&lt;li&gt;Readers expect consistent results&lt;/li&gt;
&lt;li&gt;Multi-tenant systems amplify operational problems&lt;/li&gt;
&lt;li&gt;One bad tenant refresh should not break the entire refresh cycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At small scale, REFRESH MATERIALIZED VIEW works fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At larger scale, you start caring about:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lock duration&lt;/li&gt;
&lt;li&gt;transaction scope&lt;/li&gt;
&lt;li&gt;bloat&lt;/li&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;failure isolation&lt;/li&gt;
&lt;li&gt;rollback safety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The strategy you choose depends on your workload characteristics.&lt;br&gt;
Let’s walk through the main approaches PostgreSQL engineers typically use.&lt;/p&gt;
&lt;h2&gt;
  
  
  Approach 1: Plain REFRESH MATERIALIZED VIEW
&lt;/h2&gt;

&lt;p&gt;The simplest option is the default PostgreSQL refresh behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;REFRESH&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary_123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How It Works&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL rebuilds the materialized view contents in place.&lt;/p&gt;

&lt;p&gt;During the refresh, PostgreSQL acquires an ACCESS EXCLUSIVE lock on the materialized view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That lock blocks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reads&lt;/li&gt;
&lt;li&gt;writes&lt;/li&gt;
&lt;li&gt;concurrent refreshes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Readers wait until the refresh completes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Very simple.&lt;/li&gt;
&lt;li&gt;No special schema design required.&lt;/li&gt;
&lt;li&gt;No additional storage overhead.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Usually fast for smaller views.&lt;br&gt;
&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The lock behavior is a dealbreaker for high-concurrency systems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If your refresh takes 3 minutes, readers block for 3 minutes.&lt;br&gt;
In practice, that means:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API latency spikes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;request timeouts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;connection pool exhaustion&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;cascading failures&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This becomes especially painful in multi-tenant systems where refreshes happen continuously.&lt;/p&gt;

&lt;p&gt;Even if each refresh is “only” 30 seconds, enough concurrent tenants create constant lock pressure.&lt;/p&gt;

&lt;p&gt;Verdict&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;internal tools&lt;/li&gt;
&lt;li&gt;small datasets&lt;/li&gt;
&lt;li&gt;low read concurrency&lt;/li&gt;
&lt;li&gt;maintenance windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not acceptable for continuously queried production workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: REFRESH MATERIALIZED VIEW CONCURRENTLY
&lt;/h2&gt;

&lt;p&gt;PostgreSQL provides a safer option:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;REFRESH MATERIALIZED VIEW CONCURRENTLY mv_ordersummary_123;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is the first thing most engineers try after discovering lock contention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How It Works&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of replacing the materialized view contents directly, PostgreSQL builds updated contents alongside the existing data and swaps them internally.&lt;/p&gt;

&lt;p&gt;Readers can continue querying during the refresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No blocking reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the biggest advantage.&lt;/p&gt;

&lt;p&gt;For many workloads, this alone is sufficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are important caveats.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Requires a UNIQUE index&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;PostgreSQL requires at least one unique index covering all rows.&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_mv_ordersummary_123&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary_123&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every materialized view naturally has a stable unique key.&lt;/p&gt;

&lt;p&gt;Sometimes you end up manufacturing synthetic uniqueness just to satisfy refresh requirements.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Still one massive transaction&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire concurrent refresh runs in one transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For large views, that means:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long transaction lifetimes&lt;/li&gt;
&lt;li&gt;large WAL generation&lt;/li&gt;
&lt;li&gt;vacuum delays&lt;/li&gt;
&lt;li&gt;replication lag risk&lt;/li&gt;
&lt;li&gt;Slower than regular refresh&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concurrent refreshes are usually slower than standard refreshes because PostgreSQL must maintain visibility guarantees while comparing old and new rows.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Can create bloat&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Frequent concurrent refreshes can generate table and index bloat over time.&lt;/p&gt;

&lt;p&gt;You often need aggressive autovacuum tuning or periodic maintenance.&lt;/p&gt;

&lt;p&gt;Verdict&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A strong default choice when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can define a stable unique index&lt;/li&gt;
&lt;li&gt;refresh duration is acceptable&lt;/li&gt;
&lt;li&gt;storage churn is manageable
But it still couples refresh execution to the live object itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That limitation matters at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 3: Incremental Refresh with Triggers or pg_ivm
&lt;/h2&gt;

&lt;p&gt;The next level is incremental maintenance.&lt;/p&gt;

&lt;p&gt;Instead of rebuilding the entire materialized view, &lt;em&gt;you update only the changed rows.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the idea behind extensions like pg_ivm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How It Works&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Base table writes trigger incremental updates to the materialized view.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSERT into orders
    -&amp;gt; trigger fires
    -&amp;gt; materialized aggregate updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Near real-time freshness.&lt;/li&gt;
&lt;li&gt;No expensive full rebuilds.&lt;/li&gt;
&lt;li&gt;No giant refresh windows.&lt;/li&gt;
&lt;li&gt;Excellent for low-latency analytical workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The complexity increases dramatically.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Write amplification&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every write to source tables now performs additional maintenance work.&lt;/p&gt;

&lt;p&gt;Heavy OLTP systems can suffer significant write overhead.&lt;/p&gt;

&lt;p&gt;2.** More operational complexity**&lt;/p&gt;

&lt;p&gt;Triggers become part of the critical write path.&lt;/p&gt;

&lt;p&gt;That introduces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;contention&lt;/li&gt;
&lt;li&gt;debugging complexity&lt;/li&gt;
&lt;li&gt;migration risk&lt;/li&gt;
&lt;li&gt;transactional edge cases&lt;/li&gt;
&lt;li&gt;SQL limitations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Incremental maintenance works best for simpler aggregation patterns.&lt;/p&gt;

&lt;p&gt;Complex joins, window functions, or non-deterministic logic can become difficult or unsupported.&lt;/p&gt;

&lt;p&gt;Verdict&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Excellent for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;real-time analytics&lt;/li&gt;
&lt;li&gt;event-heavy systems&lt;/li&gt;
&lt;li&gt;carefully controlled schemas
Overkill for many batch-oriented refresh pipelines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Approach 4 (Recommended): Blue/Green Swap with Shadow Views
&lt;/h2&gt;

&lt;p&gt;Instead of refreshing the live materialized view directly, we maintain a separate shadow refresh view.&lt;/p&gt;

&lt;p&gt;For every logical view, we maintain three physical objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mv_ordersummary_123           -- live
mv_ordersummary_123_refresh   -- next candidate
mv_ordersummary_123_blue      -- previous generation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The key idea:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;refresh happens entirely outside the live object&lt;/li&gt;
&lt;li&gt;validation occurs before promotion&lt;/li&gt;
&lt;li&gt;promotion is an atomic rename swap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The expensive work is isolated from readers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Refresh Flow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our refresh functionality executes roughly in this sequence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refresh _refresh&lt;/li&gt;
&lt;li&gt;Validate row count&lt;/li&gt;
&lt;li&gt;Perform atomic rename swap&lt;/li&gt;
&lt;li&gt;Reset stale refresh view&lt;/li&gt;
&lt;li&gt;Update metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The critical insight is that the live object remains untouched until promotion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Refresh the Shadow View&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;REFRESH MATERIALIZED VIEW mv_ordersummary_123_refresh;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This may take minutes.&lt;/p&gt;

&lt;p&gt;That is fine.&lt;/p&gt;

&lt;p&gt;Nobody reads from _refresh.&lt;/p&gt;

&lt;p&gt;The live materialized view continues serving traffic normally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Validate Before Promotion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We refuse to swap in obviously broken refreshes.&lt;/p&gt;

&lt;p&gt;At minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary_123_refresh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the row count is zero, we abort promotion.&lt;/p&gt;

&lt;p&gt;This catches failures like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;upstream ETL issues&lt;/li&gt;
&lt;li&gt;accidental filters&lt;/li&gt;
&lt;li&gt;empty joins&lt;/li&gt;
&lt;li&gt;bad migrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validation can be more sophisticated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;row-count deltas&lt;/li&gt;
&lt;li&gt;checksum comparisons&lt;/li&gt;
&lt;li&gt;timestamp sanity checks&lt;/li&gt;
&lt;li&gt;aggregate thresholds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important thing is validating before touching production readers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Atomic 3-Way Rename Swap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the core pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inside one transaction:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary_123&lt;/span&gt;
    &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary_123_blue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary_123_refresh&lt;/span&gt;
    &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary_123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary_123_blue&lt;/span&gt;
    &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary_123_refresh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rename sequence is effectively atomic from the application's perspective.&lt;/p&gt;

&lt;p&gt;Readers either see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the old live view&lt;/li&gt;
&lt;li&gt;or the new live view
Never a half-built state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why This Works Well&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The expensive operation is the refresh.&lt;/p&gt;

&lt;p&gt;The rename itself is extremely fast.&lt;/p&gt;

&lt;p&gt;The brief ACCESS EXCLUSIVE lock during rename typically lasts milliseconds.&lt;/p&gt;

&lt;p&gt;That is fundamentally different from holding the lock during a multi-minute refresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visualizing the Swap&lt;/strong&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIVE      -&amp;gt; mv_ordersummary_123
REFRESH   -&amp;gt; mv_ordersummary_123_refresh
OLD       -&amp;gt; mv_ordersummary_123_blue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After refresh completes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIVE      -&amp;gt; old data
REFRESH   -&amp;gt; new data
OLD       -&amp;gt; previous generation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After atomic rename transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIVE      -&amp;gt; new data
REFRESH   -&amp;gt; old data
OLD       -&amp;gt; previous live generation name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Effectively:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;live      -&amp;gt; _blue&lt;br&gt;
_refresh  -&amp;gt; live&lt;br&gt;
_blue     -&amp;gt; _refresh&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Readers continue querying the stable live name the entire time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Reset the Stale Refresh View&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is an important optimization.&lt;/p&gt;

&lt;p&gt;After promotion, the old live view becomes the next cycle’s _refresh candidate.&lt;/p&gt;

&lt;p&gt;We immediately clear it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;REFRESH&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;mv_ordersummary_123_refresh&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;NO&lt;/span&gt; &lt;span class="k"&gt;DATA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This does two things:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frees stale contents&lt;/li&gt;
&lt;li&gt;guarantees the next refresh starts clean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this step, old data lingers indefinitely and can confuse debugging or validation logic.&lt;/p&gt;

&lt;p&gt;The WITH NO DATA trick is underused and extremely useful in rotation-based refresh systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Update Metadata&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Finally:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;company_materialized_view_config&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;view_last_refreshed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;company_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This powers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dashboards&lt;/li&gt;
&lt;li&gt;alerting&lt;/li&gt;
&lt;li&gt;freshness monitoring&lt;/li&gt;
&lt;li&gt;retry logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Operational metadata matters as much as the refresh itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Blue/Green Works So Well&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This pattern solves several operational problems simultaneously.&lt;/p&gt;

&lt;p&gt;1.** Readers Never Touch the Refresh Process**&lt;/p&gt;

&lt;p&gt;The live object remains stable during expensive refresh operations.&lt;/p&gt;

&lt;p&gt;That isolation is the biggest win.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Validation Happens Before Exposure&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can reject bad refreshes safely.&lt;/p&gt;

&lt;p&gt;That alone justifies the extra complexity.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Failures Are Easy to Contain&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If refresh fails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;log it&lt;/li&gt;
&lt;li&gt;skip promotion&lt;/li&gt;
&lt;li&gt;continue processing other tenants&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The existing live view remains untouched.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Promotion Is Fast&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The swap itself is metadata-only.&lt;/p&gt;

&lt;p&gt;That minimizes lock duration dramatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;No solution is free.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;*&lt;em&gt;Approximately 2x Storage
*&lt;/em&gt;
You effectively maintain duplicate materialized views.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For very large datasets, this matters.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;*&lt;em&gt;Naming Conventions Become Important
*&lt;/em&gt;
Your orchestration layer must understand:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_refresh
_blue
live names

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

&lt;/div&gt;



&lt;p&gt;You need deterministic naming and discovery logic.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Brief Rename Locks Still Exist&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rename transaction acquires ACCESS EXCLUSIVE.&lt;/p&gt;

&lt;p&gt;But the duration is tiny compared to full refresh locking.&lt;/p&gt;

&lt;p&gt;Milliseconds instead of minutes is usually an acceptable trade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final thoughts:&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Refreshing materialized views is easy in development environments.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Refreshing them safely in production is not.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As datasets grow and concurrency increases, refresh strategy becomes an operational architecture decision rather than just a SQL command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no universally correct approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plain refresh is simplest&lt;/li&gt;
&lt;li&gt;concurrent refresh improves availability&lt;/li&gt;
&lt;li&gt;incremental maintenance provides real-time freshness&lt;/li&gt;
&lt;li&gt;blue/green swaps maximize isolation and validation safety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For our workload, the blue/green pattern provided the best balance between simplicity, reliability, and operational safety.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core idea is straightforward:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Never rebuild the object your users are actively reading.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Refresh elsewhere.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Validate.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Promote atomically.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>performance</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
    <item>
      <title>What's a data warehouse?</title>
      <dc:creator>Charity Jelimo</dc:creator>
      <pubDate>Sun, 29 Mar 2026 17:44:51 +0000</pubDate>
      <link>https://dev.to/data_with_jelimo/whats-a-data-warehouse-7f0</link>
      <guid>https://dev.to/data_with_jelimo/whats-a-data-warehouse-7f0</guid>
      <description>&lt;h2&gt;
  
  
  The data challenge in modern businesses
&lt;/h2&gt;

&lt;p&gt;Imagine you’re running an online store. Every day, customers place orders, browse products, and make payments. All this data is being captured across different systems; your website, payment service, and customer database.&lt;/p&gt;

&lt;p&gt;Now, at the end of the month, you’re asked a simple question: &lt;em&gt;“&lt;/em&gt;&lt;em&gt;What are our top-selling products, and how has revenue changed over time?&lt;/em&gt;&lt;em&gt;”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Surprisingly, answering this isn’t easy.&lt;/p&gt;

&lt;p&gt;The data is scattered, inconsistent, and stored in systems designed for daily operations, not analysis. This is the challenge most businesses face: they have data, but they can’t easily turn it into insights.&lt;/p&gt;

&lt;p&gt;This is where a &lt;strong&gt;data warehouse&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A data warehouse is a centralized system that collects data from multiple sources, cleans and organizes it, and stores it in a structured format optimized for analysis.&lt;/strong&gt; Unlike operational databases that focus on fast transactions, data warehouses are designed for querying large volumes of historical data, making them ideal for reporting, dashboards, and business intelligence.&lt;/p&gt;

&lt;p&gt;In simple terms, a data warehouse doesn’t just store data, it transforms it into something the business can actually use to make better decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data warehouse vs other data repositories
&lt;/h2&gt;

&lt;p&gt;A data warehouse is just one way to store data, and it’s important to understand how it differs from other common systems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operational databases (OLTP):&lt;/strong&gt;  &lt;em&gt;OLTP systems record business interactions as they occur in the day-to-day operation of the organization, and support querying of this data to make inferences.&lt;/em&gt; Fast and efficient, they keep the business running but they aren’t built to analyze historical trends or answer complex questions.ge-scale analysis.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data lakes:&lt;/strong&gt; &lt;em&gt;Data Lakes store everything, raw and unprocessed. This includes structured data like tables, semi-structured data like JSON files, or even unstructured data like images and logs.&lt;/em&gt; They’re perfect for data scientists and advanced analytics, but without organization, it can be difficult to get clear answers quickly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data lakehouses:&lt;/strong&gt; Data lakehouses are a hybrid. &lt;em&gt;They combine the flexibility of a data lake with the structured, query-ready features of a warehouse.&lt;/em&gt; You can store raw data while also running analytics, giving businesses the best of both worlds.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>Common Table Expressions</title>
      <dc:creator>Charity Jelimo</dc:creator>
      <pubDate>Wed, 25 Feb 2026 13:46:35 +0000</pubDate>
      <link>https://dev.to/data_with_jelimo/common-table-expressions-59bd</link>
      <guid>https://dev.to/data_with_jelimo/common-table-expressions-59bd</guid>
      <description>&lt;p&gt;A Common Table Expression (CTE) is a temporary result set that simplifies and structures SQL queries. It is defined using the WITH keyword and can improve query readability and reusability. In some cases, CTEs can also enhance performance by avoiding redundant calculations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are CTEs?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Temporary Result Set:&lt;/strong&gt;&lt;br&gt;
CTEs exist only during the execution of the query and are not stored in the database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Readability and Maintainability:&lt;/strong&gt;&lt;br&gt;
By breaking complex logic into reusable components, CTEs make queries easier to understand.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reusable Within the Query:&lt;/strong&gt;&lt;br&gt;
A CTE can be referenced multiple times within the query, avoiding repeated logic or calculations.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why use CTEs?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Simplify Complex Queries:&lt;/strong&gt;
Break large queries into smaller, named parts for clarity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eliminate Redundant Calculations:&lt;/strong&gt;
Replace repeated subqueries with a single calculation in a CTE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve Maintainability:&lt;/strong&gt;
Centralize repeated logic in one place, making updates easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable Recursive Queries:&lt;/strong&gt;
Handle hierarchical or iterative data using recursive CTEs.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Do CTEs Optimize Performance?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reduce Redundancy:&lt;/strong&gt;
CTEs calculate a result once, reducing unnecessary repetition.
Example:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT 
    o.OrderID
FROM Orders o
WHERE o.TotalAmount &amp;gt; (SELECT AVG(TotalAmount) FROM Orders)
  AND o.TotalAmount &amp;lt; (SELECT AVG(TotalAmount) FROM Orders);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; The subquery (SELECT AVG(TotalAmount) FROM Orders) is calculated twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution Using a CTE:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WITH AvgTotalAmount AS (
    SELECT AVG(TotalAmount) AS AvgAmount
    FROM Orders
)
SELECT o.OrderID
FROM Orders o
JOIN AvgTotalAmount a
WHERE o.TotalAmount &amp;gt; a.AvgAmount
  AND o.TotalAmount &amp;lt; a.AvgAmount;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simplify Execution Plans:&lt;/strong&gt;&lt;br&gt;
With a CTE, the database evaluates the logic once and reuses the result.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Readable Performance Gains:&lt;/strong&gt;&lt;br&gt;
Even if there’s no computational gain, a CTE often makes execution plans easier to debug.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>beginners</category>
      <category>database</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
