<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Samba Siva Reddy - Database Engineer | PostgreSQL Internals | Distributed Systems</title>
        <link>https://sambasivareddy.in</link>
        <description>Database engineer (R&amp;D) working on core PostgreSQL internals and distributed systems. Experienced in Query optimization, PostgreSQL, and full-stack web development.</description>
        <lastBuildDate>Sat, 05 Sep 2026 17:33:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved 2026, Samba Siva Reddy</copyright>
        <item>
            <title><![CDATA[The Anatomy of Query Structure ]]></title>
            <link>https://sambasivareddy.in/blog/the-anatomy-of-query-structue</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/the-anatomy-of-query-structue</guid>
            <pubDate>Thu, 27 Aug 2026 08:53:53 GMT</pubDate>
            <description><![CDATA[How PostgreSQL represents SQL internally: a field-by-field tour of the Query struct, from range tables and target lists to joins, grouping, and permissions.]]></description>
            <content:encoded><![CDATA[<p>Last time in <strong>“Reading PostgreSQL”,</strong> we looked at how <code>VACUUM</code> acts as a clean-up crew for the debt created by <code>INSERT</code>, <code>UPDATE</code>, and <code>DELETE</code> operations.</p><p>Today, we turn our focus to<strong> query itself</strong> that is dissecting its internal anatomy to understand how PostgreSQL transforms a <strong>raw SQL string into a structured in-memory representation: the </strong><code>Query</code><strong> tree</strong>.</p><hr /><h2>The Query Pipeline</h2><p>Before diving into the <code>Query</code> struct and how PostgreSQL represents SQL internally, let’s recap the pipeline that a query traverses after reaching the backend.</p><p>As discussed in <a target="_blank" rel="noopener noreferrer" class="text-primary underline underline-offset-4 hover:text-primary/80" href="https://sambasivareddy.in/blog/postgresql-internals-module-5-query-pipeline-parser-planner-executor">PostgreSQL Internals: Query Pipeline</a>, query processing can be viewed as a <strong>five-stage assembly line</strong>, where the output of one stage becomes the input to the next:</p><ol><li><p><strong>Parser (</strong><code>gram.y</code><strong>)</strong><br />Performs <strong>syntactic analysis</strong> using the Flex lexer and Bison-generated parser. It verifies that the SQL conforms to PostgreSQL's grammar and produces a parse tree containing nodes such as <code>RawStmt</code>, <code>SelectStmt</code>, <code>RangeVar</code>, and <code>ResTarget</code>.</p><p>At this stage, PostgreSQL understands the <strong><em>shape</em></strong> of the statement, but it does not <strong>yet resolve whether a referenced table or column actually exists</strong>.</p></li><li><p><strong>Analyzer / Transformer</strong><br />Performs <strong>semantic analysis</strong>. It resolves names against PostgreSQL's system catalogs (<code>pg_class</code>, <code>pg_attribute</code>, <code>pg_type</code>, etc.), determines data types, establishes namespaces and scopes, resolves operators and functions, and converts the raw parse tree into an annotated <code>Query</code><strong> tree</strong>.</p></li><li><p><strong>Rewriter</strong><br />Applies PostgreSQL rewrite rules, including rules associated with views and other rewrite mechanisms. It can also add security-related qualifications where applicable.</p><p>Importantly, the output of this stage is not necessarily one query. A single <code>Query</code> can be transformed into a <strong>list of </strong><code>Query</code><strong> trees</strong>.</p></li><li><p><strong>Planner / Optimizer</strong><br />Takes the rewritten <code>Query</code> tree and determines <strong><em>how</em></strong> the query should execute. It considers possible access paths, join orders, join strategies, indexes, sorting, aggregation, and other execution alternatives using statistics and cost estimates.</p><p>The result is a <code>PlannedStmt</code><strong> containing a physical </strong><code>Plan</code><strong> tree</strong>.</p></li><li><p><strong>Executor</strong><br />Executes the physical plan tree produced by the planner. It initializes the required executor nodes and recursively drives them to produce result tuples.</p></li></ol><p>A simplified view is therefore:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">SQL Statement → Parser (returns Parse Tree) → Analyzer (returns Query Tree) → Rewriter applies Rules, Views (returns a list of Query Tree) → Planner (return Plan Tree) → Executor → Result Rows</code></pre><p>The important point for this article is the transition:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">Raw SQL
   ↓
RawStmt / raw parse tree
   ↓
Query
</code></pre><p>The <code>Query</code> struct is where PostgreSQL has moved beyond simply understanding SQL syntax and has started building a <strong>semantic representation of what the query means</strong>.</p><hr /><h2>The Query Struct</h2><p>The <code>Query</code> node is defined in:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">src/include/nodes/parsenodes.h</code></pre><p>A simplified view of the structure looks like this:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct Query
{
    NodeTag         type;
    CmdType         commandType;
    QuerySource     querySource;
    uint64          queryId;
    bool            canSetTag;

    Node           *utilityStmt;

    int             resultRelation;

    bool            hasAggs;
    bool            hasWindowFuncs;
    bool            hasTargetSRFs;
    bool            hasSubLinks;
    bool            hasDistinctOn;
    bool            hasRecursive;
    bool            hasModifyingCTE;
    bool            hasForUpdate;
    bool            hasRowSecurity;
    bool            hasGroupRTE;
    bool            isReturn;

    List           *cteList;
    List           *rtable;
    List           *rteperminfos;

    FromExpr       *jointree;

    List           *mergeActionList;
    int             mergeTargetRelation;
    Node            *mergeJoinCondition;

    List           *targetList;

    OverridingKind  override;
    OnConflictExpr *onConflict;

    char           *returningOldAlias;
    char           *returningNewAlias;
    List           *returningList;

    List           *groupClause;
    bool            groupDistinct;
    List           *groupingSets;

    Node           *havingQual;

    List           *windowClause;
    List           *distinctClause;
    List           *sortClause;

    Node           *limitOffset;
    Node           *limitCount;
    LimitOption    limitOption;

    List           *rowMarks;

    Node           *setOperations;

    List           *constraintDeps;
    List           *withCheckOptions;

    ParseLoc        stmt_location;
    ParseLoc        stmt_len;
} Query;</code></pre><hr /><h2>Anatomy of the Query Struct</h2><p>Once a statement passes through parsing and analysis, PostgreSQL has a much richer representation of it.</p><p>The parser primarily answers:</p><blockquote><p><strong>“Is this valid PostgreSQL syntax?”</strong></p></blockquote><p>The analyzer answers a much more interesting question:</p><blockquote><p><strong>“What does this statement actually refer to?”</strong></p></blockquote><p>For example, consider:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT c.id
FROM companies c
WHERE c.revenue &gt; 1000000;</code></pre><p>After analysis, PostgreSQL needs to know:</p><ul><li><p>Which relation does <code>companies</code> refer to?</p></li><li><p>What OID identifies that relation?</p></li><li><p>Which attribute does <code>c.id</code> refer to?</p></li><li><p>What data type does <code>c.id</code> have?</p></li><li><p>What operator implements <code>&gt;</code>?</p></li><li><p>What type does <code>1000000</code> have in this expression?</p></li><li><p>Which table does <code>c</code> refer to?</p></li><li><p>Which expressions belong to the target list?</p></li><li><p>Which expression represents the <code>WHERE</code> qualification?</p></li></ul><p>The <code>Query</code> tree contains the information necessary for later stages to reason about those semantics.</p><hr /><h3>1. Command Metadata</h3><p>The first group of fields describes <strong>what kind of statement this is</strong> and which SQL features are present.</p><h3><code>commandType</code></h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">CmdType commandType;</code></pre><p><code>commandType</code> identifies the broad command represented by the <code>Query</code>:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">CMD_SELECT
CMD_UPDATE
CMD_INSERT
CMD_DELETE
CMD_MERGE
CMD_UTILITY</code></pre><p>For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT * FROM companies;</code></pre><p>will produce a <code>Query</code> whose <code>commandType</code> is:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">CMD_SELECT</code></pre><p>Whereas:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">UPDATE companies
SET revenue = revenue * 1.1;</code></pre><p>will have:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">CMD_UPDATE</code></pre><p>This gives later stages an immediate indication of the statement's nature.</p><hr /><h3><code>querySource</code></h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">QuerySource querySource;</code></pre><p>This describes where the query came from.</p><p>A query may originate directly from the user, or it may have been generated as part of PostgreSQL's rewrite processing.</p><p>This distinction becomes particularly useful when PostgreSQL is dealing with rewritten queries.</p><hr /><h3><code>queryId</code></h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">uint64 queryId;</code></pre><p><code>queryId</code> represents a query identifier associated with the analyzed query.</p><p>It is important not to confuse this with the <strong>query ID displayed or used by </strong><code>pg_stat_statements</code> as though <code>Query.queryId</code> itself were the statistics extension.</p><p>The query identifier is assigned as part of PostgreSQL's query-identification infrastructure and can be used by extensions such as <code>pg_stat_statements</code> to associate execution statistics with a normalized query identity.</p><p>The important distinction is:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">Query.queryId
      │
      └── Query identification
              │
              └── Used by infrastructure/extensions
                  such as pg_stat_statements
</code></pre><p>It is <strong>not</strong> a transaction ID, object OID, or plan identifier.</p><hr /><h3>2. Feature Flags</h3><p>The <code>Query</code> node also contains several boolean fields:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">bool hasAggs;
bool hasWindowFuncs;
bool hasTargetSRFs;
bool hasSubLinks;
bool hasDistinctOn;
bool hasRecursive;
bool hasModifyingCTE;
bool hasForUpdate;
bool hasRowSecurity;
bool hasGroupRTE;</code></pre><p>These flags summarise important properties discovered during analysis.</p><p>For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT department_id, count(*)
FROM employees
GROUP BY department_id;</code></pre><p>contains an aggregate function.</p><p>Therefore:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">hasAggs = true</code></pre><p>Similarly:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT name,
       row_number() OVER (ORDER BY salary)
FROM employees;</code></pre><p>contains a window function, so:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">hasWindowFuncs = true</code></pre><p>These flags allow later processing stages to quickly determine whether particular query-processing features are involved without having to rediscover them from scratch.</p><hr /><h3>3. Relations, Scope and the Range Table</h3><p>One of the most important fields in <code>Query</code> is:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">List *rtable;</code></pre><p>This is the <strong>range table</strong>.</p><p>The range table contains <code>RangeTblEntry</code> structures representing the relations and other range-table items referenced by the query.</p><p>A simplified <code>RangeTblEntry</code> looks like:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct RangeTblEntry
{
    NodeTag type;
    RTEKind rtekind;

    Oid relid;

    Alias *alias;
    Alias *eref;

    Query *subquery;

    JoinType jointype;

    /* ... additional fields ... */
} RangeTblEntry;</code></pre><p>The actual PostgreSQL structure contains considerably more information.</p><p>For a normal table reference:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT *
FROM companies;</code></pre><p>the corresponding <code>RangeTblEntry</code> contains information identifying the relation.</p><p>For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">companies
    │
    ▼
RangeTblEntry
    │
    ├── rtekind = RTE_RELATION
    ├── relid   = &lt;companies OID&gt;
    └── ...</code></pre><p>The important transformation is:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">"companies"
     ↓
catalog lookup
     ↓
relation OID
     ↓
RangeTblEntry</code></pre><p>This is one of the key differences between the raw parse tree and the analyzed <code>Query</code> tree.</p><hr /><h3>4. Range Table Indexes</h3><p>The range table is also important because other nodes refer to its entries by <strong>range-table index (</strong><code>rtindex</code><strong>)</strong>.</p><p>For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct RangeTblRef
{
    NodeTag type;
    int rtindex;
} RangeTblRef;</code></pre><p>If:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">rtable[1] → companies
rtable[2] → employees</code></pre><p>then a <code>RangeTblRef</code> with:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">rtindex = 1</code></pre><p>refers to the first range-table entry.</p><p>This gives PostgreSQL an internal relationship like:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">Query
 │
 ├── rtable
 │    │
 │    ├── [1] → companies
 │    └── [2] → employees
 │
 └── jointree
      │
      ├── RangeTblRef(1)
      └── RangeTblRef(2)</code></pre><p>The tree nodes don't need to repeatedly store the complete relation information. They can refer back to the appropriate <code>RangeTblEntry</code>.</p><hr /><h3>5. TargetList: What Should the Query Produce?</h3><p>Another critical field is:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">List *targetList;</code></pre><p>The target list represents the expressions that the query is interested in producing.</p><p>For:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT c.id, c.name
FROM companies c;</code></pre><p>the target list contains two <code>TargetEntry</code> nodes.</p><p>Conceptually:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">targetList
   │
   ├── TargetEntry
   │      └── Var → c.id
   │
   └── TargetEntry
          └── Var → c.name
</code></pre><p>A simplified <code>TargetEntry</code> looks like:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct TargetEntry
{
    Expr       *expr;
    AttrNumber  resno;
    char       *resname;
    Index       ressortgroupref;
    bool        resjunk;
} TargetEntry;</code></pre><hr /><h3>6. The Var Node</h3><p>The <code>expr</code> field can contain an expression node such as <code>Var</code>.</p><p>A simplified <code>Var</code> looks like:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct Var
{
    Expr        xpr;

    Index       varno;
    AttrNumber  varattno;
    Oid         vartype;

    /* ... additional properties ... */
} Var;
</code></pre><p>The important fields are:</p><h3><code>varno</code></h3><p>Identifies the range-table entry from which the value comes.</p><p>Conceptually:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">varno = 1
     ↓
rtable[1]
     ↓
companies</code></pre><h3><code>varattno</code></h3><p>Identifies the attribute within that relation.</p><p>For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">varattno = 1</code></pre><p>might correspond to the first user attribute of the relation.</p><h3><code>vartype</code></h3><p>Contains the PostgreSQL type OID.</p><p>For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">23 → int4
25 → text</code></pre><p>Therefore, an expression such as:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">c.id</code></pre><p>can eventually be represented conceptually as:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">Var
 ├── varno    → companies
 ├── varattno → id
 └── vartype  → int4</code></pre><p>This is a major semantic transformation.</p><p>The parser sees:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">c.id</code></pre><p>The analyzer turns that name into a structured reference to:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">Range table entry
        +
attribute number
        +
data type</code></pre><hr /><h3>7. ResultRelation: Which Relation Is Being Modified?</h3><p>For DML statements, PostgreSQL needs to identify the relation being modified.</p><p>This is represented by:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">int resultRelation;</code></pre><p>For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">UPDATE companies
SET revenue = revenue * 1.1;</code></pre><p>the <code>resultRelation</code> identifies the appropriate entry in the query's range table.</p><p>Conceptually:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">rtable
 ├── [1] → companies
 │
 └── ...

resultRelation = 1</code></pre><p>So:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">resultRelation
      │
     ▼
rtable entry
      │
     ▼
companies</code></pre><p>This allows later stages to determine which relation is the target of the modification.</p><hr /><h3>8. CteList: Common Table Expressions</h3><p>The <code>Query</code> also contains:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">List *cteList;</code></pre><p>This stores the query's Common Table Expressions (<code>WITH</code> clauses).</p><p>For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">WITH high_value AS
(
    SELECT *
    FROM companies
    WHERE revenue &gt; 1000000
)
SELECT *
FROM high_value;</code></pre><p>The CTE is represented internally as a <code>CommonTableExpr</code> node in <code>cteList</code>.</p><p>Conceptually:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">Query
 │
 ├── cteList
 │      │
 │      └── CommonTableExpr
 │              │
 │              └── Query
 │
 └── ...
</code></pre><p>A CTE therefore introduces another query representation nested inside the surrounding query structure.</p><hr /><h3>9. JoinTree: FROM + WHERE Structure</h3><p>One of the most interesting fields in <code>Query</code> is:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">FromExpr *jointree;</code></pre><p>The jointree represents the query's <strong>FROM/WHERE structure</strong>.</p><p>A simplified <code>FromExpr</code> is:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct FromExpr
{
    NodeTag type;

    List *fromlist;

    Node *quals;
} FromExpr;
</code></pre><p>It contains two important pieces:</p><h3><code>fromlist</code></h3><p>The relations and joins appearing in the <code>FROM</code> clause.</p><h3><code>quals</code></h3><p>The qualification expression, which generally corresponds to the <code>WHERE</code> condition.</p><p>For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT *
FROM companies
WHERE revenue &gt; 1000000;</code></pre><p>can be visualized as:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">FromExpr
 │
 ├── fromlist
 │      │
 │      └── RangeTblRef
 │              │
 │              └── companies
 │
 └── quals
        │
        └── 
            ├── Var → companies.revenue
            └── Const → 1000000
</code></pre><p>Notice that the <code>WHERE</code> expression is not represented as a separate "WHERE node."</p><p>It is stored as the <code>quals</code> expression tree.</p><hr /><h3>10. Representing JOINs</h3><p>For a query such as:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT *
FROM companies c
JOIN employees e
    ON c.id = e.company_id;</code></pre><p>the <code>FROM</code> structure contains a <code>JoinExpr</code>.</p><p>A simplified representation is:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct JoinExpr
{
    JoinType jointype;

    Node *larg;
    Node *rarg;

    List *usingClause;

    Node *quals;

    Alias *join_using_alias;
    int rtindex;
} JoinExpr;
</code></pre><p>Conceptually:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">JoinExpr
 │
 ├── jointype = JOIN_INNER
 │
 ├── larg
 │     └── RangeTblRef → companies
 │
 ├── rarg
 │     └── RangeTblRef → employees
 │
 └── quals
       └── =
           ├── Var → companies.id
           └── Var → employees.company_id
</code></pre><p>So the query's relational structure can be represented as a tree:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">                 JoinExpr
                /        \
               /          \
       companies        employees
              \            /
               \          /
                join condition
</code></pre><p>This representation gives later stages a structured description of the query's relational topology.</p><hr /><h3>11. GroupClause and Aggregation</h3><p>Consider:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT company_id, count(*)
FROM employees
GROUP BY company_id;</code></pre><p>The <code>Query</code> contains:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">List *groupClause;</code></pre><p>which represents the grouping specification.</p><p>The aggregate itself contributes to the expression tree in the target list.</p><p>Conceptually:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">Query
 │
 ├── targetList
 │      ├── Var → company_id
 │      └── Aggref
 │             └── count(*)
 │
 └── groupClause
        └── company_id
</code></pre><p>This separation is useful because PostgreSQL needs to reason independently about:</p><ul><li><p>what values are produced,</p></li><li><p>what expressions are aggregated,</p></li><li><p>and what expressions define the grouping.</p></li></ul><hr /><h3>12. HavingQual</h3><p><code>HAVING</code> is different from <code>WHERE</code>.</p><p>Consider:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT company_id, count(*)
FROM employees
GROUP BY company_id
HAVING count(*) &gt; 10;</code></pre><p>The <code>HAVING</code> condition is represented by:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">Node *havingQual;</code></pre><p>Conceptually:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">GROUP BY
    ↓
Aggregation
    ↓
HAVING
    ↓
Result
</code></pre><p>Whereas a <code>WHERE</code> condition generally belongs to:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">jointree-&gt;quals</code></pre><p>This distinction is important because the two predicates operate at different logical stages.</p><hr /><h3>13. Window Functions</h3><p>Window specifications are represented using:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">List *windowClause;</code></pre><p>For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT
    name,
    salary,
    row_number() OVER (ORDER BY salary DESC)
FROM employees;</code></pre><p>The query contains information describing the window specification:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">windowClause
    │
    └── Window specification
            │
            └── ORDER BY salary DESC</code></pre><p>The window function itself appears in the relevant expression tree.</p><hr /><h3>14. SortClause</h3><p><code>ORDER BY</code> information is represented through:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">List *sortClause;</code></pre><p>For:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT *
FROM employees
ORDER BY salary DESC;</code></pre><p>the query representation records the expression being sorted and the associated sort semantics.</p><p>Conceptually:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">sortClause
    │
    └── salary
          │
          └── DESC
</code></pre><p>The important distinction is that the <code>Query</code> describes <strong>what ordering is required</strong>.</p><p>It is the planner that later decides <strong>how to achieve that ordering</strong>.</p><p>For example, the planner might determine that an index can provide the required ordering, or that an explicit sort operation is necessary.</p><hr /><h3>15. LimitOffset and LimitCount</h3><p><code>LIMIT</code> and <code>OFFSET</code> are represented using:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">Node *limitOffset;
Node *limitCount;
LimitOption limitOption;</code></pre><p>For:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT *
FROM employees
LIMIT 10
OFFSET 20;</code></pre><p>the query representation contains expressions corresponding to:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">limitOffset → 20
limitCount  → 10
</code></pre><p>Again, the <code>Query</code> describes the <strong>semantic requirement</strong>.</p><p>The planner and executor determine how that requirement is physically implemented.</p><hr /><h3>16. SetOperation</h3><p>PostgreSQL also needs to represent set operations such as:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT id FROM companies
UNION
SELECT id FROM employees;</code></pre><p>The relevant information is stored through:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">Node *setOperations;</code></pre><p>This can represent operations such as:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">UNION
INTERSECT
EXCEPT</code></pre><p>and their associated query structures.</p><p>Conceptually:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">SetOperation
      │
      ├── left query
      │
      └── right query</code></pre><hr /><h3>17. RowMarks</h3><p>Queries involving row-level locking, such as:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT *
FROM employees
FOR UPDATE;</code></pre><p>carry locking information through:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">List *rowMarks;</code></pre><p>This allows PostgreSQL to preserve the semantic requirement that selected rows must be locked.</p><p>The planner later translates this requirement into an appropriate execution strategy.</p><hr /><h3>18. rteperminfos: Separating Permissions from Range Table Entries</h3><p>Alongside <code>rtable</code>, the Query struct also carries:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>List *rteperminfos;</code></pre><p>This list holds <code>RTEPermissionInfo</code> structures, one per relation that actually needs an access-control check – which is a smaller set than <code>rtable</code> itself, since not every range table entry represents a checkable relation (a subquery RTE or join RTE doesn't need its own permission check, for instance).</p><p>A simplified <code>RTEPermissionInfo</code> looks like:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>typedef struct RTEPermissionInfo
{
    NodeTag     type;

    Oid         relid;
    bool        inh;

    AclMode     requiredPerms;
    Oid         checkAsUser;

    Bitmapset  *selectedCols;
    Bitmapset  *insertedCols;
    Bitmapset  *updatedCols;
} RTEPermissionInfo;</code></pre><p>Each <code>RangeTblEntry</code> that needs checking stores an index (<code>perminfoindex</code>) back into <code>rteperminfos</code>, rather than embedding the permission fields directly on itself:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>rtable
 ├── [1] companies  ──── perminfoindex ────┐
 └── [2] employees  ──── perminfoindex ──┐ │
                                          ▼ ▼
                                   rteperminfos
                                    ├── [1] companies: requiredPerms, selectedCols...
                                    └── [2] employees: requiredPerms, selectedCols...</code></pre><p><strong>Why split it out at all?</strong> Because permission checking and range-table membership are logically different concerns. A view expands into multiple RTEs during rewriting, but the permission check should still apply to the view as the user wrote it, not to each underlying table separately. Keeping <code>RTEPermissionInfo</code> as its own list lets PostgreSQL track <strong><em>who needs to be allowed to do what</em> independently of <em>which relations the plan will actually touch</em></strong> – the two lists can diverge as the query tree is rewritten and expanded, without one bloating or corrupting the other.</p><hr /><h3>19. Tracking the Original SQL Location</h3><p>Finally, the <code>Query</code> structure contains:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">ParseLoc stmt_location;
ParseLoc stmt_len;</code></pre><p>These fields record the location and length of the statement within the original query text.</p><p>This is useful because PostgreSQL does not need to duplicate the complete SQL text inside every parse-tree node merely to remember where something originated.</p><p>Instead, nodes can retain source-location information that points back into the original query string.</p><p>Conceptually:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">Original query buffer

SELECT c.id, count(*)
FROM companies c
...

^
|
stmt_location

&lt;-------- stmt_len --------&gt;</code></pre><p>This source-location information is also useful for producing accurate error messages and other diagnostics.</p><hr /><h2>Putting Everything Together</h2><p>Let's take a concrete example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT c.id, count(*)
FROM companies c
JOIN employees e
    ON c.id = e.company_id
GROUP BY c.id;</code></pre><p>At the SQL level, we can think about the query as:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">SELECT
    c.id,
    count(*)

FROM
    companies c
    JOIN employees e
        ON c.id = e.company_id

GROUP BY
    c.id
</code></pre><p>After analysis, PostgreSQL has enough information to construct a semantic representation roughly like:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">Query
│
├── commandType
│     └── CMD_SELECT
│
├── rtable
│     ├── [1] → companies
│     └── [2] → employees
│
├── targetList
│     ├── TargetEntry
│     │      └── Var
│     │           ├── varno → 1
│     │           ├── varattno → id
│     │           └── vartype → int4
│     │
│     └── TargetEntry
│            └── Aggref
│                 └── count(*)
│
├── jointree
│     └── FromExpr
│           │
│           └── JoinExpr
│               ├── larg → RangeTblRef(1)
│               ├── rarg → RangeTblRef(2)
│               └── quals
│                     └── c.id = e.company_id
│
└── groupClause
      └── c.id</code></pre><p>This is the important conceptual leap:</p><h3>SQL text</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">c.id</code></pre><p>becomes something closer to:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">Var
 ├── varno
 ├── varattno
 └── vartype</code></pre><p>And:</p><h3>SQL relation</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">companies</code></pre><p>becomes:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">RangeTblEntry
 └── relid → relation OID</code></pre><p>And:</p><h3>SQL JOIN</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">companies c
JOIN employees e
ON c.id = e.company_id</code></pre><p>becomes a structured tree:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-text">JoinExpr
├── left relation
├── right relation
└── join qualification</code></pre><p>PostgreSQL has therefore transformed human-readable SQL names into a graph/tree of strongly typed internal nodes.</p><hr /><h2>Conclusion</h2><p>A <code>Query</code> is PostgreSQL's answer to <em>what does this SQL actually mean</em> – names resolved to <code>Var</code>s and <code>RangeTblEntry</code>s, structure captured in <code>jointree</code>, intent captured in fields like <code>groupClause</code>, <code>sortClause</code>, and <code>rteperminfos</code>. It describes the query fully, but not how to run it – that's the planner's job, and where we'll head next.</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>PostgreSQL</category>
            <category>Technical</category>
            <category>PGSourceCode</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/postgresql_query_struct_poster.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Au Revoir Zoho ❤️]]></title>
            <link>https://sambasivareddy.in/blog/au-revoir-zoho</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/au-revoir-zoho</guid>
            <pubDate>Fri, 21 Aug 2026 12:39:38 GMT</pubDate>
            <description><![CDATA[Zoho's Farewell Post!!]]></description>
            <content:encoded><![CDATA[<p>For everyone, their first job holds a special place in their heart, in one way or another. It gives them that first step into the future they once only dreamed of.</p><p>And for me, that place is <strong><mark>Zoho</mark></strong> – the company that gave me my first job, helped me take that first step on the ladder I always wanted to climb, and brought me closer to the dreams I had always carried with me.</p><p>Now, it’s time for me to take that second step.</p><p>After an incredible journey of nearly <strong><u>four years (3 years and 8 months)</u></strong>, I am finally saying goodbye to Zoho.</p><hr /><h2>January 2023</h2><p>I joined Zoho on <strong>January 18th, 2023</strong>.</p><p>I still remember feeling equally thrilled and overwhelmed by the sheer size of the workplace and its unique working culture. Everything felt new, unfamiliar, and exciting at the same time.</p><p>And, of course, I couldn’t leave out one of the perks that made Zoho even more memorable – the free food!</p><p>With three meals provided for employees every day, I inevitably put on a few extra kilos along the way. Back then, I didn’t even know what calories were. Now, well... I’ve got them under control! 😄</p><hr /><h2>Friendships</h2><p>When I joined, my only goals were simple: work hard, clear my probation, and hopefully make some friends as I stepped into this brand-new stage of my life.</p><p>Somehow, I managed to do both.</p><p>A Full-Stack Developer (I assumed through my college projects) slowly transitioned into a <strong>Database enthusiast</strong>.</p><p>And along the way, I found a wonderful set of friends with whom I loved spending most of my time – spending time chitchatting in pantries, having evening snacks, watch movies in conference rooms (not that many though), going out, or simply hanging around and doing absolutely nothing.</p><blockquote><p>Even today I randomly remembers the conversations we had in Tower pantries, watching series while having lunch at the South Plaza tent, the food the guys bought from the home, and all those random conversations over the evening snacks. <br />Those seemingly ordinary moments are the memories I’ll carry with me!! ❤️</p></blockquote><hr /><h2>The Engineer I Became</h2><p>At some point, I realised I needed to take my work more seriously – thanks to the gentle nudge from my manager about not taking work seriously enough. 😄</p><p>From that moment until this very day, I have worked hard, made meaningful contributions to the product and the projects I was part of, and learned so much about databases, especially PostgreSQL. I learned from my mentors, my teammates, and, at times, from my own mistakes.</p><p>I also got the opportunity to mentor two juniors. Looking back, I think I handled that responsibility better than I would have expected from myself – or at least, that’s what I’d like to believe.</p><p>They’re probably the ones who can tell whether that's actually true.. :)</p><blockquote><p>And somewhere along the way, my interests started changing too. Instead of thinking about building full-stack web applications, I found myself increasing interested in building tools and projects that could make working with databases easier and better.</p><p>It eventually went so far that I started writing blogs and posting them like it was a war. 😅</p></blockquote><hr /><h2>Personal Growth</h2><p>But, as life usually does, things started changing along the way.</p><p>Some friendships slowly faded – some because of my own mistakes, and some simply because we stopped being able to keep in touch. There wasn’t always a dramatic ending or maybe there is. Sometimes, people just slowly became a little more distant.</p><p>And somewhere in between all of that, I entered what I now call my <strong>solo-life phase</strong>. I started going to movies alone, sitting in cafés by myself, reading books, and, most importantly, learning to enjoy my own company.</p><p>Looking back, that phase helped me grow in ways I didn’t realise at the time. I became more comfortable with myself, more independent, and perhaps a little more mature. Ironically, learning to be comfortable alone also helped me find new people, make new friends, and, most importantly, feel secure in my own company.</p><hr /><h2>Games</h2><p>Zoho introduced me to games I had barely played before – <strong>Foosball and Table Tennis</strong> – and somehow made me fall a little more in love with <strong>Badminton</strong>.</p><p><em>I still remember the first time, I played foosball, the only thing I know is to rotate the player in full circle and hit the ball with complete randomness.</em> 😂</p><p>What started as something to do during breaks slowly became one of the ways I spent a lot of my time at Zoho – enough to make me sweat even with the A/C running throughout the building. 😄</p><p>Interestingly, many of the friends I made during the later part of my journey came through these games. Some started as people I played with, some gradually became closer friends, and some became people I genuinely enjoyed spending time with outside the game as well.</p><p>Looking back, the games were never just about winning or losing. Well, sorry, in my case, it was definitely about winning! 😄</p><p>But more than anything, they gave me a reason to meet people, spend time together, laugh at each other’s terrible shots, and, somewhere between all of that, build <strong>friendships</strong>.</p><hr /><h2>Things I Didn't Know I Had to Learn</h2><p>When I first started, I thought being a developer was mostly about writing code. I slowly realised that writing code was only a small part of it.</p><p>There were hours spent debugging issues in the source code, trying to understand why something that looked perfectly fine on the surface was behaving completely differently underneath.</p><p>There were production issues that needed to be analysed, countless lines of source code to go through, and documentation that had to be written and maintained – things I probably didn't think much about when I first imagined what being a developer would be like.</p><p>And then came the never-ending meetings and discussions. 😄 Some were about solving problems, some were about understanding problems, and some somehow led to discussions about other problems we didn't even know existed!</p><p>But somewhere in all of this, I learned that being an engineer isn't just about writing code.</p><blockquote><p>It's about understanding a problem before trying to solve it, asking the right questions, digging deeper when the obvious answer isn't the right one, documenting what you learn, and being able to explain your thinking to others.</p></blockquote><p>Those were things I didn't know I had to learn when I joined Zoho. Looking back, they probably shaped me just as much as the code I wrote.</p><hr /><p>And finally, I want to thank everyone who has been a part of this incredible journey.</p><p>To my mentors and teammates who helped me grow.</p><p>To my friends who made ordinary days memorable.</p><p>To everyone I crossed paths with along the way.</p><p>Thank you.</p><p>I may not remember every project, meeting, or line of code, but I’ll always remember the people, the conversations, the laughter, the games, the lessons, and the moments that made these nearly four years special.</p><p>And heads up – even after saying all this, <strong>I won’t be the one to ping first.</strong> 🤪<br /><br />GOOD BYE, ZOHO!! ❤️</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Personal</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/Zoho.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Vacuum - PostgreSQL way of clearing it's Debt]]></title>
            <link>https://sambasivareddy.in/blog/vacuum-postgresql-way-of-clearing-its-debt</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/vacuum-postgresql-way-of-clearing-its-debt</guid>
            <pubDate>Sat, 08 Aug 2026 10:26:25 GMT</pubDate>
            <description><![CDATA[Vacuum in PostgreSQL helps reclaiming the space occupied by dead tuples. Here in this blog, we discusses about how Vacuum do in PostgreSQL internally.]]></description>
            <content:encoded><![CDATA[<p>PostgreSQL uses <strong>MVCC</strong> (Multi-Version Concurrency Control): an UPDATE never overwrites a row in place, and a DELETE doesn't immediately erase the row. Instead:</p><ul><li><p>An <code>UPDATE</code> inserts a brand-new tuple version and sets the <code>xmax</code> on the old one; the old version stays on disk so that concurrent transactions with an older snapshot can still see it.</p></li><li><p>A <code>DELETE</code> sets the <code>xmax</code> to the transaction ID that deleted the row, but the row itself is not cleared from disk either. Transactions that started before the deleting transaction can still see this row.</p></li></ul><p>Once no active transaction or snapshot can possibly need a tuple version anymore, it becomes dead – wasted space that nothing will ever look at again. That's the debt VACUUM exists to collect.</p><hr /><h2>Part 1: Why VACUUM Exists?</h2><p>Every write operation i.e., UPDATE or DELETE in PostgreSQL creates a debt that must eventually be paid. For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>INSERT → writes 1 new tuple
       → debt: none (yet)

UPDATE → writes 1 new tuple version
       → marks old tuple dead (xmax set)
       → writes 1 new index entry per index (assume N indexes)
       → marks old index entries dead
       → debt: 1 dead heap tuple + N dead index entries

DELETE → marks tuple dead (xmax set)
       → marks index entries dead
       → debt: 1 dead heap tuple + N dead index entries
</code></pre><p>This debt accumulates on every page, and dead tuples cause:</p><ul><li><p><strong>Wasted space</strong>: pages fill with unreachable data</p></li><li><p><strong>Slower scans</strong>: every scan reads and discards dead tuples</p></li><li><p><strong>Blocked index-only scans</strong>: the all_visible bit cannot be set while dead tuples exist</p></li><li><p><strong>Wraparound risk</strong>: old xmin values must be <strong>frozen</strong> before the XID space/range is exhausted</p></li></ul><hr /><h2>Part 2: What VACUUM Does?</h2><p><code>VACUUM</code> is the debt collector, whose responsibility is to clear all the debt created above:</p><ul><li><p><strong>Reclaims space</strong> occupied by dead tuples (and their index entries) so that space can be reused by future inserts/updates.</p></li><li><p><strong>Freezes</strong> old transaction IDs so PostgreSQL doesn't run into transaction ID wraparound. XIDs are 32-bit and comparisons are circular, so an unfrozen tuple can appear to be "from the future" once wraparound occurs.</p></li><li><p>Updates the <strong>visibility map</strong> and <strong>free space map</strong> so future scans/vacuums can skip work.</p></li><li><p>Optionally <strong>truncates</strong> trailing empty pages and updates planner statistics.</p></li></ul><hr /><h2>Part 3: How VACUUM Works – The Three-Phase Design</h2><p>We can divide the process into three phases:</p><ul><li><p>Scan, Prune, and Freeze</p></li><li><p>Index Vacuuming</p></li><li><p>Heap Vacuuming</p></li></ul><p>Phase 0 below covers the entry point into the heap access method – it sits before the three phases proper, which are Phase 1 through Phase 3.</p><h3>Phase 0: Entry – from the VACUUM Command Down to the Heap AM (Access Method)</h3><p>When a VACUUM command is executed, PostgreSQL calls ExecVacuum(), which is the primary entry point for ANALYZE as well.</p><p>Code flow:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">void ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) {
   // ..... parse options into VacuumParams

   vacuum(newrels, &amp;params, bstrategy, ...); =&gt; {

      // ..... loop over relations
      vacuum_rel(vrel-&gt;oid, vrel-&gt;relation, &amp;params, bstrategy); =&gt; {
         Relation rel;

         // ..... open/lock relation, permission checks
         if (params-&gt;options &amp; VACOPT_PROCESS_MAIN) {
            if (params-&gt;options &amp; VACOPT_FULL) {
               cluster_rel(rel, InvalidOid, &amp;cluster_params); // VACUUM FULL: different path entirely (cluster.c)
               rel = NULL;
            }
            else {
               // OUR AREA OF INTEREST — plain/lazy VACUUM
               table_relation_vacuum(rel, params, bstrategy); =&gt; {
                  // src/include/access/tableam.h:1674
                  rel-&gt;rd_tableam-&gt;relation_vacuum(rel, params, bstrategy);

                  // ======================================
                  // rel-&gt;rd_tableam-&gt;relation_vacuum is a function pointer to the table access method's vacuum implementation.
                  // heap AM wires this straight to heap_vacuum_rel:
                  //   .relation_vacuum = heap_vacuum_rel  (heapam_handler.c:2656)
                  heap_vacuum_rel(rel, params, bstrategy); =&gt; {
                     LVRelState *vacrel = palloc0(sizeof(LVRelState));
                     
                     // ..... set up error callback, open indexes, copy names
                     vacrel-&gt;aggressive = vacuum_get_cutoffs(rel, params, &amp;vacrel-&gt;cutoffs); // computes OldestXmin/FreezeLimit/MultiXactCutoff
                     vacrel-&gt;rel_pages = RelationGetNumberOfBlocks(rel);
                     vacrel-&gt;vistest = GlobalVisTestFor(rel);
                     dead_items_alloc(vacrel, params-&gt;nworkers); // TID store, bounded by maintenance_work_mem

                     lazy_scan_heap(vacrel);        // === STAGE 1 ===
                     // (lazy_scan_heap internally calls lazy_vacuum() === STAGE 2+3 === when TID store fills or scan ends)

                     // ..... final index cleanup, truncate, update relfrozenxid/relminmxid, pgstat report
                     return;
                  }
               }
            }
         }
         // ..... close relation
      }
   }
}</code></pre><h3>Phase 1: Scan, Prune, and Freeze</h3><p><strong>Entry point</strong>: lazy_scan_heap()</p><p>The driver loop reads blocks via a <code>ReadStream</code>, and for each block:</p><ul><li><p><code>heap_vac_scan_next_block()</code> decides whether a block can be skipped using the <strong>visibility map</strong>. <em>Pages that are already all-visible (or all-frozen) don't need scanning, unless this is an aggressive vacuum or eager scanning wants to freeze them anyway</em>.</p></li><li><p>It takes a cleanup lock and calls <code>lazy_scan_prune</code>, which delegates the real work to <code>heap_page_prune_and_freeze</code>.</p></li><li><p><code>heap_page_prune_and_freeze</code> walks every <strong>line pointer</strong> on the page. For each tuple, it calls <code>HeapTupleSatisfiesVacuumHorizon</code>, which returns one of the following:</p><ul><li><p><code>HEAPTUPLE_DEAD</code> – deletable now</p></li><li><p><code>HEAPTUPLE_RECENTLY_DEAD</code> – dead, but some snapshots might still see it, so it's kept</p></li><li><p><code>HEAPTUPLE_LIVE</code></p></li><li><p><code>HEAPTUPLE_INSERT_IN_PROGRESS</code> / <code>HEAPTUPLE_DELETE_IN_PROGRESS</code></p></li></ul></li><li><p><strong>HOT-chain pruning</strong>: Heap-Only Tuple chains are collapsed i.e., intermediate dead tuple versions are removed. The root tuple's line pointer is turned into <code>LP_REDIRECT</code>, pointing straight to the latest live version. <em>This way, index entries pointing at the root stay valid without needing an index update</em>.</p></li><li><p><strong>Freezing</strong>: if a tuple's <code>xmin</code> is older than <code>FreezeLimit</code>, it's rewritten with a <strong>frozen XID</strong> so it's permanently visible without needing to look up the <code>CLOG</code>. Here, <em><u>FreezeLimit = nextXID - vacuum_freeze_min_age</u></em>.</p></li><li><p>Dead item pointers (<code>LP_DEAD</code>) whose tuples were removed are recorded into the TID store – a memory-efficient, radix-tree-like structure capped at <code>maintenance_work_mem</code>, which will be used later for index cleanup.</p></li><li><p>If the entire page ends up fully visible/frozen, <em>the relevant bit(s) are set in the visibility map.</em></p></li></ul><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">lazy_scan_heap(LVRelState *vacrel) { 
   ReadStream *stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE, vacrel-&gt;bstrategy,
                                                    vacrel-&gt;rel, MAIN_FORKNUM,
                                                    heap_vac_scan_next_block, vacrel, sizeof(uint8)); =&gt; {
      // called back per block: decides skip vs scan using the Visibility Map
      // OUR AREA OF INTEREST — this is what lets normal vacuums skip all-visible/all-frozen pages
   }

   while (true) {
      // ..... check TID-store fullness -&gt; if full, call lazy_vacuum() (Stage 2+3) mid-scan, then resume
      Buffer buf = read_stream_next_buffer(stream, &amp;per_buffer_data);
      if (!BufferIsValid(buf)) break;

      blkno = BufferGetBlockNumber(buf);
      vacrel-&gt;scanned_pages++;
      visibilitymap_pin(vacrel-&gt;rel, blkno, &amp;vmbuffer);

      got_cleanup_lock = ConditionalLockBufferForCleanup(buf);
      // ..... lazy_scan_new_or_empty() short-circuit for new/empty pages

      // OUR AREA OF INTEREST
      lazy_scan_prune(vacrel, buf, blkno, page, vmbuffer,
                       all_visible_according_to_vm, &amp;has_lpdead_items, &amp;vm_page_frozen); =&gt; {
         PruneFreezeResult presult;
         int prune_options = HEAP_PAGE_PRUNE_FREEZE;
         if (vacrel-&gt;nindexes == 0)
            prune_options |= HEAP_PAGE_PRUNE_MARK_UNUSED_NOW;

         // OUR AREA OF INTEREST — the real per-page workhorse
         heap_page_prune_and_freeze(rel, buf, vacrel-&gt;vistest, prune_options,
                                     &amp;vacrel-&gt;cutoffs, &amp;presult, PRUNE_VACUUM_SCAN,
                                     &amp;vacrel-&gt;offnum,
                                     &amp;vacrel-&gt;NewRelfrozenXid, &amp;vacrel-&gt;NewRelminMxid); =&gt; {
            for (offnum = maxoff; offnum &gt;= FirstOffsetNumber; offnum = OffsetNumberPrev(offnum)) {
               ItemId itemid = PageGetItemId(page, offnum);
               // ..... LP_UNUSED / LP_DEAD / LP_REDIRECT items handled directly

               // classify each normal tuple:
               prstate.htsv[offnum] = heap_prune_satisfies_vacuum(&amp;prstate, &amp;tup, buffer); =&gt; {
                  //  calls HeapTupleSatisfiesVacuumHorizon()
                  // returns HEAPTUPLE_DEAD / RECENTLY_DEAD / LIVE / INSERT_IN_PROGRESS / DELETE_IN_PROGRESS
               }

               if (!HeapTupleHeaderIsHeapOnly(htup))
                  prstate.root_items[prstate.nroot_items++] = offnum;
               else
                  prstate.heaponly_items[prstate.nheaponly_items++] = offnum; // HOT chain member
            }

            // ..... walk HOT chains: collapse dead intermediate versions,
            //       rewrite root line pointer as LP_REDIRECT to the surviving tuple

            // for each surviving tuple, consider freezing:
            if (prstate-&gt;freeze) {
               // OUR AREA OF INTEREST
               heap_prepare_freeze_tuple(htup, prstate-&gt;cutoffs, &amp;prstate-&gt;pagefrz,
                                          &amp;prstate-&gt;frozen[prstate-&gt;nfrozen],
                                          &amp;totally_frozen); =&gt; {
                  // heapam.c — decides if xmin/xmax older than FreezeLimit/MultiXactCutoff
                  // must be rewritten frozen; feeds presult.all_frozen for the VM bit
               }
               prstate-&gt;frozen[prstate-&gt;nfrozen++].offset = offnum;
            }

            // ..... apply all changes (redirect/dead/unused/frozen arrays) to the page,
            //       WAL-log via log_heap_prune_and_freeze(), set presult.all_visible/all_frozen
         }

         if (presult.lpdead_items &gt; 0) {
            qsort(presult.deadoffsets, presult.lpdead_items, sizeof(OffsetNumber), cmpOffsetNumbers);
            // OUR AREA OF INTEREST — feeds Stage 2/3
            dead_items_add(vacrel, blkno, presult.deadoffsets, presult.lpdead_items); =&gt; {
               // vacuumlazy.c:3538 — inserts TIDs into the bounded TidStore
            }
         }

         // ..... if presult.all_visible, set VM bit(s) via visibilitymap_set()
      }
   }
}
</code></pre><h3>Phase 2: Index Vacuuming</h3><p>Once the <strong>TID store</strong> fills up (or the heap scan finishes), <code>lazy_vacuum</code> calls each <strong><em>index's bulk-delete routine</em></strong> (e.g., btree's <code>btbulkdelete</code>) with the TID store, so all index entries pointing at now-dead heap tuples are removed. This can run in parallel across indexes.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">lazy_vacuum(LVRelState *vacrel) { 
   // ..... bypass-optimization check (skip index vacuuming if lpdead_item_pages is near zero)

   else if (lazy_vacuum_all_indexes(vacrel)) =&gt; {
      
      for (int idx = 0; idx &lt; vacrel-&gt;nindexes; idx++) {
         Relation indrel = vacrel-&gt;indrels[idx];
         IndexBulkDeleteResult *istat = vacrel-&gt;indstats[idx];

         // OUR AREA OF INTEREST
         vacrel-&gt;indstats[idx] = lazy_vacuum_one_index(indrel, istat, old_live_tuples, vacrel); =&gt; {
            IndexVacuumInfo ivinfo = { .index = indrel, .heaprel = vacrel-&gt;rel, /* ..... */ };

            istat = vac_bulkdel_one_index(&amp;ivinfo, istat, vacrel-&gt;dead_items,
                                           vacrel-&gt;dead_items_info); =&gt; {
               // OUR AREA OF INTEREST — calls into the *index* AM, e.g. btree's btbulkdelete
               istat = index_bulk_delete(ivinfo, istat, vac_tid_reaped, dead_items); =&gt; {
                  // indrel-&gt;rd_indam-&gt;ambulkdelete(...)
                  // walks the index, for each entry asks vac_tid_reaped(TID)
                  // whether that TID is in vacrel-&gt;dead_items (the TidStore from Stage 1),
                  // and deletes the matching index tuples
               }
            }
            return istat;
         }
      }
   }
   // ..... on success, proceeds straight into Stage 3 below
}
</code></pre><h3>Phase 3: Heap Vacuuming</h3><p><code>lazy_vacuum_heap_rel</code> / <code>lazy_vacuum_heap_page</code> revisit each page recorded in the TID store and finally flip <code>LP_DEAD</code> line pointers to <code>LP_UNUSED</code>, actually freeing the slot for reuse. This can only happen after Phase 2 guarantees no index still references those TIDs – otherwise you'd get index entries pointing at garbage or reused tuples, a classic corruption bug.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">lazy_vacuum_heap_rel(vacrel) { 
   // ..... TidStoreIterate over every blkno recorded in Stage 1's dead_items

   lazy_vacuum_heap_page(vacrel, blkno, buffer, deadoffsets, num_offsets, vmbuffer); =&gt; {
     
      Page page = BufferGetPage(buffer);
      START_CRIT_SECTION();

      for (int i = 0; i &lt; num_offsets; i++) {
         ItemId itemid = PageGetItemId(page, deadoffsets[i]);
         Assert(ItemIdIsDead(itemid) &amp;&amp; !ItemIdHasStorage(itemid));
         // OUR AREA OF INTEREST — the actual space reclamation
         ItemIdSetUnused(itemid);   // slot is now free for reuse by future inserts
         unused[nunused++] = deadoffsets[i];
      }

      PageTruncateLinePointerArray(page);   // shrink the line-pointer array if possible
      MarkBufferDirty(buffer);

      if (RelationNeedsWAL(vacrel-&gt;rel))
         log_heap_prune_and_freeze(vacrel-&gt;rel, buffer, InvalidTransactionId, false,
                                    PRUNE_VACUUM_CLEANUP, /* ..... */
                                    unused, nunused);
      END_CRIT_SECTION();

      // ..... recheck heap_page_is_all_visible() now that dead items are gone,
      //       set VM bit if so
   }
   // ..... FreeSpaceMapVacuumRange() makes the freed space visible to future inserters
}
</code></pre><h3>Phase 4: Finishing Up</h3><ul><li><p><code>FreeSpaceMapVacuumRange</code> propagates newly freed space up the <strong>FSM</strong> tree.</p></li><li><p>If trailing pages are entirely empty, the relation is truncated.</p></li><li><p><code>relfrozenxid/relminmxid</code> in <code>pg_class</code> are advanced to <code>vacrel-&gt;NewRelfrozenXid/NewRelminMxid</code>, and stats (<code>pg_stat_user_tables</code>, <code>pg_class.reltuples</code>) are updated.</p></li></ul><hr /><h2>Conclusion</h2><p>MVCC is what makes PostgreSQL's concurrency model work – readers never block writers, and writers never block readers – but that guarantee isn't free. Every <strong>UPDATE</strong> and <strong>DELETE</strong> leaves behind a dead tuple, a set of stale index entries, and eventually an unfrozen XID, and none of that debt clears itself.</p><p>VACUUM is the mechanism that pays it down, and the three-phase design exists specifically so that payoff can happen safely and efficiently:</p><ul><li><p><strong>Scan, Prune, and Freeze</strong> finds what's dead and rewrites what's old enough to freeze, using the visibility map to skip pages that don't need the work.</p></li><li><p><strong>Index Vacuuming</strong> clears every index's references to those dead tuples first — this ordering is what keeps the next phase safe.</p></li><li><p><strong>Heap Vacuuming</strong> only then flips the now-unreferenced line pointers to <code>LP_UNUSED</code>, actually handing the space back for reuse.</p></li></ul><p>That ordering – indexes before heap – is the detail worth remembering above all else. Reclaim heap space before indexes are cleaned up, and you get index entries pointing at tuples that no longer mean what they used to: exactly the class of corruption bug the two-phase split (Phase 2 then Phase 3) is built to prevent.</p><p>The freezing work matters just as much as the space reclamation, if not more — it's not an optimization, it's what keeps the 32-bit XID counter from wrapping around and making old, unfrozen rows look like they were written in the future. Skip VACUUM long enough, and that's the failure mode you eventually hit, not just table bloat.</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>PostgreSQL</category>
            <category>Technical</category>
            <category>PGSourceCode</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/postgresql-vacuum-poster.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[What I read in July 2026]]></title>
            <link>https://sambasivareddy.in/blog/what-i-read-in-july-2026</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/what-i-read-in-july-2026</guid>
            <pubDate>Fri, 31 Jul 2026 09:56:29 GMT</pubDate>
            <description><![CDATA[Summary about the books I read in July 2026]]></description>
            <content:encoded><![CDATA[<p>This July, I read a book called <strong><em>"Anxious People"</em></strong> by <strong>Fredrik Backman</strong> – a crime mystery, psychological fiction, or humorous fiction.</p><p>So many genres, right? When I bought it, I thought it was a crime mystery. But once I finished, I felt the other two genres fit just as well.</p><hr /><h3>Plot</h3><p>What is intended to be a bank robbery turns into a hostage situation when the robber fails to rob the bank and enters an apartment to escape the police.</p><p>This story is about the group of hostages and the robber – in other words, idiots (sorry, I'm not calling them that – the author did). It's about how the hostage situation brings them closer, and what happens to the robber in the end.</p><h3>My Thoughts</h3><p>This book isn't really about the robbery or hostage situation. It's about how quickly people judge others, their quirks, and who they become in the present. It also touches on the private battles everyone in that room is fighting, and the empathy that helps them through it. And that's what stuck with me long after I finished it. </p><p>My favourite quote from the book goes:</p><blockquote><p>They say that a person's personality is the sum of their experiences. But that isn't true, at least not entirely, because if our past was all that defined us, we'd never be able to put up with ourselves. We need to be allowed to convince ourselves that we're more than the mistakes we made yesterday. That we are all our next choices, too, all of our tomorrows – Fredrik Backman, Anxious People</p></blockquote><p><strong><u>My Ratings</u></strong>: 4.5/5</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Books</category>
            <category>Personal</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/anxious_people.jpeg" length="0" type="image/jpeg"/>
        </item>
        <item>
            <title><![CDATA[PostgreSQL Internals - Module 9: Replication]]></title>
            <link>https://sambasivareddy.in/blog/postgresql-internals-module-9-replication</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/postgresql-internals-module-9-replication</guid>
            <pubDate>Wed, 15 Jul 2026 04:19:03 GMT</pubDate>
            <description><![CDATA[Final blog post of the PostgreSQL Internals series explaining about Replication and it's type i.e., Physical and Logical Replication. ]]></description>
            <content:encoded><![CDATA[<p>In the previous blog, we discussed about the <strong>"Concurrency &amp; Locking" </strong>and explored how PostgreSQL manages concurrent access at every level from the 8-mode table lock hierarchy and its conflict matrix.</p><p>In this blog, we are going to discuss about the <strong>"Replication" </strong>and how it helps the PostgreSQL to keep the data safe across multiple machines. A single server in a system is always a single point of failure and replication is what turns PostgreSQL into a high-availability system. In this we are going to discuss that exact mechanics of replication: how WAL bytes flow from primary to standby, how logical decoding transforms those bytes into row-level changes, how replication slots prevent data loss, and what happens at every layer when you commit a transaction on a synchronous replica setup.</p><hr /><h2>Part 1: Replication Fundamentals</h2><p>PostgreSQL supports two fundamentally different replication approaches:</p><ol><li><p>Physical/Streaming Replication</p></li><li><p>Logical Replication</p></li></ol><h3>Physical Replication</h3><p><u>Ships raw WAL bytes from primary to standby</u>. The standby replays the exact same WAL records the primary wrote - byte for byte. The result is a <strong>binary-identical copy</strong> of the primary.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>Primary                           Standby
  │                                  │
  │─ WAL record (INSERT, page  X) ─&gt;│
  │─ WAL record (UPDATE, page  Y) ─&gt;│─ replay → apply to data files
  │─ WAL record (COMMIT, xid=5001)─&gt;│
  │&lt;─ acknowledgement (LSN) ─────│</code></pre><p>Characteristics:</p><ul><li><p>Replicates <strong>everything</strong>: all databases, all tables, system catalogs</p></li><li><p>Standby is identical at the block level: cannot have different indexes or schema</p></li><li><p>Standby can serve read-only queries (hot standby)</p></li></ul><h3>Logical Replication</h3><p><u>Decodes WAL records into SQL-level row changes (INSERT/UPDATE/DELETE) and ships those instead</u>. The subscriber applies the changes to its own tables.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>Primary                              Subscriber
  │                                     │
  │── WAL record (heap INSERT) ─&gt;     │
  │   logical decoding  ────────&gt;  │
  │   row: {id=42, name='alice'}  ───&gt;│── apply INSERT
  │&lt;── confirmation  ───────────│</code></pre><p>Characteristics:</p><ul><li><p>Replicates specific <strong>tables</strong> or <strong>publications</strong>, not the whole cluster</p></li><li><p>Subscriber can have different schema, indexes, additional columns</p></li><li><p>Can replicate between different PostgreSQL major versions</p></li><li><p>Powers CDC (change data capture) pipelines</p></li><li><p>Cannot replicate DDL automatically</p></li></ul><hr /><h2>Part 2: Physical Replication - Deep Internals</h2><p>Setting up streaming replication</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- On Primary: postgresql.conf
wal_level = replica    -- minimum for replication
max_wal_senders = 5    -- max concurrent WAL sender processes
wal_keep_size = 1GB    -- Keep at-least 1GB of WAL for lagging standbys

-- Create replication role
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'secret';

-- pg_hba.conf
host replication replicator standby_ip/32 scram-sha-256</code></pre><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- On Standby: create base-backup
pg_basebackup -h primary_ip -U replicator -D /var/lib/postgresql/data \
     -P -Xs -R

-- -Xs = stream WAL during backup
-- -R = create standby.signal and postgresql.auto.conf automatically</code></pre><h3>The WAL sender: WAL receiver pipeline</h3><p>Primary process tree:</p><ul><li><p>Postmaster</p><ul><li><p>WAL Sender (one per standby connection)</p><ul><li><p>reads WAL from <code>pg_wal/</code></p></li><li><p>sends to standby over TCP</p></li><li><p>tracks standby's replay position</p></li></ul></li></ul></li></ul><p>Standby process treee:</p><ul><li><p>Postmaster</p><ul><li><p>Startup process (replays WAL in recovery mode)</p></li><li><p>WAL receiver (receives WAL from primary)</p><ul><li><p>writes to <code>pg_wal/</code></p></li><li><p>signals startup process: new WAL available</p></li></ul></li></ul></li></ul><p><strong>The Replication Protocol - Exact Sequence</strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>Standby → Primary:  START_REPLICATION LSN timeline
Primary → Standby:  XLogData (WAL bytes, starting_lsn, current_lsn, send_time)
Standby → Primary:  Standby Status Update (every wal_receiver_status_interval)
         write_lsn   — written to pg_wal on standby
         flush_lsn   — fsynced on standby
         replay_lsn  — applied to data files on standby
         reply_time</code></pre><h3>Hot Standby: Read queries on standy</h3><p>The standby's startup process replays WAL while simultaneously serving <code>read-only</code> queries. This works because:</p><p>Standby maintains its own:</p><ul><li><p>shared buffer pool</p></li><li><p>MVCC snapshot (based on replayed transactions)</p></li><li><p>pg_stat_activity for its own connections</p></li><li><p>local lock table</p></li></ul><p>Standby does NOT have:</p><ul><li><p>writable data files (read-only replay)</p></li><li><p>its own transaction IDs (read use primary's XIDs)</p></li><li><p>ability to run <code>VACUUM</code> independently</p></li></ul><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- On standy: read-only queries work fine
SELECT count(*) FROM orders; -- OK

-- Writes are rejected:
INSERT INTO orders VALUES (...);
-- ERROR: cannot execute INSERT in a read-only transaction
</code></pre><hr /><h2>Part 3: Replication Slots</h2><p>Replication slots solve a critical problem: <strong>What if the standby falls behind?</strong></p><p>Without replication slots, if a standby disconnects, the primary might delete WAL files the standby still needs. When the standby reconnects, it cannot continue - it has to do a full base backup again.</p><h3>How replication slots work</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>-- Create a physical replication slot:
SELECT pg_create_physical_replication_slot('standby_slot');

-- The slot records:
-- restart_lsn: the oldest WAL position the standby still needs
-- confirmed_flush_lsn: the latest position the standby confirmed

-- Primary will NOT delete WAL files before restart_lsn
-- even if the standby is disconnected for days/weeks

-- Monitor replication slots:
SELECT
    slot_name,
    slot_type,              -- physical or logical
    active,                 -- is a consumer currently connected?
    restart_lsn,            -- oldest WAL the slot needs
    confirmed_flush_lsn,    -- last position confirmed by consumer
    pg_wal_lsn_diff(
        pg_current_wal_lsn(),
        restart_lsn
    ) AS retained_wal_bytes,
    pg_size_pretty(pg_wal_lsn_diff(
        pg_current_wal_lsn(),
        restart_lsn
    )) AS retained_wal
FROM pg_replication_slots;</code></pre><h3>The replication slot danger</h3><p>If a replication slot's consumer goes offline and never comes back, the slot continues to hold WAL. <code>pg_wal/</code> grows indefinitely. When the disk fills: <strong><u>PRIMARY CRASHES.</u></strong></p><p>Real scenario:</p><ul><li><p>Day 1: slot created for subscriber</p></li><li><p>Day 3: subscriber host dies</p></li><li><p>Day 10: primary disk is full</p></li><li><p>Day 10: primary crashes: <code>could not write to file pg_wal/...</code></p></li></ul><p><strong>Monitoring</strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>-- CRITICAL monitoring query — run this every 5 minutes:
SELECT
    slot_name,
    active,
    pg_size_pretty(
        pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
    ) AS wal_retained,
    pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained_bytes
FROM pg_replication_slots
ORDER BY retained_bytes DESC;

-- Alert if retained_bytes &gt; 10GB
-- Drop dead slots immediately:
SELECT pg_drop_replication_slot('dead_slot');</code></pre><hr /><h2>Part 4: Synchronous v/s Asynchronous Replication</h2><h3>Asynchronous replication (default)</h3><p><strong>Primary: </strong>COMMIT → WAL written + fsynced locally → SUCCESS returned to client</p><p><strong>Standby: </strong>receives WAL asynchronously (milliseconds to seconds later)</p><p><strong>Risk: </strong>if primary crashes immediately after COMMIT, standby may not have received that WAL yet means data loss of committed transactions</p><h3>Synchronous replication</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>-- postgresql.conf on primary:
synchronous_standby_names = 'standby1'

-- or for multiple:
synchronous_standby_names = 'FIRST 1 (standby1, standby2)'  -- any one of them
synchronous_standby_names = 'ALL (standby1, standby2)'      -- all of them</code></pre><p><strong>Primary: when COMMIT, </strong>it follows:</p><ul><li><p>write WAL locally</p></li><li><p>send WAL to standby</p></li><li><p>WAIT for acknowledgement from standby</p></li><li><p>standby confirms <code>flush_lsn</code> &gt;= <code>commit_lsn</code></p></li><li><p>SUCCESS returned to client</p></li></ul><p><strong>Guarantee: </strong>no committed transaction can be lost (both primary and standby must fail simultaneously)</p><p><strong>Cost: </strong>COMMIT latency += network round trip to standby (~0.5ms on LAN, ~5ms on WAN, ~50ms cross-region)</p><p><strong><u>synchronous_commit levels</u></strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Per-transaction control:
SET synchronous_commit = on;           -- wait for standby flush (default)
SET synchronous_commit = remote_apply; -- wait for standby to replay
SET synchronous_commit = remote_write; -- wait for standby OS write (no fsync)
SET synchronous_commit = local;        -- only wait for local fsync
SET synchronous_commit = off;          -- don't wait for anything

-- Use off for bulk loads where some data loss is acceptable:
BEGIN;
SET LOCAL synchronous_commit = off;
INSERT INTO events SELECT * FROM staging_events;
COMMIT;  -- returns immediately, standby catches up later</code></pre><hr /><h2>Part 5: Logical Replication - Deep Internals</h2><h3>The logical decoding pipeline</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>WAL stream
  │
  V
Output Plugin (pgoutput, wal2json, decoderbufs...)
  │  reads WAL records
  │  filters by publication tables
  │  reconstructs row images from before/after
  │  emits logical change messages:
  │    BEGIN xid=5001
  │    INSERT relation=orders tuple=(42,'alice',100.00)
  │    UPDATE relation=orders old=(42,'alice',100.00) new=(42,'alice',150.00)
  │    COMMIT xid=5001 lsn=0/1A3F058
  V
Replication Protocol
  │
  V
Subscriber (apply worker)
  applies changes to subscriber tables</code></pre><h3>Publications and subscription</h3><p><strong>On publisher (primary)</strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>-- Publish all tables:
CREATE PUBLICATION my_pub FOR ALL TABLES;

-- Publish specific tables:
CREATE PUBLICATION orders_pub FOR TABLE orders, customers;

-- Publish specific operations:
CREATE PUBLICATION insert_only_pub FOR TABLE events
    WITH (publish = 'insert');  -- only INSERT, not UPDATE/DELETE

-- See what's published:
SELECT * FROM pg_publication;
SELECT * FROM pg_publication_tables;</code></pre><p><strong>On subscriber</strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>-- Create subscription:
CREATE SUBSCRIPTION my_sub
    CONNECTION 'host=primary_ip dbname=mydb user=replicator password=secret'
    PUBLICATION my_pub;

-- PostgreSQL automatically:
-- 1. Creates a replication slot on the publisher
-- 2. Takes a snapshot of current data
-- 3. Copies existing table data (initial sync)
-- 4. Starts streaming new changes

-- Detailed per-table sync status:
SELECT * FROM pg_subscription_rel;
-- srsubstate: i=initializing, d=data copy, s=synchronized, r=ready</code></pre><h3>REPLICA IDENTITY - how UPDATE/DELETE know what changed</h3><p>For UPDATE and DELETE, the subscriber needs to identify which row to modify. This requires a <strong>REPLICA IDENTITY</strong>:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>-- Default: REPLICA IDENTITY DEFAULT
-- Uses primary key columns as identifier
-- If no PK: cannot replicate UPDATE/DELETE

-- FULL: send entire old row (expensive, no PK needed)
ALTER TABLE orders REPLICA IDENTITY FULL;

-- USING INDEX: use a unique index
ALTER TABLE orders REPLICA IDENTITY USING INDEX idx_orders_uuid;

-- Check:
SELECT relname, relreplident FROM pg_class WHERE relname = 'orders';
-- d = default (PK), f = full, i = index, n = nothing</code></pre><hr /><h2>Part 6: Logical Replication Slots - The Decoding Engine</h2><p>A logical replication slot is more complex than a physical one:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>-- Create a logical slot manually (for CDC, not subscription):
SELECT pg_create_logical_replication_slot('my_slot', 'pgoutput');
SELECT pg_create_logical_replication_slot('cdc_slot', 'wal2json');

-- Peek at decoded changes without consuming:
SELECT * FROM pg_logical_slot_peek_changes('my_slot', NULL, NULL);

-- Consume changes (advances the slot):
SELECT * FROM pg_logical_slot_get_changes('my_slot', NULL, NULL);

-- Output with wal2json (JSON format for CDC pipelines):
SELECT * FROM pg_logical_slot_get_changes(
    'cdc_slot', NULL, NULL,
    'pretty-print', '1',
    'include-timestamp', '1'
);</code></pre><p>The logical slot stores:</p><ul><li><p><code>restart_lsn</code> : WAL must be kept from here (same as physical)</p></li><li><p><code>confirmed_flush_lsn</code> : last change confirmed consumed by client</p></li><li><p><code>catalog_xmin</code> : oldest XID needed for catalog lookups during decoding</p></li></ul><p>The <code>catalog_xmin</code> is a second wraparound risk specific to logical slots - it prevents VACUUM from cleaning system catalog dead tuples. Monitor it:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>SELECT slot_name, catalog_xmin, age(catalog_xmin)
FROM pg_replication_slots
WHERE slot_type = 'logical';
-- age &gt; 500M is dangerous</code></pre><hr /><h2>Part 7: Cascading Replication and Standby Promotion</h2><h3>Cascading Replication</h3><p>A standby can itself have standbys - WAL flows from primary → standby 1 → standby 2:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>Primary ── WAL──&gt; Standby 1 ──WAL──&gt; Standby 2
                       │                    │
                   (hot standby)        (hot standby)
                    reads OK             reads OK

-- On Standby 1: allow WAL senders to Standby 2
-- postgresql.conf:
max_wal_senders = 3
hot_standby = on

-- Standby 2 connects to Standby 1 as its upstream
-- primary_conninfo = 'host=standby1_ip ...'</code></pre><h3>Standby promotion</h3><p>When the primary fails, a standby is promoted to become the new primary:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code># Promote standby to primary:
pg_ctl promote -D /var/lib/postgresql/data

# Or create a trigger file (older method):
touch /var/lib/postgresql/data/failover.trigger</code></pre><p>What happens internally:</p><ul><li><p>Startup process stops replaying WAL</p></li><li><p>Standby completes any in-progress WAL record</p></li><li><p>Writes a new timeline history files to <code>pg_wal/</code></p></li><li><p>Increments timeline ID (e.g., timeline 1 -&gt; timeline 2)</p></li><li><p>Opens for read-write connections</p></li><li><p>Other standbys detect timeline change</p><ul><li><p>reconnect to new primary</p></li><li><p>start replicating from the divergence point</p></li></ul></li></ul><hr /><h2>Part 8: pg_basebackup and Point-in-Time Recovery (PITR)</h2><h3>Base backup</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code># Full base backup:
pg_basebackup \
    -h primary_ip \
    -U replicator \
    -D /backup/basebackup_$(date +%Y%m%d) \
    -Ft \          # tar format
    -z \           # gzip compress
    -Xs \          # stream WAL
    -P             # show progress

# Backup manifest (PG 13+): verifies backup integrity
pg_verifybackup /backup/basebackup_20240115/</code></pre><h3>WAL archiving for PITR</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>-- postgresql.conf:
archive_mode = on
archive_command = 'aws s3 cp %p s3://my-wal-archive/%f'
-- %p = full path to WAL file
-- %f = filename only

-- Monitor archiving:
SELECT archived_count, failed_count,
       last_archived_wal, last_archived_time,
       last_failed_wal, last_failed_time
FROM pg_stat_archiver;</code></pre><h3>Point-in-time recovery</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql"># recovery.conf (or postgresql.conf in PG 12+):
restore_command = 'aws s3 cp s3://my-wal-archive/%f %p'
recovery_target_time = '2024-01-15 14:30:00 UTC'
recovery_target_action = 'promote'   # open for writes after reaching target

# PostgreSQL replays WAL until the target time
# Stops at the first commit after target_time
# Then promotes (or pauses for inspection)

-- During recovery, monitor progress:
SELECT pg_last_xact_replay_timestamp();
-- Watch this advance toward your target time

-- After recovery: verify you're at the right point
SELECT max(created_at) FROM orders;  -- did you recover enough?</code></pre><hr /><h2>Part 9: Hands-On Lab</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>-- ═══ 1. Set up logical replication locally ═══

-- Publisher database (run in psql -d publisher_db):
CREATE TABLE products (
    id bigserial primary key,
    name text,
    price numeric,
    updated_at timestamptz default now()
);
INSERT INTO products(name, price)
VALUES ('Widget A', 9.99), ('Widget B', 19.99), ('Widget C', 4.99);

CREATE PUBLICATION products_pub FOR TABLE products;

SELECT * FROM pg_publication;
SELECT * FROM pg_publication_tables;

-- ═══ 2. Create subscriber ═══

-- Subscriber database (run in psql -d subscriber_db):
CREATE TABLE products (
    id bigint primary key,
    name text,
    price numeric,
    updated_at timestamptz
    -- note: no serial, subscriber doesn't generate IDs
);

CREATE SUBSCRIPTION products_sub
    CONNECTION 'host=localhost dbname=publisher_db user=replicator'
    PUBLICATION products_pub;

-- Check initial sync:
SELECT * FROM products;
-- Should have 3 rows copied from publisher

-- ═══ 3. Watch changes replicate ═══

-- Publisher:
INSERT INTO products(name, price) VALUES ('Widget D', 29.99);
UPDATE products SET price = 11.99 WHERE name = 'Widget A';
DELETE FROM products WHERE name = 'Widget C';

-- Subscriber (immediately after):
SELECT * FROM products;
-- Should reflect all three changes

-- ═══ 4. Monitor the logical slot ═══

-- Publisher:
SELECT slot_name, confirmed_flush_lsn, restart_lsn,
       pg_size_pretty(
           pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
       ) AS retained_wal
FROM pg_replication_slots;

-- ═══ 5. Peek at raw logical changes ═══

SELECT pg_create_logical_replication_slot('inspect_slot', 'pgoutput');

-- Make some changes:
INSERT INTO products(name, price) VALUES ('Widget E', 39.99);

-- Peek at the decoded stream:
SELECT lsn, xid, data
FROM pg_logical_slot_peek_binary_changes(
    'inspect_slot', NULL, NULL,
    'proto_version', '1',
    'publication_names', 'products_pub'
);

-- Clean up:
SELECT pg_drop_replication_slot('inspect_slot');

-- ═══ 6. Simulate replication lag ═══

-- Pause WAL replay on standby:
SELECT pg_wal_replay_pause();

-- Make changes on primary...
INSERT INTO products(name, price)
SELECT 'Lag test ' || g, g FROM generate_series(1,1000) g;

-- Check lag:
SELECT
    pg_wal_lsn_diff(
        pg_last_wal_receive_lsn(),
        pg_last_wal_replay_lsn()
    ) AS lag_bytes;

-- Resume replay:
SELECT pg_wal_replay_resume();

-- Watch lag decrease:
SELECT pg_wal_lsn_diff(
    pg_last_wal_receive_lsn(),
    pg_last_wal_replay_lsn()
) AS lag_bytes;

-- ═══ 7. PITR practice ═══

-- Record a restore target:
SELECT now() AS restore_target, pg_current_wal_lsn() AS restore_lsn;
-- Save these values

-- Make destructive change:
DELETE FROM products WHERE price &lt; 10;

-- Recover to saved LSN:
-- recovery_target_lsn = '0/1A3F058'  (your saved LSN)
-- recovery_target_action = 'promote'</code></pre><hr /><h2>Conclusion</h2><p>This is the final module of the PostgreSQL Internals series. We have now traced the complete lifecycle of data in PostgreSQL from the moment SQL text arrives over the wire, through the parser, analyzer, rewriter, planner, and executor; through MVCC visibility checks reading <code>xmin</code> and <code>xmax</code> on heap pages; through WAL records being written and fsynced on <code>COMMIT</code>; through VACUUM reclaiming dead tuples and setting visibility map bits; through lock acquisition and deadlock detection and now through WAL bytes flowing across the network to replicas, being decoded into row-level changes, and powering high-availability and CDC pipelines.</p><p></p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>PostgreSQL</category>
            <category>Technical</category>
            <category>PGInternals</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/postgres-replication-poster.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[What I read in June 2026]]></title>
            <link>https://sambasivareddy.in/blog/what-i-read-in-june-2026</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/what-i-read-in-june-2026</guid>
            <pubDate>Sun, 28 Jun 2026 07:39:27 GMT</pubDate>
            <description><![CDATA[Summary about the books I read in the month ofJune 2026]]></description>
            <content:encoded><![CDATA[<p>June was a two-book month, both by Japanese authors - one a semi-autobiography, the other an edge-of-the-seat crime thriller. They are:</p><ol><li><p><strong>Norwegian Wood</strong> - by Haruki Murakami</p></li><li><p><strong>The Devotion of Suspect X</strong> - by Keigo Higashino</p></li></ol><hr /><h2>Norwegian Wood</h2><p>A semi-autobiographical novel disclosing a portion of the author's life. I bought this book after coming across a fair share of posts about it on Instagram. But when I started reading, I just couldn't connect with the storytelling. I tried hard pushed through 50 pages - but it didn't click, so I discontinued it.</p><p>So this isn't a review or an opinion - others might love it, it just wasn't for me this time.</p><hr /><h2>The Devotion of Suspect X</h2><p>At this point, a month never goes by without me reading a crime thriller or suspense novel. The same happened in June, and this time it was <em>The Devotion of Suspect X</em> , a crime thriller revolving around a man's murder.</p><h3>The Setup</h3><p>Yasuko lives a quiet life, working in a Tokyo bento shop, a good mother to her only child. But when her ex-husband appears at her door without warning one day, her comfortable world is shattered. When Detective Kusanagi of the Tokyo Police tries to piece together the events of that day, and finds himself confronted by the most puzzling, mysterious circumstances he has ever investigated. Nothing quite makes sense, and it will take a genius to understand the genius behind this particular crime...</p><h3>My Take</h3><p>What I like about this book is that it never moves in one straight line i.e., crime, investigation, and capture. It has small, emotional subplots, each with its own significance, adding value to the main plot. Every character involved in this story has a part to play.</p><p>Also, this book isn't really a typical who did it, you already know more than the detective does at certain points. And that changes the whole feel of it. It's less about who did it and more about how far someone would go for a person they love. Higashino doesn't rush to a clever twist just for the sake of it, the emotion stays right there with the mystery throughout.</p><p><strong>My Ratings: </strong>4.2/5</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Books</category>
            <category>Personal</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/june-read.jpeg" length="0" type="image/jpeg"/>
        </item>
        <item>
            <title><![CDATA[PostgreSQL Table Statistics: When, Where, and How They're Calculated]]></title>
            <link>https://sambasivareddy.in/blog/postgresql-table-statistics-when-where-and-how-theyre-calculated</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/postgresql-table-statistics-when-where-and-how-theyre-calculated</guid>
            <pubDate>Sun, 21 Jun 2026 10:59:36 GMT</pubDate>
            <description><![CDATA[Understand and Learn about the PostgreSQL Table's Statistics and in-depth code walkthrough]]></description>
            <content:encoded><![CDATA[<p>In the previous blog, we explored TableAmRoutine - what it is, and how it gives us the ability to hook a custom storage engine into PostgreSQL without changing any of its core functionality.</p><p>In today's blog, we'll explore PostgreSQL's table statistics - when, where, and how they get calculated - and finally, take a brief look at how these stats actually help our queries.</p><hr /><h2>Part 1: What are Table Statistics &amp; Why they are important?</h2><p>Table statistics are <strong><u>estimates about the data that lives inside a table</u></strong> - things like the most common values in a column and their frequencies, the number of distinct values that exist in a column, correlation between columns, and so on.</p><p>When you run a query, it enters the PostgreSQL planner, which has to find an efficient and optimised way - in terms of memory usage and data access, to produce the required result. But on what basis does the planner decide whether a generated plan is actually optimised and efficient? That's exactly what statistics tell it.</p><p>If the stats are stale, they can easily mislead the planner into generating a bad plan - and a bad plan means bad query performance.</p><hr /><h2>Part 2: Where Do These Statistics Live?</h2><p>Usually, statistics are stored in <strong>two distinct layers</strong>: in-memory during ANALYZE, then persisted across three system catalogs.</p><h3>Layer-1: In-Memory - VacAttrStats</h3><blockquote><p>Reference: vacuum.h</p></blockquote><p>During the ANALYZE operation, each column's computed stats live in a <code>VacAttrStats</code> struct:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct VacAttrStats {
    // Set by ANALYZE before calling typanalyze:
    int         attstattarget;      // sample size target
    Oid         attrtypid;          // column's type OID
    Form_pg_type attrtype;          // pg_type row
    Oid         attrcollid;

    // Set by typanalyze (e.g. std_typanalyze):
    AnalyzeAttrComputeStatsFunc compute_stats;  // fn pointer
    int         minrows;
    void       *extra_data;   // e.g. StdAnalyzeData (eq/lt operators)

    // Filled by compute_stats:
    bool        stats_valid;
    float4      stanullfrac;
    int32       stawidth;
    float4      stadistinct;
    int16       stakind[5];         // slot type codes
    Oid         staop[5];           // operator OIDs per slot
    Oid         stacoll[5];
    float4     *stanumbers[5];      // numeric arrays (frequencies, correlation)
    Datum      *stavalues[5];       // value arrays (MCV values, histogram bounds)
} VacAttrStats;</code></pre><p>This is a transient structure - it exists only during <code>ANALYZE</code> and is freed once the stats are flushed to disk.</p><h3>Layer-2: On-Disk - Three System Catalogs</h3><p>Once the stats are computed, those stats are flushed into the PostgreSQL catalog tables namely:</p><ul><li><p><code>pg_class</code></p></li><li><p><code>pg_statistic</code></p></li><li><p><code>pg_statistic_ext</code> + <code>pg_statistic_ext_data</code> - Extended stats</p></li></ul><p><strong><u>pg_class</u></strong></p><p><code>pg_class</code> catalog table holds the table-level stats. Like:</p><table><tbody><tr><th colspan="1" rowspan="1"><p>Column</p></th><th colspan="1" rowspan="1"><p>What it stores</p></th></tr><tr><td colspan="1" rowspan="1"><p>reltuples</p></td><td colspan="1" rowspan="1"><p>Estimated number of live rows</p></td></tr><tr><td colspan="1" rowspan="1"><p>relpages</p></td><td colspan="1" rowspan="1"><p>Number of disk pages (blocks)</p></td></tr><tr><td colspan="1" rowspan="1"><p>relallvisible</p></td><td colspan="1" rowspan="1"><p>Number of all-visible pages (used by index-only scans)</p></td></tr></tbody></table><p><strong><u>pg_statistic</u></strong></p><p><code>pg_statistic</code> catalog table holds per-column stats. One row per <code>(table, column, stainherit)</code> triple.</p><blockquote><p>Reference: <code>update_attstats()</code></p></blockquote><table><tbody><tr><th colspan="1" rowspan="1"><p>Column</p></th><th colspan="1" rowspan="1"><p>What it stores</p></th></tr><tr><td colspan="1" rowspan="1"><p>starelid</p></td><td colspan="1" rowspan="1"><p>Relid of the table</p></td></tr><tr><td colspan="1" rowspan="1"><p>staattnum</p></td><td colspan="1" rowspan="1"><p>Column position in the table</p></td></tr><tr><td colspan="1" rowspan="1"><p>stainherit</p></td><td colspan="1" rowspan="1"><p>Is this column inherited by child tables?</p></td></tr><tr><td colspan="1" rowspan="1"><p>stanullfrac</p></td><td colspan="1" rowspan="1"><p>Fraction of null values in this column</p></td></tr><tr><td colspan="1" rowspan="1"><p>stawidth</p></td><td colspan="1" rowspan="1"><p>Width/Size of the data it stores</p></td></tr><tr><td colspan="1" rowspan="1"><p>stadistinct</p></td><td colspan="1" rowspan="1"><p>No. of distinct data values it has</p></td></tr><tr><td colspan="1" rowspan="1"><p>stakind<code>1..5</code></p></td><td colspan="1" rowspan="1"><p>Integer kind code (what type of stat is in this slot)</p></td></tr><tr><td colspan="1" rowspan="1"><p>staop<code>1..5</code></p></td><td colspan="1" rowspan="1"><p>Operator OID used (e.g. <code>=</code> for MCV, <code>&lt;</code> for histogram)</p></td></tr><tr><td colspan="1" rowspan="1"><p>stacoll<code>1..5</code></p></td><td colspan="1" rowspan="1"><p>Collation OID</p></td></tr><tr><td colspan="1" rowspan="1"><p>stanumbers<code>1..5</code></p></td><td colspan="1" rowspan="1"><p><code>float4[]</code> - numeric data (frequencies, correlation coefficient)</p></td></tr><tr><td colspan="1" rowspan="1"><p>stavalues<code>1..5</code></p></td><td colspan="1" rowspan="1"><p><code>anyarray</code> — actual data values (MCV values, histogram bounds)</p></td></tr></tbody></table><p>The <strong>kind codes</strong> defined in <code>pg_statistic.h</code> determine what each slot means:</p><table><tbody><tr><th colspan="1" rowspan="1"><p>stakind</p></th><th colspan="1" rowspan="1"><p>Name</p></th><th colspan="1" rowspan="1"><p>stanumbers hold</p></th><th colspan="1" rowspan="1"><p>stavalues hold</p></th></tr><tr><td colspan="1" rowspan="1"><p>1</p></td><td colspan="1" rowspan="1"><p><code>STATISTIC_KIND_MCV</code></p></td><td colspan="1" rowspan="1"><p>frequenices (most-&gt;least)</p></td><td colspan="1" rowspan="1"><p>the K most common values</p></td></tr><tr><td colspan="1" rowspan="1"><p>2</p></td><td colspan="1" rowspan="1"><p><code>STATISTIC_KIND_HISTOGRAM</code></p></td><td colspan="1" rowspan="1"><p>NULL</p></td><td colspan="1" rowspan="1"><p>M equi-depth boundary values (first=MIN, last=MAX)</p></td></tr><tr><td colspan="1" rowspan="1"><p>3</p></td><td colspan="1" rowspan="1"><p><code>STATISTIC_KIND_CORRELATION</code></p></td><td colspan="1" rowspan="1"><p>Pearson r coefficient</p></td><td colspan="1" rowspan="1"><p>NULL</p></td></tr><tr><td colspan="1" rowspan="1"><p>4</p></td><td colspan="1" rowspan="1"><p><code>STATISTIC_KIND_MCELEM</code></p></td><td colspan="1" rowspan="1"><p>element frequencies + min/max</p></td><td colspan="1" rowspan="1"><p>most common array / tsvector elements</p></td></tr><tr><td colspan="1" rowspan="1"><p>5</p></td><td colspan="1" rowspan="1"><p><code>STATISTIC_KIND_DECHIST</code></p></td><td colspan="1" rowspan="1"><p>distinct-element-count histogram + avg</p></td><td colspan="1" rowspan="1"><p>NULL</p></td></tr><tr><td colspan="1" rowspan="1"><p>6</p></td><td colspan="1" rowspan="1"><p><code>STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM</code></p></td><td colspan="1" rowspan="1"><p>fraction of empty ranges</p></td><td colspan="1" rowspan="1"><p>histogram of range lengths</p></td></tr><tr><td colspan="1" rowspan="1"><p>7</p></td><td colspan="1" rowspan="1"><p><code>STATISTIC_KIND_BOUNDS_HISTOGRAM</code></p></td><td colspan="1" rowspan="1"><p>NULL</p></td><td colspan="1" rowspan="1"><p>interleaved lower/upper bound histogram</p></td></tr></tbody></table><p><code>pg_statistic_ext + pg_statistic_ext_data</code><u> - Extended stats</u></p><p>Created by <code>CREATE STATISTICS</code>, populated by ANALYZE. split across two tables by design:</p><p><code>pg_statistic_ext</code> (<code>OID 3381</code>) - the <em>definition</em> (created once, survives until <code>DROP STATISTICS</code>):</p><table><tbody><tr><th colspan="1" rowspan="1"><p>Column</p></th><th colspan="1" rowspan="1"><p>Meaning</p></th></tr><tr><td colspan="1" rowspan="1"><p>stxrelid</p></td><td colspan="1" rowspan="1"><p>Table OID</p></td></tr><tr><td colspan="1" rowspan="1"><p>stxname/stxnamespace</p></td><td colspan="1" rowspan="1"><p>Name and Schema</p></td></tr><tr><td colspan="1" rowspan="1"><p>stxkeys</p></td><td colspan="1" rowspan="1"><p><code>int2vector</code> of columns attnums covered</p></td></tr><tr><td colspan="1" rowspan="1"><p>stxkind</p></td><td colspan="1" rowspan="1"><p>What stat kinds to compute: <code>d</code> = distinct, <code>f</code> = dependencies, <code>m</code> = MCV, <code>e</code> = expressions</p></td></tr><tr><td colspan="1" rowspan="1"><p>stxexprs</p></td><td colspan="1" rowspan="1"><p>Expression tress for expression statistics</p></td></tr></tbody></table><p><code>pg_statistic_ext_data</code>(<code>OID 3429</code>) — the <em>computed data</em> (overwritten each ANALYZE):</p><table><tbody><tr><th colspan="1" rowspan="1"><p>Column</p></th><th colspan="1" rowspan="1"><p>Meaning</p></th></tr><tr><td colspan="1" rowspan="1"><p>stxoid</p></td><td colspan="1" rowspan="1"><p>FK → <code>pg_statistic_ext</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>stxdinherit</p></td><td colspan="1" rowspan="1"><p>Includes child tables?</p></td></tr><tr><td colspan="1" rowspan="1"><p>stxdndistinct</p></td><td colspan="1" rowspan="1"><p>Serialized multi-column n-distinct coefficients</p></td></tr><tr><td colspan="1" rowspan="1"><p>stxddependencies</p></td><td colspan="1" rowspan="1"><p>Serialized functional dependency degrees</p></td></tr><tr><td colspan="1" rowspan="1"><p>stxdmcv</p></td><td colspan="1" rowspan="1"><p>Serialized multi-column MCV list</p></td></tr><tr><td colspan="1" rowspan="1"><p>stxdexpr</p></td><td colspan="1" rowspan="1"><p>Per-expression stats (same <code>pg_statistic</code> row format)</p></td></tr></tbody></table><hr /><h2>Part 3: When Is ANALYZE Called</h2><p>There are two distinct ways <code>ANALYZE</code> gets triggered: <strong>manually</strong>, by you running the command yourself, and <strong>automatically</strong>, via autovacuum's analyze sub-process.</p><h3>Trigger 1: Manual ANALYZE</h3><p>We can invoke it directly:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">ANALYZE table_name;            -- single table
ANALYZE table_name(col1, col2); -- specific columns only
ANALYZE;                        -- entire database</code></pre><p>This runs synchronously, in our session, immediately recomputing stats for the target relation(s).</p><h3>Trigger 2: Autoanalyze - The Threshold Formula</h3><p>PostgreSQL's autovacuum launcher doesn't just watch for dead tuples (which trigger <code>VACUUM</code>) - it separately tracks how many rows have been <strong>inserted, updated, or deleted</strong> since the last <code>ANALYZE</code>, and compares that against a computed threshold:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">analyze threshold = autovacuum_analyze_threshold + (autovacuum_analyze_scale_factor * reltuples)</code></pre><p>Where:</p><ul><li><p><code>autovacuum_analyze_threshold</code> : a flat minimum, default <strong>50</strong> rows</p></li><li><p><code>autovacuum_analyze_scale_factor</code> : a fraction of table size, default <strong>0.1</strong> (10%)</p></li><li><p><code>reltuples</code> : the table's estimated row count, read from <code>pg_class</code></p></li></ul><p>So for a table with <code>100,000</code> rows, using defaults: the analyze threshold equals the analyze base threshold plus the analyze scale factor multiplied by the number of tuples, giving <code>50 + (0.1 × 100,000) = 10,050</code>. Once <code>10,050+</code> rows have changed since the last <code>ANALYZE</code>, the table becomes a candidate for autoanalyze.</p><hr /><h2>Part 4: How Are They Actually Computed? - The ANALYZE Call Tree</h2><p>Here's the full execution path, from the moment we run <code>ANALYZE</code> to the moment stats land in <code>pg_statistic</code>:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-cpp">ANALYZE command =&gt; {
    analyze_rel() =&gt; {
        do_analyze_rel() =&gt; {
            // examine_attribute() is called once per column to analyze
            // Builds VacAttrStats per column
            examine_attribute() =&gt; {
                std_typanalyze(); // sets the compute_stats fn pointer
            }

            // acquire_sample_rows() is called once per column to sample rows
            // samples ~300 × target rows using reservoir sampling
            acquire_sample_rows() =&gt; {
                BlockSampler_Init(); // picks random blocks to read
                reservoir sampling; // keeps a uniform random subset
            }

            // compute_stats() is called once per column to compute statistics
            // computes scalar, distinct, and trivial statistics
            compute_stats() =&gt; {
                compute_scalar_stats(); // computes scalar statistics 
                compute_distinct_stats(); // computes distinct statistics
                compute_trivial_stats(); // computes trivial statistics
            }
            
            update_attstats() =&gt; {
                INSERT/UPDATE into pg_statistic
            }
        }
    }
}</code></pre><hr /><h2>Part 5: Why Any of This Matters - Stats and the Query Planner</h2><p>Everything we've covered so far - <code>pg_statistic</code>, the <code>ANALYZE</code> call tree, the sampling - exists for one reason: so that when the planner sees a query, it can guess <strong>how many rows will come out of each step</strong>, before actually running it. Those row estimates are what decide whether you get a fast index scan or a slow sequential scan.</p><p>Let's see this directly, with a table simple enough to set up in seconds:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>CREATE TABLE sample (id INT PRIMARY KEY, col1 TEXT);
INSERT INTO sample SELECT i, 'col' || i FROM generate_series(1, 1000000) i;
ANALYZE sample;</code></pre><p>A million rows, one indexed integer column, one text column. Now:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>EXPLAIN ANALYZE SELECT * FROM sample WHERE id = 50042;</code></pre><p>Output:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>Index Scan using sample_pkey on sample  (cost=0.42..8.44 rows=1 width=13) (actual time=0.012..0.012 rows=1.00 loops=1)
  Index Cond: (id = 50042)
  Index Searches: 1
  Buffers: shared hit=4
Planning:
  Buffers: shared hit=17
Planning Time: 0.290 ms
Execution Time: 0.028 ms</code></pre><p>Look at <code>rows=1</code> in the planner's estimate, and then <code>rows=1.00</code> in the <strong>actual</strong> execution result right next to it. The planner predicted exactly one row and it was right.</p><p><strong>Why? </strong>Because <code>id</code> is the primary key, which means it's backed by a unique index. When <code>ANALYZE</code> ran, it stored <code>n_distinct = 1,000,000</code> for this column (every value distinct), with no MCV list at all - no value is any more common than another. So when the planner sees <code>id = 50042</code>, it doesn't need a histogram or anything fuzzy: it knows there's exactly one matching value, period. That's why it confidently chose an <strong>Index Scan</strong> instead of scanning the whole table - <code>cost=0.42..8.44</code> is tiny, because the estimate told it there's almost nothing to fetch.</p><p>This is the entire point of everything in Parts 1 through 5. The planner never touched all 1,000,000 rows to make this decision. It looked at the statistics - built from a sample, stored in <code>pg_statistic</code>, computed back when <code>ANALYZE</code> ran - and used them to predict the outcome before running anything.</p><hr /><h2>Part 6: Putting It All Together</h2><p>We started this post asking a simple question: when, where, and how does PostgreSQL calculate the statistics that its planner depends on?</p><p>We've now traced the entire lifecycle:</p><ul><li><p><strong>What stats are</strong> and why a planner can't make good decisions without them </p></li><li><p><strong>Where they live</strong>: transiently in <code>VacAttrStats</code> during <code>ANALYZE</code>, and permanently across <code>pg_class</code>, <code>pg_statistic</code>, and the extended-stats catalogs </p></li><li><p><strong>When they get triggered</strong> : manually, or automatically once autovacuum's analyze threshold formula is crossed </p></li><li><p><strong>How they're actually computed</strong> : the full call tree from <code>analyze_rel()</code> down through sampling and per-column stat computation, to being written back via <code>update_attstats()</code> </p></li><li><p><strong>And finally, why it all matters</strong> - watching the planner use exactly these numbers, live, in a real <code>EXPLAIN ANALYZE</code>, to confidently pick an index scan over a sequential scan</p></li></ul><p></p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>PostgreSQL</category>
            <category>Technical</category>
            <category>PGSourceCode</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/postgresql-table-statistics-poster.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Will I Ever Go Out?]]></title>
            <link>https://sambasivareddy.in/blog/will-i-ever-go-out</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/will-i-ever-go-out</guid>
            <pubDate>Tue, 16 Jun 2026 16:33:17 GMT</pubDate>
            <description><![CDATA[Yes, I go out. But there's a catch.]]></description>
            <content:encoded><![CDATA[<p>Anyone who knows me or has known me at any point in this lifetime, will always ask one question:</p><p><strong><em>"Will you ever go out?"</em></strong></p><p>Well. That is really a tough question for me to answer, to be honest.</p><p><strong><u>And yes. I go out.</u></strong></p><hr /><p><em>FYI, I am not talking about BTech - because I was a big NOO! guy for every single invitation I got from friends to go out, and eventually they stopped asking me anyway- even I would have stopped asking, if someone says NO always. Honestly, not much of a difference </em>- <em>and since my sister was in the same city, it didn't bother me much either then, </em>though it stings a little now<em>. Current working days are not very different either, but I will say - I did go out and hang out with my office friends for a bit (not a bit, maybe some good 1 year), before the NOO! guy in me woke up again. And you can guess what follows.</em></p><hr /><p>Fast forward to present - 3 years of work experience. I said I go out, and I also said the NOO! guy returned after 1 year. So what happened in the remaining 2 years?</p><p>Learnt PostgreSQL and got deep expertise in my domain - oh! Sorry, wrong blog. Back to the topic.</p><p>I worked on myself (not at 100%, but still), got better state of mind, and then one day, my Instagram feed showed a "solo date" reel, which <em>stuck</em> me in mind.</p><hr /><h2>Experimental Day Out</h2><p>On one fine day - not a weak day, mind you; dressed up, wore new shoes, went to a cafe ~20KM from where I stay, sat by myself and ordered coffee and pasta.</p><p>There were friends, families and couples sitting around me, eating, chatting and laughing. And there was me - sitting all by myself, sipping coffee and playing that one feel-good romantic song on Spotify.</p><p>You think awkward right? of course it was, but I sat there through it all to complete the very last bite of the food I ordered. But I kinda liked it, be it taking small length videos, some random pics of random places and food.</p><p><em>So, the catch is - yes, I go out. But alone.</em></p><hr /><h2>The Tradition</h2><p>Since then, I went to movies, restaurants, cafes and even a cricket match - all by myself. Sitting at a table, earphones in, eating. Watching a movie in a theatre, no earphones there, FYI. Sitting in a stadium surrounded by groups cheering together. And then me, just me, in the middle of all of it.</p><p>I won't say I didn't miss going out with people, but at the same time I enjoyed my company after a very very long time, after a series of really bad years.</p><p>The only downside of this tradition is <strong>"It is costly"</strong> and only my wallet is suffering for it :)</p><hr /><h2>To Anyone Reading This</h2><p>It is not like, I would love always go out all by myself, I do love going trips, outings, some gossips and enjoying with friends, even I had some of the great memories in my previous trips, and memories don't have to be so expensive or expressive, a 10-20 minute conversation will do.</p><p><strong>So if any of you have plans to include me, just ping me. I would love to come. <em>Fair warning, there is NOO! guy still in me, so I might say No, but don't take it to heart.</em></strong></p><p><em>See Yaa!!</em></p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Personal</category>
            <category>Thoughts</category>
        </item>
        <item>
            <title><![CDATA[TableAmRoutine - your way of integrating a custom storage engine into PostgreSQL]]></title>
            <link>https://sambasivareddy.in/blog/tableamroutine-your-way-of-integrating-a-custom-storage-engine-into-postgresql</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/tableamroutine-your-way-of-integrating-a-custom-storage-engine-into-postgresql</guid>
            <pubDate>Sat, 13 Jun 2026 10:49:45 GMT</pubDate>
            <description><![CDATA[Learn how TableAmRoutine works under the hood and use it to plug your custom storage engine into PostgreSQL without touching the core.]]></description>
            <content:encoded><![CDATA[<p>Ever thought about building a <strong>custom storage engine</strong> for PostgreSQL like columnar, in-memory, compressed without rewriting the planner or executor? That's exactly what <code>TableAmRoutine</code> enables. You just implement ~30 callbacks, tell PostgreSQL where to look, and the rest of the engine works as-is.</p><hr /><h2>Part 1: What is TableAmRoutine?</h2><p><code>TableAmRoutine</code> is the <strong>Table Access Method (AM) API</strong> - a vtable (struct of function pointers) that defines the <em>complete interface between the PostgreSQL executor/planner and a storage engine</em>. This is a core extensibility feature that lets change how PostgreSQL store and retrieve the data without changing the planner and executor layer.</p><p>Every table in PostgreSQL has a <code>pg_am</code> catalog entry. When the executor needs to do anything with a table be it scan, insert, vacuum. It goes through <code>rel-&gt;rd_tableam</code> , which points to the registered <code>TableAmRoutine</code> for that table.</p><hr /><h2>Part 2: Some understanding with an Example</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">CREATE TABLE sample (id int primary key, col1 int);

SELECT * FROM pg_am;
 oid  | amname |      amhandler       | amtype
------+--------+----------------------+--------
    2 | heap   | heap_tableam_handler | t
  403 | btree  | bthandler            | i
  405 | hash   | hashhandler          | i
  783 | gist   | gisthandler          | i
 2742 | gin    | ginhandler           | i
 4000 | spgist | spghandler           | i
 3580 | brin   | brinhandler          | i
(7 rows)

SELECT relname, relam FROM pg_class WHERE oid = 'sample'::regclass;
 relname | relam
---------+-------
 sample  |     2
(1 row)</code></pre><p>Now if you see the example provided, <code>relam</code> of "sample" table points to <code>2</code> i.e, <code>heap</code> in pg_am. Now when we ran a query on the top of the table "sample", now postgresql calls the handler function i.e <code>heap_tableam_handler</code> which returns the <code>TableAmRoutine</code> containing all the necessary function definitions can be used to perform the operations like SELECT, DMLs, VACUUM etc..</p><blockquote><p><strong>Reference</strong>: <code>heapam_methods</code> is the Heap's TableAmRoutine exists at <code>access/heap/heapam_handler.c</code></p></blockquote><hr /><h2>Part 3: The Core Design</h2><h3>TableAmRoutine</h3><p>An API struct for a table AM (Access Methods), which should be allocated in a server-lifetime, typically as a <code>static const struct</code> .</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct TableAmRoutine {
    NodeTag type;        // must be T_TableAmRoutine

    // ~30 function pointers grouped by category
    const TupleTableSlotOps *(*slot_callbacks)(Relation rel);
    TableScanDesc (*scan_begin)(...);
    bool (*tuple_insert)(...);
    // ...
} TableAmRoutine;</code></pre><blockquote><p><strong>Reference</strong>: <code>tableam.h</code></p></blockquote><p>Now whatever the custom storage engine we are writing should define the respective function definitions and map to the respective pointers in the <code>TableAmRoutine</code>. To explain this further, we are going to use existing Heap Access Methods.</p><h3>Heap AM Vtable</h3><p>The heap AM implements its own <code>TableAmRoutine</code> vtable as a <code>static const struct</code> as explained above in the <code>heapam_handler.c</code></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>static const TableAmRoutine heapam_methods = {
    .type = T_TableAmRoutine,
    .slot_callbacks       = heapam_slot_callbacks,
    .scan_begin           = heap_beginscan,
    .tuple_insert         = heapam_tuple_insert,
    .tuple_update         = heapam_tuple_update,
    .relation_vacuum      = heap_vacuum_rel,
    // ...
};</code></pre><h3>The Handler Function</h3><p>This is the entry point which PostgreSQL calls to get the particular custom AM's <code>TableAmRoutine</code>. In heap case, it is defined in the file <code>heapam_handler.c</code> as:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">Datum
heap_tableam_handler(PG_FUNCTION_ARGS)
{
	PG_RETURN_POINTER(&amp;heapam_methods);
}</code></pre><p>If you observe the name of the handler function <code>heap_tableam_handler</code> . It is the same name stored in the <code>pg_am</code> 's <code>amhandler</code> shown in the part 2.</p><h3>Register the Access Method</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">CREATE ACCESS METHOD heap TYPE TABLE HANDLER heap_tableam_handler;</code></pre><p>This triggers <code>CreateAccessMethod()</code> in <code>amcmds.c</code>, which:</p><p>1. Resolves <code>heap_tableam_handler</code> and validates it returns <code>TABLE_AM_HANDLEROID</code></p><p>2. Inserts one row into <code>pg_am</code> with <code>amtype = 't'</code> (<code>t</code> for table, <code>i</code> for index)</p><p>3. Records a dependency: <strong>dropping the handler function also drops the AM</strong></p><h3>Usage</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- New table:
CREATE TABLE my_table (id int, data text) USING heap; -- default is heap anyway, used just for example

-- Existing table (rewrites data):
ALTER TABLE existing_table SET ACCESS METHOD heap;

-- Make it the default for the session:
SET default_table_access_method = heap;</code></pre><hr /><h2>Part 4: How PostgreSQL accesses this when querying?</h2><p>Every time PostgreSQL tries to open a Relation/Table, it usually loads all the required information into the struct <code>Relation</code> (alias of <code>RelationData</code> ) be it the tuple descriptor, table's OID, PK information etc. In those, there are two properties related to access methods i.e, <code>oid rd_amhandler</code> the OID of the access method (like heap) using by this table and <code>TableAmRoutine *rd_tableam</code> containing the vtable given by the <code>amhandler</code> , in our case methods defined in <code>heapam_methods</code>.</p><h3>Code flow path in case of relation_open()</h3><p>Now let's see what is the path of the code which populates the access methods when a relation/table is opened using the function <code>relation_open()</code> .</p><blockquote><p>Note: The nested <code>=&gt; { }</code> style below is simplified pseudocode showing the call chain - see <code>relation.c</code> , <code>relcache.c</code> for the actual C source.</p></blockquote><p><strong>Entry Point</strong>: relation_open() in the file <code>common/relation.c</code> </p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">Relation relation_open(oid relationId) {
   Relation r;
   // .....
   r = RelationIdGetRelation(relationId); =&gt; {
      Relation rd;
      // ..... Here looks into cache, if hit return that, else build Relation

      // Cache Miss: Building from scratch
      rd = RelationBuildDesc(relationId, true); =&gt; {
          // Here all the necessary information will be populated
          Relation	relation;

          // ......
          else if (RELKIND_HAS_TABLE_AM(relation-&gt;rd_rel-&gt;relkind) ||
                   relation-&gt;rd_rel-&gt;relkind == RELKIND_SEQUENCE) {
                    // OUR AREA OF INTEREST
                    RelationInitTableAccessMethod(relation); =&gt; {
                      // Here, the cache lookup will be done to get
                      // the OID of Access method using by this relation
                      tuple = SearchSysCache1(AMOID,
                               ObjectIdGetDatum(relation-&gt;rd_rel-&gt;relam));
                      aform = (Form_pg_am) GETSTRUCT(tuple);
                      relation-&gt;rd_amhandler = aform-&gt;amhandler; //OID saved here

                      /*
                       * Now we can fetch the table AM's API struct
                       */
                      InitTableAmRoutine(relation); =&gt; {
                         relation-&gt;rd_tableam = GetTableAmRoutine(relation-&gt;rd_amhandler); =&gt; {
                             datum = OidFunctionCall0(amhandler);
                             routine = (TableAmRoutine *) DatumGetPointer(datum);
                             // Some checks will be done here
                             return routine; // THIS IS OUR TABLEAMROUTINE
                         }
                      }
                  }
          }
          // ......
          return relation;
      }
      // .....
      return rd;
   }
   // .....
   return r;
}</code></pre><hr /><h2>Part 5: Callback Groups</h2><p>Below table summarizing the callback pointer functions declared in <code>TableAmRoutine</code> </p><table><tbody><tr><th colspan="1" rowspan="1"><p>Group</p></th><th colspan="1" rowspan="1"><p>Key Callbacks</p></th><th colspan="1" rowspan="1"><p>Purpose</p></th></tr><tr><td colspan="1" rowspan="1"><p>Slot</p></td><td colspan="1" rowspan="1"><p><code>slot_callbacks</code></p></td><td colspan="1" rowspan="1"><p>What kind of <code>TupleTableSlot</code> to use for this AM</p></td></tr><tr><td colspan="1" rowspan="1"><p>Sequential Scan</p></td><td colspan="1" rowspan="1"><p><code>scan_begin</code>, <code>scan_end</code>, <code>scan_getnextslot</code></p></td><td colspan="1" rowspan="1"><p>Full table scans</p></td></tr><tr><td colspan="1" rowspan="1"><p>TID Range Scan</p></td><td colspan="1" rowspan="1"><p><code>scan_set_tidrange</code>, <code>scan_getnextslot_tidrange</code></p></td><td colspan="1" rowspan="1"><p>Range-restricted scans</p></td></tr><tr><td colspan="1" rowspan="1"><p>Parallel Scan</p></td><td colspan="1" rowspan="1"><p><code>parallelscan_estimate</code>, <code>parallelscan_initialize</code></p></td><td colspan="1" rowspan="1"><p>Parallel query support</p></td></tr><tr><td colspan="1" rowspan="1"><p>Index Scan</p></td><td colspan="1" rowspan="1"><p><code>index_fetch_begin</code>, <code>index_fetch_tuple</code></p></td><td colspan="1" rowspan="1"><p>Fetching tuples via index TIDs</p></td></tr><tr><td colspan="1" rowspan="1"><p>DML</p></td><td colspan="1" rowspan="1"><p><code>tuple_insert</code>, <code>tuple_update</code>, <code>tuple_delete</code>, <code>tuple_lock</code></p></td><td colspan="1" rowspan="1"><p>Row mutations</p></td></tr><tr><td colspan="1" rowspan="1"><p>Bulk Insert</p></td><td colspan="1" rowspan="1"><p><code>multi_insert</code>, <code>finish_bulk_insert</code></p></td><td colspan="1" rowspan="1"><p>COPY/bulk load path</p></td></tr><tr><td colspan="1" rowspan="1"><p>DDL</p></td><td colspan="1" rowspan="1"><p><code>relation_set_new_filelocator</code>, <code>relation_nontransactional_truncate</code>, <code>relation_copy_data</code></p></td><td colspan="1" rowspan="1"><p>CREATE TABLE, TRUNCATE, CLUSTER</p></td></tr><tr><td colspan="1" rowspan="1"><p>Maintenance</p></td><td colspan="1" rowspan="1"><p><code>relation_vacuum</code>, <code>scan_analyze_next_block/tuple</code>,<code>index_build_range_scan</code></p></td><td colspan="1" rowspan="1"><p>VACUUM, ANALYZE, CREATE INDEX</p></td></tr><tr><td colspan="1" rowspan="1"><p>Planner</p></td><td colspan="1" rowspan="1"><p><code>relation_estimate_size</code></p></td><td colspan="1" rowspan="1"><p>Cost estimation</p></td></tr><tr><td colspan="1" rowspan="1"><p>Executor</p></td><td colspan="1" rowspan="1"><p><code>scan_bitmap_next_tuple</code>, <code>scan_sample_next_block/tuple</code></p></td><td colspan="1" rowspan="1"><p>Bitmap scans, TABLESAMPLE</p></td></tr></tbody></table><hr /><h2>Part 6: Example - How executor uses it?</h2><p>Let's see the case of Executor, how it uses the access methods. The executor never calls these function pointers directly. Instead it uses <code>table_*</code> wrapper functions (<code>tableam.h</code>) which dispatch through <code>rd_tableam</code> :</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-go">static inline TableScanDesc
table_beginscan(Relation rel, Snapshot snapshot, int nkeys, ScanKeyData *key)
{
    uint32 flags = SO_TYPE_SEQSCAN | SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE;
    return rel-&gt;rd_tableam-&gt;scan_begin(rel, snapshot, nkeys, key, NULL, flags);
    //          ^^^^^^^^^^^^ vtable dispatch which calls "heap_beginscan" internally
}</code></pre><hr /><h2>Part 7: Why TableAmRoutine Matters</h2><ol><li><p><strong>Pluggable storage</strong> - You can write a custom AM (columnar, in-memory, compressed, etc.) without touching the core executor. Just implement the ~30 callbacks and register with <code>CREATE ACCESS METHOD</code>.</p></li><li><p><strong>Separation of concerns</strong> - MVCC visibility, tuple format, physical layout, and vacuum strategy are all encapsulated inside the AM. The executor only sees slots and TIDs.</p></li><li><p><strong>Real-world uses</strong> - Citus's columnar AM, Zedstore (experimental), and third-party AMs like Orioledb all use this interface. The default <code>heap</code> AM (<code>DEFAULT_TABLE_ACCESS_METHOD = "heap"</code>, tableam.h:29) is just one implementation.</p></li><li><p><strong>TOAST delegation</strong> - <code>relation_needs_toast_table</code>, <code>relation_toast_am</code>, and <code>relation_fetch_toast_slice</code> let each AM decide its own large-value storage strategy.</p></li></ol><hr /><h2>Closing notes</h2><p>This post covered the interface, the wiring, and how PostgreSQL loads your AM at runtime. What it didn't cover is which callbacks are mandatory versus stubbable, TOAST integration, and the visibility/snapshot contract your AM needs to honour, those deserve their own deep dives. For now, <code>heapam_handler.c</code> and the Orioledb source are the best two codebases to have open while you build.</p><p></p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>PostgreSQL</category>
            <category>Technical</category>
            <category>PGSourceCode</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/tableamroutine_blog_banner_v2.svg" length="0" type="image/svg"/>
        </item>
        <item>
            <title><![CDATA[Building a Local PostgreSQL Internals Assistant - Zero API Costs]]></title>
            <link>https://sambasivareddy.in/blog/building-a-local-postgresql-internals-assistant-zero-api-costs</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/building-a-local-postgresql-internals-assistant-zero-api-costs</guid>
            <pubDate>Fri, 05 Jun 2026 16:36:24 GMT</pubDate>
            <description><![CDATA[Local PostgreSQL internals assistant - semantic search over PG source via pgvector + Ollama, exposed as MCP tools in Zed and VSCode. Free, offline, zero API costs.]]></description>
            <content:encoded><![CDATA[<p>If you work deep in PostgreSQL internals, you know the problem: the source is ~1.5 million lines of C spread across hundreds of files. You want to understand how <code>ReadBuffer</code> relates to <code>StrategyGetBuffer</code>, or trace the WAL flush path from <code>XLogInsert</code> to disk - and you end up either <code>grep</code>-ing blindly or burning through API credits asking a hosted LLM that may not even have the right source version in context.</p><p>I wanted something better: a setup where I could ask natural language questions about PostgreSQL source code and get answers grounded in the actual C files on my machine - not from some LLM's training data. And I wanted it free, local, and integrated into my editor.</p><p>This post walks through exactly that.</p><hr /><h2>What We're Building</h2><p>A three-layer stack:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>ctags + cscope          → deterministic symbol navigation
pgvector on PG 18       → semantic search over indexed source chunks
Ollama (local LLM)      → natural language reasoning over retrieved context
MCP server              → exposes all of this as tools inside Zed / VSCode
</code></pre><p>The key insight: these layers answer <em>different types of questions</em>.</p><ul><li><p><strong>ctags/cscope</strong>: "Where is <code>ReadBuffer</code> defined? Who calls it?" - deterministic, instant</p></li><li><p><strong>pgvector</strong>: "Find code related to buffer eviction clock sweep" - semantic, fuzzy</p></li><li><p><strong>LLM</strong>: "Explain what this function does in context of buffer management" - reasoning</p></li></ul><p>No single tool does all three well. Together, they cover the full range of questions you ask when reading unfamiliar source code.</p><hr /><h2>Prerequisites</h2><ul><li><p>PostgreSQL running locally with pgvector extension</p></li><li><p>PostgreSQL Source code</p></li><li><p>Zed or VSCode with GitHub Copilot / Agent Panel</p></li></ul><hr /><h2>Step 1: Install Ollama</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-bash"># Install via official installer (not Homebrew — the brew version
# may be missing the llama-server binary)
curl -fsSL https://ollama.com/install.sh | sh
</code></pre><p>Pull two models — one for embeddings, one for chat:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-bash"># Lightweight code reasoning model (~4.5GB, runs well on 16GB M-series)
ollama pull qwen2.5-coder:7b

# Embedding model (~500MB)
ollama pull nomic-embed-text
</code></pre><p>A note on RAM: <code>qwen2.5-coder:7b</code> uses ~4.5GB when loaded. Models only load into RAM when a request comes in and unload after ~5 minutes of idle. So the server itself (<code>ollama serve</code>) is just ~50MB - start it when you need it, kill it when done.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-bash"># Start manually
ollama serve

# Stop (Ctrl+C, or from another terminal)
pkill ollama
</code></pre><hr /><h2>Step 2: Set Up pgvector</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-bash">brew install pgvector
(or) 
make PG_CONFIG=$pg_config install</code></pre><p>Connect to your PostgreSQL cluster and set up the schema:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">CREATE DATABASE pg_source_index;
\c pg_source_index

CREATE EXTENSION vector;

CREATE TABLE code_chunks (
    id            SERIAL PRIMARY KEY,
    file_path     TEXT NOT NULL,
    function_name TEXT,
    chunk_text    TEXT NOT NULL,
    embedding     vector(768),
    start_line    INT,
    end_line      INT,
    subsystem     TEXT
);

-- HNSW index for fast cosine similarity search
CREATE INDEX ON code_chunks
USING hnsw (embedding vector_cosine_ops);
</code></pre><p>I set it up on PostgreSQL 18, the table ends up at ~297MB for the full PostgreSQL source (33,734 chunks). </p><hr /><h2>Step 3: Build cscope + ctags Index</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-bash">brew install cscope universal-ctags</code></pre><p>From your PostgreSQL source root:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-bash"># ctags — jump-to-definition, call hierarchies
ctags -R --languages=C --fields=+iaS --extras=+q .

# cscope — cross-reference index (who calls what, where defined)
find . -name "*.c" -o -name "*.h" | xargs ls 2&gt;/dev/null &gt; cscope.files
cscope -b -q -k
</code></pre><p>You'll get a <code>tags</code> file and three <code>cscope.out</code> files. These are deterministic and fast — no LLM involved.</p><blockquote><p><strong>Note</strong>: You may see warnings about missing generated files (<code>fmgroids.h</code>, <code>nodetags.h</code>, etc.). These are build-time generated files. The index still builds correctly for all existing source files.</p></blockquote><hr /><h2>Step 4: Index PG Source into pgvector</h2><p>Create a Python virtual environment:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-bash">python3 -m venv ~/pg-tools-env
source ~/pg-tools-env/bin/activate
pip install psycopg2-binary requests mcp
</code></pre><p>Save this as <code>index_pg_source.py</code>:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-python">import os
import re
import psycopg2
import requests

PG_CONN   = "dbname=pg_source_index"
PG_SRC    = "/path/to/postgresql"       # your PG source root
OLLAMA    = "http://localhost:11434"
EMB_MODEL = "nomic-embed-text"

SUBSYSTEMS = {
    "storage/buffer": "buffer",
    "storage/smgr":   "storage",
    "access/heap":    "heap",
    "executor":       "executor",
    "storage/lmgr":   "locking",
    "access/transam": "mvcc",
    "replication/walreceiver": "wal",
    "access/rmgrdesc": "wal",
}

def get_subsystem(path):
    for pattern, name in SUBSYSTEMS.items():
        if pattern in path:
            return name
    return "other"

def extract_functions(content):
    chunks = []
    lines = content.split('\n')
    current_chunk = []
    current_start = 1
    brace_depth = 0
    in_function = False

    for i, line in enumerate(lines, 1):
        current_chunk.append(line)
        brace_depth += line.count('{') - line.count('}')
        if brace_depth == 0 and in_function:
            chunk_text = '\n'.join(current_chunk)
            if len(chunk_text.strip()) &gt; 50:
                chunks.append((chunk_text, current_start, i))
            current_chunk = []
            current_start = i + 1
            in_function = False
        elif brace_depth &gt; 0:
            in_function = True

    return chunks

def embed(text):
    resp = requests.post(f"{OLLAMA}/api/embeddings", json={
        "model": EMB_MODEL,
        "prompt": text[:2000]
    })
    return resp.json()["embedding"]

def index_file(cur, filepath):
    with open(filepath, 'r', errors='ignore') as f:
        content = f.read()

    subsystem = get_subsystem(filepath)
    chunks = extract_functions(content)

    for chunk_text, start, end in chunks:
        match = re.search(r'^(\w+)\s*\(', chunk_text, re.MULTILINE)
        fn_name = match.group(1) if match else None
        embedding = embed(chunk_text)
        cur.execute("""
            INSERT INTO code_chunks
                (file_path, function_name, chunk_text, embedding,
                 start_line, end_line, subsystem)
            VALUES (%s, %s, %s, %s, %s, %s, %s)
        """, (filepath, fn_name, chunk_text, embedding, start, end, subsystem))

def main():
    conn = psycopg2.connect(PG_CONN)
    conn.autocommit = True
    cur = conn.cursor()

    c_files = []
    for root, _, files in os.walk(PG_SRC):
        for f in files:
            if f.endswith('.c') or f.endswith('.h'):
                c_files.append(os.path.join(root, f))

    print(f"Indexing {len(c_files)} files...")
    for i, fp in enumerate(c_files):
        print(f"[{i+1}/{len(c_files)}] {fp}")
        try:
            index_file(cur, fp)
        except Exception as e:
            print(f"  ERROR: {e}")

    print("Done.")

if __name__ == "__main__":
    main()
</code></pre><p>Run it (takes 15–30 minutes, one-time):</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-bash">ollama serve &amp;
python index_pg_source.py
</code></pre><p>Verify:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT COUNT(*) FROM code_chunks;
</code></pre><hr /><h2>Step 5: The MCP Server</h2><p>This is where it comes together. Instead of switching to a terminal to query the index, we expose it as MCP tools directly inside the editor.</p><p>Save as <code>pg_mcp_server.py</code>:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-python">import subprocess
import psycopg2
import requests
from mcp.server.fastmcp import FastMCP

PG_CONN     = "dbname=pg_source_index"
OLLAMA      = "http://localhost:11434"
EMB_MODEL   = "nomic-embed-text"
CHAT_MODEL  = "qwen2.5-coder:7b"
TOP_K       = 5
PG_SRC      = "/path/to/postgresql"     # where cscope.out lives

mcp = FastMCP("pg-source-explorer")

def embed(text):
    resp = requests.post(f"{OLLAMA}/api/embeddings", json={
        "model": EMB_MODEL, "prompt": text[:2000]
    })
    return resp.json()["embedding"]

def vector_search(query, subsystem=None, top_k=TOP_K):
    conn = psycopg2.connect(PG_CONN)
    cur  = conn.cursor()
    q_embed = str(embed(query))
    if subsystem:
        cur.execute("""
            SELECT file_path, function_name, chunk_text, subsystem,
                   1 - (embedding &lt;=&gt; %s::vector) AS similarity
            FROM code_chunks WHERE subsystem = %s
            ORDER BY embedding &lt;=&gt; %s::vector LIMIT %s
        """, (q_embed, subsystem, q_embed, top_k))
    else:
        cur.execute("""
            SELECT file_path, function_name, chunk_text, subsystem,
                   1 - (embedding &lt;=&gt; %s::vector) AS similarity
            FROM code_chunks
            ORDER BY embedding &lt;=&gt; %s::vector LIMIT %s
        """, (q_embed, q_embed, top_k))
    rows = cur.fetchall()
    cur.close(); conn.close()
    return rows

@mcp.tool()
def ask_pg(question: str, subsystem: str = "") -&gt; str:
    """
    Ask a natural language question about PostgreSQL internals.
    Searches indexed PG source and returns an LLM answer grounded
    in actual source context.

    Args:
        question  : e.g. "How does PostgreSQL evict dirty buffers?"
        subsystem : Optional filter — buffer, wal, executor, mvcc,
                    heap, storage, locking. Leave empty for all.
    """
    results = vector_search(question, subsystem=subsystem.strip() or None)
    if not results:
        return "No relevant chunks found. Is the index built?"

    context = "\n\n---\n\n".join([
        f"File: {r[0]}\nFunction: {r[1]}\nSubsystem: {r[3]}\n\n{r[2]}"
        for r in results
    ])
    prompt = f"""You are an expert in PostgreSQL internals at C source level.
Use the source code context below to answer the question.
Always reference specific function names and file paths.

CONTEXT:
{context}

QUESTION: {question}
Answer:"""

    resp = requests.post(f"{OLLAMA}/api/generate", json={
        "model": CHAT_MODEL, "prompt": prompt, "stream": False
    })
    return resp.json()["response"]

@mcp.tool()
def search_pg(query: str, subsystem: str = "", top_k: int = 5) -&gt; str:
    """
    Semantic search over indexed PostgreSQL source.
    Returns top matching code chunks with file paths and similarity scores.

    Args:
        query     : e.g. "buffer eviction clock sweep"
        subsystem : Optional filter (buffer, wal, executor, etc.)
        top_k     : Number of results (default 5, max 10)
    """
    results = vector_search(query, subsystem=subsystem.strip() or None,
                            top_k=min(top_k, 10))
    if not results:
        return "No results found."

    output = []
    for i, r in enumerate(results, 1):
        output.append(
            f"── Result {i} ──\n"
            f"File: {r[0]}  Function: {r[1]}  "
            f"Subsystem: {r[3]}  Similarity: {r[4]:.3f}\n\n"
            f"{r[2][:800]}{'...' if len(r[2]) &gt; 800 else ''}"
        )
    return "\n\n".join(output)

@mcp.tool()
def search_symbol(symbol: str, search_type: str = "callers") -&gt; str:
    """
    Look up a C symbol in PostgreSQL source using cscope.

    Args:
        symbol      : e.g. "ReadBuffer", "BufMgrLock"
        search_type : callers | definition | references | callees
    """
    type_map = {"callers": "3", "definition": "1",
                "references": "0", "callees": "2"}
    flag = type_map.get(search_type, "3")

    try:
        result = subprocess.run(
            ["cscope", "-d", "-f", f"{PG_SRC}/cscope.out",
             "-L", f"-{flag}", symbol],
            capture_output=True, text=True, timeout=10
        )
        if not result.stdout.strip():
            return f"No {search_type} found for '{symbol}'."

        lines = result.stdout.strip().split('\n')
        output = [f"cscope {search_type} for '{symbol}':\n"]
        for line in lines[:30]:
            parts = line.split()
            if len(parts) &gt;= 3:
                output.append(f"  {parts[0]}:{parts[2]}  (in {parts[1]})")
            else:
                output.append(f"  {line}")
        if len(lines) &gt; 30:
            output.append(f"\n  ... and {len(lines) - 30} more")
        return "\n".join(output)

    except FileNotFoundError:
        return "cscope not found. Run: brew install cscope"
    except subprocess.TimeoutExpired:
        return "cscope timed out."

if __name__ == "__main__":
    print("Starting pg-source-explorer MCP server...")
    print(f"  Chat model : {CHAT_MODEL}")
    print(f"  Embed model: {EMB_MODEL}")
    mcp.run(transport="stdio")
</code></pre><hr /><h2>Step 6: Wire into Zed</h2><p>In Zed <code>settings.json</code> (<code>Cmd+Shift+P</code> → open settings):</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-json">{
  "context_servers": {
    "pg-source-explorer": {
      "source": "custom",
      "command": {
        "path": "/Users/yourname/pg-tools-env/bin/python",
        "args": ["/path/to/pg_mcp_server.py"]
      }
    }
  }
}
</code></pre><p>Restart Zed. Check the Agent Panel for a green dot next to <code>pg-source-explorer</code>.</p><h2>Step 6 (alt) — Wire into VSCode</h2><p>Create <code>~/.vscode/mcp.json</code> for global access:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-json">{
  "servers": {
    "pg-source-explorer": {
      "type": "stdio",
      "command": "/Users/yourname/pg-tools-env/bin/python",
      "args": ["/path/to/pg_mcp_server.py"],
      "env": {}
    }
  }
}
</code></pre><p>Open the file in VSCode — click the <strong>Start</strong> CodeLens button that appears above the server entry. Then switch Copilot Chat to Agent mode and enable the tools.</p><hr /><h2>Using It</h2><p>Always start your PostgreSQL cluster and Ollama first:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-bash">bin/pg_ctl -D /path/to/cluster start
source ~/pg-tools-env/bin/activate
ollama serve
</code></pre><p>Then in the Agent Panel:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code># Full RAG answer grounded in source
Use the ask_pg tool to explain how PostgreSQL evicts dirty buffers

# Raw source chunks
Use search_pg to find code related to clock sweep

# Call graph
Use search_symbol to find who calls ReadBuffer</code></pre><hr /><h2>Does It Actually Help?</h2><p>Honest answer: yes, with caveats.</p><p>The biggest value is <strong>cscope + ctags</strong> — deterministic, instant, no LLM needed. </p><p>The RAG layer (<code>ask_pg</code>) is genuinely useful when you want to understand <em>what a function does in context</em>, not just navigate to it. The answer is grounded in your local source, not the LLM's training data - so it references the exact version of PG you're working with.</p><p>Here's a concrete example. When I asked about <code>MemoryContext</code>, the RAG answer surfaced the <strong>callback mechanism</strong> (<code>MemoryContextCallback</code>) - a detail the hosted LLM skipped entirely. That came directly from <code>mcxt.h</code> chunks in the index.</p><p>The LLM layer (<code>qwen2.5-coder:7b</code>) is a 7B model - fine for code explanation, not as strong as Claude for architecture reasoning. I use local LLM for source navigation, hosted Claude for design decisions.</p><table><tbody><tr><th colspan="1" rowspan="1"><p>Component</p></th><th colspan="1" rowspan="1"><p>RAM</p></th></tr><tr><td colspan="1" rowspan="1"><p>Ollama (idle)</p></td><td colspan="1" rowspan="1"><p>~50MB</p></td></tr><tr><td colspan="1" rowspan="1"><p>nomic-embed-text loaded</p></td><td colspan="1" rowspan="1"><p>~550MB</p></td></tr><tr><td colspan="1" rowspan="1"><p>qwen2.5-coder:7b loaded</p></td><td colspan="1" rowspan="1"><p>~4.5GB</p></td></tr><tr><td colspan="1" rowspan="1"><p>PG 18 cluster</p></td><td colspan="1" rowspan="1"><p>~200MB</p></td></tr><tr><td colspan="1" rowspan="1"><p>Zed + browser</p></td><td colspan="1" rowspan="1"><p>~2–3GB</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Total</strong></p></td><td colspan="1" rowspan="1"><p><strong>~7.5GB</strong></p></td></tr></tbody></table><p>Comfortable even on 16GB. Models unload after 5 minutes idle, so you're not paying the RAM cost the whole session.</p><hr /><h2>The Full Stack</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>Zed / VSCode Agent Panel
        ↓
pg-source-explorer MCP (3 tools)
        ↓
ask_pg → pgvector similarity (HNSW) → qwen2.5-coder:7b
search_pg → pgvector similarity
search_symbol → cscope
        ↓
33,734 chunks of PostgreSQL 18 source — on your machine, for free
</code></pre><p>The code is on GitHub: <a target="_blank" rel="noopener noreferrer" class="text-primary underline underline-offset-4 hover:text-primary/80" href="https://github.com/samsiva-dev/pg_mcp_server">https://github.com/samsiva-dev/pg_mcp_server</a></p><hr /><p></p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Technical</category>
            <category>Artificial Intelligence</category>
            <category>LLM</category>
        </item>
        <item>
            <title><![CDATA[pg_ext_memcheck - The memory bug detector that speaks PostgreSQL]]></title>
            <link>https://sambasivareddy.in/blog/pg_ext_memcheck-the-memory-bug-detector-that-speaks-postgresql</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/pg_ext_memcheck-the-memory-bug-detector-that-speaks-postgresql</guid>
            <pubDate>Wed, 27 May 2026 04:26:26 GMT</pubDate>
            <description><![CDATA[A PostgreSQL extension that catches MemoryContext leaks, wrong-context allocations, and shmem overruns — from inside the backend.]]></description>
            <content:encoded><![CDATA[<p><em>Today I'm tagging the first beta of </em><a target="_blank" rel="noopener noreferrer" class="text-primary underline underline-offset-4 hover:text-primary/80" href="https://github.com/samsiva-dev/pg_ext_memcheck"><em>pg_ext_memcheck</em></a><em> — a PostgreSQL extension that finds the class of memory bugs Valgrind structurally cannot see, because they aren't bugs at the </em><code>malloc/free</code><em> layer. They're bugs in the PostgreSQL memory model.</em></p><blockquote><p><strong>TL;DR</strong> — <code>pg_ext_memcheck</code> runs <strong>inside a PostgreSQL backend</strong> and watches the <code>MemoryContext</code> tree, shared-memory boundaries, and DSM lifecycle of another extension under test. It catches context leaks, wrong-context allocations, bloat, and shmem overruns — and attributes each finding to the library that caused it. It complements Valgrind; it does not replace it. CI is green on PG <strong>15, 16, 17, and 18</strong>. MIT-licensed.</p></blockquote><hr /><h2>The gap nobody else fills</h2><p>Valgrind and AddressSanitizer are excellent. If your extension does a use-after-free on a raw <code>malloc</code>'d buffer, or overruns a <code>calloc</code> region, they'll catch it. They've earned their reputation.</p><p>But to those tools, a PostgreSQL backend is just another C program that happens to call <code>malloc</code> and <code>free</code> a lot. They have no idea that PostgreSQL layers a <strong>MemoryContext</strong> abstraction on top of <code>malloc</code>, and that this abstraction has its own correctness rules:</p><ul><li><p>A <code>palloc()</code> in the wrong context (say, <code>TopMemoryContext</code> instead of a per-query context) is a permanent leak for the lifetime of the backend process — but every byte of it is a perfectly valid heap allocation. Valgrind is silent.</p></li><li><p>A child <code>MemoryContext</code> created in <code>_PG_init</code> and never deleted accumulates across every query — but again, the heap allocations are valid. Valgrind is silent.</p></li><li><p>An extension writes one byte past its declared <code>ShmemAlloc()</code> region. The page is still mapped, the write doesn't fault, and the corruption only shows up later when a neighbouring struct gives wrong answers. Valgrind, in most builds, is silent.</p></li><li><p>A <code>MemoryContextReset()</code> doesn't call <code>free()</code> on individual allocations — it bulk-resets a block list. A pointer into a reset context is <strong>not</strong> a use-after-free in the <code>malloc</code> sense, but dereferencing it is still a bug. Valgrind is silent.</p></li></ul><p>These are the bugs that cause "weird, hard-to-reproduce" behaviour: session bloat after a few hours, stale-pointer corruption only under concurrent load, OOMs in long-running connections. Every PostgreSQL extension author has seen at least one. None of them are caught by raw-heap tools.</p><p><code>pg_ext_memcheck</code> is built specifically for that class. The intended workflow stays simple:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-xml">Valgrind / ASan        →  fix raw heap bugs first
pg_ext_memcheck        →  fix PostgreSQL-semantics bugs second ship
</code></pre><hr /><h2>What v0.1.0-beta detects</h2><p>I want to be precise about what's in the box, because a correctness tool that overstates is worse than useless.</p><table><tbody><tr><th colspan="1" rowspan="1"><p>Bug Class</p></th><th colspan="1" rowspan="1"><p>Status</p></th></tr><tr><td colspan="1" rowspan="1"><p>MemoryContext Leak (new contexts that outlive their query)</p></td><td colspan="1" rowspan="1"><p>✅</p></td></tr><tr><td colspan="1" rowspan="1"><p>Wrong Context Allocation (growth in Top/Cache MemoryContext)</p></td><td colspan="1" rowspan="1"><p>✅</p></td></tr><tr><td colspan="1" rowspan="1"><p>Context Bloat over repeated invocations (linear/superlinear growth)</p></td><td colspan="1" rowspan="1"><p>✅</p></td></tr><tr><td colspan="1" rowspan="1"><p>Shared-memory Boundary overrun (sentinel-byte probe)</p></td><td colspan="1" rowspan="1"><p>✅</p></td></tr><tr><td colspan="1" rowspan="1"><p>DSM Segment Leak</p></td><td colspan="1" rowspan="1"><p>±</p></td></tr><tr><td colspan="1" rowspan="1"><p>Use-after-reset (forced reset + re-invoke, crash-safe)</p></td><td colspan="1" rowspan="1"><p>❌</p></td></tr></tbody></table><p>Findings land in a shared-memory ring buffer with severity bands (<code>INFO</code>, <code>WARNING</code>, <code>ERROR</code>), a human-readable detail string, a backend PID, a timestamp — and <strong>attribution back to the library that caused them</strong>, resolved via <code>dladdr()</code> over the planner/executor hook chain. That attribution is what turns a generic "TopMemoryContext grew by 8 KB" message into something you can actually act on: <em>"…and the library that grew it was </em><code>my_extension.so</code><em>."</em></p><hr /><h2>How it works, briefly</h2><p>Three pieces, one ring buffer:</p><ol><li><p><strong>Context walker</strong> — snapshots the <code>MemoryContext</code> tree starting at <code>TopMemoryContext</code>, recording <code>(name, depth, parentHash, totalAllocated, totalFree)</code> for every live context. Identity is name-and-depth based (pointers are unstable across runs), so diffs are stable across PostgreSQL versions.</p></li><li><p><strong>Hook layer</strong> — <code>planner_hook</code> and <code>ExecutorStart</code>/<code>ExecutorEnd</code> hooks bracket a query: before-snapshot at the start, after-snapshot at the end, diff in between. The before-snapshot lives in a dedicated long-lived <code>MemoryContext</code> so its lifetime is independent of whichever transient context the planner happens to be running in.</p></li><li><p><strong>Violation log</strong> — a 2048-entry LWLock-guarded ring buffer in shared memory. Queryable from SQL (<code>ext_memcheck.end()</code> returns the current session's findings; <code>ext_memcheck.flush_violations()</code> drains it into a regular table for persistence).</p></li></ol><p>A shmem <strong>sentinel probe</strong> plants byte <code>0xDE</code> just past the declared end of registered segments and verifies it after the workload runs — that's the shmem-overrun detector. DSM segments can be tracked manually via <code>track_dsm_handle()</code> and reported as leaks at session end.</p><p>Read More about the project at: <a target="_blank" rel="noopener noreferrer" class="text-primary underline underline-offset-4 hover:text-primary/80" href="https://pg-ext-memcheck.vercel.app/">Docs</a></p><hr /><h2>A concrete example</h2><p>Suppose your extension accidentally does this in an executor hook:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">MemoryContextAlloc(TopMemoryContext, 8192);   /* whoops */
</code></pre><p>That's a perfect 8 KB-per-query permanent leak. With <code>pg_ext_memcheck</code>:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">CREATE EXTENSION pg_ext_memcheck;             -- requires shared_preload_libraries

SET pg_ext_memcheck.memcheck_mode = 'all';    -- snapshot both planner and executor
SELECT ext_memcheck.begin('');                -- open the test window

SELECT my_extension.do_work('input');
SELECT my_extension.do_work('more_input');

SELECT * FROM ext_memcheck.end();
</code></pre><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code> check_type      | severity | detail                                                         | source_lib
-----------------+----------+----------------------------------------------------------------+-----------------
 wrong_ctx_alloc | WARNING  | context 'TopMemoryContext' (depth 0): allocated grew by 8192…  | my_extension.so
 wrong_ctx_alloc | WARNING  | context 'TopMemoryContext' (depth 0): allocated grew by 8192…  | my_extension.so
</code></pre><p>Two findings, one per query, naming the offending context and the library that grew it. That's the entire intended user experience.</p><hr /><h2>Tested against real bugs (positive detection)</h2><p>A regression suite that only asserts "the call returned without erroring" tells you nothing about whether the tool actually catches the bug it claims to catch. v0.1.0 ships with a deliberately-buggy companion extension — <code>buggy_pg_ext</code> — that leaks 8 KB into <code>TopMemoryContext</code> on every query via its <code>ExecutorStart</code> hook.</p><p>The regression test does three things end-to-end:</p><ol><li><p>Loads <code>buggy_pg_ext</code> per session (no global preload — other tests stay clean).</p></li><li><p>Runs two <code>SELECT</code> queries.</p></li><li><p>Asserts that <code>ext_memcheck.end()</code> returns at least one <code>wrong_ctx_alloc</code> row whose detail mentions <code>TopMemoryContext</code> <strong>and</strong> whose <code>source_lib</code> contains <code>buggy_pg_ext</code>.</p></li></ol><p>A second test sets <code>allowed_contexts = ['TopMemoryContext']</code> and asserts the violations vanish — proving the pattern filter actually filters, not that the result was accidental.</p><p>That's positive detection: the bug exists, the tool catches it, the tool attributes it, and the filter behaves as documented. CI runs the full suite green on PostgreSQL 15, 16, 17, and 18.</p><hr /><h2>Honest scope — what's <em>not</em> in this beta</h2><ul><li><p><strong>Use-after-reset detection</strong> is Phase 2. The design calls for a <code>BackgroundWorker</code> to run crash-inducing tests in isolation (so a SIGSEGV confirms the bug rather than killing your session). The worker harness is a stub today; the scenario <code>context_reset_storm</code> will land alongside it.</p></li><li><p><strong>DSM auto-tracking</strong> is bounded by PostgreSQL itself. There is no <code>dsm_create_hook</code> or <code>dsm_attach_hook</code>, and the per-backend <code>dsm_segment</code> list isn't enumerable from public API. So the realistic path for a v0.2 improvement is <code>emit_log_hook</code> — capturing PostgreSQL's own "resource was not closed: dynamic shared memory segment" warnings and folding them into the violation log. That catches the common un-pinned leak case. Pinned-segment leaks remain fundamentally indistinguishable from intentional persistence.</p></li><li><p><strong>Additional scenarios</strong> (<code>concurrent_backends</code>, <code>cursor_leak</code>, <code>oom_simulation</code>, <code>cold_warm_cold</code>) are designed but not implemented.</p></li><li><p><strong>Not safe for production monitoring.</strong> Snapshot/diff has measurable per-query overhead. This is a <strong>testing tool</strong>: explicit <code>begin()</code>, run your workload, <code>end()</code>, done.</p></li></ul><hr /><h2>Try it</h2><p>Requirements: PostgreSQL 15+ with server headers, a C compiler, and <code>pg_config</code> on <code>PATH</code>.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-bash">git clone https://github.com/samsiva-dev/pg_ext_memcheck.git
cd pg_ext_memcheck
make
sudo make install
</code></pre><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-ini"># postgresql.conf
shared_preload_libraries = 'pg_ext_memcheck'
</code></pre><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">CREATE EXTENSION pg_ext_memcheck;
SELECT ext_memcheck.begin('');
-- exercise your extension
SELECT * FROM ext_memcheck.end();
</code></pre><p>The four built-in stress scenarios — <code>growth_benchmark</code>, <code>tx_abort_loop</code>, <code>wrong_context_probe</code>, <code>shmem_sentinel_probe</code> — are good first probes if you don't have a target query in mind.</p><hr /><h2>Contributing</h2><p>If you maintain a PostgreSQL extension and point this at it, <strong>I'd genuinely like to know what it finds</strong> — true positives and false positives both. The issue tracker is the right place, and there's an open invitation in the README. PRs are welcome too: the Phase 2 BGWorker harness, the <code>emit_log_hook</code> DSM bridge, and the missing stress scenarios are all good first targets.</p><p>⭐ <strong>Repository:</strong> <a target="_blank" rel="noopener noreferrer" class="text-primary underline underline-offset-4 hover:text-primary/80" href="https://github.com/samsiva-dev/pg_ext_memcheck">https://github.com/samsiva-dev/pg_ext_memcheck</a> </p><p>📜 <strong>License:</strong> MIT </p><p>🏷️ <strong>Tag:</strong> v0.1.0-beta</p><p>Thanks for reading — and if you ship an extension, please run this against it.</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Technical</category>
            <category>Project</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/pg_ext_memcheck_poster.svg" length="0" type="image/svg"/>
        </item>
        <item>
            <title><![CDATA[PostgreSQL Internals - Module 8: Concurrency & Locking]]></title>
            <link>https://sambasivareddy.in/blog/postgresql-internals-module-8-concurrency-locking</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/postgresql-internals-module-8-concurrency-locking</guid>
            <pubDate>Mon, 25 May 2026 15:24:49 GMT</pubDate>
            <description><![CDATA[PostgreSQL locking deep dive: lock hierarchy, row-level locks in xmax, MultiXactId, deadlock detection, advisory locks, SKIP LOCKED queue, and pg_locks queries.]]></description>
            <content:encoded><![CDATA[<p>In the previous blog, we explored VACUUM and how it keeps our PostgreSQL system clean by reclaiming dead tuple space, how autovacuum works under the hood, and what properties need to be configured to keep it running effectively.</p><p>In this blog, we are going into <strong>Module 8: Concurrency &amp; Locking</strong> - the mechanism PostgreSQL uses to coordinate simultaneous access to shared data without corruption.</p><hr /><h2>The Lock Hierarchy - Two Levels</h2><p>PostgreSQL has two distinct locking systems that work together:</p><ul><li><p>Table-Level locks</p></li><li><p>Row-Level Locks</p></li></ul><hr /><h2>Part 1: Table-level locks (relation locks)</h2><p>Stored in shared memory in the <strong>lock table</strong>. Eight lock modes, ordered by restrictiveness:</p><ol><li><p><strong>AccessShareLock</strong> - <code>SELECT</code></p></li><li><p><strong>RowShareLock</strong> - <code>SELECT FOR UPDATE</code> / <code>FOR SHARE</code></p></li><li><p><strong>RowExclusiveLock</strong> - <code>INSERT</code> , <code>DELETE</code> , <code>UPDATE</code></p></li><li><p><strong>ShareUpdateExclusiveLock </strong>- <code>VACUUM</code> , <code>ANALYZE</code> , <code>CREATE INDEX CONCURRENTLY</code></p></li><li><p><strong>ShareLock </strong>- <code>CREATE INDEX</code> (non-concurrent)</p></li><li><p><strong>ShareRowExclusiveLock </strong>- <code>CREATE TRIGGER</code> , some <code>ALTER TABLE</code></p></li><li><p><strong>ExclusiveLock </strong>- <code>REFRESH MATERIALIZED VIEW CONCURRENTLY</code></p></li><li><p><strong>AccessExclusiveLock </strong>- <code>DROP</code> , <code>TRUNCATE</code> , <code>VACUUM FULL</code> , <code>ALTER TABLE</code> , <code>LOCK TABLE</code> , most DDLs.</p></li></ol><p><strong><u>The conflict matrix - what blocks what</u></strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>                           AS  RS  RE  SUE  S  SRE  E  AE
AccessShareLock      (AS)  .   .   .   .   .   .   .   X
RowShareLock         (RS)  .   .   .   .   .   .   X   X
RowExclusiveLock     (RE)  .   .   .   .   X   X   X   X
ShareUpdateExclusiveLock(SUE) .  .  .  X   X   X   X   X
ShareLock            (S)   .   .   X   X   .   X   X   X
ShareRowExclusiveLock(SRE) .   .   X   X   X   X   X   X
ExclusiveLock        (E)   .   X   X   X   X   X   X   X
AccessExclusiveLock  (AE)  X   X   X   X   X   X   X   X

X = conflict (one blocks the other)
. = compatible (both can proceed)</code></pre><p><strong>Key observations from the matrix:</strong></p><ul><li><p><code>SELECT</code> never blocks <code>SELECT</code></p><ul><li><p>AS vs AS = Compatible → Reads never block reads</p></li></ul></li><li><p><code>SELECT</code> blocks <code>DDL</code> :</p><ul><li><p>AS vs AE = Conflict → Alter Table waits for all SELECTs to finish</p></li></ul></li><li><p>Writes never block reads <code>MVCC</code> :</p><ul><li><p>RE vs AS = compatible → INSERT/UPDATE/DELETE never blocks SELECT</p></li></ul></li><li><p>VACUUM never blocks normal DML</p><ul><li><p>SUE vs AS/RS/RE = compatible → autovacuum runs alongside production traffic</p></li></ul></li><li><p>The most dangerous: <code>ALTER TABLE</code> acquires AE</p><ul><li><p>It must wait for ALL existing locks to clear AND new queries queue behind it.</p></li></ul></li></ul><hr /><h2>Part 2: How Table Locks are Acquired and Stored</h2><h3>The lock table in shared memory</h3><p>PostgreSQL maintains a <strong>lock table </strong>in shared memory - a hash table keyed by <code>(database OID, relation OID)</code> . Each entry contains:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct LOCK {
    LOCKTAG     tag;           /* unique identifier for the lockable object */
    LOCKMASK    grantMask;     /* bitmask of lock modes currently granted */
    LOCKMASK    waitMask;      /* bitmask of lock modes being waited for */
    SHM_QUEUE   procLocks;     /* list of PROCLOCK objects for this lock */
    PROC_QUEUE  waitProcs;     /* list of waiting backends */
    int         requested[MAX_LOCKMODES];  /* count of each mode requested */
    int         granted[MAX_LOCKMODES];    /* count of each mode granted */
    int         nRequested;    /* total requested locks */
    int         nGranted;      /* total granted locks */
} LOCK;</code></pre><p>Each backend also has a <strong>PROCLOCK</strong> entry linking it to a lock:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">typedef struct PROCLOCK {
    PROCLOCKTAG tag;          /* (LOCK *, PGPROC *) pair */
    PGPROC      *groupLeader; /* lock group leader */
    LOCKMASK    holdMask;     /* bitmask of lock modes held */
    LOCKMASK    releaseMask;  /* bitmask of lock modes to release */
    SHM_QUEUE   lockLink;     /* list of PROCLOCKs for same LOCK */
    SHM_QUEUE   procLink;     /* list of PROCLOCKs for same PGPROC */
} PROCLOCK;</code></pre><h3>Lock escalation does not exist</h3><p>Unlike some other databases, PostgreSQL does <strong>not </strong>escalate row locks to table locks. A transaction that updates 10 million rows holds 10 million rows holds 10 million row-level locks (via <code>xmax</code> , not the lock table) and still only holds one <code>RowExclusiveLock</code> on the table. There is no automatic promotion to a table-level write lock.</p><h3>Fast-path locking</h3><p>For the common case of <code>AccessShareLock</code> and <code>RowExclusiveLock</code> on regular tables, PostgreSQL uses a <strong>fast-path</strong> optimization - it stores up to 16 weak locks per backend in the <code>PGPROC</code> struct directly, bypassing the shared lock table entirely. Only when a conflicting lock appears does it fall back to the main lock table.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- See all current locks:
SELECT pid,
       locktype,
       relation::regclass,
       mode,
       granted
FROM pg_locks
WHERE relation IS NOT NULL
ORDER BY relation, pid;</code></pre><hr /><h2>Part 3: Row-Level Locks - No Lock Table Entry</h2><p>Row-level locks are fundamentally different from table-level locks. They are <strong>not stored in the lock table</strong>. Instead they are encoded directly in the tuple header - specifically in <code>xmax</code> and <code>t_infomask</code>.</p><h3>How a row lock is stored</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">SELECT * FROM orders WHERE id = 42 FOR UPDATE;</code></pre><p>What happens on the heap page:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>Before FOR UPDATE:
  tuple: xmin=1000, xmax=0, t_infomask=0x0900
                                        ↑
                              XMIN_COMMITTED | XMAX_INVALID

After FOR UPDATE (transaction 5001):
  tuple: xmin=1000, xmax=5001, t_infomask=0x0440
                                            ↑
                              XMAX_EXCL_LOCK | XMAX_LOCK_ONLY</code></pre><p>The key flags in <code>t_infomask</code> :</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">HEAP_XMAX_LOCK_ONLY  0x0080  — xmax is a lock, not a delete
HEAP_XMAX_EXCL_LOCK  0x0040  — exclusive lock (FOR UPDATE)
HEAP_XMAX_KEYSHR_LOCK 0x0010 — key-share lock (FOR KEY SHARE)</code></pre><p>When the locking transaction commits or rolls back, the <code>xmax</code> is left in place. The next transaction to visit the tuple checks whether <code>xmax</code> is still active (via CLOG) - if the locking transaction is done, the lock is considered released. This is why row locks have zero cleanup cost - they disappear when the transaction ends.</p><h3>Lock modes for row-level operation</h3><ul><li><p><code>FOR KEY SHARE</code> - weakest. Prevents key updates and deletes. Used by foreign key checks on referenced rows.</p></li><li><p><code>FOR SHARE</code> - prevents updates and deletes.</p></li><li><p><code>FOR NO KEY UPDATE</code> - like FOR <code>UPDATE</code> but allows key-share lockers. Used by <code>UPDATE</code> on non-key columns.</p></li><li><p><code>FOR UPDATE</code> - strongest. Prevents all concurrent modification. Used by <code>UPDATE</code> and <code>DELETE</code>.</p></li></ul><p><strong><u>Conflict matrix for row locks</u></strong>:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>                   KEY SHARE   SHARE   NO KEY UPDATE   UPDATE
FOR KEY SHARE        .          .          .             X
FOR SHARE            .          .          X             X
FOR NO KEY UPDATE    .          X          X             X
FOR UPDATE           X          X          X             X</code></pre><h3>Example</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Demonstrate row lock:
-- Terminal 1:
BEGIN;
SELECT * FROM orders WHERE id = 42 FOR UPDATE;
-- Row is now locked

-- Terminal 2:
SELECT * FROM orders WHERE id = 42 FOR UPDATE;
-- Blocks! Waiting for Terminal 1 to commit/rollback

-- See the wait:
SELECT pid, locktype, relation::regclass, page, tuple, mode, granted
FROM pg_locks
WHERE NOT granted;</code></pre><hr /><h2>Part 4: MultiXactId - When Multiple Transactions Lock One Row</h2><p>What happens when two transactions both hold a <code>FOR SHARE</code> lock on the same row at the same time? They are compatible, so both should be able to lock it. But the tuple's <code>xmax</code> field is only 32 bits - it can only store one XID.</p><p>PostgreSQL solves this with <strong>MultiXactId</strong>:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">Normal row lock:
  xmax = 5001  (single locker XID)
  t_infomask: XMAX_KEYSHR_LOCK | XMAX_LOCK_ONLY

Multiple shared lockers:
  xmax = 123   ← this is a MultiXactId, not a real XID!
  t_infomask: XMAX_IS_MULTI | XMAX_KEYSHR_LOCK | XMAX_LOCK_ONLY</code></pre><p>The <code>MultiXactId</code> is a pointer into the <strong>multixact</strong> subsystem - files in <code>$PGDATA/pg_multixact/</code> that store arrays of (XID, lock_mode) pairs:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-c">pg_multixact/members/  → arrays of (XID, mode) per MultiXact
pg_multixact/offsets/  → MultiXactId → offset into members file</code></pre><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- MultiXact occupies just as much space as XID — 32 bits
-- It also has a wraparound problem: vacuum_multixact_freeze_max_age
-- Monitor it like XID age:

SELECT datname,
       age(datfrozenxid)     AS xid_age,
       mxid_age(datminmxid)  AS mxid_age
FROM pg_database
ORDER BY mxid_age DESC;</code></pre><hr /><h2>Part 5: Deadlock Detection</h2><p>A deadlock occurs when two or more transactions are each waiting for a lock held by the other:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>T1 holds lock on row A, waits for lock on row B
T2 holds lock on row B, waits for lock on row A
→ neither can proceed → deadlock</code></pre><h3>How PostgreSQL detects deadlocks</h3><p>PostgreSQL does <strong>not</strong> use a timeout to detect deadlocks. It uses a <strong>wait-for graph</strong> algorithm:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-xml">1. When T1 waits for a lock, it sleeps for deadlock_timeout (default 1s)
   — this is the "optimistic" wait: most locks clear within 1s

2. After deadlock_timeout elapses, T1 calls DeadLockCheck():
   a. Build a directed graph: T1 → T2 means "T1 is waiting for T2"
   b. Walk the graph using DFS looking for cycles
   c. If cycle found: pick one transaction to abort (the "victim")
      — PostgreSQL picks the transaction that would be cheapest to restart
      — usually the one that has done the least work

3. The victim receives:
   ERROR: deadlock detected
   DETAIL: Process 1234 waits for ShareLock on transaction 5001
           Process 5678 waits for ShareLock on transaction 1234
   HINT:  See server log for query details.</code></pre><h3>Example</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Demonstrate deadlock:
-- Terminal 1:
BEGIN;
UPDATE orders SET status='x' WHERE id=1;
-- (now pause)

-- Terminal 2:
BEGIN;
UPDATE orders SET status='y' WHERE id=2;
UPDATE orders SET status='y' WHERE id=1;  -- waits for T1

-- Terminal 1:
UPDATE orders SET status='x' WHERE id=2;  -- waits for T2 → deadlock!

-- After deadlock_timeout (1s), one transaction gets:
-- ERROR:  deadlock detected

-- Monitor deadlocks:
SELECT deadlocks FROM pg_stat_database WHERE datname = current_database();</code></pre><hr /><h2>Part 6: Lock Queuing - The Hidden Traffic Jam</h2><p>Understanding lock queuing is critical for production systems. Locks are not just granted or denied - they queue.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-xml">Timeline:

t=0  T1 acquires AccessShareLock (SELECT) — granted immediately
t=1  T2 acquires AccessShareLock (SELECT) — granted immediately
t=2  T3 requests AccessExclusiveLock (ALTER TABLE) — BLOCKED by T1, T2
     T3 enters the wait queue for AE lock

t=3  T4 requests AccessShareLock (SELECT) — BLOCKED by T3!
     Even though AS is compatible with AS, T4 must wait because T3 is
     already in the queue. This prevents T3 from starving.

t=4  T1 commits — T3 still waiting (T2 still holds AS)
t=5  T2 commits — T3 granted AE lock
     T3 is now running ALTER TABLE

t=6  T4 finally gets AS lock (after T3 completes)</code></pre><p><strong>This is the ALTER TABLE traffic jam.</strong> A single <code>ALTER TABLE</code> on a busy table:</p><ol><li><p>Waits for all existing connections to release locks</p></li><li><p>Blocks ALL new queries behind it (even simple SELECTs)</p></li><li><p>Can cascade into hundreds of waiting connections</p></li></ol><h3>Safe ALTER TABLE pattern</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Instead of running ALTER TABLE alone (dangerous on busy tables):
ALTER TABLE orders ADD COLUMN notes text;

-- Use lock_timeout + retry loop:
SET lock_timeout = '2s';
BEGIN;
ALTER TABLE orders ADD COLUMN notes text;
COMMIT;
-- If it times out, retry during a quieter period

-- Even safer: check for blocking sessions first:
SELECT count(*) FROM pg_stat_activity
WHERE query NOT LIKE '%pg_stat_activity%'
  AND state != 'idle'
  AND pid != pg_backend_pid();
-- Only run ALTER TABLE when this is near zero</code></pre><hr /><h2>Part 7: Advisory Locks</h2><p>Advisory locks are application-level locks with no automatic association to database objects. PostgreSQL provides the locking mechanism; your application decides what it means.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Session-level advisory lock (held until released or session ends):
SELECT pg_advisory_lock(12345);
-- ... do work ...
SELECT pg_advisory_unlock(12345);

-- Transaction-level advisory lock (released at COMMIT/ROLLBACK):
BEGIN;
SELECT pg_advisory_xact_lock(12345);
-- ... do work ...
COMMIT;  -- lock released automatically

-- Try-lock (non-blocking):
SELECT pg_try_advisory_lock(12345);
-- Returns TRUE if lock acquired, FALSE if already held

-- Lock with two 32-bit integers (more namespace):
SELECT pg_advisory_lock(hashtext('job_processor'), job_id);</code></pre><h3>SKIP LOCKED - the queue pattern</h3><p><code>SKIP LOCKED</code> is not an advisory lock - it is a row-level lock modifier. It tells PostgreSQL: <strong>"if you encounter a row that is already locked, skip it instead of waiting."</strong> This is the foundation of efficient work queues:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Multiple workers can safely dequeue without contention:
-- Worker 1:
SELECT * FROM jobs WHERE status='pending' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
-- Locks job_id=1

-- Worker 2 (simultaneously):
SELECT * FROM jobs WHERE status='pending' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
-- Sees job_id=1 is locked → skips to job_id=2
-- No blocking, no waiting</code></pre><hr /><h2>Part 8: Lock Monitoring in Production</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- ═══ 1. See everything currently locked ═══

SELECT
    pid,
    locktype,
    CASE locktype
        WHEN 'relation' THEN relation::regclass::text
        WHEN 'tuple'    THEN relation::regclass::text || ':' || page::text || ',' || tuple::text
        WHEN 'transactionid' THEN transactionid::text
        ELSE locktype
    END AS object,
    mode,
    granted,
    waitstart
FROM pg_locks
ORDER BY granted, waitstart NULLS LAST;

-- ═══ 2. Find blocked queries and their blockers ═══

SELECT
    blocked.pid                          AS blocked_pid,
    blocked.usename                      AS blocked_user,
    now() - blocked.query_start          AS wait_time,
    blocked.query                        AS blocked_query,
    blocking.pid                         AS blocking_pid,
    now() - blocking.query_start         AS blocking_duration,
    blocking.query                       AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
    ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) &gt; 0
ORDER BY wait_time DESC;

-- ═══ 3. Identify long-running transactions (lock holders) ═══

SELECT pid,
       usename,
       now() - xact_start           AS tx_duration,
       now() - query_start          AS query_duration,
       state,
       left(query, 100)             AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
  AND state != 'idle'
ORDER BY xact_start ASC
LIMIT 10;

-- ═══ 4. Count lock waits per table (operational health) ═══

SELECT
    relation::regclass AS table,
    mode,
    count(*) FILTER (WHERE granted)     AS granted,
    count(*) FILTER (WHERE NOT granted) AS waiting
FROM pg_locks
WHERE relation IS NOT NULL
GROUP BY relation, mode
HAVING count(*) FILTER (WHERE NOT granted) &gt; 0
ORDER BY waiting DESC;

-- ═══ 5. Kill a blocking session (use carefully!) ═══

-- Soft kill (waits for current query to finish):
SELECT pg_cancel_backend(pid);

-- Hard kill (terminates session immediately):
SELECT pg_terminate_backend(pid);

-- Kill all idle-in-transaction sessions &gt; 10 minutes:
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND now() - state_change &gt; interval '10 minutes';</code></pre><hr /><h2>Part 9: Lock Timeout and Statement Timeout</h2><ul><li><p><code>lock_timeout</code> - Prevent queries from waiting forever for locks.</p><ul><li><p>If a lock cannot be acquired within specified time (say <code>5s</code>), it throws <strong>"ERROR: canceling statement due to lock timeout"</strong></p></li></ul></li><li><p><code>statement_timeout</code> - Prevent long-running queries from holding locks.</p><ul><li><p>If a query runs longer than specified time (say <code>30s</code>), it throws <strong>"ERROR: canceling statement due to statement timeout"</strong></p></li></ul></li><li><p><code>idle_in_transaction_session_timeout</code> - Prevent idle transaction from holding locks.</p><ul><li><p>If a transaction is open but idle for the given time (say <code>5min</code>), it throws <strong>"ERROR: termination connection due to idle-in-transaction timeout"</strong></p></li></ul></li></ul><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Best practice: set all three in postgresql.conf
SET lock_timeout                        = '30s'
SET statement_timeout                   = '60s'
SET idle_in_transaction_session_timeout = '5min'

-- Per-session override for maintenance work:
SET LOCAL lock_timeout = '0';        -- no timeout for this transaction
SET LOCAL statement_timeout = '0';   -- no timeout for this transaction</code></pre><hr /><h2>Part 10: Hands-On Lab</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- ═══ 1. Observe lock acquisition ═══

-- See your own locks:
BEGIN;
SELECT * FROM orders LIMIT 1;
SELECT pid, locktype, relation::regclass, mode, granted
FROM pg_locks
WHERE pid = pg_backend_pid();
-- Should see AccessShareLock on orders
-- Also see ExclusiveLock on your transaction ID
COMMIT;

-- ═══ 2. Demonstrate lock conflict ═══

-- Terminal 1:
BEGIN;
LOCK TABLE orders IN SHARE MODE;
SELECT pg_sleep(30);  -- hold the lock

-- Terminal 2:
SET lock_timeout = '3s';
BEGIN;
INSERT INTO orders VALUES (...);  -- blocked by SHARE lock
-- After 3s: ERROR: canceling statement due to lock timeout

-- ═══ 3. Observe row-level locking in tuple ═══

CREATE TABLE lock_demo (id int, val text);
INSERT INTO lock_demo VALUES (1, 'hello');

BEGIN;
SELECT * FROM lock_demo WHERE id = 1 FOR UPDATE;

-- Inspect the tuple while locked:
SELECT lp, t_xmin, t_xmax, t_infomask
FROM heap_page_items(get_raw_page('lock_demo', 0));
-- t_xmax = your transaction XID (lock recorded in tuple!)
-- t_infomask has XMAX_EXCL_LOCK | XMAX_LOCK_ONLY bits set

COMMIT;

-- After commit, check again:
SELECT lp, t_xmin, t_xmax, t_infomask
FROM heap_page_items(get_raw_page('lock_demo', 0));
-- t_xmax still shows the XID — but CLOG says committed
-- Next accessor checks CLOG → sees lock released

-- ═══ 4. Reproduce and observe a deadlock ═══

-- Terminal 1:
BEGIN;
UPDATE lock_demo SET val='T1' WHERE id=1;

-- Terminal 2:
BEGIN;
UPDATE lock_demo SET val='T2' WHERE id=2;
UPDATE lock_demo SET val='T2' WHERE id=1;  -- waits

-- Terminal 1:
UPDATE lock_demo SET val='T1' WHERE id=2;  -- deadlock!
-- One terminal will get: ERROR: deadlock detected

-- Check deadlock counter:
SELECT deadlocks FROM pg_stat_database
WHERE datname = current_database();

-- ═══ 5. SKIP LOCKED queue pattern ═══

CREATE TABLE work_queue (
    id bigserial primary key,
    payload text,
    status text DEFAULT 'pending'
);
INSERT INTO work_queue(payload)
SELECT 'job_' || g FROM generate_series(1,100) g;

-- Simulate two workers claiming jobs simultaneously:
-- Worker 1:
BEGIN;
SELECT id, payload FROM work_queue
WHERE status = 'pending'
ORDER BY id
LIMIT 5
FOR UPDATE SKIP LOCKED;
-- Claims jobs 1-5

-- Worker 2 (simultaneously):
BEGIN;
SELECT id, payload FROM work_queue
WHERE status = 'pending'
ORDER BY id
LIMIT 5
FOR UPDATE SKIP LOCKED;
-- Claims jobs 6-10 (skipped 1-5 which are locked)
-- No blocking!

-- ═══ 6. Advisory lock for distributed job ═══

-- Only one session can run this job:
SELECT CASE
    WHEN pg_try_advisory_lock(12345) THEN 'Got the lock — running job'
    ELSE 'Another worker has the lock — skipping'
END;

-- See advisory locks in pg_locks:
SELECT pid, locktype, classid, objid, mode, granted
FROM pg_locks
WHERE locktype = 'advisory';

SELECT pg_advisory_unlock(12345);</code></pre><hr /><h2>Conclusion</h2><p>In this module, we explored how PostgreSQL manages concurrent access at every level — from the 8-mode table lock hierarchy and its conflict matrix, to row-level locks encoded directly in the tuple's <code>xmax</code> field with zero lock table overhead, to <code>MultiXactId</code> for shared row locks, deadlock detection via wait-for graph cycle analysis, the hidden lock queuing problem that turns a single <code>ALTER TABLE</code> into a traffic jam, and advisory locks with <code>SKIP LOCKED</code> for contention-free work queues.</p><p><strong>What's Next → Module 9: Replication</strong></p><p>You now understand how PostgreSQL coordinates concurrent writes safely. Module 9 goes into how those writes are streamed to replicas - physical streaming replication at the WAL level, logical replication and the decoding pipeline, replication slots and their wraparound risk, and synchronous vs asynchronous commit.</p><p></p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>PostgreSQL</category>
            <category>Technical</category>
            <category>PGInternals</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/module8_banner.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Underline Less!!]]></title>
            <link>https://sambasivareddy.in/blog/underline-less</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/underline-less</guid>
            <pubDate>Sat, 23 May 2026 18:08:45 GMT</pubDate>
            <description><![CDATA[Turns out the secret to studying, gymming, and life was always the same - underline less.]]></description>
            <content:encoded><![CDATA[<p>When was the last time you underlined a text in a book to highlight the importance of it? Me?? I can't remember, maybe in my B.Tech 1st year before the COVID pandemic hit and I bought my first second-hand laptop. So not in 6 years - until I started reading again recently.</p><p>Then I remembered my 10th class days - those were the days we were all glued to our textbooks grinding everything over and over again, didn't we? I used to underline paragraph after paragraph, sometimes I was genuinely confused about why the hell I underlined that line, when I read the textbook again during revision.</p><p>One day, my teacher flipped through my book, saw all those underlined paragraphs, and explained the importance of underlining. He said: don't underline everything - underline the word or two that actually carries the meaning. It helped me study better, and I followed that through BTech 1st year - until my laptop and smartphone replaced the need for a textbook, simply through PDFs.</p><p>Now that I think about it, it hits differently - how a simple tweak in approach can make all the difference in studying better. It made me think about life too - we all try to do things that are meant to change us, but we burn out or exhaust ourselves halfway through. For instance, I started going to the gym 5 days a week. I kept it up for 2 months - then my body got burned out and all that work went to waste over the next 2 months. So I switched to 3 days a week, and my body feels better than ever.</p><p>If something isn't working, it doesn't mean you can't do it - it just means you haven't found your version of it yet. Sometimes all it takes is underlining less.</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Opinions</category>
            <category>Personal</category>
        </item>
        <item>
            <title><![CDATA[Rust Basics - Module 5: Generics and Traits]]></title>
            <link>https://sambasivareddy.in/blog/rust-basics-module-5-generics-and-traits</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/rust-basics-module-5-generics-and-traits</guid>
            <pubDate>Tue, 19 May 2026 15:13:25 GMT</pubDate>
            <description><![CDATA[Learn Rust traits, generics, trait bounds, and static vs dynamic dispatch. Understand how Rust achieves polymorphism without runtime cost using monomorphisation.]]></description>
            <content:encoded><![CDATA[<p>In the previous blog post, we discussed about the Structs, Enums, how structs help in grouping the related data under one custom data type and how enums lets us enumerate variants of same or different data types. And we end with discussing Pattern matching, a primary way of working with Enums. In this blog post, we will cover the topics, <strong>Traits and Generics</strong> in Rust.</p><blockquote><p><strong>Traits</strong> are how Rust achieves <strong>polymorphism</strong> - shared behaviour across different types without inheritance. <strong>Generics</strong> are how you write code that works over many types <strong>without duplicating the logic.</strong></p></blockquote><p>Let's start with What problem we are trying to solve with Traits &amp; Generics, then about Traits, Generics and bringing all together with an example.</p><hr /><h2>The Problem</h2><p>Consider we want to find the largest elements in a slice of different data types say <code>i32</code> , <code>f64</code> etc. Without traits and generics, we need to write a separate function for each data type:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn largest_i32(list: &amp;[i32]) -&gt; i32 {
    let mut largest = list[0];

    for &amp;item in list.iter() {
        if item &gt; largest {
            largest = item;
        }
    }

    largest
}

fn largest_f64(list: &amp;[f64]) -&gt; f64 {
    let mut largest = list[0];

    for &amp;item in list.iter() {
        if item &gt; largest {
            largest = item;
        }
    }

    largest
}</code></pre><p>Here, the logic is a pure duplication of code as we wrote for two different data types and which is redundant and unnecessary. Traits and Generics helps in eliminating these problems. Before looking at the solution, here's the mental model:</p><blockquote><p>Generics eliminate the duplication. Traits are constraints that implementing types must satisfy.</p></blockquote><p>Together, the solution would be:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn largest&lt;T: PartialOrd&gt;(list: &amp;[T]) -&gt; &amp;T { ... }
// works for any T that can be compared</code></pre><ul><li><p>T - a generic type placeholder; can be <code>i32</code> , <code>f32</code> or even a <code>String</code></p></li><li><p>PartialOrd - a trait constraint meaning the type must support comparison operators, or in other words, only types that implement <code>PartitalOrd</code> can be passed as a slice <code>&amp;[T]</code> to this function.</p></li></ul><hr /><h2>Traits: defining shared behaviour</h2><p>A trait defines <strong>a set of method signatures that a type (data type, structs, enums etc..) must implement</strong>. Think of it as an interface. Like Java interfaces, traits define a contract - but unlike Java, Rust traits can have default implementations and can be added to existing types.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">trait Summary {
  fn summarise(&amp;self) -&gt; String;
}</code></pre><p>This says: any type that implements <code>Summary</code> must provide a <code>summarise</code> method that takes <code>&amp;self</code> and returns a <code>String</code> . The trait does not say <strong>how, </strong>implementing <strong>type decides that.</strong></p><h3>Example</h3><p>Implementing Summary trait on a type:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">struct Article {
  title: String,
  author: String,
  content: String,
}

struct Tweet {
  username: String,
  content: String,
}

impl Summary for Article {
  fn summarise(&amp;self) -&gt; String {
    format!("{} by {}", self.title, self.author)
  }
}

impl Summary for Tweet {
  fn summarise(&amp;self) -&gt; String {
    format!("{}: {}", self.username, self.content)
  }
}</code></pre><p>Now both <code>Article</code> and <code>Tweet</code> satisfy the <code>Summary</code> contract. The method call is the same regardless of which type you have:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let article = Article{ 
  title: "Rust Module-5",
  author: "Siva",
  content: "Generics &amp; Traits", 
};

let tweet = Tweet{ 
  username: "sambasiva",
  content: "Releasing a series on Rust",
};

println!("{}", article.summarise());  // Prints "Rust Module-5 by Siva"
println!("{}", tweet.summarise());    // Prints "sambasiva: Releasing a series on Rust"</code></pre><h3>Default implementation</h3><p>A trait can provide a <strong>default body that types can override if they wanted to or else inherit the default behaviour</strong>.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">trait Summary {
	fn summarise(&amp;self) -&gt; String {
		String::from("(Read more...)") // Default behaviour
	}
}

impl Summary for Article {} // Uses default summarise

// Or Override it
impl Summary for Tweet {
	fn summarise(&amp;self) -&gt; String {
		format!("{}: {}", self.username, self.content)
	}
}

fn main() {
  let article = Article{ 
    title: "Rust Module-5",
    author: "Siva",
    content: "Generics &amp; Traits", 
  };

  let tweet = Tweet{ 
    username: "sambasiva",
    content: "Releasing a series on Rust",
  };

  println!("{}", article.summarise());  // Prints "(Read more...)"
  println!("{}", tweet.summarise());    // Prints "sambasiva: Releasing a series on Rust"
}</code></pre><hr /><h2>Generics</h2><p>Generics <strong>let you write a function or struct that is parameterised over a type</strong>, resolved at compile time. First we will start with functions.</p><h3>Generic Functions</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn first&lt;T&gt;(list: &amp;[T]) -&gt; &amp;T {
	&amp;list[0]
}</code></pre><p><code>T</code> is a type parameter - a placeholder. When you call <code>first(&amp;[1, 2, 3])</code> , the compiler substitutes <code>T = i32</code> . When you call <code>first(&amp;["a", "b"])</code> , it substitues <code>T = &amp;str</code> . Two separate compiled functions - zero runtime cost. This is called <strong><u>monomorphisation</u></strong>.</p><h3>Generic Structs</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">struct Pair&lt;T&gt; {
	first: T,
	second: T,
}

impl&lt;T&gt; Pair&lt;T&gt; {
	fn new(first: T, second: T) -&gt; Self {
		Pair { first, second }
	}
}

fn main() {
  let pair1 = Pair { first: 3, second: 4 } // Pass
  let pair2 = Pair { first: 3, second: 4.0 } // Throws error
}</code></pre><p>Why error for <code>pair2</code>? Because we defined only one generic parameter to the struct <code>Pair</code> i.e <code>T</code> . In case of <code>pair2</code> , <code>first</code> is <code>i32</code> and <code>second</code> is <code>f64</code> , two different data types, so the error. The fix is simple, we can pass the multiple type parameters each with different identifier. For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">struct Pair&lt;T, K&gt; {
  first: T,
  second: K,
}

impl&lt;T, K&gt; Pair&lt;T, K&gt; {
	fn new(first: T, second: K) -&gt; Self {
		Pair { first, second }
	}
}

fn main() {
  let pair1 = Pair { first: 3, second: 4 } // Pass 
  let pair2 = Pair { first: 3, second: 4.0 } // Now Pass
}</code></pre><hr /><h2>Trait Bounds - constraining Generics</h2><p>A bare <code>T</code> can be anything - you can't call any methods on it because the compiler doesn't know what <code>T</code> supports. Traits bounds express the requirements for those types:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn largest&lt;T: PartialOrd&gt;(list: &amp;[T]) -&gt; &amp;T {
	let mut largest = &amp;list[0];
	for item in list {
		if item &gt; largest { // requires PartialOrd
			largest = item;
		}
	}
	largest
}</code></pre><p><code>T: PartialOrd</code> means "T must implement the <code>PartialOrd</code> trait" - i.e., <code>T</code> must support <code>&gt;</code> , <code>&lt;</code> , <code>&gt;=</code> , <code>&lt;=</code> . Without this bound, <code>item &gt; largest</code> is a compile error because the compiler can't guarantee <code>T</code> supports <strong>comparison. </strong>So this function works for any type that implements <code>PartialOrd</code> - integers, floats, even <code>String</code> . A custom struct with no ordering would fail this bound.</p><p></p><h3>Multiple Bounds with "+"</h3><p>We can define as many trait bounds on a Generic type as we want with the help of <code>+</code> .</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn print_summary&lt;T: Summary + std::fmt::Display&gt;(item: &amp;T) {
	println!("{}", item.summarise());
}</code></pre><p>In the above example, we can call this function on the types which implements both <code>Summary</code> and <code>std::fmt::Display</code> traits.</p><p><code>where</code> clause: a clear syntax for complex bounds:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn compare&lt;T, U&gt;(t: &amp;T, u:&amp;U) -&gt; String
where
	T: Summary + Clone,
	U: Summary + std::fmt::Debug,
{
	//...
}</code></pre><p>Both forms are equivalent - <code>where</code> is just more readable when bounds get long.</p><hr /><h2>Traits as Function Parameters - impl Trait</h2><p>Instead of a generic type parameter, we can also use <code>impl Trait</code> syntax directly in the parameter.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn notify(item: &amp;impl Summary) {
	println!("{}", item.summarise());
}</code></pre><p>This says "accept any reference to any type that implements <code>Summary</code> ". It is syntactic sugar for a generic with a bound:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn notify&lt;T: Summary&gt;(item: &amp;T) { ... } // equivalent to above impl Summary</code></pre><p>Use <code>impl Trait</code> for simple cases. Use explicit generics when you need to refer to <code>T</code> multiple times or return it.</p><hr /><h2>Returning traits - dyn Trait</h2><p>Sometimes you want to return "some type that implements a trait" without specifying which. This requires a different mechanism - traits objects with <code>dyn</code> :</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn make_summary(is_article: bool) -&gt; Box&lt;dyn Summary&gt; {
    if is_article {
        Box::new(Article { ... })
    } else {
        Box::new(Tweet { ... })
    }
}</code></pre><p><code>Box&lt;dyn Summary&gt;</code>is a fat pointer - a pointer to the data plus a pointer to a vtable of method implementations. The concrete type is decided at runtime, not compile time - this is slower than generics but allows more flexibility.</p><hr /><h2>Key Standard Library Traits</h2><p>These are the traits you will encounter constantly. Knowing what they mean is essential:</p><p><code>Display</code> — defines how a type is formatted with <code>{}</code>. Implement this to make your type printable.</p><p><code>Debug</code> — defines formatting with <code>{:?}</code>. Usually derived automatically.</p><p><code>Clone</code> — provides the <code>.clone()</code> method for explicit deep copy.</p><p><code>Copy</code> — marker trait, makes assignment copy instead of move. Requires <code>Clone</code>.</p><p><code>PartialOrd</code> / <code>Ord</code> — comparison operators <code>&lt;</code>, <code>&gt;</code> etc.</p><p><code>PartialEq</code> / <code>Eq</code> — equality operators <code>==</code>, <code>!=</code>.</p><p><code>Iterator</code> — the trait behind every <code>for</code> loop and iterator chain. One required method: <code>next()</code>.</p><p>Most of these can be <strong>derived</strong> automatically by the compiler for your types:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1.clone();
println!("{:?}", p1);       // requires Debug
println!("{}", p1 == p2);   // requires PartialEq</code></pre><p><code>#[derive(...)]</code> is a macro that auto-generates the trait implementation for you based on the struct's fields.</p><hr /><h2>Complete Example</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">use std::fmt;

trait Area {
    fn area(&amp;self) -&gt; f64;
    fn describe(&amp;self) -&gt; String {
        format!("This shape has area {:.2}", self.area())
    }
}

#[derive(Debug, Clone)]
struct Circle {
    radius: f64,
}

#[derive(Debug, Clone)]
struct Rectangle {
    width: f64,
    height: f64,
}

impl Area for Circle {
    fn area(&amp;self) -&gt; f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

impl Area for Rectangle {
    fn area(&amp;self) -&gt; f64 {
        self.width * self.height
    }
}

impl fmt::Display for Circle {
    fn fmt(&amp;self, f: &amp;mut fmt::Formatter) -&gt; fmt::Result {
        write!(f, "Circle(r={})", self.radius)
    }
}

fn print_area(shape: &amp;impl Area) {
    println!("{}", shape.describe());
}

fn largest_area(shapes: &amp;[Box&lt;dyn Area&gt;]) -&gt; f64 {
    shapes.iter()
          .map(|s| s.area())
          .fold(0.0_f64, f64::max)
}

fn main() {
    let c = Circle { radius: 5.0 };
    let r = Rectangle { width: 4.0, height: 6.0 };

    print_area(&amp;c);
    print_area(&amp;r);

    let shapes: Vec&lt;Box&lt;dyn Area&gt;&gt; = vec![
        Box::new(Circle { radius: 3.0 }),
        Box::new(Rectangle { width: 10.0, height: 2.0 }),
        Box::new(Circle { radius: 7.0 }),
    ];

    println!("Largest area: {:.2}", largest_area(&amp;shapes));
}</code></pre><hr /><h2>Conclusion</h2><p>Traits and Generics together solve the duplication problem we started with - traits define <em>what</em> a type must do, generics let you write code that works for <em>any</em> type that qualifies. The compiler resolves generics at compile time (monomorphisation, zero cost), while <code>dyn Trait</code> defers the decision to runtime when flexibility matters more than speed. Master these two, and you hold the key to nearly every abstraction in Rust's standard library.</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Technical</category>
            <category>Rust</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/poster_v2_module5_traits.svg" length="0" type="image/svg"/>
        </item>
        <item>
            <title><![CDATA[What I read in May 2026]]></title>
            <link>https://sambasivareddy.in/blog/what-i-read-in-may-2026</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/what-i-read-in-may-2026</guid>
            <pubDate>Mon, 18 May 2026 06:14:17 GMT</pubDate>
            <description><![CDATA[Summary about the books I read in the month of May 2026]]></description>
            <content:encoded><![CDATA[<p>May was a two-book month - one a campus love story rooted in real events, the other a quiet sci-fi fable I'm still making my way through.</p><p>The Books:</p><ol><li><p>Your Dreams Are Mine Now - Ravinder Singh</p></li><li><p>A Psalm For The Wild Built - Becky Chambers (Partial)</p></li></ol><hr /><h2>Your Dreams Are Mine Now</h2><p>This is the love story between two students studying at <strong>Delhi University </strong>and what happened to them when tried to bring the change in University Politics.</p><h3>The Plot</h3><p>The story is between the two students - <strong>Rupali, </strong>a Bihari girl who joined DU with bigger ambitions and <strong>Arjun, </strong>a Delhite, second year student who is active in the DU student politics and fighting against changes to the reservation quota in the university. The story revolves around these two polar opposite souls, how various circumstances brought them together, made them fall in love, and pushed them to fight for change.</p><h3>My Thoughts</h3><p>After completion of this book, the title doesn't reflect what actually unfolds in the book even though the author tried to craft an ending that resonates with the title. The author drew from a real incident that happened in Delhi and built a story around it. Some conversations feel stretched where a few lines would've done. That said, it never bores you - overall a good, breezy read.<br /></p><hr /><p></p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Books</category>
            <category>Personal</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/IMG_5883.jpeg" length="0" type="image/jpeg"/>
        </item>
        <item>
            <title><![CDATA[Summer is not Summering]]></title>
            <link>https://sambasivareddy.in/blog/summer-is-not-summering</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/summer-is-not-summering</guid>
            <pubDate>Wed, 13 May 2026 16:06:05 GMT</pubDate>
            <description><![CDATA[Summers used to mean cricket, cousins, clay pots, and ice apple cart races. Now? Just heat. A trip down memory lane to the summers that actually summered.]]></description>
            <content:encoded><![CDATA[<p>So, how's your summer going? Mine, if I go out I would definitely come out fried and tired.</p><p>But then I remembered my summers before 2019. What happened after 2019? Well, that's a story for another blog post. Not that summers before are hot, but there was pure joy in them.</p><p>I used to wake up at 7 o'clock but not to get ready for work, but to play cricket. We built the wickets with whatever we found like bricks, wooden sticks etc. and played until the sun is over our heads. But not once did we get dehydrated, Guess why? Because we had clay pots that kept the water cool. Some days we would even cycle kilometres to play against a neighbouring village - no complaints, no excuses, just cricket. Not to mention getting cursed out by the farmers every time the ball landed in their fields - a guarantee, not a possibility.</p><p>Every summer, my village would suddenly feel fuller. Cousins arriving at my grandparent's house - chaos, noise, and instant comfort all at once. Carrom boards would come out, and so would the arguments over who's cheating. We'd tease each other over the silliest things, fight over even sillier ones, and five minutes later it was forgotten. Writing this now, I'm smiling without even realising it.</p><p>How can we continue talking about Summers without paying tribute to Mangoes, Ice Apple and Ice Cream? These were the OGs of summer, then and now. Dozens of mangoes, no worrying about pimples. And those ice apple shells? We'd turn them into little carts and race each other down the street.</p><p>And every single day ended with us all sitting on the benches, chairs, chit-chatting about everything - men discussing politics, women discussing serials, we boys teasing each other, cousins sitting under the moonlight and staring at the stars and gossiping. I miss those simpler days. </p><hr /><p>Now summers are just summers - at least for me. We have smartphones to capture every moment, cameras ready at all times. But somehow, these moments are captured perfectly in memory...</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Opinions</category>
            <category>Personal</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/village.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[PostgreSQL Internals - Module 7: VACUUM, Autovacuum & Bloat]]></title>
            <link>https://sambasivareddy.in/blog/postgresql-internals-module-7-vacuum-autovacuum-bloat</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/postgresql-internals-module-7-vacuum-autovacuum-bloat</guid>
            <pubDate>Tue, 12 May 2026 04:18:18 GMT</pubDate>
            <description><![CDATA[Inside PostgreSQL VACUUM: dead tuple collection, OldestXmin watermark, autovacuum triggers, XID freeze mechanics, bloat diagnosis, and pg_repack vs VACUUM FULL.]]></description>
            <content:encoded><![CDATA[<p>In the previous blog, we studied about the Index in the PostgreSQL, how they will be picked to generate a query plan, different types of indexes available in PostgreSQL. And we ended the blog with possible chance of <code>bloat</code> in indexes when the frequent deletes happen.</p><p>In this blog, we'll explore how VACUUM cleans dead tuples and removes bloat, how it prevents XID wraparound, and how autovacuum - PostgreSQL's background maintenance engine decides when and how aggressively to act.</p><hr /><h2>Part 1: Why VACUUM Exists?</h2><p>Every write operation in PostgreSQL creates a debt that must eventually be paid. For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">INSERT → writes 1 new tuple
       → debt: none (yet)

UPDATE → writes 1 new tuple version
       → marks old tuple dead (xmax set)
       → writes 1 new index entry per index (assume N indexes)
       → marks old index entries dead
       → debt: 1 dead heap tuple + N dead index entries

DELETE → marks tuple dead (xmax set)
       → marks index entries dead
       → debt: 1 dead heap tuple + N dead index entries</code></pre><p>This debt accumulates on every page. And Dead tuples cause:</p><ul><li><p>Waste space: pages fill with unreachable data</p></li><li><p>Slow scans: every scan reads and discards dead tuples</p></li><li><p>Block index-only scans - <code>all_visible</code> bit cannot be set while dead tuples exists</p></li><li><p>Risk wraparound - old <code>xmin</code> values must be frozen before XID space/range exhausts</p></li></ul><p>And VACUUM is the debt collector, whose responsibility to clear all the debt being created, which will discuss in the next part in clear.</p><hr /><h2>Part 2: What VACUUM Actually Does?</h2><p>VACUUM on a table is <strong>not a single operation</strong>. It is a sequence of precise steps</p><h3>Step 1: Acquire ShareUpdateExclusiveLock</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">VACUUM orders;</code></pre><p>VACUUM acquires <code>ShareUpdateExclusiveLock</code> - the weakest table lock. This:</p><ul><li><p>Allows concurrent <code>SELECT</code> , <code>INSERT</code> , <code>UPDATE</code> and <code>DELETE</code></p></li><li><p>Block concurrent <code>VACUUM</code> - only one VACUUM/table at a time</p></li><li><p>Blocks DDL statements on the table like <code>ALTER TABLE</code> and <code>DROP TABLE</code></p></li></ul><p>This is why standard VACUUM does not block normal operations. <code>VACUUM FULL</code> is different altogether, it acquires <code>AccessExclusiveLock</code> and blocks everything.</p><h3>Step 2: Compute OldestXmin</h3><p>Before touching any page, VACUUM computes <code>OldestXmin</code> - the oldest transaction ID that any active transaction could need to see:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>OldestXmin = min(
    all active transaction XIDs,
    all open cursor XIDs,
    all prepared transaction XIDs,
    all replication slot restart_lsn XIDs
)</code></pre><p>Any tuple with <code>xmax &lt; OldestXmin</code> is dead to everyone i.e., no active transaction can ever see it again. These are safe to remove.</p><h3>Step 3: Scan the heap - page by page</h3><p>VACUUM reads every heap page sequentially. For each page, it performs the following checks:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>1. Read page into shared buffers

2. Check visibility map:
   if all_visible bit SET → SKIP this page entirely
      i.e., no dead tuples possible, no work to do here
   if all_frozen bit SET → SKIP for freeze pass too

3. For each tuple on page:
   a. Check xmin/xmax against OldestXmin
   b. If dead (xmax &lt; OldestXmin AND xmax is COMMITTED)
         add to dead_tuples[] array (stored in maintenance_work_mem)
   c. If live but xmin old enough for freezing
         mark for freeze (set HEAP_XMIN_FROZEN bit)
         WAL - log the change

4. If any tuples frozen: WAL - log a FREEZE record</code></pre><p>The <code>dead_tuples[]</code> array in <code>maintenance_work_mem</code> accumulates the <code>TIDs</code> of dead tuples. When it fills up, VACUUM must flush it by cleaning indexes before it can continue scanning the heap; this is why <code>maintenance_work_mem</code> matters for VACUUM performance.</p><h3>Step 4: Clean Indexes</h3><p>For each index on the table, VACUUM calls <code>ambulkdelete()</code> :</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">For B-Tree:
    Scan index leaf page
    For each index entry whose TID is in dead_tuples[]:
        mark the index entry as dead (LP_DEAD flag)
    Return count 

This is O(indexSize), not O(deadTuples) complexity
Why? VACUUM must scan the entire index to find dead entries</code></pre><p>This is why a table with many indexes is expensive to VACUUM. Each index gets a full scan.</p><h3>Step 5: Heap Cleanup</h3><p>After index cleanup, VACUUM makes a <strong><u>second pass</u></strong> (first is to mark dead tuples) over the heap pages that had dead tuples:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>For each page with dead tuples:
  1. Remove dead tuple data
  2. Update ItemId flags: LP_DEAD → LP_UNUSED
  3. Compact the page (shift live tuples toward the end)
  4. Update pd_lower (free space pointer moves back)
  5. Update FSM: report new free space to free space map
  6. If ALL tuples on page are visible to everyone:
       set all_visible bit in visibility map
  7. If ALL tuples on page are frozen:
       set all_frozen bit in visibility map
  8. WAL-log all changes</code></pre><h3>Step 6: Update Statistics</h3><p>Once all the above steps done, it's time for the VACUUM to update the stats in <code>pg_class</code> , <code>pg_stat_user_tables</code> and Truncate trailing empty pages if possible.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">1. Update pg_class:
   relpages    ← new page count estimate
   reltuples   ← new live tuple count estimate
   relallvisible ← pages with all_visible bit set

2. Update pg_stat_user_tables:
   n_dead_tup  ← reset to 0
   last_vacuum ← now()</code></pre><hr /><h2>Part 3: Freezing - Preventing XID Wraparound</h2><p>XID is <code>32</code>bit. After <code>~4</code> billion transactions, it wraps around i.e., transaction IDs restart from 3 (XIDs 0–2 are reserved for system use). Without intervention, old tuples with small <code>xmin</code> values would appear to be <strong><em><u>"in the future"</u></em></strong> - invisible to everyone and causes database corruption.</p><h3>The freeze threshold</h3><p>Following parameters control when tuples are frozen:</p><ul><li><p><code>vacuum_freeze_min_age</code></p><ul><li><p>A tuple is eligible for freezing when its <code>xmin</code> is this many transactions old. VACUUM won't freeze newer tuples.</p></li><li><p>Default: <code>50M</code> XIDs</p></li></ul></li><li><p><code>vacuum_freeze_table_age</code></p><ul><li><p>When the table's <code>relfrozenxid</code> is this old, VACUUM performs an aggressive freeze scan - visits every page regardless of the visibility map.</p></li><li><p>Default: <code>150M</code> XIDs</p></li></ul></li><li><p><code>autovacuum_freeze_max_age</code></p><ul><li><p>When a table's relfrozenxid reaches this age, autovacuum is forced to run on it - even if it's otherwise inactive. This is the safety net. If it triggers, you have a problem.</p></li><li><p>Default: <code>200M</code> XIDs</p></li></ul></li></ul><h3>What freezing does?</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Before freeze;
-- t_xmin = 9582, t_infomask = 0x0100 (HEAP_XMIN_COMITTED)

VACUUM FREEZE orders;

-- After freeze
-- t_xmin = 9582 (UNCHANGED - PG 14+ preserves original XID)
-- t_infomask = 0x0300 (HEAP_XMIN_COMMITTED | HEAP_XMIN_FROZEN) 
   -- added frozen bit (0x0200)</code></pre><p>Once frozen bit is set, <code>XID</code> value becomes irrelevant and never be checked again for freezing or vacuuming.</p><h3>Monitor wraparound proximity</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Most critical query for PostgreSQL health:
SELECT datname,
       age(datfrozenxid)                          AS xid_age,
       2000000000 - age(datfrozenxid)             AS xids_remaining,
       round(100.0 * age(datfrozenxid) / 2000000000, 2) AS pct_toward_wraparound
FROM pg_database
ORDER BY age(datfrozenxid) DESC;

-- Per-table view:
SELECT schemaname,
       relname,
       age(relfrozenxid)                          AS table_xid_age,
       pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_stat_user_tables
ORDER BY age(relfrozenxid) DESC
LIMIT 20;

-- ⚠️  WARNING thresholds:
-- age &gt; 1,500,000,000 → URGENT: manual VACUUM FREEZE immediately
-- age &gt; 1,800,000,000 → CRITICAL: PostgreSQL will shut down soon
-- age &gt; 2,100,000,000 → PostgreSQL refuses new transactions</code></pre><hr /><h2>Part 4: Autovacuum - The Background Maintenance Engine</h2><p>Manual <code>VACUUM</code> is not practical at scale. Autovacuum runs in the background automatically. Understanding its decision algorithm is essential for tuning it.</p><h3>Autovacuum Architecture</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">postmaster
  └── autovacuum launcher  (1 process, always running)
        └── autovacuum worker  (up to autovacuum_max_workers, default 3)
              └── one worker per database, one table at a time</code></pre><p>The launcher wakes up every <code>autovacuum_naptime</code> seconds (default: 1 minute), checks <code>pg_stat_user_tables</code> for tables needing work, and forks workers.</p><h3>The Vacuum trigger formula</h3><p>A table is eligible for autovacuum when:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">n_dead_tup &gt; autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × reltuples

Default:
  n_dead_tup &gt; 50 + 0.02 × reltuples
  = dead tuples exceed 50 + 2% of live tuples

Example:
  Table with 10,000,000 rows:
  threshold = 50 + 0.02 × 10,000,000 = 200,050 dead tuples
  → autovacuum triggers after 200K dead tuples accumulate

  Table with 1,000 rows:
  threshold = 50 + 0.02 × 1,000 = 70 dead tuples
  → triggers after just 70 dead tuples</code></pre><h3>The Analyze trigger formula</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">n_mod_since_analyze &gt; autovacuum_analyze_threshold + autovacuum_analyze_scale_factor × reltuples

Default:
  n_mod_since_analyze &gt; 50 + 0.1 × reltuples
  = modifications exceed 50 + 10% of live tuples</code></pre><h3>Cost-based Throttling - why autovacuum seems slow</h3><p>Autovacuum is intentionally throttled to avoid I/O saturation:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">autovacuum_vacuum_cost_delay    = 2ms   (pause between cost limit hits)
autovacuum_vacuum_cost_limit    = 200   (cost units before pausing)

Cost per operation:
  vacuum_cost_page_hit   = 1    (page found in shared_buffers)
  vacuum_cost_page_miss  = 10   (page read from disk)
  vacuum_cost_page_dirty = 20   (page dirtied by VACUUM)

Example: 100% cache miss workload
  200 cost limit / 10 cost per miss = 20 pages per 2ms burst
  = 20 pages × 8KB = 160KB per 2ms
  = ~80 MB/s maximum VACUUM throughput
  For a 100GB table: 100GB / 80MB/s ≈ 21 minutes minimum</code></pre><blockquote><p>This is by design — autovacuum should not compete with your production workload. But if it can't keep up with your write rate, you need to tune cost limits and scale factors per table:</p></blockquote><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Per-table autovacuum tuning (overrides global settings):
ALTER TABLE orders SET (
    autovacuum_vacuum_scale_factor = 0.01,   -- trigger at 1% instead of 2%
    autovacuum_vacuum_cost_delay = 0,         -- no throttling for this table
    autovacuum_vacuum_cost_limit = 1000       -- higher cost limit
);

-- For large, frequently-updated tables:
ALTER TABLE events SET (
    autovacuum_vacuum_scale_factor = 0.005,  -- 0.5%
    autovacuum_vacuum_threshold = 1000,
    autovacuum_analyze_scale_factor = 0.02
);</code></pre><hr /><h2>Part 5: Table Bloat - Diagnosing the Problem</h2><p><code>Bloat</code> is the gap between the <strong><u>logical size of your data and the physical size of the files</u></strong> on disk. It comes in two forms.</p><h3>Heap Bloat</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Quick bloat estimate using pgstattuple:
CREATE EXTENSION pgstattuple;

SELECT * FROM pgstattuple('orders');
-- table_len        = 1073741824  (1 GB physical size)
-- tuple_count      = 5000000     (live rows)
-- tuple_len        = 600000000   (bytes used by live tuples)
-- tuple_percent    = 55.9        (% of pages with live data)
-- dead_tuple_count = 800000      (dead rows)
-- dead_tuple_len   = 96000000    (bytes used by dead tuples)
-- dead_tuple_percent = 8.9       (% of pages with dead data)
-- free_space       = 377741824   (free bytes in pages)
-- free_percent     = 35.2        ← 35% of the table is wasted!

-- Full bloat scan (slow but accurate):
SELECT pg_size_pretty(table_len)          AS total,
       pg_size_pretty(tuple_len)          AS live_data,
       pg_size_pretty(dead_tuple_len)     AS dead_data,
       pg_size_pretty(free_space)         AS free,
       round(dead_tuple_percent::numeric, 1) AS dead_pct,
       round(free_percent::numeric, 1)    AS free_pct
FROM pgstattuple('orders');</code></pre><h3>Index Bloat</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Index bloat via pgstattuple:
SELECT * FROM pgstatindex('idx_orders_status');
-- index_size        = 209715200   (200 MB)
-- leaf_pages        = 24000
-- empty_pages       = 1200        ← pages with no live entries
-- deleted_pages     = 3400        ← pages fully reclaimed
-- avg_leaf_density  = 68.4        ← B-tree pages 68% full (100% = no bloat)
-- leaf_fragmentation = 12.3       ← % of leaf pages out of logical order

-- Bloat estimate without pgstattuple (from catalog stats):
SELECT
    indexrelname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
    round(100.0 * pg_relation_size(indexrelid) /
          nullif(pg_total_relation_size(relid), 0), 1) AS pct_of_table,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
WHERE relname = 'orders'
ORDER BY pg_relation_size(indexrelid) DESC;</code></pre><p>Once you've confirmed severe bloat with the queries above, standard VACUUM won't be enough, it can only reclaim space within existing pages, not shrink the file itself.</p><hr /><h2>Part 6: VACUUM FULL vs pg_repack</h2><p>When bloat is severe, standard VACUUM cannot help - it reclaims space within pages but cannot return pages to the OS or compact the file. For that you need a table rewrite.</p><h3>VACUUM FULL</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">VACUUM FULL orders;</code></pre><p><strong>What it does:</strong></p><ul><li><p>Acquires <code>AccessExclusiveLock</code> - blocks all reads and writes</p></li><li><p>Creates a brand new heap file</p></li><li><p>Copies all live tuples into the new file (compact)</p></li><li><p>Rebuilds all indexes from scratch</p></li><li><p>Drops the old file</p></li><li><p>Releases lock</p></li><li><p>Vacuum full also reset <code>relfrozenxid</code> to the current XID - so after a VACUUM FULL, the table's wraparound age resets to 0</p></li></ul><p><strong>Pros</strong>: Table is fully compacted; all bloat is eliminated.</p><p><strong>Cons</strong>:</p><ul><li><p>Complete table lock for the entire duration</p></li><li><p>duration = proportional to table size</p></li><li><p>100GB table = potentially hours of downtime</p></li></ul><h3>pg_repack - zero-downtime alternative</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-sql">-- Install
CREATE EXTENSION pg_repack;

-- Repack a table online
pg_repack -d mydb -t orders

-- Repack only indexes (even faster)
pg_repack -d mydb -t orders --only-indexes</code></pre><p>How <strong><u>pg_repack</u></strong> works without locking:</p><ol><li><p>Create a new empty table (shadow table)</p></li><li><p>Copy all live tuples to shadow table (no lock, reads allowed)</p></li><li><p>Create a trigger on original table: log all changes to a delta table</p></li><li><p>Apply delta changes to shadow table (catch up with live writes)</p></li><li><p>Repeat step 4 until delta is small (milliseconds of lag)</p></li><li><p>Acquire brief <code>AccessExclusiveLock</code> (milliseconds only)</p></li><li><p>Apply final delta, swap table OIDs, drop original</p></li><li><p>Release lock</p></li></ol><p><strong><u>Downtime = milliseconds</u></strong> (just the final <code>OID</code> swap), not hours.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>VACUUM FULL:  simple, built-in, requires maintenance window
pg_repack:    complex, extension required, truly online</code></pre><hr /><h2>Part 7: Reading VACUUM VERBOSE Output</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>VACUUM (VERBOSE, ANALYZE) orders;</code></pre><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">INFO:  vacuuming "public.orders"
INFO:  scanned index "orders_pkey" to remove 84000 row versions  -- index cleanup pass
      DETAIL: CPU: user: 0.42s, system: 0.08s, elapsed: 1.23s
INFO:  scanned index "idx_orders_status" to remove 84000 row versions
      DETAIL: CPU: user: 0.38s, system: 0.05s, elapsed: 1.01s
INFO:  "orders": removed 84000 row versions in 7200 pages          -- heap cleanup
      DETAIL: CPU: user: 0.12s, system: 0.04s, elapsed: 0.18s
INFO:  index "orders_pkey" now contains 5000000 row versions in 14000 pages
      DETAIL: 84000 index row versions were removed.
              0 index pages have been deleted, 0 are currently reusable.
INFO:  "orders": found 84000 removable, 5000000 nonremovable row versions
                 in 62800 out of 62800 pages
      DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 1234567
              There were 12000 unused item identifiers.              -- reusable slots
              Skipped 0 pages due to buffer pins, 0 frozen pages.
              0 pages are entirely empty.
              CPU: user: 1.24s, system: 0.23s, elapsed: 4.87s
INFO:  analyzing "public.orders"
INFO:  "orders": scanned 30000 of 62800 pages, containing 2400000 live rows
                 and 0 dead rows; 30000 rows in sample, 5000000 estimated total rows</code></pre><p>After running VACUUM VERBOSE, these are the numbers that tell you whether VACUUM did its job:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-markdown">84000 removable row versions   → dead tuples successfully collected
0 cannot be removed yet        → blocked by long-running transaction
Skipped N frozen pages         → visibility map working (those pages skipped)
0 pages entirely empty         → no file truncation possible (no trailing empty pages)
14000 pages in pkey index      → index is growing</code></pre><hr /><h2>Conclusion</h2><p>In this module, we explored how VACUUM and autovacuum work under the hood, starting from:</p><ol><li><p>Why VACUUM exists? every <code>UPDATE</code> and <code>DELETE</code> leaves dead tuples and dead index entries behind due to MVCC's append-only write model, and without VACUUM the table grows forever, index-only scans stop working, and XID wraparound eventually corrupts the entire database.</p></li><li><p>The exact VACUUM sequence: computing <code>OldestXmin</code> as the reclaim watermark, scanning heap pages while skipping <code>all_visible</code> ones, collecting dead TIDs into <code>maintenance_work_mem</code>, cleaning indexes via <code>ambulkdelete()</code>, compacting pages, updating the FSM, and setting visibility map bits.</p></li><li><p>How autovacuum decides when to act: the trigger formula, cost-based throttling parameters, and how to tune aggressiveness per table using <code>ALTER TABLE ... SET (autovacuum_vacuum_scale_factor = ...)</code> for large high-churn tables.</p></li><li><p>How XID freezing prevents wraparound: setting the <code>HEAP_XMIN_FROZEN</code> infomask bit in PostgreSQL 14+, the three age thresholds that control when freezing happens, and how to monitor proximity to the 2.1 billion XID danger zone using <code>age(datfrozenxid)</code>.</p></li><li><p>When to use <code>VACUUM FULL</code> versus <code>pg_repack</code> : full table lock for hours versus online shadow-table rewrite with milliseconds of downtime.</p></li></ol><hr /><h2>What's Next → Module 8: Concurrency &amp; Locking</h2><p>We've now seen that VACUUM itself needs locks to run, that long-running transactions block <code>OldestXmin</code> from advancing, and that <code>VACUUM FULL</code> holds <code>AccessExclusiveLock</code> for its entire duration. Module 8 goes into the locking system itself; the full lock hierarchy from <code>AccessShareLock</code> to <code>AccessExclusiveLock</code>, how row-level locks work through <code>xmax</code> and <code>MultiXactId</code> rather than a lock table, deadlock detection, advisory locks, and the <code>pg_locks</code> queries that diagnose blocking in production.</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>PostgreSQL</category>
            <category>Technical</category>
            <category>PGInternals</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/module7_banner.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Rust Basics - Module 4: Structs, Enums and Pattern Matching]]></title>
            <link>https://sambasivareddy.in/blog/rust-basics-4-structs-enums-and-pattern-matching</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/rust-basics-4-structs-enums-and-pattern-matching</guid>
            <pubDate>Sun, 10 May 2026 03:34:20 GMT</pubDate>
            <description><![CDATA[Learn Rust structs, enums, Option<T>, and pattern matching. Understand algebraic data types and how match eliminates null pointer bugs at compile time.]]></description>
            <content:encoded><![CDATA[<p>In the previous blog post, we discussed about <strong>"Borrowing and References"</strong> and how they make ownership practical. <span>Without borrowing, every function call would consume its arguments, and that makes ownership extremely awkward to work with. In this blog post, we will cover Structs, Enums and Pattern Matching in the Rust. </span><br /><br /><span>Structs are the <strong>custom data types</strong> which allow us to group related data types into one. And Enums are <strong>algebraic data types</strong> i.e., each variant can carry its own data of different types unlike 'C' enums - </span>a <strong>user-defined data type</strong> used to assign names to integer constants.</p><p>Let's start with structs, then move to enums and bring it all together with pattern matching.</p><hr /><h2>Part 1: Structs</h2><p>A struct or structure, is a <strong>custom data type that lets us group and name multiple related values that make up a meaningful group</strong> under a <strong><u>named type</u></strong>.</p><h3>Definition</h3><p>Using <code>struct</code> keyword we can define our own structure in Rust. For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">struct User {
    username: String,   // Calls as Field
    email: String,
    age: u32,
    active: bool,
}</code></pre><h3>Creating Instance and Access</h3><p>To use a struct after we defined it, we have to create an instance by giving a concrete value to the each field in the struct. We can do that by stating the name of the struct and then add the curly brackets containing the <code>key:value</code> pairs, where the keys are the names of the fields and values are the data we wanted to store.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn main() {
    let user = User {
        username: String::from("siva"),
        email: String::from("siva@example.com"),
        age: 25,
        active: true,
    };
  
    println!("{}", user.username);
}</code></pre><blockquote><p><strong><u>Note</u></strong>: Since <code>struct</code> acts as a template for a group, we don't need to create/assign the fields in the same order as we defined in the struct.</p></blockquote><p>Now, if we want to access a value from the instance we just created, we can use the <code>.</code> notation to access the particular filed and get the value.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let username = user.username;</code></pre><p>Structs are <strong>immutable</strong> by default - the same rule as any variable. To mutate the fields, the entire binding must be <code>mut</code> , as Rust does not allow marking each individual fields as <code>mut</code> . <strong>It is all or nothing.</strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let mut user = User {
    username: String::from("siva"),
    email: String::from("siva@example.com"),
    age: 25,
    active: true,
};

user.age = 24; // Allowed now</code></pre><p>And also, we can create a new instance reusing fields from an existing one, or what we call <strong>Struct update syntax.</strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let user2 = User {
    email: String::from("other@example.com"),
    ..user   // fills remaining fields from user defined above
} </code></pre><blockquote><p><strong><u>Note</u></strong>: <code>..user</code> moves fields that are not <code>Copy</code> . If username is a <code>String</code> , it moves out of <code>user</code> and no long fully usable, so use when it is absolute necessary.</p></blockquote><hr /><h2>Part 2: Types of Struct</h2><p>In Rust, we can define or use a struct in three ways, say:</p><ol><li><p>Named field struct</p></li><li><p>Tuple struct</p></li><li><p>Unit struct</p></li></ol><h3>Named Field Struct</h3><p>Standard way of defining the structs as we discussed above.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>struct User {
    username: String,
    email: String,
    age: u32,
    active: bool,
}</code></pre><h3>Tuple Struct</h3><p>Tuple structs is <strong>named tuples</strong>, useful when the <strong>field's position</strong> carries meaning:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">struct Point(f64, f64);
struct Color(u8, u8, u8);

let p = Point(3.0, 4.0);
let c = Color(255, 128, 0);

println!("{}", p.0); // Access fields by position number</code></pre><h3>Unit Structs</h3><p>Unit-like structs can be useful when you need to implement a <code>trait</code> (we'll discuss more in upcoming modules) on some type but don't have any data that you want to store in the type itself.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}</code></pre><hr /><h2>Part 3: Behaviour with impl - Methods &amp; Associated Functions</h2><p>Methods are similar to functions. We declare them with the <code>fn</code> keyword followed by a name, parameters and a return value. Unlike functions, methods are defined within the context of a struct (or enum/trait), and their first parameter is always <code>self</code>, which represents the instance of the struct the method is being called on. The only exception for <code>self</code> is, if the function is an <strong><u>associated function to struct</u></strong> like the function create/initialize a struct.</p><p>Rust separates data definition from behaviour. We can define the set of methods in an <code>impl</code> block, not in the struct itself.</p><h3>Example</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">// Struct (data definition)
struct Rectangle {
    width: f64,
    height: f64,
} 

// Methods (behaviour)
impl Rectangle {
    // associated
    fn new(width: f64, height: f64) -&gt; Rectangle {
        Rectangle { width, height } // field init shortand when name matches
    }

    // method - takes &amp;self, borrows the instance
    fn area(&amp;self) -&gt; f64 {
        self.width * self.height
    }

    // mutable method - takes &amp;mut self
    fn scale(&amp;mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }

    // consuming method - takes self, moves the instance
    fn into_square(self) -&gt; Rectangle {
        let side = self.width.min(self.height);
        Rectangle { width: side, height: side }
    }
}

fn main() {
    let mut r = Rectangle::new(10.0, 5.0);
    println!("area: {}", r.area());
    r.scale(2.0);
    println!("after scale: {}x{}", r.width, r.height);
}</code></pre><p>The three method receives types - <code>&amp;self</code> , <code>&amp;mut self</code> , <code>self</code> - map directly to the borrowing rules. <code>&amp;self</code> borrows immutably. <code>&amp;mut self</code> borrows mutably. <code>self</code> takes ownership - the caller loses the instance after calling this method.</p><p><code>Rectangle::new</code> is an associated function (no <code>self</code> ). It is the Rust convention for constructors. There is no new keyword - it is just a naming convention.</p><hr /><h2>Part 4: Enums</h2><p>Also called Enumerations. Enums allow you to define a type by enumerating its possible variants. And this is where Rust diverges sharply from C. Rust enums are algebraic data types - each variant can carry its own data of different types.</p><h3>Defining the Enums</h3><p>Using <code>enum</code> keyword, we can define an enum in the Rust. For example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle(f64, f64, f64),
}</code></pre><p>This is a single type <code>Shape</code> that can be any one of three variants, each carrying different data. Compare this to C where we would need a <code>union</code> plus a tag field plus discipline to not access the wrong union member. <strong><em><u>Rust eliminates that entire pattern: the compiler knows which variant is active, and pattern matching forces you to handle each one explicitly.</u></em></strong></p><p>Another example:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}</code></pre><p>This enum has four variants with different types:</p><ul><li><p><code>Quit</code> : Has no data associated with it at all</p></li><li><p><code>Move</code> : Has named fields, like a <code>struct</code> does</p></li><li><p><code>Write</code> : Includes a single <code>String</code></p></li><li><p><code>ChangeColor</code> : Includes three <code>i32</code> values.</p></li></ul><h3>Enum Values</h3><p>We can create instances of each of the three variants of <code>Shape</code> like this:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let c = Shape::Circle(10.0);
let r = Shape::Rectangle(10.0, 15.0);
let t = Shape::Triangle(5.0, 6.0, 10.0);</code></pre><h3>Standard Rust's Enums</h3><p>The most important enums in all of Rust are built into the standard library:</p><ol><li><p><code>Option&lt;T&gt;</code></p></li><li><p><code>Result&lt;T, E&gt;</code></p></li></ol><p><code>Option&lt;T&gt;</code> </p><p>Represents a value that may or may not exists. Rust has no <code>null</code> . Instead:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">enum Option&lt;T&gt; {
    Some(T), // there is a value
    None,    // there is no value
}

let maybe: Option&lt;i32&gt; = Some(42);
let nothing: Option&lt;i32&gt; = None;</code></pre><p>You cannot use an <code>Option&lt;T&gt;</code> as if it were a <code>T</code> directly - the compiler forces you to handle the <code>None</code> case. <strong><u>This eliminates an entire class of null pointer bugs.</u></strong></p><p><code>Result&lt;T, E&gt;</code> </p><p>Represents either success or failure.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">enum Result&lt;T, E&gt; {
    Ok(T),   // Success, carries the value
    Err(E),  // Failure, carries the error
}

fn divide(a: f64, b: f64) -&gt; Result&lt;f64, String&gt; {
    if b == 0.0 {
        Err(String::from("division by zero"))
    } else {
        Ok(a/b)
    }
}</code></pre><hr /><h2>Part 5: Pattern Matching - <code>match</code> </h2><p><code>match</code> is the primary way to work with enums. It is exhaustive - the compiler forces you to handle every variant. No case can be silently ignored. An example using <code>Shape</code> enum defined above:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn area(shape: &amp;Shape) -&gt; f64 {
	match shape {
		Shape::Circle(r) =&gt; std::f64::consts::PI * r * r,
		Shape::Rectangle(w, h) =&gt; w * h,
		Shape::Triangle(a, b, c) =&gt; {
			let s = (a + b + c) / 2.0;
			(s * (s-a) * (s-b) * (s-c)).sqrt() 
		}
	}
}</code></pre><p>Each arm is <code>pattern -&gt; expression</code> . The pattern de-structure the variant and bind its inner values to names. If you miss a variant, the compiler tells you - no runtime surprises.</p><p>For <code>Option</code> :</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let value: Option&lt;i32&gt; = Some(42);

match value {
    Some(n) =&gt; println!("got: {}", n),
    None =&gt; println!("nothing"),
};</code></pre><p><strong>Catch-all patterns</strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">match number {
    1 =&gt; println!("one"),
    2 =&gt; println!("two"),
    _ =&gt; println!("something else"), // _ matches anything, binds nothing
}</code></pre><p><code>if let</code> - shorthand when you only care about one variant:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">if let Some(n) = value {
    println!("got: {}", n);
} // equivalent to matching Some and ignoring None</code></pre><p><code>while let</code> - loop while a pattern matches:</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">while let Some(top) = stack.pop() {
    println!("{}", top);
}</code></pre><hr /><h2>Part 6: Exercises</h2><ol><li><p>Define a struct <code>Transaction</code> with fields: <code>id: u64</code> , <code>amount: f64</code> , <code>status: String</code> . Add an <code>impl</code> block with a constructor <code>new</code> and a method <code>is_large(&amp;self) -&gt; bool</code> that returns true if <code>amount &gt; 1000.0</code> .</p></li></ol><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">struct Transaction {
    id: u64,
    amount: f64,
    status: String,
}

impl Transaction {
    fn new(id: u64, amount: f64, status: String) -&gt; Transaction {
        Transaction { id, amount, status }
    }

    fn is_large(&amp;self) -&gt; bool {
        self.amount &gt; 1000.0
    }
}

fn main() {
    let txn1 = Transaction::new(1, 900.0, String::from("success"));
    let txn2 = Transaction::new(2, 1001.0, String::from("success"));

    println!("Txn1 is large = {}", txn1.is_large());
    println!("Txn2 is large = {}", txn2.is_large());
}</code></pre><ol><li><p>Define an enum <code>Command</code> with variants: <code>Quit</code> , <code>Move { x: i32, y: i32 }</code> , <code>Print(String)</code> . Write a <code>match</code> that handles all three and prints something meaningful for each.</p></li></ol><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">enum Command {
    Quit,
    Move { x: i32, y: i32 },
    Print(String)
}

fn execute_command(cmd: &amp;Command) {
    match cmd {
        Command::Quit        =&gt; println!("Quitting"),
        Command::Move{x, y}  =&gt; println!("Moving x:{} and y:{}", x, y),
        Command::Print(s)    =&gt; println!("{}", s)
    }
}

fn main() {
    let cmd1 = Command::Quit;
    let cmd2 = Command::Move {
        x: 10,
        y: 10,
    };
    let cmd3 = Command::Print("Hello".to_string());

    execute_command(&amp;cmd1);
    execute_command(&amp;cmd2);
    execute_command(&amp;cmd3);
}</code></pre><hr /><h2>Conclusion</h2><p>Structs let you group related data into meaningful types. Enums let a single type represent multiple distinct variants, each carrying its own payload. <code>match</code> ties it all together - exhaustive, compiler-enforced, no silent ignores. Together, they replace the scattered nulls, union hacks, and runtime surprises you'd find in C. This is Rust making correctness a compile-time guarantee, not a runtime hope.</p><p><strong><u>What's Next: Traits &amp; Generics</u></strong><br />So far, every type we've written is fixed to one kind of data. Traits and Generics change that letting you write code that works across many types while keeping the compiler's full safety guarantees. That's the next module.</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Technical</category>
            <category>Rust</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/poster_v2_module4_enums.svg" length="0" type="image/svg"/>
        </item>
        <item>
            <title><![CDATA[Rust Basics - Module 3: Borrowing and References]]></title>
            <link>https://sambasivareddy.in/blog/rust-basics-module-3-borrowing-and-references</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/rust-basics-module-3-borrowing-and-references</guid>
            <pubDate>Fri, 08 May 2026 01:38:11 GMT</pubDate>
            <description><![CDATA[Learn how Rust's borrowing and references work - immutable vs mutable refs, borrow checker rules, dangling pointers, &str vs String, and slices explained.]]></description>
            <content:encoded><![CDATA[<p>In the previous blog post, we have discussed about the <strong>"Ownership" </strong>and its three rules of ownership and how it enforces only one owner for the given data at the given time, so that our code is free from memory bugs. In this post, we'll cover references, borrowing rules, the borrow checker in action, and slices.</p><p>If ownership is the rule, borrowing is the exception that makes the rule practical. </p><p>Without borrowing, every function call would consume its arguments, and that's genuinely painful to work with.</p><hr /><h2>Part 1: The Core Idea - References</h2><p><strong><u>A Reference is a pointer to a value that you do not own</u></strong>. We are borrowing it, like lending a book. The lender still owns it. When the borrow ends, the book goes back.</p><p>The syntax is <code>&amp;T</code> for an immutable reference, <code>&amp;mut T</code> for a mutable one.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let s = String::from("hello");
let r = &amp;s                      // r borrows s - s still owns the heap data

println!("{}", r);              // Valid, prints "hello"
println!("{}", s);              // Valid, prints "hello"</code></pre><p><strong>The </strong><code>&amp;</code><strong> in </strong><code>&amp;s</code><strong> creates a reference</strong>. The value <code>r</code> on the stack is just a pointer to the address of <code>s</code> . It does not own the heap data. <strong><em>When </em></strong><code>r</code><strong><em> goes out of scope, nothing is freed</em></strong>. When <code>s</code> goes out of scope, the heap is freed - because <code>s</code> is still the owner.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">Stack
----------------------
r(&amp;String) ptr --&gt; s
----------------------
s(owner)
ptr -&gt; heap
len: 5
cap: 5                  -------------&gt; Heap | Heap data 'h' 'e' 'l' 'l' 'o' |
----------------------                 </code></pre><hr /><h2>Part 2: Immutable vs Mutable References</h2><p>Rust has exactly two references types, and the rules around them are asymmetric by design.</p><ol><li><p>Immutable Reference <code>&amp;T</code></p></li><li><p>Mutable Reference <code>&amp;mut T</code></p></li></ol><p>An immutable reference <code>&amp;T</code> <strong><u>lets you read the value</u></strong>. <strong>You can have as many of these as you want simultaneously</strong>. No one can modify the value while any immutable reference exists.</p><p>A mutable reference <code>&amp;mut T</code> <strong><u>lets you both read and write the value</u></strong>. <strong>You can have exactly one at a time</strong>. No immutable references can exist simultaneously with a mutable one.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let mut s = String::from("hello");

let r1 = &amp;s;           // fine, read-only
let r2 = &amp;s;           // fine, read-only // multiple immutable refs allowed at a same time
println!("{} {}", r1, r2);  

let r3 = &amp;mut s;       // fine, r1 &amp; r2 are no longer used after above println
r3.push_str(" world");  // r3 is a mutable one, so it can make changes to s's heap
println!("{}", r3);

// Not allowed, error: cannot borrow `s` as mutable because it is also borrowed as immutable
let r4 = &amp;s;
let r5 = &amp;mut s;
println!("{} {}", r4, r5); </code></pre><blockquote><p>The rule stated precisely: <strong>at any given point in the code, you may have either one mutable reference OR any number of immutable references - never both at the same time.</strong></p></blockquote><hr /><h2>Part 3: The Borrow Checker in action</h2><h3>Case 1: Simultaneous Mutable and Immutable References</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let mut s = String::from("hello");
let r1 = &amp;s;            // immutable borrow begins
let r2 = &amp;mut s;        // cannot borrw s as mutable because r1 is still alive

println!("{}", r1);</code></pre><p>If this were allowed, <code>r2</code> could modify <code>s</code> 's heap buffer, while <code>r1</code> still holds the old pointer - classic use-after-free case.</p><h3>Case 2: Two mutable references</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let mut s = String::from("hello");
let r1 = &amp;mut s;
let r2 = &amp;mut s;        // cannot borrow s as mutable "more than once"
r1.push_str(" world");</code></pre><p>If both <code>r1</code> and <code>r2</code> could mutate <code>s</code> simultaneously, we have a <strong>data race </strong>- even on a single thread, the mutation order is ambiguous and the internal state (ptr, len, cap) in the stack could become inconsistent.</p><h3>Case 3: Dangling Reference</h3><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn dangle() -&gt; &amp;String {            // returning a reference to local data
  let s = String::from("hello");
  &amp;s                                // 's' will be dropped when function returns
}</code></pre><p>The compiler rejects this: <code>s</code> will be freed/deallocated when <code>dangle</code> returns, so the reference would point to freed memory when we tried to access it. In C this compiles fine and produces a dangling pointer. Rust refuses to compile it.</p><p>The solution is to return the <code>String</code> itself by giving up the ownership to the caller</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn no_dangle() -&gt; String {
   let s = String::from("hello");
   s
}</code></pre><p>This works fine. Ownership is moved, and nothing is deallocated.</p><hr /><h2>Part 4: &amp;str VS String - a crucial distinction</h2><p>Now that we understand references, <code>&amp;str</code> should make complete sense.</p><p><code>String</code> is an owned, heap-allocated, growable string. <code>&amp;str</code> is a borrowed reference to a string slice, it is just a pointer and a length. It can point into a <code>String</code> 's heap buffer, or into a string literal in the binary's read-only segment.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let s: String = String::from("hello world");
let slice: &amp;str = &amp;s[0..5];    // borrows 5 bytes of s's heap buffer
let literal: &amp;str = "hello";   // points into read-only binary segment</code></pre><p>The idiomatic guideline: if your function only needs to read a string, take <code>&amp;str</code> - not <code>&amp;String</code> . This is more flexible because both <code>String</code> and <code>&amp;str</code> can be passed as <code>&amp;str</code> :</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn print_it(s: &amp;str) {
   println!("{}", s);
}

let owned = String::from("hello");
print_it(&amp;owned);                   // &amp;String auto-coerces to &amp;str
print_it("world");                  // &amp;str literal works directly</code></pre><p>This auto-coercion is called <strong><em>de-ref coercion</em></strong><em> </em>- <code>&amp;String</code> automatically becomes <code>&amp;str</code> when the context demands it. We will discuss more about this in the traits module.</p><p>Before we wrap up, there's one more borrowed type worth understanding - <strong>slices</strong>.</p><hr /><h2>Part 5: Slices - references to a contiguous sequence</h2><p>The same idea generalises beyond strings. A slice <code>&amp;[T]</code> is a reference to a contiguous portion of an array or <code>Vec</code>.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let arr = [1, 2, 3, 4, 5];
let slice: &amp;[i32] = &amp;arr[1..4];      // borrows elements 2,3,4
println!("{:?}", slice);             // [2,3,4]</code></pre><p>A slice is always a fat pointer - two words on the stack: a pointer to the first element, and the length. <strong>No ownership, no allocation.</strong></p><hr /><h2>Conclusion</h2><p>Borrowing is Rust's answer to a hard problem: how do you share data without copying it, and without the chaos of unchecked aliasing?</p><p>The rules are simple to state but deep in consequence:</p><ol><li><p> References let you read or modify data without owning it</p></li><li><p>Immutable references can coexist freely; mutable references demand exclusivity</p></li><li><p>The borrow checker enforces these rules at compile time — no runtime cost,  no garbage collector needed</p></li></ol><p>Once these rules click, &amp;str, slices, and the broader Rust type system start to feel less like restrictions and more like a contract that makes your code provably safe. Ownership tells you who is responsible. Borrowing tells you who is allowed to look or touch and for how long.</p><p>In the next post, we'll move into Structs, Enums and Pattern Matching.</p><p></p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Technical</category>
            <category>Rust</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/poster_v2_module3_borrowing.svg" length="0" type="image/svg"/>
        </item>
        <item>
            <title><![CDATA[Rust Basics - Module 2: Ownership]]></title>
            <link>https://sambasivareddy.in/blog/rust-basics-2-ownership</link>
            <guid isPermaLink="false">https://sambasivareddy.in/blog/rust-basics-2-ownership</guid>
            <pubDate>Wed, 06 May 2026 01:10:56 GMT</pubDate>
            <description><![CDATA[Learn about the ownership in Rust and how every other things like borrowing, lifetimes, concurrency safety is built on top of the ownership in details.]]></description>
            <content:encoded><![CDATA[<p>In the previous blog post of this series, we have learned about the Variables, Types, Functions and Control Flow which are very important to get start with any programming language.</p><p>Today, we are going to discuss little deeper and most important topic in all of Rust i.e., Ownership. Everything else like borrowing, lifetimes, concurrency safety is built on top of what Ownership is.</p><hr /><h2>Part 1: The Problem Rust is Solving</h2><p>In C, memory bugs fall into a few categories:</p><ul><li><p><strong>Use-after-free</strong>: You free a buffer and then dereference it.</p></li><li><p><strong>Double-free</strong>: Two code paths both call free on the same pointer.</p></li><li><p><strong>Memory Leak</strong>: Nobody calls free at all.</p></li><li><p><strong>Dangling Pointer</strong>: a pointer outlives the data it points to or in simpler word accessing a pointer beyond its scope.</p></li></ul><p>Languages like C++ and Java solve this with a <strong><em><u>Garbage Collector</u></em></strong> - a runtime process that tracks all live references and frees memory when nothing points to it anymore. It brings <strong>GC pauses, heap pressure, and no control over layout or freeing timing.</strong></p><p>Rust's answer is different: <mark>Encode the rules of memory ownership into the type variables, and have the compiler enforce them at compile time</mark>. <em>Zero runtime cost, zero garbage collection, zero undefined behaviour.</em></p><p>And the rules are called the "<strong>Ownership System</strong>".</p><hr /><h2>Part 2: The Ownership Rules</h2><p>There are three axioms/rules. Everything else is a consequence of them.</p><ol><li><p><em>Every value in Rust has exactly one owner i.e a Variable.</em></p></li><li><p><em>When the owner goes out of scope, the value is dropped (memory freed).</em></p></li><li><p><em>There can only be one owner at a time.</em></p></li></ol><p>If any of the rules are violated, <strong><u>the program won't compile</u></strong>. And these features of ownership won't slow down our program as well since these will be checked at compile time itself, so if compilation passes, we can safely assume our program is memory bug free.</p><hr /><h2>Part 3: Stack v/s Heap</h2><p>Before going deep dive into Ownership, let's discuss where the values and data live. Rust explicitly holds these some data structures based on type system.</p><p>Many programming languages don't require you to think about the stack and the heap very often. But in a systems programming language like Rust, <em>whether a value is on the stack or the heap affects how the language behaves</em> and why you have to make certain decisions.</p><h3>Stack</h3><ol><li><p>Stack stores the values in the order it gets them, and remove in opposite, or we can say it follows <strong>LIFO</strong> (Last In First Out) principle.</p></li><li><p>All the data stored on stack must have a <strong>known, fixed size at compile time</strong>. Data with an unknown size or a size that might change must be stored on the <strong>heap</strong> instead.</p></li><li><p>On function return, its entire stack frame is gone i.e., all local variables on it are freed automatically.</p></li><li><p>Fast Access and No Runtime allocation.</p></li></ol><h3>Heap</h3><ol><li><p>The heap is less organized: When you put data on the heap, you request a certain amount of space. The memory allocator finds an empty spot in the heap (which is big enough), marks it as being in use, and returns a pointer, which is the address of that location.</p></li><li><p>Request memory at Runtime via an Allocator,</p></li><li><p>Persists until explicitly freed.</p></li><li><p><strong>Stack stores a pointer to it.</strong></p></li></ol><p>In Rust, types that are entirely stack-resident implement the <code>Copy</code> trait (more on this below) - <strong><u>integers, booleans, floats, tuples, fixed arrays</u></strong>.</p><p><strong>So what is the </strong><code>Copy</code><strong> trait?</strong> In Rust, when we assigns a value of one variable to another variable, if the value is copied to another variable instead of giving a reference/pointer, then that type has a <code>Copy</code> trait. Types that has own heap memory are like <code>String</code> , <code>Vec&lt;T&gt;</code> , <code>Box&lt;T&gt;</code> - do not implement Copy. This distinction drives the <strong><em><u>move semantics</u></em></strong> which we are going to learn now.</p><hr /><h2>Part 4: Move Semantics</h2><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn main() {
   let num1 = 5;
   let num2 = num1;                   // Now "5" is copied to num2 as well
   println!("{} {}", num1, num2);     // Prints 5, 5
   
   let s1 = String::from("hello");
   let s2 = s1;                     // Ownership moved from s1 to s2
   println!("{}", s1);              // Compile Error: borrow of moved value: `s1`
}</code></pre><h3>Why does this fail?</h3><p>Let's look closer at what String actually is in memory:</p><p>As discussed above, String memory will be allocated in the Heap, but a pointer to that Heap location is stored in the stack (4th point in Part 3 Heap section). Now let's look closer at what String actually is in memory:</p><p>A String on the stack is a three-field struct:</p><ol><li><p>a pointer to heap data</p></li><li><p>a length</p></li><li><p>a capacity</p></li></ol><p>So, when we write <code>let s2 = s1</code> , <u>Rust does not deep-copy the heap data. Instead it copies the three stack fields (pointer, len, cap) to </u><code>s2</code><u>, and then immediately invalidates </u><code>s1</code> . This is a <strong><u>Move</u></strong>. Ownership of the heap data transferred from <code>s1</code> to <code>s2</code> . And there is now exactly one owner to the memory location in Heap i.e <code>s2</code>(Axiom 1 in ownership rules).</p><h3>Why does Rust do this instead of Copying?</h3><p>Because copying heap data is an <code>O(n)</code> operation. <strong>Rust never silently does expensive things. </strong>If we want a deep copy, we must ask for it explicitly with <code>.clone()</code> .</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn main() {
   let s1 = String::from("hello");
   let s2 = s1.clone();         // Explicit deep copy - heap data duplicated
   println!("{}", s1);          // s1 is valid and prints "hello"
   println!("{}", s2);          // s2 is ALSO valid and prints "hello", has it's own copy
}</code></pre><p><code>.clone()</code> is a signal in code review: "this is an allocation, this costs memory and time." It is intentional, never hidden.</p><hr /><h2>Part 5: Copy Types - the exception</h2><p>For stack-only types, there is no heap to worry about i.e., copying is just copying bits. Rust mark these types with the <code>Copy</code> trait, and for them, assignment copies instead of moving.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">let x: i32 = 5;
let y = x;  // x is Copy - this is a bitwise copy, not move

println!("{}", x); // x is still valid
println!("{}", y); // y has it's own copy</code></pre><p>The <code>Copy</code> types include: all integer types, f32/f64, bool, char, tuples and arrays composed entirely of Copy types.</p><p>String is <strong>NOT</strong> Copy because it owns heap memory. <code>&amp;str</code> (a string reference - not owner) <strong>IS</strong> Copy, because it is just a pointer, it does not own the heap data.</p><hr /><h2>Part 6: Ownership and Functions</h2><p>What happens when we pass the values to functions as Parameters/Arguement, a <strong><u>Move</u></strong><u> </u><strong><u>happens, not just an assignment.</u></strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn print_string(s: String) {          //s become the owner now
	println!("{}", s);
}                                    // s dropped here and heap freed

fn main() {
	let s = String::from("hello");
	print_string(s);               // ownership moves into the function
	println!("{}", s);            // compile error: borrow of moved value: `s`
}</code></pre><p>The function <code>print_string</code> took ownership of <code>s</code> . When it returned, <code>s</code> was dropped. The caller no longer has it.</p><p>We have two ways out of this:</p><ol><li><p>Brute-force</p></li><li><p>Borrowing (will discuss in next module)</p></li></ol><h3>Option 1: Return Ownership</h3><p>The brute-force option is to return the ownership back to the original owner who passed it.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">fn print_string(
   s: String        // Takes the ownership from the Caller
) -&gt; String /* Returning String returns ownership to the caller back */ { 
	println!("{}", s);
	s 
}

fn main() {
	let s = String::from("hello");
	let s = print_string(s);     // get ownership back
	println!("{}", s);           // valid now
}</code></pre><p>This is intentionally verbose. Rust is showing us the cost. The real solution is lending without transferring ownership i.e Borrowing.</p><h3>Option 2: Borrowing</h3><p>If ownership is the rule, borrowing is the exception that makes the rule practical. Without borrowing every function call would consume its arguments and it’s painful.</p><p>Borrowing lets you hand data to a function/variable without giving up ownership of it.</p><p><strong><u>Example</u></strong></p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code>let s = String::from("hello");
let r = &amp;s // r borrows s - s still owns the heap data

println!("{}", r); // Valid, prints "hello"
println!("{}", s); // Valid, prints "hello"</code></pre><p><code>&amp;</code> let us borrow the heap from <code>s</code> to <code>r</code> , then <code>r</code> can work on the borrowed data. We will discuss in detailed in next module</p><hr /><h2>Part 7: Drop - Automatic Cleanup</h2><p><strong>When a value's owner goes out of scope, Rust calls it's drop function automatically.</strong> This is analogous to a C++ destructor, except in Rust it run deterministically, at a known point in the code, not whenever a Garbage Collector (GC) decides to collect.</p><pre class="hljs rounded-lg p-4 overflow-x-auto my-4"><code class="language-rust">{
   let s = String::from("hello");     // heap allocated
   // Use 's' as per the use-case
   // &lt;-- 's' goes out of scope here. Drop called, heap freed.
}</code></pre><p>We never call <code>drop</code> manually (well, <code>std::mem::drop(s)</code> exists for forced early drops, but it's uncommon in practice). The compiler inserts the call automatically.</p><p>This is how Rust achieves C-level performance without a Garbage collector, memory is freed as soon as it's no longer needed, deterministically, with zero runtime bookkeeping.</p><hr /><h2>Conclusion</h2><p>Ownership is Rust's core bet: instead of managing memory at runtime (GC) or leaving it to the programmer (C/C++), encode the rules into the type system and let the compiler enforce them. The three axioms: one owner, drop on scope exit, no simultaneous owners - are simple. But everything that follows (borrowing, lifetimes, <code>Arc</code>, <code>Mutex</code>) is just the compiler asking <em>"does this respect ownership?"</em> at increasingly complex levels.</p><p>In the next post, we'll look at <strong>Borrowing and References</strong> - the mechanism that lets you use data without taking ownership of it, which is how real Rust code avoids the verbose "return ownership back" pattern we saw in Part 6.</p>]]></content:encoded>
            <author>Samba Siva</author>
            <category>Technical</category>
            <category>Rust</category>
            <enclosure url="https://pub-b8d5ca13188446a08ac9941fcca1304e.r2.dev/poster_v2_module2_ownership.svg" length="0" type="image/svg"/>
        </item>
    </channel>
</rss>