<script data-pm-proxy="intercept"></script><?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Gowtham’s Substack]]></title><description><![CDATA[My personal Substack]]></description><link>https://dataengineeringtamil.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!Gmi5!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbd44a9-0747-49b5-afc2-43bba0b25920_144x144.png</url><title>Gowtham’s Substack</title><link>https://dataengineeringtamil.substack.com</link></image><generator>Substack</generator><lastBuildDate>Tue, 01 Sep 2026 06:45:53 GMT</lastBuildDate><atom:link href="/__u/dataengineeringtamil.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Gowtham SB]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[dataengineeringtamil@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[dataengineeringtamil@substack.com]]></itunes:email><itunes:name><![CDATA[Gowtham SB]]></itunes:name></itunes:owner><itunes:author><![CDATA[Gowtham SB]]></itunes:author><googleplay:owner><![CDATA[dataengineeringtamil@substack.com]]></googleplay:owner><googleplay:email><![CDATA[dataengineeringtamil@substack.com]]></googleplay:email><googleplay:author><![CDATA[Gowtham SB]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[The Most Important Soft Skills for a Successful IT Career]]></title><description><![CDATA[When people hear the term soft skills, they often think about speaking fluent English, giving presentations, or answering interview questions.]]></description><link>https://dataengineeringtamil.substack.com/p/the-most-important-soft-skills-for</link><guid isPermaLink="false">https://dataengineeringtamil.substack.com/p/the-most-important-soft-skills-for</guid><dc:creator><![CDATA[Gowtham SB]]></dc:creator><pubDate>Tue, 18 Aug 2026 13:18:06 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Gmi5!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbd44a9-0747-49b5-afc2-43bba0b25920_144x144.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><p>When people hear the term <em>soft skills</em>, they often think about speaking fluent English, giving presentations, or answering interview questions.</p><p>In reality, soft skills are much broader than that.</p><p>Soft skills are your ability to understand people, adapt to situations, communicate effectively, and work professionally in different environments. They often determine how far you grow in your career, especially after the first few years.</p><p>I&#8217;ve worked with highly talented engineers who struggled because they lacked soft skills. I&#8217;ve also seen average engineers grow into leadership positions because they were excellent at working with people.</p><p>Here are the most important soft skills every IT professional should develop, ranked by their long-term impact on career growth.</p><h2>1. Emotional Intelligence (EQ)</h2><p>If there is one soft skill that influences almost every other soft skill, it is Emotional Intelligence.</p><p>EQ is the ability to understand and manage your emotions while also understanding the emotions, motivations, and perspectives of others.</p><p>People with strong EQ:</p><ul><li><p>Handle criticism professionally</p></li><li><p>Stay calm during production incidents</p></li><li><p>Manage disagreements without damaging relationships</p></li><li><p>Understand what others are trying to communicate</p></li><li><p>Build trust within teams</p></li></ul><p>Many workplace problems are not technical problems. They are people problems.</p><p>A person with strong EQ can navigate those situations effectively.</p><h2>2. Communication</h2><p>Communication is not about speaking more.</p><p>It is about ensuring that your message is understood by the right person in the right way.</p><p>Good communication includes:</p><ul><li><p>Explaining technical concepts clearly</p></li><li><p>Writing effective emails and documentation</p></li><li><p>Providing project updates</p></li><li><p>Conducting meetings efficiently</p></li><li><p>Presenting ideas confidently</p></li></ul><p>One of the biggest misconceptions about communication is that there are fixed rules.</p><p>For example, some people say you should never repeat information already present in your resume when answering &#8220;Tell me about yourself.&#8221;</p><p>In reality, effective communication depends on context. Different interviewers expect different things.</p><p>The real skill is understanding your audience and adapting accordingly.</p><h2>3. Adaptability</h2><p>The technology industry changes faster than most professions.</p><p>New tools emerge.<br>New frameworks appear.<br>Teams get reorganized.<br>Managers change.<br>Business priorities shift.</p><p>People who resist change often struggle.</p><p>People who adapt thrive.</p><p>Adaptability means:</p><ul><li><p>Learning new technologies</p></li><li><p>Adjusting to new work environments</p></li><li><p>Working with different personalities</p></li><li><p>Handling changing priorities</p></li></ul><p>The ability to adapt is often more valuable than any single technical skill.</p><h2>4. Problem Solving</h2><p>Organizations do not hire engineers simply to write code.</p><p>They hire engineers to solve problems.</p><p>Strong problem solvers:</p><ul><li><p>Investigate root causes</p></li><li><p>Think critically</p></li><li><p>Analyze options</p></li><li><p>Make informed decisions</p></li><li><p>Focus on solutions rather than complaints</p></li></ul><p>When production systems fail or projects encounter obstacles, problem-solving skills become more valuable than technical knowledge alone.</p><h2>5. Stakeholder Management</h2><p>As your career grows, your success becomes less dependent on code and more dependent on collaboration.</p><p>You will work with:</p><ul><li><p>Product Managers</p></li><li><p>Business Teams</p></li><li><p>Architects</p></li><li><p>Project Managers</p></li><li><p>Clients</p></li><li><p>Senior Leadership</p></li></ul><p>Each group has different priorities.</p><p>A good engineer understands how to communicate effectively with each stakeholder and align everyone toward a common goal.</p><h2>6. Ownership and Accountability</h2><p>One of the biggest differences between junior and senior professionals is ownership.</p><p>Junior mindset:</p><blockquote><p>That&#8217;s not my responsibility.</p></blockquote><p>Ownership mindset:</p><blockquote><p>Let me see how I can help solve this.</p></blockquote><p>Ownership does not mean doing everyone else&#8217;s work.</p><p>It means taking responsibility for outcomes, communicating proactively, and ensuring issues are addressed rather than ignored.</p><p>Managers consistently trust people who demonstrate ownership.</p><h2>7. Time Management</h2><p>Technical expertise is valuable, but it loses effectiveness if work is consistently delayed.</p><p>Good time management involves:</p><ul><li><p>Prioritizing tasks</p></li><li><p>Estimating work realistically</p></li><li><p>Managing deadlines</p></li><li><p>Avoiding unnecessary distractions</p></li><li><p>Communicating delays early</p></li></ul><p>Professionals who manage their time well become more reliable and less stressed.</p><h2>8. Conflict Resolution</h2><p>Disagreements are inevitable.</p><p>Teams disagree on:</p><ul><li><p>Architecture decisions</p></li><li><p>Priorities</p></li><li><p>Resource allocation</p></li><li><p>Project timelines</p></li></ul><p>The goal is not to avoid conflict.</p><p>The goal is to resolve conflict professionally.</p><p>Strong professionals can disagree without becoming personal, emotional, or defensive.</p><h2>9. Decision Making</h2><p>Many workplace situations do not have perfect answers.</p><p>Often, you must choose between:</p><ul><li><p>Speed vs quality</p></li><li><p>Cost vs performance</p></li><li><p>Simplicity vs flexibility</p></li></ul><p>Strong decision-makers gather information, evaluate trade-offs, and move forward confidently rather than remaining stuck in analysis.</p><h2>10. Learning Agility</h2><p>Technology evolves continuously.</p><p>The skills that are valuable today may become less relevant tomorrow.</p><p>Learning agility is the ability to:</p><ul><li><p>Learn quickly</p></li><li><p>Stay curious</p></li><li><p>Explore new technologies</p></li><li><p>Apply new knowledge effectively</p></li></ul><p>The most successful professionals are usually lifelong learners.</p><h2>Final Thoughts</h2><p>Many people think soft skills are about using polished language or following communication formulas.</p><p>They are not.</p><p>Soft skills are about understanding people, situations, and context, then responding appropriately.</p><p>Technical skills help you build solutions.</p><p>Soft skills help you work effectively with people.</p><p>In the long run, career growth depends on both.</p><p>If you focus on developing Emotional Intelligence, Communication, Adaptability, Problem Solving, and Stakeholder Management, you&#8217;ll build a foundation that will serve you throughout your entire career, regardless of the technologies you work with.</p>]]></content:encoded></item><item><title><![CDATA[How Does LinkedIn Know Your Image Is AI-Generated?]]></title><description><![CDATA[Post a ChatGPT image to LinkedIn and a little &#8220;CR&#8221; badge shows up, quietly flagging it as AI.]]></description><link>https://dataengineeringtamil.substack.com/p/how-does-linkedin-know-your-image</link><guid isPermaLink="false">https://dataengineeringtamil.substack.com/p/how-does-linkedin-know-your-image</guid><dc:creator><![CDATA[Gowtham SB]]></dc:creator><pubDate>Mon, 27 Jul 2026 14:55:21 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Gmi5!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbbd44a9-0747-49b5-afc2-43bba0b25920_144x144.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Post a ChatGPT image to LinkedIn and a little <strong>&#8220;CR&#8221;</strong> badge shows up, quietly flagging it as AI. Post one from another tool, and... nothing. Why?</p><p>Here&#8217;s the surprise: <strong>LinkedIn isn&#8217;t looking at your pixels.</strong> It never analyzes the image to guess whether it&#8217;s AI. Instead, it reads a hidden label the AI tool stamped into the file when it was created.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dataengineeringtamil.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Gowtham&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>That label is called <strong>C2PA</strong> (Content Credentials). Think of it as a signed nutrition label baked into the file &#8212; what made it, when, and whether AI was involved. OpenAI adds it to every image. Many other tools don&#8217;t. So images without it arrive at LinkedIn wearing no name tag, and there&#8217;s nothing to flag.</p><p>Which leads to the one thing worth remembering:</p><blockquote><p><strong>No badge doesn&#8217;t mean the image is real.</strong></p></blockquote><p>It&#8217;s also fragile &#8212; a screenshot or a metadata strip erases it. So treat the badge as a helpful disclosure, not a lie detector.</p><p>The badge is small. The shift toward &#8220;every file carries its origin&#8221; isn&#8217;t.</p><p></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b893fc85-a408-4439-ac0a-d7c2f248024f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">#!/usr/bin/env python3
"""
c2pa_tool.py &#8212; Detect and remove C2PA Content Credentials from images.

Install deps:
    pip install c2pa-python Pillow

Usage:
    python c2pa_tool.py detect  image.jpg
    python c2pa_tool.py strip   image.jpg  out.jpg            # surgical (JPEG: keeps EXIF)
    python c2pa_tool.py strip   image.jpg  out.jpg --resave   # brute force (drops ALL metadata)

Note: C2PA is a voluntary transparency signal. Some jurisdictions (e.g. the EU AI
Act) place disclosure obligations on AI-generated media; removing the marker to
pass synthetic content off as human-made may run against those rules or a
platform's terms. Use on your own files and for legitimate reasons.
"""

import sys
import struct
import mimetypes

try:
    from c2pa import Reader
except ImportError:
    Reader = None


# ---------- DETECTION ----------

def detect(path):
    """Return a dict of C2PA info, or None if the file carries no manifest."""
    if Reader is None:
        raise RuntimeError("c2pa-python not installed: pip install c2pa-python")
    import json
    mime = mimetypes.guess_type(path)[0] or "image/jpeg"
    with open(path, "rb") as f:
        reader = Reader.try_create(mime, f)   # returns None when no manifest
        if reader is None:
            return None
        store = json.loads(reader.json())
    active = store.get("active_manifest")
    m = store.get("manifests", {}).get(active, {})
    return {
        "claim_generator": m.get("claim_generator"),
        "title": m.get("title"),
        "is_embedded": reader.is_embedded(),
        "num_manifests": len(store.get("manifests", {})),
    }


# ---------- REMOVAL: surgical (JPEG only, preserves EXIF etc.) ----------

def strip_c2pa_jpeg(in_path, out_path):
    """Drop only APP11 segments that carry C2PA/JUMBF data. Keeps other metadata.
    Returns the number of segments removed."""
    with open(in_path, "rb") as f:
        data = f.read()
    if data[:2] != b"\xff\xd8":
        raise ValueError("Not a JPEG &#8212; use --resave for other formats")

    out = bytearray(b"\xff\xd8")
    i, n, removed = 2, len(data), 0
    while i &lt; n:
        if data[i] != 0xFF:
            out.extend(data[i:]); break
        marker = data[i + 1]
        if marker == 0xDA:                       # start of scan: copy rest verbatim
            out.extend(data[i:]); break
        if marker in (0xD8, 0xD9) or 0xD0 &lt;= marker &lt;= 0xD7:   # standalone markers
            out.extend(data[i:i + 2]); i += 2; continue
        seg_len = struct.unpack("&gt;H", data[i + 2:i + 4])[0]
        seg = data[i:i + 2 + seg_len]
        payload = data[i + 4:i + 2 + seg_len]
        if marker == 0xEB and (b"jumb" in payload[:60] or b"c2pa" in payload[:120]):
            removed += 1                          # this is the C2PA segment: skip it
        else:
            out.extend(seg)
        i += 2 + seg_len

    with open(out_path, "wb") as f:
        f.write(out)
    return removed


# ---------- REMOVAL: brute force (any format, drops ALL metadata) ----------

def strip_by_resave(in_path, out_path, quality=95):
    """Re-encode pixels into a fresh file. Removes C2PA and every other metadata."""
    from PIL import Image
    img = Image.open(in_path)
    clean = Image.new(img.mode, img.size)
    clean.putdata(list(img.getdata()))
    save_kwargs = {"quality": quality} if out_path.lower().endswith((".jpg", ".jpeg")) else {}
    clean.save(out_path, **save_kwargs)


# ---------- CLI ----------

def main():
    args = sys.argv[1:]
    if not args or args[0] not in ("detect", "strip"):
        print(__doc__); sys.exit(1)

    if args[0] == "detect":
        info = detect(args[1])
        if info is None:
            print("No C2PA / Content Credentials found.")
        else:
            print("C2PA FOUND:")
            for k, v in info.items():
                print(f"  {k}: {v}")
        return

    # strip
    in_path, out_path = args[1], args[2]
    if "--resave" in args:
        strip_by_resave(in_path, out_path)
        print(f"Re-saved (all metadata dropped) -&gt; {out_path}")
    else:
        removed = strip_c2pa_jpeg(in_path, out_path)
        print(f"Removed {removed} C2PA segment(s) -&gt; {out_path}")

    # verify
    still = detect(out_path)
    print("Verification:", "C2PA still present!" if still else "clean, no C2PA.")


if __name__ == "__main__":
    main()</code></pre></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dataengineeringtamil.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Gowtham&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[SQL MERGE Explained: One Statement That Inserts and Updates at the Same Time]]></title><description><![CDATA[If you have ever written code that looks like this, you are the reason MERGE exists:]]></description><link>https://dataengineeringtamil.substack.com/p/sql-merge-explained-one-statement</link><guid isPermaLink="false">https://dataengineeringtamil.substack.com/p/sql-merge-explained-one-statement</guid><dc:creator><![CDATA[Gowtham SB]]></dc:creator><pubDate>Fri, 24 Jul 2026 10:17:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!vAnI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!vAnI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!vAnI!, /__u/dataengineeringtamil.substack.com/w_424, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_webp, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png 424w, /__u/substackcdn.com/image/fetch/$s_!vAnI!, /__u/dataengineeringtamil.substack.com/w_848, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_webp, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png 848w, /__u/substackcdn.com/image/fetch/$s_!vAnI!, /__u/dataengineeringtamil.substack.com/w_1272, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_webp, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png 1272w, /__u/substackcdn.com/image/fetch/$s_!vAnI!, /__u/dataengineeringtamil.substack.com/w_1456, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_webp, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!vAnI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png" width="1456" height="772" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:772,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:251565,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://dataengineeringtamil.substack.com/i/208314209?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!vAnI!, /__u/dataengineeringtamil.substack.com/w_424, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_auto, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png 424w, /__u/substackcdn.com/image/fetch/$s_!vAnI!, /__u/dataengineeringtamil.substack.com/w_848, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_auto, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png 848w, /__u/substackcdn.com/image/fetch/$s_!vAnI!, /__u/dataengineeringtamil.substack.com/w_1272, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_auto, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png 1272w, /__u/substackcdn.com/image/fetch/$s_!vAnI!, /__u/dataengineeringtamil.substack.com/w_1456, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_auto, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04ab1516-a39e-47e5-83bb-1b16fae9a942_2760x1464.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>If you have ever written code that looks like this, you are the reason <code>MERGE</code> exists:</p><pre><code><code>-- Does this student already exist?
SELECT COUNT(*) FROM students WHERE id = 1;

-- If yes...
UPDATE students SET marks = 95 WHERE id = 1;

-- If no...
INSERT INTO students VALUES (1, 'Anu', 95);
</code></code></pre><p>Three round trips to the database, an <code>if</code> statement in your application code, and a race condition waiting to happen if two people run it at once. <code>MERGE</code> collapses all of that into a single statement.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dataengineeringtamil.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Gowtham&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>This post walks through it from scratch. No prior knowledge assumed beyond <code>INSERT</code>, <code>UPDATE</code>, and <code>JOIN</code>.</p><div><hr></div><h2>The problem in one sentence</h2><p>You have fresh data arriving, and some of it is new while some of it is an update to rows you already have. You want both handled in one pass.</p><p>This pattern is so common it has a nickname: <strong>upsert</strong> (update + insert). <code>MERGE</code> is the ANSI SQL way to do it.</p><div><hr></div><h2>Setting up the example</h2><p>We need two tables: the one we want to change, and the one holding the new data.</p><h3>The target table &#8212; what we already have</h3><pre><code><code>CREATE TABLE students (
    id    INT PRIMARY KEY,
    name  VARCHAR(50),
    marks INT
);

INSERT INTO students VALUES (1, 'Anu',  70);
INSERT INTO students VALUES (2, 'Bala', 80);
</code></code></pre><p>id name marks 1 Anu 70 2 Bala 80</p><h3>The source table &#8212; what just arrived</h3><pre><code><code>CREATE TABLE students_new (
    id    INT PRIMARY KEY,
    name  VARCHAR(50),
    marks INT
);

INSERT INTO students_new VALUES (1, 'Anu',    95);
INSERT INTO students_new VALUES (3, 'Chitra', 88);
</code></code></pre><p>id name marks 1 Anu 95 3 Chitra 88</p><p>Look carefully at what is going on here, because it is the whole point of the exercise:</p><ul><li><p><strong>id 1</strong> exists in both tables, but the marks are different &#8594; needs an <strong>UPDATE</strong></p></li><li><p><strong>id 3</strong> exists only in the new data &#8594; needs an <strong>INSERT</strong></p></li><li><p><strong>id 2</strong> exists only in the old data and is not mentioned in the new data &#8594; should be <strong>left alone</strong></p></li></ul><div><hr></div><h2>The MERGE statement</h2><pre><code><code>MERGE INTO students AS target
USING students_new AS source
ON target.id = source.id

WHEN MATCHED THEN
    UPDATE SET target.marks = source.marks

WHEN NOT MATCHED THEN
    INSERT (id, name, marks)
    VALUES (source.id, source.name, source.marks);
</code></code></pre><p>That is it. Run it, then check the table:</p><pre><code><code>SELECT * FROM students ORDER BY id;
</code></code></pre><p>id name marks 1 Anu <strong>95</strong> 2 Bala 80 3 <strong>Chitra</strong> <strong>88</strong></p><p>Anu&#8217;s marks were updated. Bala was untouched. Chitra was inserted. One statement, three different outcomes.</p><div><hr></div><h2>Reading the statement line by line</h2><p>Do not try to memorise the syntax. Understand what each line is answering.</p><p>Clause The question it answers <code>MERGE INTO students</code> Which table am I changing? <code>USING students_new</code> Where is the new data coming from? <code>ON target.id = source.id</code> How do I know if a row already exists? <code>WHEN MATCHED</code> Found it &#8212; now what? <code>WHEN NOT MATCHED</code> Didn&#8217;t find it &#8212; now what?</p><p>The mental model that makes it click: <code>MERGE</code><strong> is a </strong><code>JOIN</code><strong> that takes action.</strong> The <code>ON</code> clause is a join condition, exactly like in a <code>SELECT</code>. Rows that join successfully fall into <code>WHEN MATCHED</code>. Rows in the source that find no partner fall into <code>WHEN NOT MATCHED</code>.</p><p>The words <code>target</code> and <code>source</code> are just aliases. You could call them <code>t</code> and <code>s</code>, or <code>old</code> and <code>new</code>. They carry no special meaning to the database.</p><div><hr></div><h2>The source does not have to be a table</h2><p>This trips people up because tutorials always use two tables. In reality the <code>USING</code> clause accepts anything that produces rows &#8212; a subquery, a join, or a single hardcoded row:</p><pre><code><code>MERGE INTO students AS target
USING (SELECT 4 AS id, 'Dinesh' AS name, 76 AS marks) AS source
ON target.id = source.id
WHEN MATCHED THEN
    UPDATE SET target.marks = source.marks
WHEN NOT MATCHED THEN
    INSERT (id, name, marks)
    VALUES (source.id, source.name, source.marks);
</code></code></pre><p>This is how <code>MERGE</code> gets used for a single record coming from an application, rather than a bulk load.</p><div><hr></div><h2>Adding conditions to a branch</h2><p>You can attach an extra condition to <code>WHEN MATCHED</code> so the update only fires when it is actually worth doing:</p><pre><code><code>WHEN MATCHED AND target.marks &lt;&gt; source.marks THEN
    UPDATE SET target.marks = source.marks
</code></code></pre><p>Now a student whose marks have not changed is skipped entirely. On a large table this saves a lot of pointless writes, and it keeps your &#8220;rows affected&#8221; count honest.</p><div><hr></div><h2>The third branch: deleting</h2><p>There is a clause for rows that exist in the target but are missing from the source:</p><pre><code><code>WHEN NOT MATCHED BY SOURCE THEN
    DELETE
</code></code></pre><p>Add that to our example and Bala disappears, because he is not in <code>students_new</code>.</p><p><strong>Be careful with this one.</strong> It is the fastest way to accidentally empty a table. If your source data is incomplete for any reason &#8212; a partial file, a failed export, a filtered query &#8212; this clause will happily delete every row that was missing. Many teams ban it outright and use a soft-delete flag instead:</p><pre><code><code>WHEN NOT MATCHED BY SOURCE THEN
    UPDATE SET target.is_active = 0
</code></code></pre><div><hr></div><h2>Four mistakes beginners make</h2><p><strong>1. Forgetting the semicolon.</strong> In SQL Server, a <code>MERGE</code> statement must end with <code>;</code>. Leave it off and you get an error that does not obviously point at the missing semicolon. This is the single most common first-attempt failure.</p><p><strong>2. Putting filters in the </strong><code>ON</code><strong> clause.</strong> The <code>ON</code> clause is the matching rule, not a <code>WHERE</code> clause. Writing <code>ON target.id = source.id AND source.marks &gt; 50</code> does not mean &#8220;only process rows above 50&#8221; &#8212; it means rows below 50 fail to match and get sent to <code>WHEN NOT MATCHED</code>, where they are inserted as duplicates. If you want to filter the source, filter it inside the <code>USING</code> subquery instead.</p><p><strong>3. Assuming </strong><code>MERGE</code><strong> exists everywhere.</strong> It does not. More on this below.</p><p><strong>4. Running it against production without a transaction.</strong> <code>MERGE</code> touches many rows at once and there is no undo. Wrap it, check the result, then commit:</p><pre><code><code>BEGIN TRANSACTION;

MERGE INTO students AS target
USING students_new AS source
ON target.id = source.id
WHEN MATCHED THEN UPDATE SET target.marks = source.marks
WHEN NOT MATCHED THEN INSERT (id, name, marks)
     VALUES (source.id, source.name, source.marks);

SELECT * FROM students ORDER BY id;   -- inspect before committing

-- COMMIT;      -- happy?
-- ROLLBACK;    -- not happy?
</code></code></pre><div><hr></div><h2>Which databases actually support MERGE?</h2><p>Database Support SQL Server Yes, full support Oracle Yes, full support PostgreSQL Yes, from version 15 onwards MySQL / MariaDB No &#8212; use <code>INSERT ... ON DUPLICATE KEY UPDATE</code> SQLite No &#8212; use <code>INSERT ... ON CONFLICT DO UPDATE</code></p><p>If you are on MySQL, the equivalent is shorter:</p><pre><code><code>INSERT INTO students (id, name, marks)
VALUES (1, 'Anu', 95) AS new
ON DUPLICATE KEY UPDATE marks = new.marks;
</code></code></pre><p>And on PostgreSQL or SQLite, most people still reach for <code>ON CONFLICT</code> even where <code>MERGE</code> is available, because it is more concise for simple cases:</p><pre><code><code>INSERT INTO students (id, name, marks)
VALUES (1, 'Anu', 95)
ON CONFLICT (id) DO UPDATE
SET marks = EXCLUDED.marks;
</code></code></pre><p>Here <code>EXCLUDED</code> is a virtual table holding the row you tried to insert &#8212; it plays the same role that <code>source</code> plays in <code>MERGE</code>.</p><p>Note that these shorter forms need a <code>PRIMARY KEY</code> or <code>UNIQUE</code> constraint on the column to work. Without one, the database has no definition of &#8220;duplicate&#8221; and the conflict never triggers.</p><div><hr></div><h2>One caveat worth knowing about</h2><p>SQL Server&#8217;s <code>MERGE</code> has a well-documented history of bugs and concurrency problems under heavy concurrent load, and some experienced DBAs avoid it in high-traffic systems, preferring an explicit <code>UPDATE</code> followed by a conditional <code>INSERT</code> inside a transaction with appropriate locking hints.</p><p>For learning, for batch jobs, and for the overwhelming majority of ordinary applications, <code>MERGE</code> is completely fine. But if a senior engineer on your team pushes back on it during code review, this is why &#8212; and it is a reasonable position, not pedantry.</p><div><hr></div><h2>Summary</h2><p><code>MERGE</code> answers a single question: <em>for each incoming row, does it already exist?</em> Then it branches.</p><ul><li><p><code>WHEN MATCHED</code> &#8594; update</p></li><li><p><code>WHEN NOT MATCHED</code> &#8594; insert</p></li><li><p><code>WHEN NOT MATCHED BY SOURCE</code> &#8594; delete, if you dare</p></li></ul><p>Get comfortable with the two-table example above, run it, change the data, run it again, and watch what happens. Once the join-that-takes-action model clicks, every variation is just detail.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dataengineeringtamil.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Gowtham&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[The Physics of Bad Data: Time, Space, and Consistency]]></title><description><![CDATA[For engineers: Time, Space, and Consistency in Distributed Systems]]></description><link>https://dataengineeringtamil.substack.com/p/the-physics-of-bad-data-time-space</link><guid isPermaLink="false">https://dataengineeringtamil.substack.com/p/the-physics-of-bad-data-time-space</guid><dc:creator><![CDATA[Gowtham SB]]></dc:creator><pubDate>Thu, 23 Jul 2026 13:43:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!OP_9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!OP_9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!OP_9!, /__u/dataengineeringtamil.substack.com/w_424, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_webp, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png 424w, /__u/substackcdn.com/image/fetch/$s_!OP_9!, /__u/dataengineeringtamil.substack.com/w_848, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_webp, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png 848w, /__u/substackcdn.com/image/fetch/$s_!OP_9!, /__u/dataengineeringtamil.substack.com/w_1272, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_webp, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png 1272w, /__u/substackcdn.com/image/fetch/$s_!OP_9!, /__u/dataengineeringtamil.substack.com/w_1456, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_webp, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!OP_9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png" width="1456" height="772" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:772,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:251565,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://dataengineeringtamil.substack.com/i/208201340?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!OP_9!, /__u/dataengineeringtamil.substack.com/w_424, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_auto, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png 424w, /__u/substackcdn.com/image/fetch/$s_!OP_9!, /__u/dataengineeringtamil.substack.com/w_848, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_auto, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png 848w, /__u/substackcdn.com/image/fetch/$s_!OP_9!, /__u/dataengineeringtamil.substack.com/w_1272, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_auto, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png 1272w, /__u/substackcdn.com/image/fetch/$s_!OP_9!, /__u/dataengineeringtamil.substack.com/w_1456, /__u/dataengineeringtamil.substack.com/c_limit, /__u/dataengineeringtamil.substack.com/f_auto, /__u/dataengineeringtamil.substack.com/q_auto:good, /__u/dataengineeringtamil.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19b8ee83-8657-4c21-aa6a-ed1f3a6d545a_2760x1464.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Every data engineer has been handed a bug that &#8220;cannot happen.&#8221; A primary key that duplicated. A <code>COUNT(*)</code> that changed with no writes. A join on two identical strings that matched nothing.</p><p>These aren&#8217;t sloppy mistakes. They&#8217;re what happens when a single-machine mental model meets a system that is distributed across time and space and only eventually consistent. The failures are as predictable as physics &#8212; once you know which force is acting on you.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dataengineeringtamil.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Gowtham&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Twenty of them, with the fix for each.</p><div><hr></div><h2>Part I &#8212; Time</h2><h3>1. The primary key that duplicated itself</h3><p><strong>Symptom:</strong> A &#8220;guaranteed unique&#8221; key appears twice, for two genuinely different events, roughly twice a year.</p><p><strong>Cause:</strong> The key is derived from <em>local wall-clock</em> time &#8212; <code>YYYYMMDDHHMMSS + sequence</code>, or a natural key built from a local date-hour. During the DST fall-back, the 01:00&#8211;02:00 hour happens twice, so two real events stamp the same key. (Note: UTC-based schemes like Snowflake IDs are immune. This bites legacy ERP, telecom, and batch systems.)</p><p><strong>Fix:</strong> Generate and store all keys and timestamps in UTC; render local time only at the presentation layer. If a local-time key is unavoidable, include the UTC offset in the key. Add a uniqueness assertion in the pipeline instead of trusting the generator.</p><h3>2. The event that arrived before it was created</h3><p><strong>Symptom:</strong> Negative latency in your metrics. <code>received_at &lt; created_at</code>.</p><p><strong>Cause:</strong> Clock skew between services. Service A&#8217;s clock runs a few hundred milliseconds ahead of Service B&#8217;s. Worse than the ugly metric: &#8220;keep the latest record&#8221; dedupe logic silently retains the <em>older</em> version, because skew reordered them.</p><p><strong>Fix:</strong> Never order events across machines by wall-clock time. Use a broker-assigned sequence (Kafka offsets), a monotonic version column, or a logical clock. Enforce NTP and alert on drift. Clamp negative latencies and count them &#8212; a rising count is your skew monitor.</p><h3>3. The day that gained or lost revenue</h3><p><strong>Symptom:</strong> Daily totals disagree with the source system by a few hours&#8217; worth of transactions, but the monthly total matches.</p><p><strong>Cause:</strong> Timezone-naive timestamps. The warehouse buckets by UTC date, the business defines &#8220;day&#8221; in local time, and everything between midnight and the offset lands in the neighbouring day. Multi-region businesses have several answers to &#8220;which day is it.&#8221;</p><p><strong>Fix:</strong> Store <code>timestamp with time zone</code> (or UTC plus an explicit source-timezone column). Define one business calendar and one canonical reporting timezone, in a documented, queryable dimension table. Bucket by that, not by the raw timestamp.</p><h3>4. The row that arrived after the door closed</h3><p><strong>Symptom:</strong> Yesterday&#8217;s dashboard number changes weeks later &#8212; or never does, and quietly stays wrong.</p><p><strong>Cause:</strong> Late-arriving data landing in a partition that was already processed. Mobile clients buffer offline events for days. Upstream systems replay. The batch job scanned that partition on schedule and moved on.</p><p><strong>Fix:</strong> Partition by ingestion time and <em>query</em> by event time, or maintain a watermark plus a rolling reprocess window (e.g. re-run the last N days each night). Track and chart event-time-to-ingestion-time lag so you can size the window from evidence rather than hope.</p><div><hr></div><h2>Part II &#8212; Space</h2><h3>5. The <code>COUNT(*)</code> that changed with no writes</h3><p><strong>Symptom:</strong> Same query, same table, ten seconds apart, no ETL running &#8212; two different numbers.</p><p><strong>Cause:</strong> Load-balanced read connections routed to different read replicas, each at a different point in the replication stream. From the analyst&#8217;s side it&#8217;s &#8220;the same table.&#8221; Physically it&#8217;s several copies converging asymmetrically.</p><p><strong>Fix:</strong> Pin reconciliation and reporting queries to the primary or to a designated replica. Expose replication lag as a first-class metric next to the dashboard. For anything auditable, read from a versioned snapshot (a table format with snapshot isolation) rather than &#8220;the current table.&#8221;</p><h3>6. The write that vanished right after you made it</h3><p><strong>Symptom:</strong> Insert succeeds, immediate read returns nothing, second read a moment later returns the row.</p><p><strong>Cause:</strong> Read-after-write against an asynchronous replica. Common after a failover, when the new primary is momentarily behind what clients think they wrote.</p><p><strong>Fix:</strong> Route read-your-own-write paths to the primary, or use session/monotonic consistency if your engine offers it. Never build orchestration logic that writes then immediately polls a replica for confirmation.</p><h3>7. The file that isn&#8217;t in the listing</h3><p><strong>Symptom:</strong> A job writes 500 files; the downstream job processes 497. No error anywhere.</p><p><strong>Cause:</strong> Eventually consistent object storage listing &#8212; a <code>LIST</code> returns stale or partial results depending on which node served it. Also produced by data lake table formats used without strict snapshot isolation.</p><p><strong>Fix:</strong> Don&#8217;t discover work by listing a prefix. Have the writer emit an explicit manifest of what it produced, and have the reader consume the manifest. Use a table format with atomic commits (Iceberg, Delta, Hudi) so readers see a consistent snapshot or nothing.</p><h3>8. The row that exists but no query finds it</h3><p><strong>Symptom:</strong> <code>SELECT *</code> on the whole table finds it. Every dated or filtered query misses it. Row counts and row sums permanently disagree.</p><p><strong>Cause:</strong> A record whose partition key was updated and which physically moved, or which landed in a partition outside the queried range. Partition pruning then legitimately skips it.</p><p><strong>Fix:</strong> Treat partition keys as immutable &#8212; delete and re-insert rather than update. Run a periodic reconciliation that compares unpartitioned totals against the sum of partition totals; a persistent gap is the alarm.</p><h3>9. The update that lost to an older update</h3><p><strong>Symptom:</strong> A correction is applied, confirmed, and then silently reverts to the previous value.</p><p><strong>Cause:</strong> Multi-region or multi-master writes resolved by last-write-wins using wall-clock timestamps. The &#8220;later&#8221; write had an earlier clock, so the correct value lost the tiebreak. Data loss with no error and no log line.</p><p><strong>Fix:</strong> Resolve conflicts with version vectors, monotonic revision numbers, or a CRDT &#8212; not timestamps. Where the business can tolerate it, route all writes for a given entity to one region. Log every conflict resolution so losses are visible instead of silent.</p><h3>10. The dimension that changed under a running join</h3><p><strong>Symptom:</strong> A report built from two joined tables shows a total that matches neither source, and can&#8217;t be reproduced.</p><p><strong>Cause:</strong> The fact table and dimension table were read at different moments. The dimension was updated in between &#8212; a customer&#8217;s region changed mid-run &#8212; so part of the join used the old value and part the new one.</p><p><strong>Fix:</strong> Read all inputs of a job from one consistent snapshot (a single transaction, or pinned table versions). For dimensions, model them as slowly-changing with validity ranges and join on effective date rather than on current state.</p><div><hr></div><h2>Part III &#8212; Consistency and Determinism</h2><h3>11. The <code>SUM()</code> that returns a different total each run</h3><p><strong>Symptom:</strong> Finance reports a reconciliation break of a few paise. Rerun the query and it&#8217;s fine. Rerun again and it&#8217;s back.</p><p><strong>Cause:</strong> Floating-point addition isn&#8217;t associative. A parallel engine partitions the data differently on each run, adds the values in a different order, and the trailing decimal places drift.</p><p><strong>Fix:</strong> Store money as <code>DECIMAL</code>/<code>NUMERIC</code>, never <code>FLOAT</code> or <code>DOUBLE</code>. Where floats are unavoidable, round at a defined precision before comparison and give reconciliation checks an explicit tolerance.</p><h3>12. The pagination that skips and repeats rows</h3><p><strong>Symptom:</strong> An export of 100k rows in pages of 1,000 ends up with duplicates and gaps. Total row count is right, contents aren&#8217;t.</p><p><strong>Cause:</strong> <code>ORDER BY</code> on a non-unique column plus <code>LIMIT/OFFSET</code>. Rows with equal sort values have no defined order, so each page request may order them differently.</p><p><strong>Fix:</strong> Always add a unique tiebreaker to the sort key. Better, paginate by keyset (<code>WHERE (sort_col, id) &gt; (last_sort, last_id)</code>) rather than by offset, which is also faster on large tables.</p><h3>13. The <code>GROUP BY</code> that picks a value at random</h3><p><strong>Symptom:</strong> A column returns a plausible but different value on every run.</p><p><strong>Cause:</strong> Selecting a non-aggregated column that isn&#8217;t in the <code>GROUP BY</code>. Some engines reject this; some pick an arbitrary row from the group and don&#8217;t tell you.</p><p><strong>Fix:</strong> Enable strict mode (<code>ONLY_FULL_GROUP_BY</code> and equivalents) so this becomes an error. Make the intent explicit with <code>MIN</code>, <code>MAX</code>, or a windowed <code>ROW_NUMBER()</code> over a defined ordering.</p><h3>14. The backfill that didn&#8217;t match the original run</h3><p><strong>Symptom:</strong> Rerunning a historical job produces different output than the first execution, for the same input window.</p><p><strong>Cause:</strong> Non-deterministic functions baked into the logic &#8212; <code>CURRENT_TIMESTAMP</code>, <code>NOW()</code>, <code>RAND()</code>, <code>CURRENT_USER</code> &#8212; or a view that reads &#8220;today&#8217;s&#8221; state rather than the state as of the processing date.</p><p><strong>Fix:</strong> Pass the logical execution date in as a parameter and use it everywhere instead of <code>NOW()</code>. Ban non-deterministic functions from transformation logic and enforce it in code review or a linter. A job should be a pure function of (input snapshot, execution date).</p><h3>15. The retry that double-counted revenue</h3><p><strong>Symptom:</strong> Totals are exactly right most days and inflated on days when a task failed and was retried.</p><p><strong>Cause:</strong> A non-idempotent pipeline step. The task appended rows, failed partway through a later step, and the retry appended them again. The scheduler considers this a success.</p><p><strong>Fix:</strong> Make every write idempotent &#8212; <code>MERGE</code>/upsert on a business key, or delete-the-partition-then-insert. Use atomic commits so a partial write is never visible. Carry an idempotency key through any external API call the pipeline makes.</p><div><hr></div><h2>Part IV &#8212; Identity</h2><h3>16. The ID that was issued twice</h3><p><strong>Symptom:</strong> Two entirely different orders share an order ID, months apart.</p><p><strong>Cause:</strong> Either an auto-increment counter reset after a restart (older MySQL kept it in memory and recomputed it as <code>MAX(id)+1</code>, reissuing IDs that downstream systems had already consumed), or a promoted replica that hadn&#8217;t replicated the latest sequence state and resumed handing out IDs the old primary already used. The database&#8217;s uniqueness guarantee is real &#8212; it&#8217;s just per node, per lifetime.</p><p><strong>Fix:</strong> Use UUIDv4/v7 or a coordinated ID service for anything that leaves the database. Never treat a source auto-increment ID as globally unique in the warehouse: key on <code>(source_system, source_id, load_date)</code> and add a hard duplicate check at ingestion.</p><h3>17. The unique constraint that allowed duplicates</h3><p><strong>Symptom:</strong> Twelve &#8220;unique&#8221; rows for the same customer, with the constraint still in place and valid.</p><p><strong>Cause:</strong> <code>UNIQUE(customer_id, region_code)</code> doesn&#8217;t prevent duplicates when <code>region_code</code> is NULL, because NULL is not equal to NULL in most engines.</p><p><strong>Fix:</strong> Make the columns of a unique key <code>NOT NULL</code> and use an explicit sentinel for &#8220;unknown.&#8221; Alternatively use a unique index over <code>COALESCE(col, sentinel)</code> if the engine supports expression indexes.</p><h3>18. The hash key with real collisions</h3><p><strong>Symptom:</strong> Two unrelated records collapse into one, or a &#8220;collision-proof&#8221; surrogate key produces duplicates far too often for a cryptographic accident.</p><p><strong>Cause:</strong> A surrogate key built as <code>MD5(concat(col_a, col_b))</code> with no delimiter, so <code>('AB','C')</code> and <code>('A','BC')</code> hash identically. Not a hash collision &#8212; a concatenation bug wearing one&#8217;s costume.</p><p><strong>Fix:</strong> Concatenate with a delimiter that cannot appear in the data, and represent NULLs explicitly rather than as empty strings. Normalise casing and trimming <em>before</em> hashing, not after, so the rule is applied consistently everywhere.</p><h3>19. The upsert that merged two people</h3><p><strong>Symptom:</strong> No duplicate &#8212; the opposite. Two customers become one row with blended history, and nobody gets an error.</p><p><strong>Cause:</strong> A merge key of <code>LOWER(TRIM(email))</code>. Two people at a company share <code>info@acme.com</code>. Or a case-folding rule maps distinct characters onto one (the Turkish dotless-i is the classic) and the collation treats them as equal.</p><p><strong>Fix:</strong> Merge on a genuinely identifying key, not on a contact attribute. Where fuzzy matching is required, write matches to a review queue rather than applying them silently, and keep the pre-merge rows so a merge can be undone.</p><div><hr></div><h2>Part V &#8212; Encoding and Types</h2><h3>20. The join on identical strings that matched nothing &#8212; and the column that became null</h3><p><strong>Symptom:</strong> Two values look identical on screen and don&#8217;t join. Separately, Namibia disappears from the country list.</p><p><strong>Cause:</strong> Two failures of the same kind &#8212; the data changed in transit.</p><ul><li><p><em>Unicode:</em> &#8220;Jos&#233;&#8221; stored as a single precomposed <code>&#233;</code> in one system (NFC) and as <code>e</code> plus a combining accent in another (NFD). Byte-different, visually identical. Same story with non-breaking spaces from a copy-paste, trailing whitespace, and zero-width characters from web forms.</p></li><li><p><em>Type inference:</em> the country code <code>NA</code> parsed as NaN by a default CSV reader; leading zeros stripped from postcodes and account numbers; IDs like <code>1.23E5</code> coerced to floats; gene names converted to dates by a spreadsheet.</p></li></ul><p><strong>Fix:</strong> Normalise all text to NFC at ingestion and strip invisible characters explicitly. Never rely on inferred schemas at a system boundary &#8212; declare every column&#8217;s type, read identifiers as strings, and set the null sentinel explicitly (<code>keep_default_na=False</code> and equivalents). Add a contract test that fails when an inferred type changes.</p><div><hr></div><h2>Closing</h2><p>The pattern under all twenty: each one is impossible in a mental model with one clock, one machine, and one copy of the data. Production has none of those things. Once you stop being surprised by that, these stop being ghost stories and start being a checklist.</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://dataengineeringtamil.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Gowtham&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>