<?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: Vaishnav Prabhu</title>
    <description>The latest articles on DEV Community by Vaishnav Prabhu (@vaishnavprabhu).</description>
    <link>https://dev.to/vaishnavprabhu</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%2F4171730%2F14c355dd-21d1-4933-849d-24092af86939.png</url>
      <title>DEV Community: Vaishnav Prabhu</title>
      <link>https://dev.to/vaishnavprabhu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vaishnavprabhu"/>
    <language>en</language>
    <item>
      <title>Write-Audit-Publish: Never Promote a Bad Table Again</title>
      <dc:creator>Vaishnav Prabhu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:08:21 +0000</pubDate>
      <link>https://dev.to/vaishnavprabhu/write-audit-publish-never-promote-a-bad-table-again-94i</link>
      <guid>https://dev.to/vaishnavprabhu/write-audit-publish-never-promote-a-bad-table-again-94i</guid>
      <description>&lt;p&gt;Most quality checks run &lt;em&gt;after&lt;/em&gt; bad data is already live. You load the table, then test it, then&lt;br&gt;
discover the problem — but by now a dashboard already served wrong numbers, or an agent already&lt;br&gt;
answered with them. Write-Audit-Publish (WAP) flips the order: you &lt;strong&gt;prove the data is good&lt;br&gt;
before anyone can see it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The three steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write&lt;/strong&gt; — build the new data into a &lt;em&gt;staging&lt;/em&gt; location consumers can't see: a temp table, a
separate schema, or a zero-copy clone/branch of the target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit&lt;/strong&gt; — run your quality gates against that staged copy: uniqueness at the grain, row-count
vs baseline, freshness, reconciliation against a trusted source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish&lt;/strong&gt; — only if every check passes, promote the staged data to production with an
&lt;strong&gt;atomic swap&lt;/strong&gt; (a rename or clone-swap), so consumers flip from old-good to new-good with no
bad state in between. If a check fails, nothing is published; production keeps serving the last
known-good data while you investigate.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why it's better than audit-after-load
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consumers never see bad data.&lt;/strong&gt; The failure mode becomes "today's refresh is delayed,"
not "the dashboard is wrong." Stale-but-correct beats fresh-but-wrong almost every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The swap is atomic.&lt;/strong&gt; No window where half the table is updated and queries see a mix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback is trivial.&lt;/strong&gt; The previous version is still right there; promoting it back is one
operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Making it cheap
&lt;/h2&gt;

&lt;p&gt;The pattern used to be expensive — you'd copy the whole table. Modern warehouses make it nearly&lt;br&gt;
free with &lt;strong&gt;zero-copy clones&lt;/strong&gt; and &lt;strong&gt;table/catalog branching&lt;/strong&gt;: you stage on a clone, audit it,&lt;br&gt;
and publish by swapping pointers, with no bulk data movement. That's what turned WAP from a&lt;br&gt;
nice-to-have into a default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits in your stack
&lt;/h2&gt;

&lt;p&gt;Wire the audit step to the same tests you already write (dbt tests, a data-quality scan) and let&lt;br&gt;
their result gate the publish. The mental model: your transformation job doesn't "load the&lt;br&gt;
table," it "proposes a new version," and only a passing audit promotes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Validate on a staged copy and publish only what passes — consumers never see bad data.&lt;/li&gt;
&lt;li&gt;Promote with an atomic swap so there's no half-updated state, and rollback is one step.&lt;/li&gt;
&lt;li&gt;Zero-copy clones and branching make WAP cheap enough to be the default.&lt;/li&gt;
&lt;li&gt;Reframe loads as "propose a version"; a passing audit is what makes it live.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>sql</category>
      <category>dataquality</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Vector-Native Analytics: Embeddings as a First-Class Warehouse Column</title>
      <dc:creator>Vaishnav Prabhu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:04:04 +0000</pubDate>
      <link>https://dev.to/vaishnavprabhu/modeling-your-warehouse-so-ai-can-actually-use-it-3mid</link>
      <guid>https://dev.to/vaishnavprabhu/modeling-your-warehouse-so-ai-can-actually-use-it-3mid</guid>
      <description>&lt;p&gt;For years, "AI data" lived somewhere else — a separate vector database bolted onto the side of&lt;br&gt;
your warehouse, kept in sync with brittle pipelines. That's changing. Modern warehouses can&lt;br&gt;
store &lt;strong&gt;embeddings as a column&lt;/strong&gt; and run similarity search in SQL, right next to your structured&lt;br&gt;
data. That unlocks a genuinely new modeling pattern: &lt;strong&gt;hybrid queries&lt;/strong&gt; that filter with&lt;br&gt;
ordinary SQL and rank by semantic similarity in the same statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why keeping vectors in the warehouse matters
&lt;/h2&gt;

&lt;p&gt;One copy of the data, one governance model, one query engine. You can write something like&lt;br&gt;
"find the records &lt;em&gt;in this region, from last quarter&lt;/em&gt; most similar to this description" — a&lt;br&gt;
structured filter and a semantic rank together — without shuttling data between systems or&lt;br&gt;
reconciling two sources of truth. No sync pipeline to break at 2am.&lt;/p&gt;

&lt;h2&gt;
  
  
  The modeling decisions that make or break it
&lt;/h2&gt;

&lt;p&gt;Embeddings are data, so they need the same discipline as any model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick the embedding grain.&lt;/strong&gt; What is one vector &lt;em&gt;of&lt;/em&gt;? A row? A text field? A record plus its
context? This is a grain decision, and it determines whether retrieval returns signal or
noise. Too coarse and results are vague; too fine and they lose meaning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version the embeddings.&lt;/strong&gt; The moment you change embedding models, old and new vectors aren't
comparable. Store the model/version alongside each vector so you know what's stale, and plan
how you'll re-embed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep rich metadata.&lt;/strong&gt; Store source, date, entity, and permissions next to each vector so you
can &lt;em&gt;filter&lt;/em&gt; retrieval, not rely on similarity alone. Metadata is what makes hybrid search work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind the cost.&lt;/strong&gt; Embedding generation and vector indexes aren't free. Model the refresh
cadence deliberately — re-embed on change, not on every run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it's good for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semantic search&lt;/strong&gt; over your own records — tickets, docs, products — governed like the rest
of your data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entity resolution / dedup&lt;/strong&gt; — near-duplicate records that exact matching misses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG over the warehouse&lt;/strong&gt; — serving trusted, filtered context to an LLM straight from
governed tables instead of a disconnected store.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A caution
&lt;/h2&gt;

&lt;p&gt;Vector search returns the &lt;em&gt;nearest&lt;/em&gt; match, not the &lt;em&gt;correct&lt;/em&gt; one — it's confident even when&lt;br&gt;
it's wrong. Treat similarity as a ranking signal, keep a relevance threshold, and always carry&lt;br&gt;
metadata so you can filter and audit what came back. Semantic ≠ accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Warehouses can now hold embeddings as columns and do similarity search in SQL — one copy, one
governance model.&lt;/li&gt;
&lt;li&gt;Hybrid queries (SQL filter + semantic rank) are the new superpower; no separate vector DB to
sync.&lt;/li&gt;
&lt;li&gt;Treat embedding grain, versioning, metadata, and refresh cost as first-class modeling choices.&lt;/li&gt;
&lt;li&gt;Nearest isn't correct — threshold and filter with metadata.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>ai</category>
      <category>sql</category>
      <category>database</category>
    </item>
    <item>
      <title>Modeling Your Warehouse So AI Can Actually Use It</title>
      <dc:creator>Vaishnav Prabhu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:01:58 +0000</pubDate>
      <link>https://dev.to/vaishnavprabhu/fan-out-joins-the-silent-cause-of-inflated-metrics-3pfd</link>
      <guid>https://dev.to/vaishnavprabhu/fan-out-joins-the-silent-cause-of-inflated-metrics-3pfd</guid>
      <description>&lt;p&gt;Everyone wants to point an LLM at their data — "ask questions in plain English," "let&lt;br&gt;
the agent pull the numbers." Then it confidently returns the wrong revenue figure, and&lt;br&gt;
trust evaporates. The problem usually isn't the model. It's the data layer underneath&lt;br&gt;
it. An LLM is only as good as the structure, definitions, and correctness of what it&lt;br&gt;
reads. Here's how to model for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the model a contract, not a guess
&lt;/h2&gt;

&lt;p&gt;If you let a text-to-SQL tool or an agent freelance joins against raw tables, it &lt;em&gt;will&lt;/em&gt;&lt;br&gt;
invent a definition of "active customer" that doesn't match anyone else's. A&lt;br&gt;
&lt;strong&gt;semantic layer&lt;/strong&gt; (or a set of certified metric models) fixes this: define each&lt;br&gt;
metric once — its grain, its filters, its joins — so the model composes from trusted&lt;br&gt;
building blocks instead of reconstructing business logic from column names. The&lt;br&gt;
semantic layer becomes the contract between your warehouse and anything, human or AI,&lt;br&gt;
that queries it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make tables legible
&lt;/h2&gt;

&lt;p&gt;Models reason over your schema the way a new analyst would — from names and docs. You&lt;br&gt;
reduce hallucinated joins dramatically just by making the warehouse legible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clear names.&lt;/strong&gt; &lt;code&gt;gross_sales&lt;/code&gt; beats &lt;code&gt;amt2&lt;/code&gt;. The model can't infer what it can't read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documented grain and keys.&lt;/strong&gt; State the primary key and the grain of every model;
it's what keeps an agent from double-counting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expose curated views, not raw tables.&lt;/strong&gt; Point AI consumers at a clean, certified
layer so they can't wander into staging and half-built tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Treat embeddings as a modeling decision
&lt;/h2&gt;

&lt;p&gt;Retrieval-augmented generation (RAG) lives or dies on &lt;em&gt;what you chose as a chunk&lt;/em&gt;.&lt;br&gt;
That's a grain question, same as any fact table. Before you store vector columns next&lt;br&gt;
to your rows, decide: what is one "document"? A row? A paragraph? A record plus its&lt;br&gt;
context? Chunk too coarse and retrieval returns noise; too fine and it loses meaning.&lt;br&gt;
Keep rich &lt;strong&gt;metadata&lt;/strong&gt; alongside each embedding (source, date, entity, permissions) so&lt;br&gt;
you can filter retrieval instead of hoping similarity alone is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correctness matters &lt;em&gt;more&lt;/em&gt; with AI, not less
&lt;/h2&gt;

&lt;p&gt;A dashboard with a bad number invites a double-take. An agent with a bad number states&lt;br&gt;
it as fact and moves on. So the fundamentals you already know carry extra weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uniqueness and grain&lt;/strong&gt; — a fan-out that doubles a metric will be surfaced by an
agent with total confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness&lt;/strong&gt; — an agent won't notice the feed is a day stale; your contract has to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Definitions&lt;/strong&gt; — if "revenue" is ambiguous in the warehouse, it'll be ambiguous (and
wrong) in the answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: the data-quality gates and reconciliation checks you build for humans&lt;br&gt;
become the guardrails that keep AI honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Put a semantic/metrics layer between your warehouse and any LLM so definitions are a
contract, not a guess.&lt;/li&gt;
&lt;li&gt;Make the schema legible — clear names, documented grain and keys, curated views over
raw.&lt;/li&gt;
&lt;li&gt;Treat RAG chunking and embeddings as a grain-and-metadata modeling problem.&lt;/li&gt;
&lt;li&gt;Correctness (uniqueness, freshness, clear definitions) matters more with AI, because
an agent repeats your mistakes with confidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Related in&lt;/em&gt; In the Pipeline*: running those models safely once the data is ready.*&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>ai</category>
      <category>sql</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Fan-Out Joins: The Silent Cause of Inflated Metrics</title>
      <dc:creator>Vaishnav Prabhu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:07:22 +0000</pubDate>
      <link>https://dev.to/vaishnavprabhu/fan-out-joins-the-silent-cause-of-inflated-metrics-c86</link>
      <guid>https://dev.to/vaishnavprabhu/fan-out-joins-the-silent-cause-of-inflated-metrics-c86</guid>
      <description>&lt;p&gt;Here's a bug that has quietly wrecked more dashboards than almost anything else: you join a fact&lt;br&gt;
table to a dimension, someone later lets a duplicate sneak into that dimension, and every fact&lt;br&gt;
row that matches gets multiplied. Your &lt;code&gt;SUM()&lt;/code&gt; inflates. Nothing errors. The dimension looks&lt;br&gt;
fine. The numbers are just… bigger than reality, and nobody's sure why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a fan-out actually is
&lt;/h2&gt;

&lt;p&gt;A join multiplies rows whenever the key you join on isn't unique on the other side. Join a fact&lt;br&gt;
to a dimension that has &lt;strong&gt;two&lt;/strong&gt; rows for the same key, and every matching fact row comes back&lt;br&gt;
&lt;strong&gt;twice&lt;/strong&gt;. Aggregate that, and totals double. Three duplicate dimension rows? Triple. The fact&lt;br&gt;
table didn't change — the join invented rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's so hard to spot
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The duplicate dimension rows are often &lt;strong&gt;identical&lt;/strong&gt; on the visible attributes, so eyeballing
the dimension reveals nothing.&lt;/li&gt;
&lt;li&gt;If only a few keys are affected, the &lt;strong&gt;grand total still looks plausible&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It usually appears right after a routine load that "shouldn't have changed anything," so it's
easy to wave off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The classic source is a Slowly Changing Dimension that ended up with two "current" rows for one&lt;br&gt;
key (see &lt;em&gt;SCD Type-2 Without the Duplicate-Current-Row Footgun&lt;/em&gt;). But any non-unique join key —&lt;br&gt;
a mapping table with a dupe, a dimension built from a fan-out of its own — does it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to catch it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assert the grain.&lt;/strong&gt; The join key must be unique on the dimension side. Make it a test:
&lt;/li&gt;
&lt;/ol&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;join_key&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;from&lt;/span&gt; &lt;span class="n"&gt;dim&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;join_key&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;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- must return zero rows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Watch the ratio.&lt;/strong&gt; Compare a metric to a trusted baseline. A number that's suspiciously&lt;br&gt;
close to exactly 2× or 4× is a duplication fingerprint, not real growth.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Count rows across the join.&lt;/strong&gt; If a fact table's row count jumps after a join step, the join&lt;br&gt;
is adding rows that shouldn't exist.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to prevent it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Guarantee uniqueness before you join.&lt;/strong&gt; Dedupe the dimension to one row per key, or select
the current version explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be defensive at read time.&lt;/strong&gt; A view can enforce one row per key with
&lt;code&gt;qualify row_number() over (partition by key order by valid_from desc) = 1&lt;/code&gt;, so one upstream
glitch can't fan out every metric downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the invariant in CI.&lt;/strong&gt; A uniqueness test on the join key turns a silent production
disaster into a failed build.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A non-unique join key multiplies facts and inflates every aggregate built on them.&lt;/li&gt;
&lt;li&gt;Duplicate dimension rows are often invisible and total-preserving — don't trust eyeballing.&lt;/li&gt;
&lt;li&gt;Assert key uniqueness as a test, watch for round-number ratios, and dedupe before joining.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>sql</category>
      <category>database</category>
      <category>analytics</category>
    </item>
    <item>
      <title>SCD Type-2 Without the Duplicate-Current-Row Footgun</title>
      <dc:creator>Vaishnav Prabhu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:04:13 +0000</pubDate>
      <link>https://dev.to/vaishnavprabhu/scd-type-2-without-the-duplicate-current-row-footgun-50bg</link>
      <guid>https://dev.to/vaishnavprabhu/scd-type-2-without-the-duplicate-current-row-footgun-50bg</guid>
      <description>&lt;p&gt;Slowly Changing Dimension (SCD) Type-2 is how we keep history in a dimension: when a&lt;br&gt;
row's attributes change, we close the old version and open a new one. Done right,&lt;br&gt;
exactly &lt;strong&gt;one&lt;/strong&gt; row per business key is "current" at any time. Done wrong, you get&lt;br&gt;
two current rows for the same key — and every fact that joins to that dimension&lt;br&gt;
silently &lt;strong&gt;doubles&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The failure mode
&lt;/h2&gt;

&lt;p&gt;A hand-rolled Type-2 loader usually does two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expire&lt;/strong&gt; the existing current row when a change is detected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insert&lt;/strong&gt; the new version as current.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The classic bug: the &lt;em&gt;expire&lt;/em&gt; step and the &lt;em&gt;insert&lt;/em&gt; step don't cover the same set of&lt;br&gt;
rows. For example, the insert fires for both new keys and changed keys, but the&lt;br&gt;
expire only fires for changed keys. If a key that already has a current row slips&lt;br&gt;
into the "new" path, you insert a second current row &lt;strong&gt;without closing the first&lt;/strong&gt; —&lt;br&gt;
and now the key has two &lt;code&gt;is_current = true&lt;/code&gt; records with open end-dates.&lt;/p&gt;

&lt;p&gt;Downstream, a fact table joining on that key fans out: every matching fact row is&lt;br&gt;
duplicated, and any &lt;code&gt;SUM()&lt;/code&gt; over it inflates. The dimension looks fine at a glance;&lt;br&gt;
the damage shows up as mysteriously doubled metrics.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it hides so well
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The duplicate rows are often &lt;strong&gt;identical&lt;/strong&gt; on the business attributes, so casual
inspection of the dimension reveals nothing.&lt;/li&gt;
&lt;li&gt;Aggregate totals can still look plausible if only a handful of keys are affected.&lt;/li&gt;
&lt;li&gt;It frequently appears right after a load that &lt;em&gt;should&lt;/em&gt; have been a no-op, which
makes it easy to dismiss.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How to prevent it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Prefer a single atomic &lt;code&gt;MERGE&lt;/code&gt; over separate update + insert.&lt;/strong&gt;&lt;br&gt;
A &lt;code&gt;MERGE&lt;/code&gt; keyed on the business key lets you expire-and-insert in one statement with&lt;br&gt;
consistent matching logic, removing the "two steps disagree" class of bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. If you keep update+insert, expire &lt;em&gt;every&lt;/em&gt; prior current row you're about to replace&lt;/strong&gt; — not just the subset flagged as "changed."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Add a uniqueness guarantee as a hard invariant.&lt;/strong&gt; A post-load assertion that&lt;br&gt;
must return zero 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;select&lt;/span&gt; &lt;span class="n"&gt;business_key&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;from&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&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;business_key&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;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire this into your tests (e.g., a dbt &lt;code&gt;unique&lt;/code&gt; test on the current-rows view, or a&lt;br&gt;
data-quality check) so a regression fails the pipeline instead of reaching marts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Make consumers defensive.&lt;/strong&gt; Views that read the dimension can enforce one row&lt;br&gt;
per key with a &lt;code&gt;QUALIFY ROW_NUMBER() OVER (PARTITION BY business_key ORDER BY&lt;br&gt;
valid_from DESC) = 1&lt;/code&gt;, so a single upstream glitch can't fan out sales everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"Current" must mean &lt;em&gt;exactly one&lt;/em&gt; row per key — enforce it, don't assume it.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;MERGE&lt;/code&gt; keyed on the business key avoids the update/insert mismatch entirely.&lt;/li&gt;
&lt;li&gt;Guard it with a uniqueness assertion in CI/tests, and make downstream views
defensive with a de-duplicating window function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Next in this series: fan-out joins — how a duplicated dimension row quietly inflates every metric built on top of it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>sql</category>
      <category>database</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Catching the Incident Before the Alert: Anomaly Detection for Pipelines</title>
      <dc:creator>Vaishnav Prabhu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:01:31 +0000</pubDate>
      <link>https://dev.to/vaishnavprabhu/catching-the-incident-before-the-alert-anomaly-detection-for-pipelines-52f5</link>
      <guid>https://dev.to/vaishnavprabhu/catching-the-incident-before-the-alert-anomaly-detection-for-pipelines-52f5</guid>
      <description>&lt;p&gt;Static thresholds are the smoke detectors of data engineering: useful, but they only go off&lt;br&gt;
when something's already on fire, and they miss the slow leaks entirely. "Alert if rows &amp;lt; 1000"&lt;br&gt;
won't catch a table that quietly dropped 30%, or a feed that's complete in total but broken for&lt;br&gt;
one segment. Anomaly detection asks a better question: &lt;em&gt;is today normal for this thing?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "normal" needs a baseline, not a number
&lt;/h2&gt;

&lt;p&gt;Data has shape — weekday/weekend rhythms, month-end spikes, seasonal trends. A good baseline&lt;br&gt;
captures that: compare today against a rolling window of comparable days (same day-of-week),&lt;br&gt;
not a flat constant. Then flag deviations relative to that baseline. Suddenly a Wednesday that's&lt;br&gt;
35% below recent Wednesdays stands out, even though it clears your old static floor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three signals worth watching
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Volume&lt;/strong&gt; — row counts per load vs baseline. Catches partial feeds and double loads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness&lt;/strong&gt; — is the newest record from the period you expect? Catches stalled upstreams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distribution&lt;/strong&gt; — did the &lt;em&gt;shape&lt;/em&gt; of a key column shift (null rate, category mix, a metric's
mean)? Catches silent logic bugs that keep volume normal but change meaning.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The lesson that matters most: check per segment, not just totals
&lt;/h2&gt;

&lt;p&gt;This is the subtle one. A feed can match perfectly in aggregate while being badly wrong&lt;br&gt;
underneath, because per-segment errors cancel out. One store is doubled, another is missing,&lt;br&gt;
and the fleet total looks fine. If you only monitor the grand total, you'll certify broken data&lt;br&gt;
as healthy. &lt;strong&gt;Run your anomaly checks at the grain that matters&lt;/strong&gt; — per segment, per channel,&lt;br&gt;
per role — and the offsetting errors stop hiding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep it explainable and quiet
&lt;/h2&gt;

&lt;p&gt;Fancy models are tempting, but an anomaly alert nobody understands gets muted, and a muted&lt;br&gt;
alert is worthless. Favor methods whose output you can explain in a sentence ("28% below the&lt;br&gt;
trailing 4-week median for this segment"). Tune aggressively against false positives — alert&lt;br&gt;
fatigue kills these systems faster than missed anomalies do. Add seasonality and ML only where&lt;br&gt;
a simple baseline provably isn't enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Replace flat thresholds with baselines that respect weekday and seasonal shape.&lt;/li&gt;
&lt;li&gt;Watch volume, freshness, and distribution — the last one catches silent meaning changes.&lt;/li&gt;
&lt;li&gt;Detect at the real grain, not the total, so offsetting per-segment errors can't hide.&lt;/li&gt;
&lt;li&gt;Explainable and low-false-positive beats clever-but-muted every time.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>dataquality</category>
      <category>observability</category>
      <category>devops</category>
    </item>
    <item>
      <title>Self-Healing Pipelines: Using AI to Triage Incidents</title>
      <dc:creator>Vaishnav Prabhu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:57:30 +0000</pubDate>
      <link>https://dev.to/vaishnavprabhu/self-healing-pipelines-using-ai-to-triage-incidents-54hc</link>
      <guid>https://dev.to/vaishnavprabhu/self-healing-pipelines-using-ai-to-triage-incidents-54hc</guid>
      <description>&lt;p&gt;When a pipeline fails at 2am, the slow part usually isn't the fix — it's the &lt;em&gt;triage&lt;/em&gt;: reading&lt;br&gt;
logs, figuring out which task broke, deciding whether it's a transient blip or a real data bug,&lt;br&gt;
and writing it up. That triage is exactly the kind of pattern-matching LLMs are good at. Used&lt;br&gt;
carefully, an AI layer can shrink time-to-understanding from an hour to a minute — without&lt;br&gt;
handing it the keys to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "self-healing" actually means (and doesn't)
&lt;/h2&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; mean an AI silently rewriting your pipeline. It means a bounded assistant that&lt;br&gt;
&lt;strong&gt;observes, explains, and proposes&lt;/strong&gt; — and only &lt;em&gt;acts&lt;/em&gt; within a tight, pre-approved box. Think&lt;br&gt;
of it as an always-on on-call buddy that does the first pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sensible ladder of autonomy
&lt;/h2&gt;

&lt;p&gt;Roll this out in stages, earning trust at each rung:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Explain.&lt;/strong&gt; On failure, the assistant pulls the logs and context and writes a plain-English
summary: which task, likely category (transient / data / code), and the smoking-gun lines.
Zero actions — pure triage acceleration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify and route.&lt;/strong&gt; It tags the incident and routes it: page a human for data-correctness
issues, or note "likely transient" for a timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propose remediation.&lt;/strong&gt; It drafts the fix — "rerun partition 2026-08-05 after the upstream
lands" — and a root-cause summary, ready for a human to approve with one click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act within guardrails.&lt;/strong&gt; For a small set of &lt;em&gt;reversible, pre-approved&lt;/em&gt; actions (retry a
known-flaky step, re-trigger a failed sensor), it can act automatically and log what it did.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Destructive or irreversible actions — deletes, backfills, schema changes — stay human-approved,&lt;br&gt;
always.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guardrails that make it safe
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bounded actions.&lt;/strong&gt; An allowlist of safe operations, nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full audit trail.&lt;/strong&gt; Every AI decision and action is logged and reviewable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A circuit breaker.&lt;/strong&gt; Repeated auto-retries must escalate to a human, not loop forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No masking.&lt;/strong&gt; Auto-retrying a flaky step is fine; auto-retrying until a &lt;em&gt;real&lt;/em&gt; bug happens
to pass is how you hide incidents. Track "succeeded only after N retries" so fragility still
surfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it pays off first
&lt;/h2&gt;

&lt;p&gt;Start with the highest-volume, lowest-stakes failures: transient API timeouts, late upstreams,&lt;br&gt;
flaky sensors. That's where AI triage removes the most 2am pages for the least risk — and where&lt;br&gt;
you build the track record to justify more autonomy later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The win is faster &lt;em&gt;triage and explanation&lt;/em&gt;, not autonomous rewriting.&lt;/li&gt;
&lt;li&gt;Climb an autonomy ladder: explain → classify → propose → act-in-a-box.&lt;/li&gt;
&lt;li&gt;Keep destructive actions human-approved, log everything, and add a circuit breaker.&lt;/li&gt;
&lt;li&gt;Never let auto-remediation mask a real bug — surface "passed only after retries."&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>ai</category>
      <category>devops</category>
      <category>mlops</category>
    </item>
    <item>
      <title>Running an LLM as a Pipeline Step Without 2am Surprises</title>
      <dc:creator>Vaishnav Prabhu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:55:07 +0000</pubDate>
      <link>https://dev.to/vaishnavprabhu/running-an-llm-as-a-pipeline-step-without-2am-surprises-4g2n</link>
      <guid>https://dev.to/vaishnavprabhu/running-an-llm-as-a-pipeline-step-without-2am-surprises-4g2n</guid>
      <description>&lt;p&gt;Teams are dropping LLM calls into data pipelines everywhere now — to classify tickets,&lt;br&gt;
extract fields from messy text, summarize records, tag content. It feels like adding&lt;br&gt;
one more transform. It isn't. A SQL transform is a pure function: same input, same&lt;br&gt;
output, every time. An LLM step is a slow, rate-limited call to an external service&lt;br&gt;
that can return something &lt;em&gt;different&lt;/em&gt; each run, or something that isn't even valid.&lt;/p&gt;

&lt;p&gt;Treat it like the unreliable network dependency it is, and the 2am surprises mostly&lt;br&gt;
go away. Here's the short playbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Force structure, then validate it
&lt;/h2&gt;

&lt;p&gt;Never let free-form text flow downstream. Ask the model for &lt;strong&gt;structured output&lt;/strong&gt;&lt;br&gt;
(JSON that matches a schema) and validate every response against that schema before&lt;br&gt;
you trust it. If it doesn't parse or a required field is missing, that's a failure —&lt;br&gt;
retry it, route it to a dead-letter table, or fall back. The moment you depend on a&lt;br&gt;
model "usually" returning clean output is the moment a malformed response corrupts a&lt;br&gt;
downstream table.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make re-runs cheap and safe
&lt;/h2&gt;

&lt;p&gt;LLM calls cost money and time, so a naive re-run can be expensive and non-reproducible.&lt;br&gt;
Two habits fix this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache by input hash.&lt;/strong&gt; If you've already processed this exact input with this
exact prompt and model version, reuse the result. Re-runs become near-free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin your versions.&lt;/strong&gt; Record the model name and prompt version with each output.
"The numbers changed" is impossible to debug if the model silently updated underneath
you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Budget for failure up front
&lt;/h2&gt;

&lt;p&gt;Wrap the call the way you'd wrap any flaky API: a timeout, retries with backoff, and a&lt;br&gt;
&lt;strong&gt;cost ceiling&lt;/strong&gt; so a runaway loop can't quietly burn your budget. Decide in advance&lt;br&gt;
what happens when the model is down or slow — skip, use last-known-good, or fail the&lt;br&gt;
run loudly. Don't let a provider outage become a silent data gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat evals as a data-quality gate
&lt;/h2&gt;

&lt;p&gt;This is the big one. Before an AI step's output reaches production, gate it like any&lt;br&gt;
other quality check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;% valid&lt;/strong&gt; — what share of responses passed schema validation?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accuracy on a sample&lt;/strong&gt; — hold a small labeled set and measure agreement each run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift&lt;/strong&gt; — did the distribution of outputs shift sharply versus a recent baseline?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those fall below threshold, the run goes red and stops — exactly like a row-count or&lt;br&gt;
uniqueness gate. An LLM step with no eval is a green checkmark hiding unknown quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Observe the things that only AI steps have
&lt;/h2&gt;

&lt;p&gt;Log tokens, latency, and cost per run alongside a quality metric, and alert on drift.&lt;br&gt;
These are the signals that tell you a prompt change or a model update quietly degraded&lt;br&gt;
things — long before a human notices weird results in a report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An LLM step is a non-deterministic external call, not a pure transform — engineer it
like a flaky dependency.&lt;/li&gt;
&lt;li&gt;Demand structured output and validate it; cache by input; pin model and prompt
versions.&lt;/li&gt;
&lt;li&gt;Gate outputs with evals the same way you gate data quality, and observe tokens,
cost, latency, and drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Related in&lt;/em&gt; The Grain*: modeling your warehouse so these models have correct, usable&lt;br&gt;
data to work with in the first place.*&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>ai</category>
      <category>llm</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why Your Orchestrator Task "Succeeded" But Your Data Is Wrong</title>
      <dc:creator>Vaishnav Prabhu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:52:46 +0000</pubDate>
      <link>https://dev.to/vaishnavprabhu/why-your-orchestrator-task-succeeded-but-your-data-is-wrong-1gde</link>
      <guid>https://dev.to/vaishnavprabhu/why-your-orchestrator-task-succeeded-but-your-data-is-wrong-1gde</guid>
      <description>&lt;p&gt;A green checkmark means your task ran without throwing an exception. It does &lt;strong&gt;not&lt;/strong&gt;&lt;br&gt;
mean the data it produced is correct. These are two very different guarantees, and&lt;br&gt;
confusing them is one of the most common ways bad data reaches production&lt;br&gt;
dashboards undetected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between "ran" and "right"
&lt;/h2&gt;

&lt;p&gt;Most orchestrators judge success on exit code: the process finished, nothing threw,&lt;br&gt;
mark it green. But a task can exit cleanly while:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;loading &lt;strong&gt;half&lt;/strong&gt; the expected rows (an upstream feed was incomplete),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;duplicating&lt;/strong&gt; rows because a dimension join fanned out,&lt;/li&gt;
&lt;li&gt;writing values that are internally consistent but &lt;strong&gt;wrong&lt;/strong&gt; (a silent unit or
timezone bug),&lt;/li&gt;
&lt;li&gt;or succeeding on a &lt;strong&gt;retry&lt;/strong&gt; that silently reprocessed a partial payload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these trip an exception. All of them produce a green run and wrong numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make correctness a first-class, separate signal
&lt;/h2&gt;

&lt;p&gt;The fix is to stop treating "the job ran" as a proxy for "the data is good," and add&lt;br&gt;
an explicit &lt;strong&gt;quality gate&lt;/strong&gt; as its own step that can fail the pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Row-count / volume checks&lt;/strong&gt; — did we land roughly the expected number of rows
for this period, versus a recent baseline? A 30%+ swing is worth blocking on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uniqueness checks&lt;/strong&gt; — assert the natural key is unique at the table's grain.
This is your early-warning system for fan-out and duplicate loads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness checks&lt;/strong&gt; — is the newest record actually from the period you expected?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation checks&lt;/strong&gt; — does this table agree (within tolerance) with an
independent source of truth?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run these &lt;em&gt;after&lt;/em&gt; the load, as a gate the downstream steps depend on. A failing gate&lt;br&gt;
should turn the run red and stop propagation — a loud failure now is far cheaper than&lt;br&gt;
a silent one that someone finds in a dashboard next week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the "succeeded after N retries" trap
&lt;/h2&gt;

&lt;p&gt;A task that fails twice and passes on its third attempt still shows as success. If&lt;br&gt;
the underlying action isn't &lt;strong&gt;idempotent&lt;/strong&gt;, those earlier partial attempts can leave&lt;br&gt;
duplicated or inconsistent data behind. Two defenses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make the operation idempotent (truncate-and-reload a partition, or MERGE on a key)
so a retry is always safe.&lt;/li&gt;
&lt;li&gt;Alert on &lt;em&gt;"succeeded only after retries"&lt;/em&gt; as a distinct signal, so fragility
surfaces before it becomes an outage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Exit code answers "did it run," not "is it right." Instrument both.&lt;/li&gt;
&lt;li&gt;Add uniqueness, volume, freshness, and reconciliation gates as blocking steps.&lt;/li&gt;
&lt;li&gt;Treat retries and partial reloads as correctness risks, not just reliability ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Next in this series: a repeatable RCA framework for when one of these gates finally fires.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>devops</category>
      <category>dataquality</category>
      <category>airflow</category>
    </item>
    <item>
      <title>A Repeatable RCA Framework for Data Incidents</title>
      <dc:creator>Vaishnav Prabhu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:51:19 +0000</pubDate>
      <link>https://dev.to/vaishnavprabhu/a-repeatable-rca-framework-for-data-incidents-5a9p</link>
      <guid>https://dev.to/vaishnavprabhu/a-repeatable-rca-framework-for-data-incidents-5a9p</guid>
      <description>&lt;p&gt;Most data incidents get debugged by vibes: someone opens logs, scrolls, guesses, reruns&lt;br&gt;
something, and hopes. That works until it doesn't — and it never produces a learning the&lt;br&gt;
team can reuse. A repeatable root-cause framework turns firefighting into a routine. Here's&lt;br&gt;
one that holds up: &lt;strong&gt;Localize → Characterize → Root-cause → Remediate → Prevent.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Localize
&lt;/h2&gt;

&lt;p&gt;Find &lt;em&gt;where&lt;/em&gt; the wrongness enters before asking &lt;em&gt;why&lt;/em&gt;. Walk the lineage and pin the first&lt;br&gt;
step whose output is already bad. Narrow it to a specific task, table, and time window. Most&lt;br&gt;
wasted RCA time comes from theorizing about causes while still unsure which table even broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Characterize the scope
&lt;/h2&gt;

&lt;p&gt;Measure the blast radius before chasing mechanism:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fleet-wide or local?&lt;/strong&gt; One store/segment, or everything?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One metric or many?&lt;/strong&gt; A single column, or the whole load?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New or chronic?&lt;/strong&gt; Was yesterday fine? When did it start?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope is a powerful filter. A problem that hits &lt;em&gt;one&lt;/em&gt; metric across &lt;em&gt;all&lt;/em&gt; segments points&lt;br&gt;
somewhere very different than one that hits &lt;em&gt;all&lt;/em&gt; metrics for &lt;em&gt;one&lt;/em&gt; segment.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Root-cause by comparison
&lt;/h2&gt;

&lt;p&gt;The fastest root-causing technique is &lt;strong&gt;diffing a bad run against a good one&lt;/strong&gt;: same query,&lt;br&gt;
different day; this segment vs a healthy segment; expected vs actual row counts. Differences&lt;br&gt;
light up the cause. Watch for ratios that are suspiciously round — a value that's exactly 2×&lt;br&gt;
or 4× its neighbor usually means duplication (a fan-out or a double load), not a slow drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Separate the real cause from red herrings
&lt;/h2&gt;

&lt;p&gt;Incidents are littered with distractions: a scary-looking stack trace from a &lt;em&gt;post-success&lt;/em&gt;&lt;br&gt;
callback, a deprecation warning, a slow-but-harmless step. Ask of each: &lt;em&gt;did this actually&lt;br&gt;
change the data?&lt;/em&gt; If the task was already marked successful before the error fired, that&lt;br&gt;
error is noise. Name your red herrings explicitly so nobody re-chases them next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Remediate, then prevent
&lt;/h2&gt;

&lt;p&gt;Remediation is two moves: fix the root cause, then repair the already-written bad data&lt;br&gt;
(rebuild the affected partitions — a dedupe or patch often won't fully undo it). Prevention&lt;br&gt;
is the part teams skip and shouldn't: add the check that &lt;em&gt;would have caught this&lt;/em&gt; — a&lt;br&gt;
uniqueness assertion, a volume gate, a reconciliation test. Every incident should leave&lt;br&gt;
behind one new guardrail, or you'll meet it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it a template
&lt;/h2&gt;

&lt;p&gt;Keep a short, fill-in-the-blanks incident doc: symptom, localized step, scope, the&lt;br&gt;
good-vs-bad comparison that cracked it, root cause, remediation, and the new guardrail. Five&lt;br&gt;
of these and your team has a pattern library — and the next incident resolves in minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Localize the broken step before theorizing about why.&lt;/li&gt;
&lt;li&gt;Scope first: fleet-vs-local and one-metric-vs-many narrow the search fast.&lt;/li&gt;
&lt;li&gt;Diff a bad run against a good one; distrust suspiciously round ratios.&lt;/li&gt;
&lt;li&gt;Discard post-success errors and warnings that never touched the data.&lt;/li&gt;
&lt;li&gt;Every incident ships one new guardrail.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>devops</category>
      <category>sre</category>
      <category>dataquality</category>
    </item>
  </channel>
</rss>
