Definition
- OLTP (Online Transaction Processing) – row-oriented stores (PostgreSQL, MySQL) optimized for many small, concurrent read/write transactions touching few rows.
- OLAP (Online Analytical Processing) – column-oriented stores (Redshift, BigQuery, Snowflake) optimized for scanning and aggregating few columns across billions of rows.
- Row-oriented storage – all columns of one row sit together on disk; fast for full-row reads and writes.
- Column-oriented storage – all values of one column sit together; fast for aggregating one column, excellent compression.
- Resource contention – analytics on the OLTP database compete for CPU, memory, buffer cache, and locks with live transactions.
OLTP is the busy teller counter at the National Bank of India: thousands of quick deposits and withdrawals, each touching one account, needing to be fast and correct. OLAP is the analyst in the back room who asks "what was the total deposit volume across all branches for the last five years?" If you force the teller counter to also run that giant report, the queue freezes while the machine chews through billions of rows. You keep the two jobs on separate systems.
Understanding the Concepts
What is OLTP?
OLTP (Online Transaction Processing) systems handle many small, concurrent transactions. Each transaction typically reads or writes a few rows. The database is row-oriented: all columns of one account sit together on disk.
-- OLTP: fast single-row transaction
BEGIN;
SELECT balance FROM accounts WHERE account_id = 'SAV2001' FOR UPDATE;
UPDATE accounts SET balance = balance - 25000.00
WHERE account_id = 'SAV2001';
COMMIT;
-- Completes in milliseconds, touches one row
What is OLAP?
OLAP (Online Analytical Processing) systems scan and aggregate large volumes of data. The database is column-oriented: all the amount values across millions of rows sit together on disk.
-- OLAP: aggregate across billions of rows
SELECT
branch_id,
DATE_TRUNC('month', txn_date) AS month,
SUM(amount) AS total_deposits,
COUNT(*) AS txn_count
FROM transactions
WHERE txn_date >= '2019-01-01'
GROUP BY branch_id, DATE_TRUNC('month', txn_date)
ORDER BY month;
-- Scans 5 years of data, reads only amount + txn_date + branch_id
-- Columnar store reads just those 3 columns, not all 50
How does the storage layout differ?
The core difference is physical storage layout. This determines what each system is good and bad at.
Row-Oriented (OLTP):
Row 1: [account_id, holder_name, balance, status, branch_id, ...]
Row 2: [account_id, holder_name, balance, status, branch_id, ...]
Row 3: [account_id, holder_name, balance, status, branch_id, ...]
Good at: read/write one full row (all columns together)
Bad at: sum one column across millions of rows (reads all columns)
Column-Oriented (OLAP):
account_id column: [SAV2001, SAV2002, SAV2003, ...]
balance column: [100000, 250000, 75000, ...]
status column: [ACTIVE, ACTIVE, FROZEN, ...]
Good at: sum balance across millions of rows (reads only balance)
Bad at: update one row (balance is in one file, status in another)
Why is each bad at the other's job?
A columnar store is terrible at updating one account's balance because a single logical row is scattered across many column files. A row store is terrible at summing one column across billions of rows because it reads every full row.
package com.bank.model;
/**
* Why each storage layout fails at the other's job.
*/
public class StorageLayoutTradeoffs {
// Row store scanning billions of rows for SUM(balance):
// Must read: [account_id, holder_name, balance, status, branch_id, ...]
// Needs only: balance
// Wastes I/O reading 49 unwanted columns per row
// No columnar compression to help
// Column store updating one account's balance:
// balance column file: find and update one value
// status column file: if also changed, find and update there too
// Each column is a separate file: one logical row = N file seeks
}
Why must you NOT run heavy analytics on the OLTP database?
Resource contention is the reason. A five-year aggregation reads a huge fraction of the table, evicting hot pages from the buffer cache, burning CPU, and holding long-running snapshots.
What happens when you run analytics on OLTP:
1. Analytical query scans 500M rows
2. Hot account pages evicted from buffer cache
3. Live transactions now hit disk instead of cache
4. CPU and memory consumed by the analytical scan
5. Long-running snapshot prevents vacuum from reclaiming space
6. Connection pool fills up waiting for the slow query
7. Result: customer-facing transactions slow to a crawl
-- This kills your production database:
SELECT branch_id, SUM(amount)
FROM transactions
WHERE txn_date >= '2019-01-01'
GROUP BY branch_id;
-- Scans 500M rows on the OLTP primary during business hours
-- Every customer transaction slows down
What is the correct architecture?
Separate OLTP and OLAP. Writes and live reads go to PostgreSQL. Data is replicated via CDC (Debezium + Kafka) or nightly ETL into a columnar warehouse for analytics.
Correct Architecture:
┌──────────────┐ ┌──────────────────┐
│ Application │ │ Analytics Team │
│ (Tellers) │ │ (Reports) │
└──────┬───────┘ └──────┬───────────┘
│ │
▼ ▼
┌──────────────┐ CDC/ETL ┌──────────────────┐
│ PostgreSQL │ ────────────► │ Redshift / │
│ (OLTP) │ Kafka │ BigQuery (OLAP) │
│ Row-oriented│ │ Column-oriented │
└──────────────┘ └──────────────────┘
package com.bank.config;
import javax.sql.DataSource;
/**
* Separate data sources for OLTP and OLAP workloads.
*/
public class OltpOlapConfig {
private final DataSource oltpDataSource; // PostgreSQL
private final DataSource olapDataSource; // Redshift/BigQuery
public OltpOlapConfig(DataSource oltp, DataSource olap) {
this.oltpDataSource = oltp;
this.olapDataSource = olap;
}
public DataSource forTransactions() { return oltpDataSource; }
public DataSource forAnalytics() { return olapDataSource; }
}
What about using a read replica for reporting?
A read replica of the OLTP database can handle moderate reporting queries. But true heavy analytics still belong in a columnar warehouse because the replica uses the same row-oriented storage.
Middle ground: read replica for moderate reports
Primary (OLTP) ──replication──► Replica (moderate reports)
│
│ Still row-oriented
│ Still reads all columns
│ OK for moderate queries
│ NOT OK for 5-year scans
▼
Warehouse (heavy analytics)
Column-oriented
Compressed
Parallel scan
What should a senior engineer know?
A senior should know that the separation also lets each system scale independently and use the storage format suited to its workload. CDC with Debezium ensures near-real-time data freshness in the warehouse without touching the OLTP primary.
-- Debezium CDC pipeline:
-- 1. Debezium reads PostgreSQL logical replication
-- 2. Publishes row changes to Kafka topics
-- 3. Kafka consumer loads into Redshift/BigQuery
-- 4. Analytics team queries the warehouse
-- 5. PostgreSQL is never touched by analytics
-- The warehouse query that would kill OLTP:
SELECT branch_id, SUM(amount)
FROM warehouse.transactions
WHERE txn_date >= '2019-01-01'
GROUP BY branch_id;
-- Runs in seconds on columnar storage
-- Zero impact on production PostgreSQL
Visual Overview
┌──────────────────────────────────────────────────────┐ │ OLTP vs OLAP Storage Layout │ ├──────────────────────────────────────────────────────┤ │ │ │ ROW-ORIENTED (OLTP: PostgreSQL) │ │ ┌───────────────────────────────────────────┐ │ │ │ Row 1: [id, name, balance, status, ...] │ │ │ │ Row 2: [id, name, balance, status, ...] │ │ │ │ Row 3: [id, name, balance, status, ...] │ │ │ └───────────────────────────────────────────┘ │ │ Fast: read/write one row (all columns together) │ │ Slow: SUM(balance) reads all columns per row │ │ │ │ COLUMN-ORIENTED (OLAP: Redshift) │ │ ┌─────────────┐ ┌───────────┐ ┌──────────┐ │ │ │ id column │ │ balance │ │ status │ │ │ │ SAV2001 │ │ 100000 │ │ ACTIVE │ │ │ │ SAV2002 │ │ 250000 │ │ ACTIVE │ │ │ │ SAV2003 │ │ 75000 │ │ FROZEN │ │ │ └─────────────┘ └───────────┘ └──────────┘ │ │ Fast: SUM(balance) reads only the balance column │ │ Slow: update one row across N column files │ └──────────────────────────────────────────────────────┘
Key Takeaway
OLTP and OLAP differ fundamentally in how they store data on disk. OLTP databases like PostgreSQL are row-oriented: all columns of one account sit together, which is perfect for a transaction that reads or updates a single row quickly under heavy concurrency. OLAP databases like Redshift and BigQuery are column-oriented: all values of one column sit together and are heavily compressed, which is perfect for scanning and aggregating a few columns across billions of rows.
Each is deliberately bad at the other's job. A columnar store is poor at updating one account's balance because a single logical row is scattered across many column files. A row store is poor at summing one column across billions of rows because it reads every full row, dragging in all the unwanted columns.
You must not run heavy analytics on your production OLTP database because of resource contention. A five-year aggregation reads a huge fraction of the table, evicts hot pages from the buffer cache, burns CPU, and holds long-running snapshots. At the National Bank of India, the finance team ran a five-year monthly report directly against the PostgreSQL primary during business hours, and the teller application slowed to a crawl. Pankaj moved that workload into a Redshift warehouse fed by Debezium CDC, so analysts query the warehouse while PostgreSQL stays fast for customers.
The correct architecture keeps writes and live reads on the OLTP primary and replicates data into a columnar warehouse through CDC or batch ETL. Each system scales independently using the storage format suited to its workload.
Remember one sentence: OLTP serves fast single-row transactions and OLAP scans few columns over billions of rows, so keep heavy analytics off the transactional database.
---