<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aniketh Deshpande</title>
    <description>The latest articles on DEV Community by Aniketh Deshpande (@anikethsdeshpande).</description>
    <link>https://dev.to/anikethsdeshpande</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F212621%2F92bf2932-4c1c-4ae4-87c2-96232ff403dc.jpeg</url>
      <title>DEV Community: Aniketh Deshpande</title>
      <link>https://dev.to/anikethsdeshpande</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anikethsdeshpande"/>
    <language>en</language>
    <item>
      <title>Database Engines 101: How They Work, the Main Types, and How to Swap Them in PostgreSQL</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Mon, 28 Sep 2026 12:02:03 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/database-engines-101-how-they-work-the-main-types-and-how-to-swap-them-in-postgresql-506j</link>
      <guid>https://dev.to/anikethsdeshpande/database-engines-101-how-they-work-the-main-types-and-how-to-swap-them-in-postgresql-506j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;🧪 &lt;strong&gt;This article has a hands-on lab.&lt;/strong&gt; You'll run PostgreSQL in Docker, store the same 5 million rows in&lt;br&gt;
two different storage engines, crash the database on purpose, and measure what changes.&lt;br&gt;
&lt;strong&gt;If you already know the theory, skip straight to the lab.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: A database engine is the part of a database that turns "save this row" into bytes on disk and back again.&lt;br&gt;
There's no single best engine. &lt;strong&gt;Row stores&lt;/strong&gt; suit transactions, &lt;strong&gt;LSM trees&lt;/strong&gt; suit heavy writes,&lt;br&gt;
&lt;strong&gt;column stores&lt;/strong&gt; suit analytics and &lt;strong&gt;in-memory&lt;/strong&gt; engines suit speed.&lt;br&gt;
PostgreSQL lets you choose &lt;strong&gt;per table&lt;/strong&gt; with &lt;code&gt;CREATE TABLE ... USING &amp;lt;engine&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Part 1: How an engine works&lt;/li&gt;
&lt;li&gt;Part 2: Types of engines&lt;/li&gt;
&lt;li&gt;Part 3: The lab&lt;/li&gt;
&lt;li&gt;Cheat sheet: pick an engine by use case&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 1: How an engine works
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;INSERT INTO users VALUES (1, 'alice', 1000)&lt;/code&gt;, two very different pieces of software are involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   SQL text
      │
      ▼
 ┌──────────────────────────┐
 │  Query layer             │   parse → plan → execute
 │  "what do you want?"     │
 └────────────┬─────────────┘
              │  "store this row", "give me row X"
              ▼
 ┌──────────────────────────┐
 │  Storage engine          │   pages, rows, logs, versions
 │  "how do the bytes live?"│
 └────────────┬─────────────┘
              ▼
          disk / S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This article is about the bottom box. The easiest way to see why it looks the way it does is to build the obvious version and watch it fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  The naive engine: one row per line in a text file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;users.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,user_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my machine it failed three ways almost immediately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read cost depends on which row you want.&lt;/strong&gt; Finding row N means counting N newlines. Row 0 took 0.25 ms and row 199,999 took 17.6 ms, &lt;strong&gt;70x slower&lt;/strong&gt;, on a file under 5 MB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A row that grows overwrites its neighbour.&lt;/strong&gt; The only safe fix is to rewrite everything after it. I changed 40 bytes and had to write 4.6 MB, a &lt;strong&gt;121,667x write amplification&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A crash creates a row that never existed.&lt;/strong&gt; Cut power halfway through replacing &lt;code&gt;42,alice,1000&lt;/code&gt; with &lt;code&gt;42,bob,2000&lt;/code&gt; and you get &lt;code&gt;42,bobce,1000&lt;/code&gt;. It parses fine, so nothing warns you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real engines solve these with four ideas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idea 1: Pages
&lt;/h3&gt;

&lt;p&gt;Split every file into &lt;strong&gt;fixed-size blocks called pages&lt;/strong&gt;. PostgreSQL uses &lt;strong&gt;8 KB&lt;/strong&gt;. Finding block 5,000 becomes plain arithmetic (&lt;code&gt;5000 × 8192&lt;/code&gt;), so every read costs the same. The page becomes the unit of disk I/O, caching and crash recovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idea 2: Slotted pages
&lt;/h3&gt;

&lt;p&gt;Inside each page, a small &lt;strong&gt;slot array&lt;/strong&gt; at the front points to rows stored at the back. The two regions grow toward each other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;byte 0     ┌────────────────────────────────┐
           │ Page header (24 bytes)         │
           ├────────────────────────────────┤
           │ Slot 1 │ Slot 2 │ Slot 3 │ ... │  ← slots grow DOWN ↓
           ├────────────────────────────────┤
           │          free space            │
           ├────────────────────────────────┤
           │ ... │ Row 3 │ Row 2 │ Row 1    │  ← rows grow UP ↑
byte 8192  └────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A row's address is &lt;code&gt;(page, slot)&lt;/code&gt;, not a byte offset. So the engine can move rows around inside a page to reclaim space, and every index entry pointing at &lt;code&gt;(5, 3)&lt;/code&gt; still works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idea 3: Checksums and the write-ahead log
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Checksums&lt;/strong&gt; &lt;em&gt;detect&lt;/em&gt; damage: each page stores a checksum of its own bytes, so a half-written page fails verification instead of returning &lt;code&gt;42,bobce,1000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;write-ahead log (WAL)&lt;/strong&gt; &lt;em&gt;repairs&lt;/em&gt; damage. Before touching a page, the engine appends a description of the change to a sequential log and flushes it to disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. append "page 5, slot 3: balance 1000 → 2000" to the WAL  → fsync
2. change page 5 in memory
3. write page 5 to disk ... eventually
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the database crashes before step 3, recovery replays the log. You'll watch this happen in the lab.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idea 4: MVCC
&lt;/h3&gt;

&lt;p&gt;Instead of overwriting a row, write a &lt;strong&gt;new version&lt;/strong&gt; and stamp each version with the transaction that created it (&lt;code&gt;xmin&lt;/code&gt;) and the one that replaced it (&lt;code&gt;xmax&lt;/code&gt;). Each transaction reads from a snapshot, so a long report keeps seeing the old balance while an update commits alongside it. &lt;strong&gt;Readers never block writers.&lt;/strong&gt; The cost is that old versions pile up and must be cleaned out, which is what PostgreSQL's &lt;code&gt;VACUUM&lt;/code&gt; does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Types of engines
&lt;/h2&gt;

&lt;p&gt;Those four ideas describe PostgreSQL's default engine. Other engines make different trade-offs, depending on what they're optimised for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Row-oriented store (B-tree + pages)
&lt;/h3&gt;

&lt;p&gt;Whole rows are kept together in pages and updated in place, with B-tree indexes to find them. The classic design.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Great at:&lt;/strong&gt; transactions, point lookups, frequent updates (OLTP).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak at:&lt;/strong&gt; scanning a few columns across billions of rows, because it reads whole rows anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples:&lt;/strong&gt; PostgreSQL &lt;code&gt;heap&lt;/code&gt;, MySQL InnoDB, SQLite, Oracle, SQL Server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Log-structured merge tree (LSM)
&lt;/h3&gt;

&lt;p&gt;Never update in place. Writes go to an in-memory buffer (plus a log for safety). When the buffer fills, it's flushed to disk as an immutable sorted file, and background &lt;strong&gt;compaction&lt;/strong&gt; merges those files over time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Great at:&lt;/strong&gt; very heavy write and ingest rates, since every disk write is sequential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak at:&lt;/strong&gt; reads may check several files (Bloom filters help), and compaction uses background CPU and I/O.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples:&lt;/strong&gt; RocksDB, LevelDB, Cassandra, ScyllaDB, MyRocks, CockroachDB's Pebble.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Column-oriented store
&lt;/h3&gt;

&lt;p&gt;Store each &lt;strong&gt;column&lt;/strong&gt; separately and compress it. A query that needs 2 of 12 columns reads only those 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Row store (one page)             Column store
┌──────────────────────────┐     id:      1   2   3   4  ...
│ 1 │ IN │ 12.50 │ chrome  │     country: IN  US  IN  DE ...  ← compresses
│ 2 │ US │  8.00 │ safari  │     amount:  12.5 8.0 3.2 ...      very well
│ 3 │ IN │  3.20 │ chrome  │     browser: ...
└──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Great at:&lt;/strong&gt; analytics such as aggregates, dashboards and scans over huge tables (OLAP). Much smaller on disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak at:&lt;/strong&gt; updating or deleting single rows, and fetching whole rows one at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples:&lt;/strong&gt; ClickHouse, DuckDB, Snowflake, BigQuery, Parquet files, Citus/Hydra columnar for PostgreSQL.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  In-memory store
&lt;/h3&gt;

&lt;p&gt;Keep everything in RAM. Durability comes from snapshots and append-only logs, or you give it up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Great at:&lt;/strong&gt; microsecond latency for caches, sessions, leaderboards and queues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak at:&lt;/strong&gt; RAM is expensive, and durability is weaker unless you configure it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples:&lt;/strong&gt; Redis, Memcached, MySQL &lt;code&gt;MEMORY&lt;/code&gt;, VoltDB.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pluggable engines
&lt;/h3&gt;

&lt;p&gt;Some databases let you choose the engine &lt;strong&gt;per table&lt;/strong&gt;. MySQL made this famous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;InnoDB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;cache&lt;/span&gt;  &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MEMORY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL has had the same thing since version 12, through the &lt;strong&gt;Table Access Method&lt;/strong&gt; API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;-- the default row store&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;columnar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- added by an extension&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It goes further than tables:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you can swap&lt;/th&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;Built in&lt;/th&gt;
&lt;th&gt;Added by extensions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Table engine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE TABLE ... USING x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;heap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;columnar&lt;/code&gt; (Citus, Hydra), &lt;code&gt;orioledb&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index engine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE INDEX ... USING x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;btree&lt;/code&gt;, &lt;code&gt;hash&lt;/code&gt;, &lt;code&gt;gin&lt;/code&gt;, &lt;code&gt;gist&lt;/code&gt;, &lt;code&gt;spgist&lt;/code&gt;, &lt;code&gt;brin&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;hnsw&lt;/code&gt;, &lt;code&gt;ivfflat&lt;/code&gt; (pgvector)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durability&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE UNLOGGED TABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;skip the WAL entirely&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now let's plug some in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The lab
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You need:&lt;/strong&gt; Docker, and about 15 minutes. The table load takes about a minute.&lt;/p&gt;

&lt;p&gt;We'll use the &lt;code&gt;citusdata/citus&lt;/code&gt; image: stock &lt;strong&gt;PostgreSQL 17&lt;/strong&gt; with the &lt;code&gt;columnar&lt;/code&gt; table engine already installed. We're only using its columnar engine, not its distributed features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 0: Start PostgreSQL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; pg-engines &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pw citusdata/citus:13.0
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; pg-engines psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;psql&lt;/code&gt;, turn on timing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;timing&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1: See which engines are installed
&lt;/h3&gt;

&lt;p&gt;PostgreSQL lists every engine in the &lt;code&gt;pg_am&lt;/code&gt; catalog table ("am" = access method):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;amname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;amtype&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'t'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'table'&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'index'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_am&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  engine  | kind
----------+-------
 columnar | table     ← added by the extension
 heap     | table     ← PostgreSQL's default
 brin     | index
 btree    | index
 gin      | index
 gist     | index
 hash     | index
 spgist   | index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Same table, two engines
&lt;/h3&gt;

&lt;p&gt;A realistic 12-column web-events table. The only difference between the two is the &lt;code&gt;USING&lt;/code&gt; clause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_url&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;referrer&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;smallint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latency_ms&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_col&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;columnar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load 5 million rows of fake events into both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 second'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7919&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'IN'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'DE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'BR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'JP'&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'mobile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'desktop'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'tablet'&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'chrome'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'firefox'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'safari'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'edge'&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
       &lt;span class="s1"&gt;'/products/'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'/details?ref=campaign_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="s1"&gt;'https://www.example-'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'.com/search?q=item'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events_col&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;events_col&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now compare their size on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_total_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_am&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relam&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'events_%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   table    |  engine  |  size
------------+----------+--------
 events_col | columnar | 127 MB
 events_row | heap     | 868 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same data, &lt;strong&gt;6.8x smaller&lt;/strong&gt;. Storing each column together means similar values sit next to each other, and they compress well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Run an analytics query
&lt;/h3&gt;

&lt;p&gt;Average order amount per country. &lt;code&gt;BUFFERS&lt;/code&gt; shows how many 8 KB pages each engine had to read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TIMING&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TIMING&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events_col&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trimmed output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- heap
 -&amp;gt;  Parallel Seq Scan on events_row
       Buffers: shared read=111093          ← 868 MB of pages
 Execution Time: 852 ms

-- columnar
 -&amp;gt;  Custom Scan (ColumnarScan) on events_col
       Columnar Projected Columns: country, amount
       Buffers: shared hit=3164 read=16     ← 25 MB of pages
 Execution Time: 1409 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The columnar engine read &lt;strong&gt;35x fewer pages&lt;/strong&gt;. &lt;code&gt;Projected Columns: country, amount&lt;/code&gt; shows why: it skipped the other 10 columns entirely.&lt;/p&gt;

&lt;p&gt;But look at the times. &lt;strong&gt;On my laptop, heap was still faster.&lt;/strong&gt; The whole dataset fits in RAM, so reading pages is cheap. PostgreSQL also split the heap scan across 3 CPU cores, while this columnar engine scans on one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 That's the real lesson: &lt;strong&gt;an engine is a bet on your bottleneck.&lt;/strong&gt; Columnar wins when I/O is the limit, for&lt;br&gt;
example tables bigger than RAM, network-attached cloud disks, or storage you pay for by the byte scanned.&lt;br&gt;
When everything is already in memory, the difference shrinks, and a well-parallelised row store can win.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4: Update one row
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;events_col&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UPDATE 1
ERROR:  UPDATE and CTID scans not supported for ColumnarScan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The columnar engine refuses. Compressed column chunks can't be cheaply edited in place, so this engine is &lt;strong&gt;append-only&lt;/strong&gt;. It suits event logs and history tables, not a &lt;code&gt;users&lt;/code&gt; table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Turn durability down with &lt;code&gt;UNLOGGED&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The WAL from Part 1 has a cost. PostgreSQL lets you skip it per table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;          &lt;span class="n"&gt;clicks_safe&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;UNLOGGED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;clicks_fast&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_current_wal_lsn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;before&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;gset&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;clicks_safe&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_current_wal_lsn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;middle&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;gset&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;clicks_fast&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_current_wal_lsn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;after&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;gset&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_wal_lsn_diff&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="s1"&gt;'middle'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'before'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;wal_written_safe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_wal_lsn_diff&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="s1"&gt;'after'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'middle'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;wal_written_fast&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; wal_written_safe | wal_written_fast
------------------+------------------
 199 MB           | 0 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unlogged insert wrote &lt;strong&gt;zero bytes of WAL&lt;/strong&gt; and ran about 1.5x faster (1.5 s vs 2.3 s). Both tables hold 1,000,000 rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Crash it on purpose
&lt;/h3&gt;

&lt;p&gt;Quit &lt;code&gt;psql&lt;/code&gt; (&lt;code&gt;\q&lt;/code&gt;), then kill PostgreSQL the hard way. &lt;code&gt;SIGKILL&lt;/code&gt; gives it no chance to clean up, like pulling the power cord:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; KILL pg-engines
docker start pg-engines
docker logs pg-engines 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"not properly shut down|redo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LOG:  database system was not properly shut down; automatic recovery in progress
LOG:  redo starts at 0/57A9DD88
LOG:  redo done at 0/57A9DE58
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the WAL replay from Part 1. Now reconnect and count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;clicks_safe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;safe_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;clicks_fast&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;fast_rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; safe_rows | fast_rows
-----------+-----------
   1000000 |         0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The unlogged table is empty.&lt;/strong&gt; With no WAL to replay, PostgreSQL can't know whether its pages are consistent, so after a crash it truncates them. It's a good fit for staging tables and caches you can rebuild. It's a bad fit for anything you can't afford to lose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Swap index engines
&lt;/h3&gt;

&lt;p&gt;Indexes are pluggable too. Put a B-tree and a BRIN index on the same timestamp column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;events_ts_btree&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;btree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;events_ts_brin&lt;/span&gt;  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events_row&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;brin&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_index&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'events_ts_%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      index      |  size
-----------------+--------
 events_ts_btree | 107 MB
 events_ts_brin  | 40 kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The BRIN index is &lt;strong&gt;about 2,700x smaller&lt;/strong&gt;. A B-tree stores one entry per row. BRIN ("Block Range INdex") stores only the min and max timestamp for each group of 128 pages. Since events arrive in time order, those ranges barely overlap, so the index can still skip most of the table.&lt;/p&gt;

&lt;p&gt;Both indexes answer "give me Feb 1st" correctly (86,400 rows). BRIN is &lt;em&gt;lossy&lt;/em&gt;: it returns whole page ranges, and PostgreSQL rechecks the rows, discarding 11,028 extras. If your data &lt;em&gt;isn't&lt;/em&gt; physically ordered by that column, BRIN becomes useless and you're back to the B-tree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Change a table's engine later
&lt;/h3&gt;

&lt;p&gt;You don't have to choose the engine forever. Since PostgreSQL 15, one command moves an existing table to a different engine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_total_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'clicks_safe'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;-- 174 MB&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;clicks_safe&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="k"&gt;METHOD&lt;/span&gt; &lt;span class="n"&gt;columnar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_total_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'clicks_safe'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;-- 25 MB&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;clicks_safe&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;ACCESS&lt;/span&gt; &lt;span class="k"&gt;METHOD&lt;/span&gt; &lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;-- and back again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the "plug and play" part. The table name, the columns and your queries stay the same. Only the engine underneath changes. The catch is that it &lt;strong&gt;rewrites the whole table and locks it&lt;/strong&gt; while doing so, so plan it like a migration.&lt;/p&gt;

&lt;p&gt;You can also change the default for new tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;default_table_access_method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;columnar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Clean up
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; pg-engines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cheat sheet: pick an engine by use case
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Users, orders, payments&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;heap&lt;/code&gt; + &lt;code&gt;btree&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Frequent updates, point lookups, full durability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event logs, metrics history, audit trails&lt;/td&gt;
&lt;td&gt;&lt;code&gt;columnar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Append-only, scanned by a few columns, ~7x smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETL staging, rebuildable caches&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UNLOGGED&lt;/code&gt; heap&lt;/td&gt;
&lt;td&gt;No WAL, faster writes. Emptied after a crash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Huge time-ordered tables, range filters&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;brin&lt;/code&gt; index&lt;/td&gt;
&lt;td&gt;Kilobytes instead of 100+ MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-text search, JSONB, arrays&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gin&lt;/code&gt; index&lt;/td&gt;
&lt;td&gt;Indexes the values &lt;em&gt;inside&lt;/em&gt; a column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maps and geometry (PostGIS)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gist&lt;/code&gt; index&lt;/td&gt;
&lt;td&gt;Handles overlaps and nearest-neighbour queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI embeddings, similarity search&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;hnsw&lt;/code&gt; (pgvector)&lt;/td&gt;
&lt;td&gt;Approximate nearest-neighbour search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Massive write-heavy key-value data&lt;/td&gt;
&lt;td&gt;LSM engine (RocksDB, Cassandra)&lt;/td&gt;
&lt;td&gt;Sequential writes and compaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub-millisecond cache or session store&lt;/td&gt;
&lt;td&gt;In-memory (Redis)&lt;/td&gt;
&lt;td&gt;RAM speed; durability is optional&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Want to go deeper?
&lt;/h2&gt;

&lt;p&gt;I'm learning this by building one: &lt;strong&gt;&lt;a href="https://github.com/AnikethDG/LakePG" rel="noopener noreferrer"&gt;LakePG&lt;/a&gt;&lt;/strong&gt; is a from-scratch Python storage engine that writes &lt;strong&gt;byte-for-byte PostgreSQL-compatible pages&lt;/strong&gt;, with slotted pages, checksums, tuples and MVCC. Its tests compare the pages it writes against a real PostgreSQL 17 server. It's a learning reimplementation of ideas from PostgreSQL, Neon and Databricks Lakebase, not an original design and not production software.&lt;/p&gt;

&lt;h3&gt;
  
  
  Further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/storage-page-layout.html" rel="noopener noreferrer"&gt;PostgreSQL: Database Page Layout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/tableam.html" rel="noopener noreferrer"&gt;PostgreSQL: Table Access Method Interface&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/brin.html" rel="noopener noreferrer"&gt;PostgreSQL: BRIN Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/wal-intro.html" rel="noopener noreferrer"&gt;PostgreSQL: Write-Ahead Logging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.citusdata.com/en/stable/admin_guide/table_management.html#columnar-storage" rel="noopener noreferrer"&gt;Citus columnar storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/storage-engines.html" rel="noopener noreferrer"&gt;MySQL: Alternative Storage Engines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Your Python venv Is (Mostly) a Symlink — Here's Why That Matters</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Mon, 28 Sep 2026 05:17:36 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/your-python-venv-is-mostly-a-symlink-heres-why-that-matters-33cm</link>
      <guid>https://dev.to/anikethsdeshpande/your-python-venv-is-mostly-a-symlink-heres-why-that-matters-33cm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — On Linux and macOS, &lt;code&gt;python3 -m venv .venv&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; create a copy of Python.&lt;br&gt;
The &lt;code&gt;python&lt;/code&gt; inside your venv is a &lt;strong&gt;symlink&lt;/strong&gt; that points back to the system interpreter.&lt;br&gt;
The standard library isn't copied either. Only &lt;code&gt;site-packages&lt;/code&gt; (your installed packages) really belongs to the venv.&lt;br&gt;
So if the system Python gets upgraded, moved or deleted, your venv can break outright,&lt;br&gt;
or keep running against a different Python without telling you.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The claim&lt;/li&gt;
&lt;li&gt;Part 1 — See it with your own eyes (GCP VM / EC2 lab)&lt;/li&gt;
&lt;li&gt;Part 2 — What does this mean? Pros, cons, and real-world impact&lt;/li&gt;
&lt;li&gt;Part 3 — Building a venv that doesn't betray you&lt;/li&gt;
&lt;li&gt;Cheat sheet&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The claim
&lt;/h2&gt;

&lt;p&gt;Most of us picture a virtual environment like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   What we THINK a venv is
   ───────────────────────

   .venv/
   ├── 🐍 a full private copy of Python
   ├── 📚 a full private copy of the standard library
   └── 📦 my packages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what you actually get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   What a venv ACTUALLY is (Linux/macOS default)
   ─────────────────────────────────────────────

   .venv/
   ├── bin/
   │   ├── python   ──symlink──►  python3
   │   ├── python3  ──symlink──►  /usr/bin/python3   ◄── the SYSTEM interpreter
   │   ├── pip                    (small script, absolute shebang)
   │   └── activate               (shell script)
   ├── lib/python3.12/site-packages/   ◄── the ONLY truly private part
   └── pyvenv.cfg                      ◄── "home = /usr/bin"  (a pointer back)

   Standard library (os, json, asyncio, ssl, ...)?  → still read from /usr/lib/python3.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A venv is really &lt;strong&gt;an isolated &lt;code&gt;site-packages&lt;/code&gt; folder, plus a pointer back to an interpreter that lives somewhere else.&lt;/strong&gt;&lt;br&gt;
I'll prove it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 1 — See it with your own eyes
&lt;/h2&gt;

&lt;p&gt;You can run this on any Linux box. A throwaway cloud VM is ideal because it's clean and you can wreck it without worrying.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 0: Get a VM
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Option A — Google Cloud (GCE)
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute instances create venv-lab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1-a &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--machine-type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e2-micro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-family&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ubuntu-2404-lts-amd64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ubuntu-os-cloud

gcloud compute ssh venv-lab &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1-a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Option B — AWS (EC2)
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 run-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-id&lt;/span&gt; resolve:ssm:/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-type&lt;/span&gt; t3.micro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-name&lt;/span&gt; &amp;lt;your-key-pair&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-group-ids&lt;/span&gt; &amp;lt;sg-allowing-ssh&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=instance,Tags=[{Key=Name,Value=venv-lab}]'&lt;/span&gt;

ssh &lt;span class="nt"&gt;-i&lt;/span&gt; &amp;lt;your-key&amp;gt;.pem ubuntu@&amp;lt;public-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once you're in, install the venv module (Ubuntu ships it as a separate package):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python3-venv
python3 &lt;span class="nt"&gt;--version&lt;/span&gt;          &lt;span class="c"&gt;# Python 3.12.x on Ubuntu 24.04&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 The output below comes from Ubuntu 24.04 / Python 3.12. Other distros and versions show the same pattern, with different version numbers.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Experiment 1: Look inside &lt;code&gt;bin/&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv demo
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; demo/bin/ | &lt;span class="nb"&gt;grep &lt;/span&gt;python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lrwxrwxrwx 1 ubuntu ubuntu   7 ... python -&amp;gt; python3
lrwxrwxrwx 1 ubuntu ubuntu  16 ... python3 -&amp;gt; /usr/bin/python3
lrwxrwxrwx 1 ubuntu ubuntu   7 ... python3.12 -&amp;gt; python3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;l&lt;/code&gt; at the start of each permission string and the &lt;code&gt;-&amp;gt;&lt;/code&gt; arrows mean &lt;strong&gt;these are symlinks&lt;/strong&gt;. None of them is a real binary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 2: Follow the chain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; demo/bin/python
&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /usr/bin/python3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin/python3.12
/usr/bin/python3.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They end at the &lt;strong&gt;same file&lt;/strong&gt;. You can also check that the venv entry is just a link and not a hard copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'%i  %s bytes  %n'&lt;/span&gt; /usr/bin/python3.12 demo/bin/python3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;16123164  6939856 bytes  /usr/bin/python3.12   ← the real binary (~7 MB)
18122480       16 bytes  demo/bin/python3      ← a 16-byte symlink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Symlink resolution chain
   ────────────────────────

   demo/bin/python
        │  (symlink)
        ▼
   demo/bin/python3
        │  (symlink)
        ▼
   /usr/bin/python3
        │  (symlink, owned by the distro)
        ▼
   /usr/bin/python3.12   ◄── the one and only real interpreter binary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Experiment 3: Read &lt;code&gt;pyvenv.cfg&lt;/code&gt;, the venv's "birth certificate"
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;demo/pyvenv.cfg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;home = /usr/bin
include-system-site-packages = false
version = 3.12.3
executable = /usr/bin/python3.12
command = /usr/bin/python3 -m venv /home/ubuntu/demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;home = /usr/bin&lt;/code&gt; is the key line. At startup, Python sees this file and uses it to find its &lt;strong&gt;real home&lt;/strong&gt; (and the standard library that comes with it).&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 4: Ask Python where things come from
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;demo/bin/python - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
import sys, os, json
print("sys.executable :", sys.executable)
print("realpath       :", os.path.realpath(sys.executable))
print("sys.prefix     :", sys.prefix)        # the venv
print("sys.base_prefix:", sys.base_prefix)   # the REAL Python install
print("json stdlib    :", json.__file__)
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sys.executable : /home/ubuntu/demo/bin/python
realpath       : /usr/bin/python3.12
sys.prefix     : /home/ubuntu/demo
sys.base_prefix: /usr
json stdlib    : /usr/lib/python3.12/json/__init__.py   ← NOT inside the venv!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two prefixes. &lt;code&gt;sys.prefix&lt;/code&gt; is your venv, where packages get installed. &lt;code&gt;sys.base_prefix&lt;/code&gt; is the system install, where the interpreter and the stdlib come from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 5: Check the size
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; demo
&lt;span class="nb"&gt;ls &lt;/span&gt;demo/lib/python3.12/site-packages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12M   demo
pip  pip-24.0.dist-info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Almost all of those 12 MB are &lt;strong&gt;pip&lt;/strong&gt;. The interpreter and the stdlib (tens of MB) aren't there, because they're borrowed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 6: Move the venv and watch it break
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; demo/bin/pip
&lt;span class="nb"&gt;mv &lt;/span&gt;demo demo-moved
demo-moved/bin/pip &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/home/ubuntu/demo/bin/python3
bash: demo-moved/bin/pip: /home/ubuntu/demo/bin/python3: bad interpreter: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every console script (&lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;black&lt;/code&gt;, and so on) has the &lt;strong&gt;absolute path&lt;/strong&gt; of the venv baked into its shebang line. Venvs aren't relocatable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;demo-moved demo   &lt;span class="c"&gt;# put it back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Experiment 7: Delete the base Python (the scary one)
&lt;/h3&gt;

&lt;p&gt;We don't want to wreck the VM's real &lt;code&gt;/usr/bin/python3&lt;/code&gt;, since &lt;code&gt;apt&lt;/code&gt; and cloud-init depend on it. So we'll build a &lt;strong&gt;fake "system Python"&lt;/strong&gt; in &lt;code&gt;/opt&lt;/code&gt;, create venvs from it, then "uninstall" it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Build a standalone copy of the interpreter + stdlib at /opt/fakepy&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/fakepy/bin /opt/fakepy/lib
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /usr/bin/python3.12 /opt/fakepy/bin/
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /usr/lib/python3.12 /opt/fakepy/lib/
/opt/fakepy/bin/python3.12 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys; print(sys.prefix)"&lt;/span&gt;   &lt;span class="c"&gt;# → /opt/fakepy&lt;/span&gt;

&lt;span class="c"&gt;# 2. Create two venvs from it: default (symlinks) and --copies&lt;/span&gt;
/opt/fakepy/bin/python3.12 &lt;span class="nt"&gt;-m&lt;/span&gt; venv &lt;span class="nt"&gt;--without-pip&lt;/span&gt; ~/linked
/opt/fakepy/bin/python3.12 &lt;span class="nt"&gt;-m&lt;/span&gt; venv &lt;span class="nt"&gt;--without-pip&lt;/span&gt; &lt;span class="nt"&gt;--copies&lt;/span&gt; ~/copied
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; ~/linked/bin/python3.12     &lt;span class="c"&gt;# → /opt/fakepy/bin/python3.12&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; ~/copied/bin/python3.12     &lt;span class="c"&gt;# → a real 7 MB file&lt;/span&gt;

&lt;span class="c"&gt;# 3. Simulate "the base Python was upgraded / uninstalled"&lt;/span&gt;
&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /opt/fakepy /opt/fakepy.gone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/linked/bin/python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'print("hello")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash: /home/ubuntu/linked/bin/python: No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💥 The symlink is &lt;strong&gt;dangling&lt;/strong&gt;, so the venv is dead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/copied/bin/python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import sys, os; print(sys.base_prefix, os.__file__)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr /usr/lib/python3.12/os.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;😱 The &lt;code&gt;--copies&lt;/code&gt; venv &lt;em&gt;did&lt;/em&gt; start, but &lt;code&gt;pyvenv.cfg&lt;/code&gt; pointed to a &lt;code&gt;home&lt;/code&gt; that no longer exists. Python fell back to its compiled-in prefix (&lt;code&gt;/usr&lt;/code&gt;) and &lt;strong&gt;silently switched to a different standard library&lt;/strong&gt;. Here the two happened to be identical, so nothing went wrong. On a real machine they could be different patch or minor versions, and you'd get strange &lt;code&gt;ImportError&lt;/code&gt;s or subtle bugs that nobody can explain.&lt;/p&gt;

&lt;p&gt;Clean up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /opt/fakepy.gone /opt/fakepy   &lt;span class="c"&gt;# or: sudo rm -rf /opt/fakepy.gone&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/linked ~/copied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when you're finished with the lab, delete the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute instances delete venv-lab &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1-a   &lt;span class="c"&gt;# GCP&lt;/span&gt;
aws ec2 terminate-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &amp;lt;instance-id&amp;gt;          &lt;span class="c"&gt;# AWS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 2 — What does this mean?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How a venv actually boots
&lt;/h3&gt;

&lt;p&gt;This comes from &lt;a href="https://peps.python.org/pep-0405/" rel="noopener noreferrer"&gt;PEP 405&lt;/a&gt;, the spec that defines &lt;code&gt;venv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $ .venv/bin/python app.py
            │
            ▼
  ┌──────────────────────────────────────┐
  │ Kernel follows symlinks              │
  │ .venv/bin/python → /usr/bin/python3.12│  ← executes the SYSTEM binary
  └──────────────────────────────────────┘
            │  (but argv[0] / sys.executable still says .venv/bin/python)
            ▼
  ┌──────────────────────────────────────┐
  │ Python looks for pyvenv.cfg next to  │
  │ (or one level above) sys.executable  │
  └──────────────────────────────────────┘
            │ found!
            ▼
  ┌──────────────────────────────────────┐        ┌──────────────────────────────┐
  │ sys.prefix      = .venv              │ ─────► │ .venv/lib/python3.12/        │
  │                                      │        │        site-packages/  📦     │
  │ sys.base_prefix = derived from       │        └──────────────────────────────┘
  │                   "home = /usr/bin"  │        ┌──────────────────────────────┐
  │                                      │ ─────► │ /usr/lib/python3.12/  📚      │
  └──────────────────────────────────────┘        │ (os, json, ssl, asyncio ...) │
                                                  └──────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a venv is three things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Where it lives&lt;/th&gt;
&lt;th&gt;Owned by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interpreter binary&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/usr/bin/python3.12&lt;/code&gt; (symlinked)&lt;/td&gt;
&lt;td&gt;🧑‍💼 The OS / package manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard library&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/usr/lib/python3.12/&lt;/code&gt; (referenced)&lt;/td&gt;
&lt;td&gt;🧑‍💼 The OS / package manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third-party packages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.venv/lib/python3.12/site-packages/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🙋 &lt;strong&gt;You&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Being precise:&lt;/strong&gt; "the venv is a symlink" is a handy shorthand, but the venv &lt;em&gt;directory&lt;/em&gt; is real, and so is &lt;code&gt;site-packages&lt;/code&gt;.&lt;br&gt;
What's symlinked is the &lt;strong&gt;interpreter&lt;/strong&gt;. The &lt;strong&gt;stdlib&lt;/strong&gt; is borrowed through &lt;code&gt;pyvenv.cfg&lt;/code&gt; and never linked or copied.&lt;br&gt;
On Windows, venvs use a small copied &lt;code&gt;python.exe&lt;/code&gt; launcher instead of a symlink, but it still reads &lt;code&gt;pyvenv.cfg&lt;/code&gt; and depends on the base install in the same way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why did Python's designers do it this way?
&lt;/h3&gt;

&lt;p&gt;On purpose. Venvs are meant to be &lt;strong&gt;cheap, fast and disposable&lt;/strong&gt;, not self-contained deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Pros
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pro&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tiny&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~12 MB (mostly pip) instead of ~40–100 MB per env. Having 30 projects doesn't mean 30 copies of CPython.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fast to create&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Creating a few symlinks takes milliseconds. Great for CI, tox and nox matrices.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security patches for free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;When &lt;code&gt;apt upgrade&lt;/code&gt; ships a patched &lt;code&gt;python3.12&lt;/code&gt; (say, an &lt;code&gt;ssl&lt;/code&gt; or &lt;code&gt;zipfile&lt;/code&gt; CVE fix), every venv picks it up with no rebuild.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;One interpreter to trust&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only one binary sits on disk to audit, scan and patch.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Package isolation still works&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Two projects can pin different versions of &lt;code&gt;requests&lt;/code&gt; without conflict, which is the reason venvs exist.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ❌ Cons
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Con&lt;/th&gt;
&lt;th&gt;What it looks like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fragile to base changes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Base Python removed or upgraded → &lt;code&gt;No such file or directory&lt;/code&gt; / &lt;code&gt;bad interpreter&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Silent minor-version drift&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Venv made with &lt;code&gt;python3 -m venv&lt;/code&gt; links to &lt;code&gt;/usr/bin/python3&lt;/code&gt;, &lt;em&gt;not&lt;/em&gt; &lt;code&gt;python3.12&lt;/code&gt;. If the distro repoints &lt;code&gt;python3&lt;/code&gt; to 3.13, your venv starts running &lt;strong&gt;3.13&lt;/strong&gt; and looks for &lt;code&gt;lib/python3.13/site-packages&lt;/code&gt;, which doesn't exist. Result: &lt;code&gt;ModuleNotFoundError&lt;/code&gt; for everything you installed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Not relocatable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Absolute paths live in shebangs, &lt;code&gt;pyvenv.cfg&lt;/code&gt; and &lt;code&gt;activate&lt;/code&gt;. You can't &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;cp -r&lt;/code&gt;, rsync to another path, or bake it in one Docker stage and copy it to another path.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Not portable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You can't copy &lt;code&gt;.venv&lt;/code&gt; to another machine unless the exact same Python exists at the exact same path.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compiled wheels are tied to the base&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C extensions (&lt;code&gt;numpy&lt;/code&gt;, &lt;code&gt;psycopg&lt;/code&gt;, &lt;code&gt;cryptography&lt;/code&gt;) were built against a specific ABI (&lt;code&gt;cp312&lt;/code&gt;). Swapping the base breaks them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;--copies&lt;/code&gt; is only half a fix&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;It copies the binary but still borrows the stdlib, and falls back &lt;em&gt;silently&lt;/em&gt; if &lt;code&gt;home&lt;/code&gt; disappears (see Experiment 7).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How it bites in real life
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   ┌──────────────────────────────┐
                   │   Your venv  (.venv/)        │
                   │   python3 → /usr/bin/python3 │
                   └───────────────┬──────────────┘
                                   │ depends on
                                   ▼
   ┌───────────────────────────────────────────────────────────────┐
   │            Base interpreter you DON'T control                 │
   └───────────────────────────────────────────────────────────────┘
      ▲               ▲                 ▲                   ▲
      │               │                 │                   │
 do-release-upgrade   brew upgrade   pyenv uninstall    Docker: copy venv
 22.04 → 24.04        python@3.12    3.11.4             into distroless /
 (3.10 → 3.12)        + brew cleanup                    alpine image
      │               │                 │                   │
      ▼               ▼                 ▼                   ▼
 ModuleNotFound   dangling link     dangling link      "no such file"
 (drift)          → venv dead       → venv dead        at container start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some scenarios you may have hit already:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ubuntu release upgrade.&lt;/strong&gt; 22.04 ships Python 3.10 and 24.04 ships 3.12. After &lt;code&gt;do-release-upgrade&lt;/code&gt;, venvs built with &lt;code&gt;python3 -m venv&lt;/code&gt; start running 3.12 and all your packages are gone. Venvs built with &lt;code&gt;python3.10 -m venv&lt;/code&gt; are dead links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS + Homebrew.&lt;/strong&gt; &lt;code&gt;brew upgrade&lt;/code&gt; installs a new patch version, and &lt;code&gt;brew cleanup&lt;/code&gt; deletes the old Cellar directory your venv pointed to. This is the classic "my venv broke overnight".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron jobs / systemd services on a VM.&lt;/strong&gt; You set &lt;code&gt;ExecStart=/opt/app/.venv/bin/python ...&lt;/code&gt; in a unit file. Months later, unattended upgrades or an OS image refresh change the base Python, and the service dies at 3 a.m.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker multi-stage builds.&lt;/strong&gt; You build &lt;code&gt;/opt/venv&lt;/code&gt; in &lt;code&gt;python:3.12&lt;/code&gt; and copy it into &lt;code&gt;gcr.io/distroless/base&lt;/code&gt;. &lt;code&gt;/usr/local/bin/python3&lt;/code&gt; doesn't exist there, so the venv's symlink points nowhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renaming a project folder.&lt;/strong&gt; &lt;code&gt;mv my-app my-app-v2&lt;/code&gt; breaks &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;pytest&lt;/code&gt; and every other console script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI caching.&lt;/strong&gt; You cache &lt;code&gt;.venv&lt;/code&gt; between runs. The runner image updates its Python, the cache restores a venv linked to a Python that's gone, and the build fails in confusing ways.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 3 — Building a venv that doesn't betray you
&lt;/h2&gt;

&lt;p&gt;The root problem isn't the symlink itself. It's that &lt;strong&gt;the venv depends on an interpreter you don't control.&lt;/strong&gt; So the fix is to &lt;strong&gt;control the base interpreter&lt;/strong&gt;, and to treat venvs as disposable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Which venv strategy do I need?
                    ──────────────────────────────

                  ┌──────────────────────────────┐
                  │ Is this for local dev / CI ? │
                  └───────┬───────────────┬──────┘
                       yes│               │no (deploying / shipping)
                          ▼               ▼
         ┌──────────────────────┐   ┌───────────────────────────────┐
         │ Rule 1 + Rule 2      │   │ Must it run where Python may   │
         │ pinned base + lock   │   │ be missing/different?          │
         │ file, recreate often │   └──────┬─────────────────┬──────┘
         └──────────────────────┘       yes│                 │no
                                           ▼                 ▼
                        ┌──────────────────────────┐  ┌─────────────────────┐
                        │ Rule 4: ship the         │  │ Rule 3: Docker with │
                        │ interpreter too          │  │ venv at the SAME    │
                        │ (standalone Python /     │  │ path, SAME base img │
                        │  container / PyInstaller)│  └─────────────────────┘
                        └──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rule 1 — Never build venvs on the distro's &lt;code&gt;python3&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The system Python exists &lt;strong&gt;for the OS&lt;/strong&gt; (apt, cloud-init, gcloud/aws tooling), and the OS will upgrade it whenever it needs to. Install your own interpreter at a &lt;strong&gt;pinned, versioned path&lt;/strong&gt; that only changes when you change it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option A: &lt;code&gt;uv&lt;/code&gt; (fast and simple)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh
uv python &lt;span class="nb"&gt;install &lt;/span&gt;3.12            &lt;span class="c"&gt;# downloads a standalone CPython build&lt;/span&gt;
uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 .venv       &lt;span class="c"&gt;# venv linked to *uv's* Python, not /usr/bin&lt;/span&gt;
uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Option B: &lt;code&gt;pyenv&lt;/code&gt; (compiles from source)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pyenv.run | bash      &lt;span class="c"&gt;# then follow the shell setup it prints&lt;/span&gt;
pyenv &lt;span class="nb"&gt;install &lt;/span&gt;3.12.7
~/.pyenv/versions/3.12.7/bin/python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either way, the venv is &lt;strong&gt;still a symlink&lt;/strong&gt;, but now it points at an interpreter that belongs to you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   BEFORE (fragile)                         AFTER (stable)
   ────────────────                         ──────────────
   .venv/bin/python3                        .venv/bin/python3
        │                                        │
        ▼                                        ▼
   /usr/bin/python3  ◄── apt owns this      ~/.pyenv/versions/3.12.7/bin/python3.12
        │                (changes whenever       ◄── YOU own this
        ▼                 the OS wants)              (changes only when you
   /usr/bin/python3.12                                pyenv uninstall it)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Option C: No extra tools? At least pin the minor version
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3.12 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv      &lt;span class="c"&gt;# ✅ links to python3.12 — no silent 3.12→3.13 drift&lt;/span&gt;
python3    &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv      &lt;span class="c"&gt;# ❌ links to python3   — drifts when the distro repoints it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; .venv/bin/ | &lt;span class="nb"&gt;grep &lt;/span&gt;python
&lt;span class="c"&gt;# python3.12 -&amp;gt; /usr/bin/python3.12   ✅ pinned&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rule 2 — Treat the venv as a cache, not a pet
&lt;/h3&gt;

&lt;p&gt;Assume any venv can die, and make rebuilding it a one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Lock exact versions&lt;/span&gt;
pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.lock          &lt;span class="c"&gt;# or: uv pip compile / poetry lock / pip-tools&lt;/span&gt;

&lt;span class="c"&gt;# Rebuild from scratch any time&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; .venv
python3.12 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
.venv/bin/pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never commit &lt;code&gt;.venv&lt;/code&gt;&lt;/strong&gt; to git (add it to &lt;code&gt;.gitignore&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never &lt;code&gt;mv&lt;/code&gt; a venv&lt;/strong&gt;. Recreate it at the new path instead.&lt;/li&gt;
&lt;li&gt;If you upgraded the base Python in place (same minor version), refresh the links with:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  python3.12 &lt;span class="nt"&gt;-m&lt;/span&gt; venv &lt;span class="nt"&gt;--upgrade&lt;/span&gt; .venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rule 3 — In Docker, keep the venv's world identical across stages
&lt;/h3&gt;

&lt;p&gt;Multi-stage builds work fine &lt;strong&gt;as long as the final image has the same interpreter at the same path&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ---------- build stage ----------&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.12-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv /opt/venv
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH="/opt/venv/bin:$PATH"&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.lock .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.lock

&lt;span class="c"&gt;# ---------- runtime stage ----------&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim                 # ✅ SAME base → /usr/local/bin/python3.12 exists&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /opt/venv /opt/venv # ✅ SAME path → shebangs still valid&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH="/opt/venv/bin:$PATH"&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "main.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   build stage (python:3.12-slim)          runtime stage (python:3.12-slim)
   ┌──────────────────────────────┐        ┌──────────────────────────────┐
   │ /usr/local/bin/python3.12 ◄─┐│        │ /usr/local/bin/python3.12 ◄─┐│
   │ /opt/venv/bin/python ───────┘│ COPY ► │ /opt/venv/bin/python ───────┘│
   │ /opt/venv/lib/.../site-pkgs  │        │ /opt/venv/lib/.../site-pkgs  │
   └──────────────────────────────┘        └──────────────────────────────┘
                 ✅ link target exists on both sides
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;❌ Don't copy &lt;code&gt;/opt/venv&lt;/code&gt; into &lt;code&gt;alpine&lt;/code&gt; (musl vs glibc), &lt;code&gt;distroless&lt;/code&gt; (no Python at that path), or &lt;code&gt;python:3.13-slim&lt;/code&gt; (wrong minor version).&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 4 — If Python might be missing at the destination, ship the interpreter
&lt;/h3&gt;

&lt;p&gt;When the target machine may not have a matching Python at all (air-gapped servers, customer machines, minimal images), a venv is the wrong tool. Ship the interpreter along with it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Container image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Interpreter, stdlib and venv frozen together. The most common answer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/astral-sh/python-build-standalone" rel="noopener noreferrer"&gt;python-build-standalone&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A relocatable CPython tarball. Unpack it in &lt;code&gt;/opt/myapp/python&lt;/code&gt;, build the venv from &lt;em&gt;that&lt;/em&gt; Python, and ship the whole &lt;code&gt;/opt/myapp&lt;/code&gt; directory.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;conda-pack&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Packs a full conda env (interpreter included) into a tarball you can relocate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PyInstaller / Nuitka / shiv&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bundles your app and interpreter into a single executable or zipapp.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What about &lt;code&gt;--copies&lt;/code&gt;?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3.12 &lt;span class="nt"&gt;-m&lt;/span&gt; venv &lt;span class="nt"&gt;--copies&lt;/span&gt; .venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   --symlinks (default on Linux/macOS)      --copies
   ───────────────────────────────────      ─────────────────────────────────
   bin/python3.12 → /usr/bin/python3.12     bin/python3.12   (real 7 MB copy)
   stdlib: borrowed from /usr/lib           stdlib: STILL borrowed from /usr/lib
   base removed → hard fail 💥              base removed → silent fallback 😶
   base patched → auto-upgrade ✅           base patched → stale binary +
                                                           new stdlib ⚠️ mismatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--copies&lt;/code&gt; is useful when your filesystem &lt;strong&gt;can't do symlinks&lt;/strong&gt; (some network mounts, Vagrant/VirtualBox shared folders, certain Windows setups). It is &lt;strong&gt;not&lt;/strong&gt; a way to make a venv self-contained, because the stdlib still lives in the base install. In some ways it's worse: a symlinked venv fails loudly, while a copied one can keep running with a mismatched stdlib.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus: a venv health check
&lt;/h3&gt;

&lt;p&gt;Drop this in your dotfiles or CI to spot broken or drifted venvs before they cause trouble:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# venv-doctor.sh — usage: ./venv-doctor.sh path/to/.venv [more venvs...]&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;v &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;py&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="s2"&gt;/bin/python"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$py&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"💀 &lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="s2"&gt;: interpreter link is dangling → &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$py&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;continue
  fi&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$py&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; - &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
import sys, os, re
venv = sys.argv[1]
cfg = open(os.path.join(venv, "pyvenv.cfg")).read()
home = re.search(r"^home&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sh"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sh"&gt;*(.+)&lt;/span&gt;&lt;span class="nv"&gt;$"&lt;/span&gt;&lt;span class="sh"&gt;, cfg, re.M).group(1).strip()
want = re.search(r"^version(?:_info)?&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sh"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sh"&gt;*(&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sh"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\d&lt;/span&gt;&lt;span class="sh"&gt;+)", cfg, re.M).group(1)
have = f"{sys.version_info.major}.{sys.version_info.minor}"
problems = []
if want != have:
    problems.append(f"version drift: created with {want}, now running {have}")
if not os.path.isdir(home):
    problems.append(f"home '{home}' no longer exists (stdlib fallback: {sys.base_prefix})")
print(("⚠️  " if problems else "✅ ") + venv + (": " + "; ".join(problems) if problems else f": OK (Python {have} → {os.path.realpath(sys.executable)})"))
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./venv-doctor.sh ~/proj-a/.venv ~/proj-b/.venv ~/old-thing/.venv
✅ /home/ubuntu/proj-a/.venv: OK (Python 3.12 → /home/ubuntu/.pyenv/versions/3.12.7/bin/python3.12)
⚠️  /home/ubuntu/proj-b/.venv: version drift: created with 3.10, now running 3.12
💀 /home/ubuntu/old-thing/.venv: interpreter link is dangling → /opt/fakepy/bin/python3.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is my venv's Python a symlink?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ls -l .venv/bin/python*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What does it really run?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;readlink -f .venv/bin/python&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where does it think home is?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cat .venv/pyvenv.cfg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Venv vs base prefix&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.venv/bin/python -c 'import sys;print(sys.prefix, sys.base_prefix)'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where does the stdlib come from?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.venv/bin/python -c 'import os;print(os.__file__)'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create a pinned venv&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python3.12 -m venv .venv&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create a venv on a Python you own&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;uv venv --python 3.12&lt;/code&gt; / &lt;code&gt;~/.pyenv/versions/3.12.7/bin/python -m venv .venv&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh links after base patch upgrade&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python3.12 -m venv --upgrade .venv&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rebuild from scratch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rm -rf .venv &amp;amp;&amp;amp; python3.12 -m venv .venv &amp;amp;&amp;amp; .venv/bin/pip install -r requirements.lock&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   A venv is a lightweight overlay:

        ┌───────────────────────────────┐
        │  YOUR packages (site-packages)│  ◄── isolated, yours
        ├───────────────────────────────┤
        │  stdlib                       │  ◄── borrowed
        │  interpreter binary           │  ◄── symlinked
        └───────────────────────────────┘
                 base Python install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The symlink design makes venvs &lt;strong&gt;cheap, fast and auto-patched&lt;/strong&gt;. That was a deliberate choice.&lt;/li&gt;
&lt;li&gt;It also means a venv is &lt;strong&gt;only as stable as the interpreter it points to&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;So: &lt;strong&gt;own your base interpreter&lt;/strong&gt; (uv / pyenv / a pinned &lt;code&gt;python3.X&lt;/code&gt;), &lt;strong&gt;lock your dependencies&lt;/strong&gt;, &lt;strong&gt;treat venvs as disposable&lt;/strong&gt;, and when you need portability, &lt;strong&gt;ship the interpreter&lt;/strong&gt;, not just the venv.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time a venv "randomly" breaks after an upgrade, run &lt;code&gt;ls -l .venv/bin/python&lt;/code&gt; first. There's a good chance the link is pointing at a Python that no longer exists.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? Drop a ❤️ or tell me in the comments about your worst "my venv broke overnight" story.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>linux</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Predicate Pushdown - Understanding Practically With An Example</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Wed, 17 Apr 2024 19:42:28 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/predicate-pushdown-understanding-practically-with-an-example-4b51</link>
      <guid>https://dev.to/anikethsdeshpande/predicate-pushdown-understanding-practically-with-an-example-4b51</guid>
      <description>&lt;p&gt;What is predicate pushdown?&lt;/p&gt;

&lt;p&gt;The immediate theoretical answer that we get on searching is&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Predicate pushdown is a query optimisation technique used in database technologies&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay, I got to know that it is an optimisation technique. But I still did not understand...&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How is the optimisation happening? 🤨&lt;/li&gt;
&lt;li&gt;What is predicate? 🤔&lt;/li&gt;
&lt;li&gt;What exactly is the meaning of pushed down here? 🤷‍♂️&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'm sure since you are also reading this article, even you have these questions in mind!&lt;/p&gt;

&lt;p&gt;Now, lets explore this interesting topic practically in &lt;strong&gt;&lt;em&gt;PySpark&lt;/em&gt;&lt;/strong&gt; using &lt;code&gt;explain()&lt;/code&gt;&lt;br&gt;
(Similar phenomenon could be observed in relational databases as well)&lt;/p&gt;

&lt;p&gt;1] Reading a csv file containing employee information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;emp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;\
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;header&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;\
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data/employee.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;emp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fppxfeaejoac8noucyd86.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fppxfeaejoac8noucyd86.png" alt="df.show" width="700" height="476"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;emp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formatted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxg2ushs3wglcaltw18wk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxg2ushs3wglcaltw18wk.png" alt="pyspark explain read df" width="800" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, step (1) is related to read csv file&lt;/p&gt;

&lt;p&gt;2] Lets do a group by and get number of employees on each department&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deptID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxqawodk4tq9qwuy14eu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxqawodk4tq9qwuy14eu5.png" alt="emp group by" width="284" height="278"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formatted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjehpma6xwkaybwow9kby.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjehpma6xwkaybwow9kby.png" alt="df explain" width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, step (2) is related to &lt;code&gt;group by&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;3] Lets filter data for only dept number 10&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;dept_10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deptID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dept_10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz0a88h1wfr5ccnexkdaz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz0a88h1wfr5ccnexkdaz.png" alt="Image description" width="348" height="184"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;dept_10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formatted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ma0nok0jxtp0zuky4x0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ma0nok0jxtp0zuky4x0.png" alt="Image description" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, we can see that step (2) is filtering and step (3) is grouping. &lt;/p&gt;

&lt;p&gt;Now here is the catch, &lt;br&gt;
Ideally if we go by the sequence of operations, grouping should be done first and then filtering. &lt;/p&gt;

&lt;p&gt;However, the optimiser does filtering first and then grouping, because grouping is an expensive operation and it is optimal to filter first and then group data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiywjb9ic2nu0l5ue64m7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiywjb9ic2nu0l5ue64m7.png" alt="Image description" width="544" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the physical plan we see that filtering (&lt;code&gt;predicate&lt;/code&gt;) is pushed down with respect to grouping. That is why it is called &lt;code&gt;push down&lt;/code&gt; ! &lt;/p&gt;

</description>
      <category>spark</category>
      <category>optimisation</category>
      <category>sql</category>
      <category>interview</category>
    </item>
    <item>
      <title>Write Through</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Fri, 22 Mar 2024 13:06:39 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/write-through-81n</link>
      <guid>https://dev.to/anikethsdeshpande/write-through-81n</guid>
      <description>&lt;p&gt;Write through cache is a simple to implement caching mechanism.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Here the newly arrived data is written into cache and as well as persisted into the disk or a database. Atomicity is maintained.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are two ways to implement it:&lt;br&gt;
1] The application writes data to cache and database simultaneously&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmz9ls68gioad30eo5d0v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmz9ls68gioad30eo5d0v.png" alt="Write through cache" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2] The application writes data to cache and then the cache writes the data into the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy577t78bpgorm1dqudns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy577t78bpgorm1dqudns.png" alt="Write through cache 2" width="800" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Advantages:
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;Simple to implement.&lt;/li&gt;
&lt;li&gt;Faster response times.&lt;/li&gt;
&lt;li&gt;Data integrity because of atomic nature of write operation. &lt;/li&gt;
&lt;li&gt;Lower latency for subsequent reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Disadvantages:
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cache pollution: Since every time the data is filled into cache, it can get filled with less frequently read data and more cache eviction which could introduce some latency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Not suitable for write intensive scenarios as the write operations are slower compared to other methods because data needs to be written in cache as well as persistent storage everytime.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cache</category>
      <category>redis</category>
      <category>systemdesign</category>
      <category>writepolicy</category>
    </item>
    <item>
      <title>Database Caching Strategies</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Fri, 22 Mar 2024 12:43:08 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/cache-write-policies-419b</link>
      <guid>https://dev.to/anikethsdeshpande/cache-write-policies-419b</guid>
      <description>&lt;p&gt;We often face high latencies while fetching data from Database and are unable to meet SLA. Caching is one of the solutions to implement after DB query and table optimisations. &lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Cache&lt;/strong&gt; is used to store the data so that it can be delivered faster to the client as compared to persistent systems like database or disk.&lt;/p&gt;

&lt;p&gt;Popular caching tools are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://memcached.org/" rel="noopener noreferrer"&gt;Memcached&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are multiple ways in which data can be written into and read from the cache. Let us explore the most prominently used methods or policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache Aside
&lt;/h2&gt;

&lt;p&gt;Cache Aside or Lazy Loading is one of the cache write policies or strategies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In cache aside method, the application is responsible for storing data into cache.&lt;/li&gt;
&lt;li&gt;When the client sends request to the application, it looks for data in the cache.&lt;/li&gt;
&lt;li&gt;If the data is found in the cache it is called as &lt;em&gt;cache-hit&lt;/em&gt;. The data is fetched from cache and returned to client.&lt;/li&gt;
&lt;li&gt;However, if the data is not found in the cache, also called as &lt;em&gt;cache-miss&lt;/em&gt;, the application queries for data in the database, writes the data into cache and sends the response to the client.&lt;/li&gt;
&lt;li&gt;Since we store data in cache only when it is necessary, this strategy is also called as Lazy-Loading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flh6koyxc7792iu568usl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flh6koyxc7792iu568usl.png" alt="Cache Aside - Write Strategy" width="538" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Cost effective&lt;/em&gt; because, only the frequently accessed data is stored into cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Disadvantages:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The response time can be slow when there is cache miss, because, it involves many i/o operations to fetch data from DB and store it in cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;In case the system can tolerate an initial delay and the same data is to be fetched repeatedly, then this mechanism works best.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Write through
&lt;/h2&gt;

&lt;p&gt;Write through cache is a simple to implement caching mechanism.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Here the newly arrived data is written into cache and as well as persisted into the disk or a database. Atomicity is maintained.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are two ways to implement it:&lt;br&gt;
1] The application writes data to cache and database simultaneously&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmz9ls68gioad30eo5d0v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmz9ls68gioad30eo5d0v.png" alt="Write through cache" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2] The application writes data to cache and then the cache writes the data into the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy577t78bpgorm1dqudns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy577t78bpgorm1dqudns.png" alt="Write through cache 2" width="800" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Advantages:
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;Faster response times.&lt;/li&gt;
&lt;li&gt;Data integrity because of atomic nature of write operation. &lt;/li&gt;
&lt;li&gt;Lower latency for subsequent reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Disadvantages:
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cache pollution: Since every time the data is filled into cache, it can get filled with less frequently read data and more cache eviction which could introduce some latency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Not suitable for write intensive scenarios as the write operations are slower compared to other methods because data needs to be written in cache as well as persistent storage everytime.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;In case the system cannot tolerate an initial delay and the writes are infrequent, then this mechanism works best. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Also if the number of records to be cached is also fixed, then this approach can provide the best of the results.&lt;/p&gt;

&lt;blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thank you :)&lt;/p&gt;

</description>
      <category>gratitude</category>
    </item>
    <item>
      <title>Cache Aside</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Fri, 22 Mar 2024 12:41:36 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/cache-aside-1ioa</link>
      <guid>https://dev.to/anikethsdeshpande/cache-aside-1ioa</guid>
      <description>&lt;p&gt;Cache Aside or Lazy Loading is one of the cache write policies or strategies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In cache aside method, the application is responsible for storing data into cache.&lt;/li&gt;
&lt;li&gt;When the client sends request to the application, it looks for data in the cache.&lt;/li&gt;
&lt;li&gt;If the data is found in the cache it is called as &lt;em&gt;cache-hit&lt;/em&gt;. The data is fetched from cache and returned to client.&lt;/li&gt;
&lt;li&gt;However, if the data is not found in the cache, also called as &lt;em&gt;cache-miss&lt;/em&gt;, the application queries for data in the database, writes the data into cache and sends the response to the client.&lt;/li&gt;
&lt;li&gt;Since we store data in cache only when it is necessary, this strategy is also called as Lazy-Loading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flh6koyxc7792iu568usl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flh6koyxc7792iu568usl.png" alt="Cache Aside - Write Strategy" width="538" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Advantages:
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;The implementation is simple.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Disadvantages:
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;The response time can be slow when there is cache miss, because, it involves many io operations to fetch data from DB and store it in cache.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gratitude</category>
      <category>tailwindcss</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Shallow Copy Vs Deep Copy</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Sat, 29 Oct 2022 12:57:16 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/shallow-copy-vs-deep-copy-56jf</link>
      <guid>https://dev.to/anikethsdeshpande/shallow-copy-vs-deep-copy-56jf</guid>
      <description>&lt;p&gt;In our day to day development tasks, we come across the need to copy objects and perform various operations.&lt;/p&gt;

&lt;p&gt;Python provides two important functions in the &lt;strong&gt;copy&lt;/strong&gt; library - &lt;em&gt;copy&lt;/em&gt; and &lt;em&gt;deepcopy&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Let us understand the difference between the two and their respective use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shallow Copy - copy.copy(obj)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;It makes a copy of the obj at the surface level. &lt;/li&gt;
&lt;li&gt;It copies all the contents of the obj.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferp0i8785js06ednujn9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferp0i8785js06ednujn9.png" alt="copy.copy()" width="752" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;However, the point to be noted here is that, in case the contents of x are mutable, then, y has reference to the contents of x. Any modification to the contents of x would be reflected in y as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67ef2wud6bu5y3rq9ve7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67ef2wud6bu5y3rq9ve7.png" alt="shallow copy" width="800" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example we see that modifying the contents of x[1], also modified the contents of y[1].&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3o45byvy1gst3yy8q5tw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3o45byvy1gst3yy8q5tw.png" alt="shallow copy" width="800" height="151"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Similarly, we see that modifying the contents of y, also modified the contents of x.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep Copy - copy.deepcopy(obj)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Deepcopy copies the object recursively. Recursively means, it copies the contents of the object and not merely its reference.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;So we should use deep copy in case we need an independent copy of the contents of the obj.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53buud6tqel6affms9k9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53buud6tqel6affms9k9.png" alt="Deepcopy" width="726" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In deepcopy, any changes to the contents of x do not effect y, unlike shallow copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn23a8du7apog6p8shsfx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn23a8du7apog6p8shsfx.png" alt="Deepcopy" width="774" height="172"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Copying objects properly is a very important &lt;em&gt;basic python concepts&lt;/em&gt;. This knowledge helps in building error free code wrt copying objects.&lt;/p&gt;

&lt;p&gt;Thank you&lt;br&gt;
Aniketh Deshpande&lt;/p&gt;

</description>
      <category>python</category>
      <category>deepcopy</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Dead Letter Queue</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Sun, 23 Oct 2022 11:50:55 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/dead-letter-queue-1ml2</link>
      <guid>https://dev.to/anikethsdeshpande/dead-letter-queue-1ml2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is going to be a very short introduction to dead letter queues. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What is a dead letter queue??
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Dead letter queues are messages queues specifically deployed to holding messages that could not be delivered to their intended queues.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Messages sometimes fail to get delivered to their intended queues as they might be unavailable or the queue is full.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Popular message queue tools that support or do not support DLQ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS SQS (Simple Queue Service) supports DLQ.&lt;/li&gt;
&lt;li&gt;RabbitMQ (free and open source) also supports DLQ.&lt;/li&gt;
&lt;li&gt;Redis does not support DLQ.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;p&gt;Thanks for reading :)&lt;br&gt;
DLQ is a very important component of a scalable and resilient software architecture. This article only provides an introduction to the concept and helps readers with useful links.&lt;/p&gt;

&lt;p&gt;Following are some of the useful resources that give more indepth information.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2021/12/amazon-sqs-dead-letter-queue-management-experience-queues/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2021/12/amazon-sqs-dead-letter-queue-management-experience-queues/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/antirez/disque#dead-letter-queue" rel="noopener noreferrer"&gt;https://github.com/antirez/disque#dead-letter-queue&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/13824879/how-to-resend-from-dead-letter-queue-using-redis-mq" rel="noopener noreferrer"&gt;https://stackoverflow.com/questions/13824879/how-to-resend-from-dead-letter-queue-using-redis-mq&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I request the readers of this post to kindly add more helpful URLs in the comment section, or add your experiences using a DLQ in a real world project. I believe that would help beginners get even better idea about how to use this in their projects.&lt;/p&gt;

&lt;p&gt;Thank You&lt;br&gt;
Aniketh Deshpande&lt;/p&gt;

</description>
      <category>messagequeue</category>
      <category>systemdesign</category>
      <category>aws</category>
      <category>rabbitmq</category>
    </item>
    <item>
      <title>Change Data Capture - PostgreSQL</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Sat, 22 Oct 2022 14:58:18 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/change-data-capture-postgresql-bi8</link>
      <guid>https://dev.to/anikethsdeshpande/change-data-capture-postgresql-bi8</guid>
      <description>&lt;p&gt;&lt;strong&gt;Change Data Capture&lt;/strong&gt; is the concept of recording the changes in the database table fields.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It is very helpful in use cases where we want to track creation, updation, deletion of records in the table.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We might want to use this information to make changes in other databases or notify customers or notify other services.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Example: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save a copy of this data in a warehouse post transform.&lt;/li&gt;
&lt;li&gt;Trigger notification service to notify users about this change.&lt;/li&gt;
&lt;li&gt;Cache the data.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CDC In Postgres
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Using &lt;strong&gt;Notify/Listen&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NOTIFY&lt;/code&gt; provides a mechanism for interprocess communication between the database and the service that is &lt;code&gt;LISTEN&lt;/code&gt;ing to this notification channel.&lt;/li&gt;
&lt;li&gt;One or more services could be listening to this notification channel.&lt;/li&gt;
&lt;li&gt;Name of this channel is usually the name of the database. However the user is free to set suitable names for these channels.&lt;/li&gt;
&lt;li&gt;Any change in the table is captured by the DB and a trigger is initiated, which calls a function that formats the message to notify.&lt;/li&gt;
&lt;li&gt;This usually contains the table name and the payload string.&lt;/li&gt;
&lt;li&gt;The listening server registers to the channel and gets the message from the DB.&lt;/li&gt;
&lt;li&gt;The service can then use this message and perform operations on it.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Pros:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Simple to implement. Use a trigger and a function to notify. Implement a listen service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cons:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Weak reliability. There is always a risk of loss of message especially when the listening service is down. Messages in the queue do not persist.&lt;/li&gt;
&lt;/ul&gt;




&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Using &lt;strong&gt;Debezium&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debezium is an open source tool used for capturing changes in the database tables based on the WAL (write ahead log).&lt;/li&gt;
&lt;li&gt;The tool provides connectors to connect to a variety of databases.&lt;/li&gt;
&lt;li&gt;The &lt;em&gt;source connector&lt;/em&gt; is used to capture changes in the source database.&lt;/li&gt;
&lt;li&gt;The &lt;em&gt;sync connector&lt;/em&gt; is used to sync data directly in the destination database. &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Pros:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;The changes are persistent as they can be streamed to kafka. Hence highly reliable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cons:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Debezium does not take into account changes in the schema, users need to update the schema changes. Otherwise there would be data loss.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Thank you&lt;br&gt;
Aniketh Deshpande&lt;/p&gt;

</description>
      <category>debezium</category>
      <category>postgres</category>
      <category>cdc</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>DB Locking - Why and How?</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Fri, 21 Oct 2022 18:39:15 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/db-locking-why-and-how-2c2p</link>
      <guid>https://dev.to/anikethsdeshpande/db-locking-why-and-how-2c2p</guid>
      <description>&lt;p&gt;If you have sent concurrent requests to your DB to modify its contents, you would have come across a phenomenon called &lt;strong&gt;The Double Booking Problem!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Double Booking Problem arises when &lt;strong&gt;&lt;em&gt;two or more&lt;/em&gt;&lt;/strong&gt; threads read the same data point and one thread incorrectly overwrites the changes done by another thread resulting in inconsistency.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lets see an example to get better understanding of it.&lt;/li&gt;
&lt;li&gt;Suppose its a bus booking application. &lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;User 1 and User 2, both read Seat_21 as available.&lt;/li&gt;
&lt;li&gt;User 1 books the seat. However User 2 is unaware of this as he has read Seat_21 is available.&lt;/li&gt;
&lt;li&gt;User 2 also books the seat. Now the seat info is overwritten and the seat is allotted to User 2.&lt;/li&gt;
&lt;li&gt;Due to this phenomenon of double time booking of resources, it is named as &lt;em&gt;The Double Booking Problem!&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Following flow diagram gives clear idea about it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiux83lrregifjdthinrc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiux83lrregifjdthinrc.png" alt="Double Booking Problem Flowchart" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In order to avoid this, we need to use locking.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Locking in DynamoDB
&lt;/h3&gt;

&lt;p&gt;Let us see how we can lock dynamodb objects.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Optimistic Locking
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The spotlight feature of optimistic locking is, it does not have a lock as such. Instead a &lt;strong&gt;version number&lt;/strong&gt; is attached to the record and it is incremented whenever the record is updated.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple users are allowed to read the document or record.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When the users try to commit their changes, the change related to the first request is accepted as the version numbers match in the record and in the commit request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Subsequent users trying to commit their changes are sent a &lt;em&gt;Validation Exception&lt;/em&gt;!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Based on this, the users can sync the updated record and make their changes and retry committing the changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Therefore, the double booking problem is eliminated.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Pessimistic Locking
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;In pessimistic locking mechanism, &lt;strong&gt;Locks&lt;/strong&gt; are implemented. The record that is being updated is locked when a user starts a transaction.&lt;/li&gt;
&lt;li&gt;Since the record is locked, the other users trying to update the record are notified that the record is locked and the read also fails.&lt;/li&gt;
&lt;li&gt;This mechanism although equally effective, has an overhead of implementing locks.&lt;/li&gt;
&lt;li&gt;Therefore, the double booking problem is eliminated.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Thanks for reading this blog.&lt;br&gt;
Aniketh Deshpande&lt;br&gt;
India&lt;/p&gt;

</description>
      <category>database</category>
      <category>locking</category>
      <category>systemdesign</category>
      <category>dynamodb</category>
    </item>
    <item>
      <title>TimescaleDB Tablespaces</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Fri, 21 Oct 2022 04:59:45 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/timescaledb-tablespaces-4dh3</link>
      <guid>https://dev.to/anikethsdeshpande/timescaledb-tablespaces-4dh3</guid>
      <description>&lt;h3&gt;
  
  
  Tablespace
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tablespace&lt;/strong&gt; is a storage location where the actual data is stored. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The tuples belonging to the same table could be stored in different tablespaces.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tablespaces are mainly used to separately store data of different priority in different kind of disk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: Data of active users, or recent active customers can be stored in &lt;em&gt;fast disk types&lt;/em&gt; like ssd or flash. Data that is old and unused frequently or archived, can be stored in a &lt;em&gt;less expensive and slower&lt;/em&gt; data storage like HDD. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  TimescaleDB
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;TimescaleDB is an open source time series database. It is extends PostgreSQL and supports most of the commands of postgres.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker Image:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker pull timescale/timescaledb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;TimescaleDB has a concept of hypertables and chunks. Hypertables are Postgresql tables, that partition the data into chunks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The chunks are created based on primarily the time field.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The chunks older than certain date can be moved to a slow disk like HDD and the latest data which would be heavily used can be used in a fast disk like SSD and Flash.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The concept of &lt;em&gt;tablespaces&lt;/em&gt; helps in here.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TSDB code to move chunks:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT move_chunk(
  chunk =&amp;gt; '_timescaledb_internal._hyper_1_4_chunk',
  destination_tablespace =&amp;gt; 'tablespace_2',
  index_destination_tablespace =&amp;gt; 'tablespace_3',
  reorder_index =&amp;gt; 'conditions_device_id_time_idx',
  verbose =&amp;gt; TRUE
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For detailed information, use the following link: &lt;a href="https://legacy-docs.timescale.com/v1.7/api#move_chunk" rel="noopener noreferrer"&gt;https://legacy-docs.timescale.com/v1.7/api#move_chunk&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  AWS Volumes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If the TSDB instance is hosted in a kubernetes cluster in AWS, the TSDB pod would be provided with an AWS Volume for persistent storage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS supports the following volume classes. Based on speed and cost requirements, we can select the appropriate volumes for TSDB tablespaces.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS volume types: &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Thank you for reading the blog :) &lt;br&gt;
Aniketh Deshpande&lt;/p&gt;

</description>
      <category>timescaledb</category>
      <category>tablespace</category>
      <category>scaling</category>
      <category>aws</category>
    </item>
    <item>
      <title>Redis Timeseries</title>
      <dc:creator>Aniketh Deshpande</dc:creator>
      <pubDate>Thu, 20 Oct 2022 19:00:04 +0000</pubDate>
      <link>https://dev.to/anikethsdeshpande/redis-timeseries-4bnm</link>
      <guid>https://dev.to/anikethsdeshpande/redis-timeseries-4bnm</guid>
      <description>&lt;p&gt;Redis is an amazing tool to cache data. It supports different data types to help us cache different kinds of data.&lt;/p&gt;

&lt;p&gt;Following are the the data types supported as of Redis 7.0&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Strings&lt;/li&gt;
&lt;li&gt;Hashes&lt;/li&gt;
&lt;li&gt;Lists&lt;/li&gt;
&lt;li&gt;Sets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sorted Sets&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timeseries&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this article, we shall focus mainly on caching timeseries data in redis.&lt;/p&gt;

&lt;p&gt;We can cache timeseries data in the following ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Using the Redis-Timeseries extention&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Save data using &lt;code&gt;ADD KEY Timestamp Record&lt;/code&gt;&lt;br&gt;
where, Key is the name of the timeseries.&lt;br&gt;
Timestamp is the field used for sorting the elements.&lt;br&gt;
Record is the field representing the value at the given timestamp.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fetch records using &lt;code&gt;range&lt;/code&gt; command. &lt;code&gt;RANGE KEY FROM_TS TO_TS&lt;/code&gt; where from_ts and to_ts represent the upper bound and lower bound of the timestamp in the search space.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE: the record field is of type decimal. It supports only numbers.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Therefore, it is very helpful for saving single value records and not lists or maps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: stock values, moisture in soil etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;However it is not possible to save lists or tuples.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In that case, we can make use of Sorted Sets.&lt;/p&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Sorted Sets&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ZADD KEY Timestamp RECORD&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Here, zadd is used to save data in sorted sets. Key is the series name. Timestamp is the field used for sorting. Record can be of type string. Hence we can save json strings in the record field.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To fetch data from sorted sets, use command &lt;code&gt;ZRANGE FROM_TS TO_TS BYSCORE=TRUE&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;code&gt;by_score&lt;/code&gt; to get data based on timestamp and not index.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Docker Image For Redis Timeseries
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker pull redislabs/redistimeseries&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Link: &lt;a href="https://hub.docker.com/r/redislabs/redistimeseries" rel="noopener noreferrer"&gt;https://hub.docker.com/r/redislabs/redistimeseries&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Thank you for reading the blog. Please suggest improvements and like the blog.&lt;/p&gt;

&lt;p&gt;Aniketh Deshpande&lt;/p&gt;

</description>
      <category>redis</category>
      <category>timeseries</category>
      <category>cache</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
