Chapter 15: Time-Series Databases & OLAP

Analytical workloads differ fundamentally from transactional ones. This chapter explores specialized databases for time-series data and analytical processing (OLAP), their storage optimizations, and architectural patterns for real-time analytics.

15.1 OLTP vs OLAP

OLTP (Online Transaction Processing) systems handle operational workloads with many small read/write transactions. OLAP (Online Analytical Processing) systems handle analytical queries scanning large datasets. The fundamental difference drives completely different storage and optimization strategies.

Storage Layout Comparison

graph TB subgraph "OLTP: Row-Oriented Storage" R1["Row 1: id=1, name='Alice', age=30, city='NYC', salary=100k"] R2["Row 2: id=2, name='Bob', age=25, city='LA', salary=80k"] R3["Row 3: id=3, name='Carol', age=35, city='NYC', salary=120k"] R1 --> R2 --> R3 end subgraph "OLAP: Column-Oriented Storage" C1["ID Column: [1, 2, 3]"] C2["Name Column: ['Alice', 'Bob', 'Carol']"] C3["Age Column: [30, 25, 35]"] C4["City Column: ['NYC', 'LA', 'NYC']"] C5["Salary Column: [100k, 80k, 120k]"] end Q1["Query: SELECT * WHERE id=2"] --> R1 Q2["Query: SELECT AVG(salary) WHERE city='NYC'"] --> C4 Q2 --> C5 style Q1 fill:#4a90e2 style Q2 fill:#e24a90
Characteristic OLTP OLAP
Workload Many small transactions (INSERT, UPDATE, DELETE, point SELECT) Few large queries scanning millions/billions of rows
Query Pattern Fetch entire rows by primary key or index Aggregate few columns across many rows (AVG, SUM, COUNT)
Storage Row-oriented (entire row stored together) Column-oriented (each column stored separately)
Write Pattern Random writes, high write volume, ACID required Bulk inserts/imports, append-mostly, eventual consistency OK
Data Size GB to TB, latest data TB to PB, historical data
Users Many concurrent users (thousands) Fewer analysts (tens to hundreds)
Latency Milliseconds, predictable Seconds to minutes, variable
Indexes B-trees, hash indexes on keys Columnar compression, bitmap indexes, skip indexes
Schema Normalized (3NF), minimize redundancy Denormalized (star/snowflake), optimize for reads
Examples PostgreSQL, MySQL, Oracle, SQL Server ClickHouse, BigQuery, Snowflake, Redshift

Why Separate Systems?

  • Resource Contention: Heavy analytical queries can starve OLTP transactions of resources
  • Different Optimization Goals: Row storage for transactions vs columnar for analytics
  • Backup/Recovery: OLTP needs point-in-time recovery; OLAP can rebuild from source
  • Schema Design: Normalized for consistency vs denormalized for query speed
  • Data Lifecycle: OLTP keeps recent data; OLAP keeps years of history with compression
Interview Tip: When asked about analytics, immediately clarify if OLTP queries must run on the same database. If analytics are important, propose ETL to a separate OLAP system (data warehouse). Mention tools like Fivetran/Airbyte for CDC, and dbt for transformations.

15.2 Columnar Storage

Columnar databases store each column separately on disk. This enables massive compression (same data types together) and allows reading only needed columns. SIMD/vectorized execution processes entire columns in CPU registers.

Row vs Column Layout

graph LR subgraph "Row-Oriented (PostgreSQL, MySQL)" D1["Disk Block 1
[Row1: A,B,C,D]
[Row2: A,B,C,D]"] D2["Disk Block 2
[Row3: A,B,C,D]
[Row4: A,B,C,D]"] end subgraph "Column-Oriented (Parquet, ClickHouse)" C1["Block 1: Column A
[R1_A, R2_A, R3_A, R4_A]"] C2["Block 2: Column B
[R1_B, R2_B, R3_B, R4_B]"] C3["Block 3: Column C
[R1_C, R2_C, R3_C, R4_C]"] C4["Block 4: Column D
[R1_D, R2_D, R3_D, R4_D]"] end Q1["SELECT * FROM t
WHERE id=3"] --> D1 Q1 --> D2 Q2["SELECT AVG(B)
FROM t"] --> C2 style Q1 fill:#4a90e2 style Q2 fill:#e24a90

Parquet File Structure

graph TB subgraph "Parquet File" H1["Header: Magic Number + Metadata"] RG1["Row Group 1 (128MB)"] RG2["Row Group 2 (128MB)"] RG3["Row Group 3 (128MB)"] subgraph "Row Group 1 Detail" CG1["Column Chunk A
Pages: [Page1][Page2]...
Encoding: DICT
Compression: SNAPPY"] CG2["Column Chunk B
Pages: [Page1][Page2]...
Encoding: RLE
Compression: GZIP"] CG3["Column Chunk C
Pages: [Page1][Page2]...
Encoding: PLAIN
Compression: ZSTD"] end F["Footer: Schema + Column Metadata
Min/Max/Nulls per Row Group"] end H1 --> RG1 --> RG2 --> RG3 --> F RG1 -.-> CG1 RG1 -.-> CG2 RG1 -.-> CG3 style F fill:#e2c04a

Benefits of Columnar Storage

Benefit Explanation Example
I/O Reduction Read only columns needed, skip irrelevant data SELECT AVG(salary): read 1 column, not 20
Compression Same data types compress better (RLE, dictionary) Status column ['active','active','active'] -> RLE: (active,3)
Vectorized Execution SIMD instructions process arrays of values in parallel AVX-512: add 16 floats in one CPU instruction
Predicate Pushdown Skip entire column chunks using min/max statistics WHERE date='2024-01-01': skip chunks with date range outside Jan 2024
Late Materialization Defer reconstructing rows until last possible moment Filter on col A, aggregate col B -> never reconstruct full rows
Cache Efficiency CPU cache holds more values (homogeneous data) 64-byte cache line: 16 ints vs 2-3 rows of mixed types

Vectorized Execution Example

// Scalar (row-by-row): 1 operation per loop
for (int i = 0; i < n; i++) {
    result[i] = array_a[i] + array_b[i];
}

// Vectorized (SIMD): 8 operations per loop (AVX)
for (int i = 0; i < n; i += 8) {
    __m256i a = _mm256_loadu_si256(&array_a[i]);
    __m256i b = _mm256_loadu_si256(&array_b[i]);
    __m256i sum = _mm256_add_epi32(a, b);
    _mm256_storeu_si256(&result[i], sum);
}
// 8x faster for integer addition
Interview Tip: When designing analytics systems, mention columnar formats like Parquet or ORC for data lakes. Explain that columnar storage is why BigQuery and Snowflake scan TB in seconds. Bonus: mention "projection pushdown" (read only needed columns) and "predicate pushdown" (filter before reading).

15.3 Column Compression Techniques

Columnar storage enables specialized compression schemes that exploit patterns in typed data. Compression reduces I/O (read less from disk) and fits more data in memory/cache.

Compression Schemes

graph TB subgraph "Run-Length Encoding (RLE)" R1["Data: [active, active, active, inactive, inactive, active]
Compressed: [(active,3), (inactive,2), (active,1)]"] end subgraph "Dictionary Encoding" D1["Data: [USA, Canada, USA, USA, Mexico, Canada]
Dict: {0:USA, 1:Canada, 2:Mexico}
Encoded: [0,1,0,0,2,1] (2-bit ints)"] end subgraph "Bitmap Encoding (for low cardinality)" B1["Data: [A,B,A,C,B,A]
Bitmap A: 101001
Bitmap B: 010010
Bitmap C: 000100"] end subgraph "Delta Encoding (for sorted/sequential)" DE1["Timestamps: [1000, 1005, 1008, 1012]
Base: 1000, Deltas: [5, 3, 4]
(smaller ints compress better)"] end subgraph "Gorilla (Time-Series)" G1["Values: [1.23, 1.24, 1.23, 1.25]
XOR with previous:
0 -> 1.23 (base)
1 -> XOR(1.24, 1.23) -> few bits differ
Store only differing bits"] end

Compression Comparison

Scheme Best For Compression Ratio Random Access Examples
RLE Sorted data, many consecutive duplicates 10-100x for sorted columns Good (skip runs) Status columns, flags, sorted timestamps
Dictionary Low/medium cardinality (thousands of unique values) 5-20x for strings Excellent (direct index) Country, category, user ID
Bitmap Very low cardinality (2-100 unique values) 8-64x (1 bit per value) Excellent (bit operations) Gender, boolean flags, enum types
Delta Monotonic sequences, timestamps 2-10x (smaller integers) Good (decode from base) Auto-increment IDs, sorted timestamps
Gorilla Slowly changing floats/timestamps 10-20x for metrics Poor (sequential decode) CPU usage, temperature, stock prices
LZ4/ZSTD General-purpose (post-encoding) 2-5x Block-level After dictionary/RLE, or for text

Gorilla Algorithm Detail

Developed by Facebook for time-series compression. Exploits the fact that consecutive float values in metrics change slowly, so XOR produces many zeros.

// Gorilla compression for floats
timestamp: delta-of-delta encoding (most deltas ~same)
  1000, 1060, 1120, 1180 -> deltas: 60,60,60 -> delta-of-deltas: 0,0
  Store: base=1000, delta=60, then stream of 0s (1 bit each)

value: XOR + variable-bit encoding
  12.5, 12.6, 12.5 -> XOR(12.6, 12.5) = few bits differ
  Leading zeros: 54, Trailing zeros: 3, Meaningful bits: 7
  Store: control bits + 7 bits (not 64 bits)
Interview Tip: Mention compression when discussing data lake architectures or time-series systems. For S3 storage costs, note that Parquet with Snappy/ZSTD reduces size by 10-20x vs CSV. For time-series, mention that Gorilla achieves 1.37 bytes/point vs 16 bytes uncompressed (12x).

15.4 Time-Series Databases

Time-series databases (TSDB) optimize for append-heavy workloads with timestamped data. Key features: automatic downsampling, retention policies, time-based aggregations, and specialized compression.

InfluxDB Architecture

graph TB W["Write: Line Protocol
cpu,host=server1 value=0.64 1672531200"] subgraph "InfluxDB TSM Engine" WAL["WAL (Write-Ahead Log)
Durability"] Cache["In-Memory Cache
(sorted by time)"] TSM1["TSM File 1
Index: SeriesKey -> BlockOffset
Blocks: compressed time/value pairs
(2h of data)"] TSM2["TSM File 2
(older data)"] TSM3["TSM File 3
(oldest data)"] CQ["Continuous Queries
(auto-downsample)"] RP["Retention Policies
(auto-delete old data)"] end R["Query: InfluxQL / Flux
SELECT MEAN(value)
FROM cpu
WHERE time > now()-1h
GROUP BY time(5m)"] W --> WAL --> Cache Cache --> TSM1 TSM1 --> TSM2 --> TSM3 TSM1 --> CQ --> TSM2 TSM3 --> RP R --> Cache R --> TSM1 R --> TSM2 style WAL fill:#4ae290 style CQ fill:#e2c04a style RP fill:#e24a90

TimescaleDB Architecture

graph TB subgraph "TimescaleDB (PostgreSQL Extension)" HT["Hypertable: metrics
(virtual table)"] C1["Chunk 1
2024-01-01 to 2024-01-07
(1 week of data)
Standard PG table"] C2["Chunk 2
2024-01-08 to 2024-01-14"] C3["Chunk 3
2024-01-15 to 2024-01-22"] C4["Chunk 4
2024-01-23 to 2024-01-29"] CA["Compression
(columnar, per chunk)"] AGG["Continuous Aggregates
(materialized views)"] RET["Retention Policy
(drop old chunks)"] end Q["SELECT * FROM metrics
WHERE time > '2024-01-15'
(query planner routes to chunks)"] HT --> C1 HT --> C2 HT --> C3 HT --> C4 C1 --> CA C2 --> CA HT --> AGG C1 --> RET Q --> HT style HT fill:#4ae290 style AGG fill:#e2c04a

Prometheus Architecture

graph LR subgraph "Targets" T1["App Server 1
/metrics endpoint"] T2["App Server 2
/metrics endpoint"] T3["Database
/metrics endpoint"] end subgraph "Prometheus Server" SD["Service Discovery
(Kubernetes, Consul, static)"] SC["Scraper
(HTTP pull every 15s)"] WAL["WAL"] MEM["In-Memory DB
(2h head block)"] B1["Block 1 (2h)
index + chunks
compressed"] B2["Block 2 (2h)"] B3["Block 3 (2h)"] COMP["Compaction
(merge blocks)"] end Q["PromQL Query:
rate(http_requests_total[5m])"] SD --> SC SC --> T1 SC --> T2 SC --> T3 SC --> WAL --> MEM MEM --> B1 B1 --> COMP --> B2 --> B3 Q --> MEM Q --> B1 style SC fill:#4ae290 style COMP fill:#e2c04a

Time-Series Database Comparison

Database Architecture Write Model Query Language Strengths Use Cases
InfluxDB TSM engine, LSM-tree like Push (line protocol) InfluxQL, Flux Best compression, continuous queries, easy setup IoT sensors, application metrics, single-node deployments
TimescaleDB PostgreSQL extension, hypertables with chunks Push (SQL INSERT) SQL Full SQL support, JOINs, geo-spatial, continuous aggregates Complex queries, existing PG users, relational + time-series
QuestDB Column-oriented, zero-GC Java Push (InfluxDB protocol, PostgreSQL wire) SQL (extended with time) Extremely fast ingestion (3M+ rows/sec), low latency queries High-frequency trading, real-time dashboards, market data
Prometheus Pull-based scraper, local TSDB with 2h blocks Pull (scrape /metrics) PromQL Service discovery, alerting, ecosystem (Grafana), simple Kubernetes monitoring, microservices observability, DevOps
VictoriaMetrics Prometheus-compatible, optimized storage Push/Pull (Prometheus remote write) PromQL, MetricsQL Better compression than Prometheus, lower resource usage, clustering Large-scale Prometheus replacement, cost reduction
OpenTSDB Built on HBase Push (HTTP/Telnet) HTTP API Scales horizontally (HBase), proven at scale Legacy systems, HBase users, massive scale

Retention Policies & Downsampling

-- InfluxDB retention policies
CREATE RETENTION POLICY "one_week" ON "mydb"
  DURATION 7d REPLICATION 1 DEFAULT;

CREATE RETENTION POLICY "one_year" ON "mydb"
  DURATION 52w REPLICATION 1;

-- Continuous query: downsample to 5-minute averages
CREATE CONTINUOUS QUERY "cq_5m" ON "mydb"
BEGIN
  SELECT mean(value) INTO "one_year"."downsample"
  FROM "one_week"."measurements"
  GROUP BY time(5m), *
END

-- TimescaleDB continuous aggregate
CREATE MATERIALIZED VIEW metrics_5m
WITH (timescaledb.continuous) AS
SELECT time_bucket('5 minutes', time) AS bucket,
       host,
       AVG(cpu_usage) AS avg_cpu
FROM metrics
GROUP BY bucket, host;
Interview Tip: For metrics/monitoring, suggest Prometheus + Grafana (standard stack). For IoT/sensors with complex queries, suggest TimescaleDB (SQL familiarity). For high-frequency financial data, mention QuestDB. Always discuss retention policies and downsampling to control storage costs.

15.5 OLAP Databases

OLAP databases excel at analytical queries over large datasets. Modern systems combine columnar storage, vectorized execution, distributed query processing, and intelligent caching.

ClickHouse Architecture

graph TB subgraph "ClickHouse Cluster" Q["Query: SELECT country, COUNT(*)
FROM events
WHERE date >= '2024-01-01'
GROUP BY country"] QP["Query Planner
(distributed execution)"] S1["Shard 1"] S2["Shard 2"] S3["Shard 3"] subgraph "Shard 1 Detail" MT1["MergeTree Table
(sorted by date, country)"] P1["Partition: 2024-01
(directory per month)"] P2["Partition: 2024-02"] PART["Part 1 (granule)
Primary Index: sparse
Columns: compressed
min/max marks"] end R1["Replica 1-1"] R2["Replica 1-2"] MV["Materialized View
(auto-updated on insert)"] end Q --> QP QP --> S1 QP --> S2 QP --> S3 S1 --> MT1 MT1 --> P1 --> PART MT1 --> P2 S1 --> R1 S1 --> R2 MT1 --> MV style QP fill:#4ae290 style PART fill:#e2c04a style MV fill:#e24a90

Druid Architecture

graph TB subgraph "Druid Real-Time + Batch" RT["Real-Time Ingestion
(Kafka, Kinesis)"] RT1["Real-Time Node
(recent data, row-oriented)"] BATCH["Batch Ingestion
(S3, HDFS)"] HN["Historical Node
(immutable segments)"] SEG1["Segment 1
2024-01-01 1h
Columnar + Bitmap Indexes
Dictionary Encoded"] SEG2["Segment 2
2024-01-01 2h"] SEG3["Segment 3
2024-01-01 3h"] DEEP["Deep Storage
(S3, HDFS)"] BR["Broker Node
(query router)"] COORD["Coordinator
(segment assignment)"] end Q["Query: Druid SQL"] RT --> RT1 BATCH --> DEEP DEEP --> HN HN --> SEG1 HN --> SEG2 HN --> SEG3 RT1 --> SEG1 Q --> BR BR --> RT1 BR --> HN COORD --> HN style RT1 fill:#4ae290 style SEG1 fill:#e2c04a style BR fill:#e24a90

BigQuery Architecture

graph TB subgraph "BigQuery (Serverless)" Q["SQL Query"] QE["Query Engine (Dremel)
Root Server"] M1["Mixer (intermediate agg)"] M2["Mixer"] M3["Mixer"] L1["Leaf Node 1
(read Colossus)"] L2["Leaf Node 2"] L3["Leaf Node 3"] L4["Leaf Node 4"] L5["Leaf Node 5"] L6["Leaf Node 6"] STORE["Colossus (Distributed FS)
Capacitor (columnar format)
Encrypted, replicated"] CACHE["BI Engine Cache
(in-memory, optional)"] SLOT["Slot Allocation
(dynamic, shared pool)"] end Q --> QE QE --> M1 QE --> M2 QE --> M3 M1 --> L1 M1 --> L2 M2 --> L3 M2 --> L4 M3 --> L5 M3 --> L6 L1 --> STORE L2 --> STORE L3 --> STORE L4 --> STORE L5 --> STORE L6 --> STORE STORE --> CACHE SLOT --> QE style QE fill:#4ae290 style STORE fill:#e2c04a style CACHE fill:#e24a90

Snowflake Architecture

graph TB subgraph "Snowflake Multi-Cluster" Q["SQL Query"] VW1["Virtual Warehouse 1
(X-Small: 1 node)
Compute Cluster"] VW2["Virtual Warehouse 2
(Medium: 4 nodes)
Compute Cluster"] CS["Cloud Services Layer
Query Optimizer
Metadata
Access Control
Time Travel"] STORE["Storage Layer (S3/Azure)
Micro-partitions (50-500MB)
Columnar, compressed
Immutable"] META["Metadata Store
Min/Max/Bloom per partition
Clustering info"] CACHE["Result Cache
(24h, shared)"] LCACHE["Local Cache
(SSD on compute nodes)"] TT["Time Travel
(7 days default)"] end Q --> CS CS --> VW1 CS --> VW2 VW1 --> STORE VW2 --> STORE STORE --> META CS --> CACHE VW1 --> LCACHE VW2 --> LCACHE STORE --> TT style CS fill:#4ae290 style STORE fill:#e2c04a style CACHE fill:#e24a90

OLAP Database Comparison

Database Deployment Storage Strengths Weaknesses Best For
ClickHouse Self-hosted, ClickHouse Cloud MergeTree (local disk) Fastest queries, real-time inserts, SQL, low cost Manual ops, no transactions, limited JOIN Event analytics, logs, real-time dashboards, cost-sensitive
Druid Self-hosted, Imply Cloud Segments (deep storage: S3) Real-time + historical, fast aggregations, high concurrency Complex setup, limited SQL, no JOINs Real-time analytics, user-facing dashboards, ad-tech
Pinot Self-hosted, Startree Cloud Segments (StarTree index) Low-latency queries (<100ms), upserts, high QPS Complex setup, limited SQL features User-facing analytics (LinkedIn feeds, Uber dashboards)
BigQuery Serverless (GCP) Colossus (Capacitor format) Zero ops, petabyte scale, ML integration, pay-per-query Cold start latency, cost unpredictable, vendor lock-in Data warehousing, ad-hoc analysis, infrequent queries
Snowflake Serverless (AWS/Azure/GCP) S3/Azure (micro-partitions) Zero ops, storage/compute separation, time travel, cloning Cost (always-on warehouses), cold start, vendor lock-in Data warehousing, BI tools, multi-tenant analytics
Redshift Provisioned clusters (AWS) Columnar (local + S3) AWS integration, mature, Postgres-compatible Slower than modern systems, manual tuning, concurrency limits Existing AWS users, BI workloads, batch ETL
Interview Tip: For self-hosted with cost sensitivity, recommend ClickHouse. For zero-ops cloud, suggest BigQuery (GCP) or Snowflake (multi-cloud). For user-facing analytics with low latency, mention Druid or Pinot. Always separate storage (data lake on S3) from compute (query engine) for flexibility.

15.6 Star Schema & Dimensional Modeling

Star schema is the standard for data warehouses. A central fact table contains measures (metrics), surrounded by dimension tables with descriptive attributes. Optimized for analytical queries with few JOINs.

Star Schema Structure

graph TB subgraph "Star Schema: Sales Data Warehouse" F["FACT TABLE: sales_facts
---------------
sale_id (PK)
date_key (FK)
product_key (FK)
customer_key (FK)
store_key (FK)
---------------
quantity (measure)
revenue (measure)
cost (measure)
profit (measure)"] D1["DIM TABLE: dim_date
----------
date_key (PK)
date
day_of_week
month
quarter
year
is_holiday"] D2["DIM TABLE: dim_product
-----------
product_key (PK)
product_name
category
brand
price"] D3["DIM TABLE: dim_customer
-----------
customer_key (PK)
name
email
city
state
segment"] D4["DIM TABLE: dim_store
-----------
store_key (PK)
store_name
city
state
region
manager"] end D1 --> F D2 --> F D3 --> F D4 --> F style F fill:#4ae290 style D1 fill:#e2c04a style D2 fill:#e2c04a style D3 fill:#e2c04a style D4 fill:#e2c04a

Star vs Snowflake Schema

graph LR subgraph "Star Schema (Denormalized)" FS["Fact: Sales"] DS["Dim: Product
product_id
name
category
subcategory
brand"] FS --> DS end subgraph "Snowflake Schema (Normalized)" FSN["Fact: Sales"] DSN["Dim: Product
product_id
name
category_id"] CAT["Dim: Category
category_id
category_name
subcategory_id"] SUB["Dim: Subcategory
subcategory_id
name"] FSN --> DSN --> CAT --> SUB end style DS fill:#4ae290 style DSN fill:#e24a90

Slowly Changing Dimensions (SCD)

Dimension attributes change over time (e.g., customer moves to new city). Strategies to track history:

Type Strategy Example Pros/Cons
SCD Type 1 Overwrite old value (no history) Customer moves: update city='NYC' -> city='LA' Simple, but loses history
SCD Type 2 Add new row with version/date range Row 1: city='NYC', valid_from='2020-01-01', valid_to='2023-06-30'
Row 2: city='LA', valid_from='2023-07-01', valid_to='9999-12-31'
Full history, but complex queries (filter by valid_from/to)
SCD Type 3 Add columns for current + previous city_current='LA', city_previous='NYC' Simple queries, but limited history (1-2 versions)
SCD Type 4 Separate history table dim_customer (current), dim_customer_history (past versions) Clean separation, but JOIN complexity

SCD Type 2 Example

-- Dimension table with Type 2 tracking
CREATE TABLE dim_customer (
    customer_key SERIAL PRIMARY KEY,  -- surrogate key
    customer_id INT,                   -- natural key
    name VARCHAR(100),
    email VARCHAR(100),
    city VARCHAR(50),
    state VARCHAR(2),
    valid_from DATE,
    valid_to DATE,                     -- 9999-12-31 for current
    is_current BOOLEAN                 -- true for current version
);

-- Query: sales in last year, using customer's city at time of sale
SELECT c.city, SUM(f.revenue)
FROM sales_facts f
JOIN dim_customer c
  ON f.customer_key = c.customer_key
 AND f.sale_date BETWEEN c.valid_from AND c.valid_to
WHERE f.sale_date >= CURRENT_DATE - INTERVAL '1 year'
GROUP BY c.city;
Interview Tip: For data warehouse design, always propose star schema. Mention that denormalization (star) beats normalization (snowflake) for query performance. Discuss SCD Type 2 for tracking history (e.g., customer address changes). Use surrogate keys (auto-increment) for dimension PKs, not natural keys.

15.7 Materialized Views & Pre-Aggregation

Materialized views store query results physically, trading storage and freshness for query speed. Essential for expensive aggregations (GROUP BY on billions of rows).

Materialized View Architecture

graph TB subgraph "Base Table" BT["events
(1 billion rows)
timestamp, user_id, action, value"] end subgraph "Materialized View" MV["events_hourly_agg
(pre-computed)
hour, action, COUNT(*), SUM(value), AVG(value)"] RF["Refresh Strategy"] FULL["Full Refresh
(recompute all)"] INC["Incremental Refresh
(delta only)"] STREAM["Streaming Refresh
(continuous, real-time)"] end Q1["Query: hourly metrics
(fast, uses MV)"] Q2["Query: raw events
(slow, scans base table)"] BT --> MV MV --> RF RF --> FULL RF --> INC RF --> STREAM Q1 --> MV Q2 --> BT style MV fill:#4ae290 style STREAM fill:#e24a90

Refresh Strategies

Strategy How It Works Freshness Cost Best For
Full Refresh Drop and rebuild entire view Minutes to hours stale High (recompute all) Small tables, nightly batch jobs
Incremental Refresh Compute only new/changed rows, merge into view Minutes stale (scheduled) Medium (compute delta) Append-only logs, daily/hourly refresh
Streaming Refresh Update view on every insert/update to base table Real-time (seconds) Low (per-row overhead) Dashboards needing real-time data
On-Demand Refresh Refresh when query hits view Real-time (lazy compute) Variable (query time penalty) Rarely accessed views, ad-hoc queries

Examples in Different Systems

-- PostgreSQL: Manual refresh
CREATE MATERIALIZED VIEW sales_daily AS
SELECT DATE(order_date) AS day, SUM(amount) AS revenue
FROM orders
GROUP BY day;

REFRESH MATERIALIZED VIEW sales_daily;  -- manual trigger

-- ClickHouse: Streaming materialized view (auto-updated on insert)
CREATE MATERIALIZED VIEW events_hourly_mv
ENGINE = SummingMergeTree()
ORDER BY (hour, action)
AS SELECT
    toStartOfHour(timestamp) AS hour,
    action,
    count() AS count,
    sum(value) AS total_value
FROM events
GROUP BY hour, action;

-- BigQuery: Scheduled refresh
CREATE MATERIALIZED VIEW project.dataset.sales_daily
PARTITION BY day
CLUSTER BY product_id
AS SELECT DATE(order_date) AS day, product_id, SUM(amount) AS revenue
FROM project.dataset.orders
GROUP BY day, product_id;

-- Auto-refresh every 4 hours (BigQuery setting)

-- Snowflake: Automatic refresh (experimental)
CREATE MATERIALIZED VIEW sales_daily AS
SELECT DATE(order_date) AS day, SUM(amount) AS revenue
FROM orders
GROUP BY day;

-- Auto-refresh when base table changes (Snowflake manages)

Cube Pre-Aggregation

For multi-dimensional analytics (OLAP cubes), pre-compute aggregations at multiple granularities.

graph TB subgraph "OLAP Cube: Sales by [Date, Product, Store]" L0["Level 0: Base Table
(date, product, store, revenue)
1 billion rows"] L1A["Level 1a: (date, product)
100M rows"] L1B["Level 1b: (date, store)
50M rows"] L1C["Level 1c: (product, store)
10M rows"] L2A["Level 2a: (date)
1000 rows"] L2B["Level 2b: (product)
10K rows"] L2C["Level 2c: (store)
500 rows"] L3["Level 3: Total
(1 row)"] end L0 --> L1A L0 --> L1B L0 --> L1C L1A --> L2A L1A --> L2B L1B --> L2A L1B --> L2C L1C --> L2B L1C --> L2C L2A --> L3 L2B --> L3 L2C --> L3 style L0 fill:#4ae290 style L3 fill:#e24a90
Interview Tip: Propose materialized views for dashboards with fixed queries (daily revenue, top products). Mention trade-off: storage + staleness vs speed. For real-time needs, discuss ClickHouse's streaming MVs or Druid's real-time ingestion. Avoid over-engineering: start with simple queries, add MVs when slow.

15.8 Approximate Query Processing

For massive datasets, approximate algorithms provide fast answers with bounded error. Key use cases: COUNT DISTINCT (unique users), top-K (most frequent items), percentiles (P95 latency).

Approximate Algorithms

graph TB subgraph "HyperLogLog (COUNT DISTINCT)" HLL["Registers: 16384 buckets
Each bucket: max leading zeros
Hash each value -> bucket -> update max
Estimate = harmonic mean * constant"] HLL_ERR["Error: 0.81% @ 16KB memory
Exact: 1B unique values = 8GB
HLL: 1B unique values = 16KB (500x smaller)"] end subgraph "Count-Min Sketch (FREQUENCY)" CMS["2D array: d x w (d hash functions)
Increment: hash(item) -> d positions, increment all
Query: min of d positions"] CMS_ERR["Over-estimates (collisions)
Error: +/- (total_count / w) with prob 1-e^(-d)"] end subgraph "T-Digest (PERCENTILES)" TD["Merge clusters of nearby values
Keep more clusters near tails (P99, P1)
Query: interpolate within clusters"] TD_ERR["Error: ~1% for extreme percentiles
Memory: 100-1000 clusters (few KB)"] end subgraph "Reservoir Sampling (RANDOM SAMPLE)" RS["Maintain k samples from stream
For item n: replace random sample with prob k/n
Result: uniform random sample"] RS_ERR["Exact distribution, but sample size k
Error = statistical (sample size)"] end style HLL fill:#4ae290 style CMS fill:#e2c04a style TD fill:#e24a90 style RS fill:#4a90e2

Algorithm Comparison

Algorithm Use Case Memory Error Bound Example Query
HyperLogLog COUNT(DISTINCT x) 1.5 KB (std), 12 KB (++) 0.81% standard error Unique visitors, unique IPs
Count-Min Sketch Frequency of item (top-K) d * w * 4 bytes (e.g., 5 * 10000 * 4 = 200KB) Over-estimate by epsilon with prob delta Most common user agents, top URLs
T-Digest Percentiles (P50, P95, P99) Few KB (100-1000 clusters) ~1% error at extreme percentiles P99 latency, P95 response time
Quantile Digest Percentiles (alternative) Logarithmic in data range Max rank error (epsilon * N) Median salary, P90 revenue
Reservoir Sampling Uniform random sample k * row_size (e.g., 1000 rows) Statistical (sample size k) Sample 1000 rows for preview
Bloom Filter Set membership (is X in set?) m bits (e.g., 10 bits/item) False positive rate = (1-e^(-kn/m))^k Has user seen this article?

HyperLogLog Example

-- PostgreSQL with HLL extension
CREATE EXTENSION hll;

-- Add HLL column to table
ALTER TABLE events ADD COLUMN user_hll hll;

-- Insert: hash user_id into HLL
UPDATE events SET user_hll = hll_add(hll_empty(), hll_hash_text(user_id));

-- Query: approximate COUNT(DISTINCT user_id)
SELECT hll_cardinality(hll_union_agg(user_hll)) AS approx_unique_users
FROM events
WHERE date >= '2024-01-01';

-- Exact (slow): 10 seconds, 1B rows scanned
SELECT COUNT(DISTINCT user_id) FROM events WHERE date >= '2024-01-01';

-- Approximate (fast): 100ms, 1.5KB per HLL aggregate
-- Error: actual=1,000,000, estimate=1,008,234 (0.8% error)

When to Use Approximate Queries

  • Real-time dashboards: Users don't need exact counts (1,234,567 vs 1,234,890)
  • Large cardinality: COUNT(DISTINCT) on billions of values is slow and memory-intensive
  • Streaming data: Compute metrics on infinite streams without storing all data
  • Monitoring: P99 latency approximation is acceptable (99.1% vs 99% is fine)
  • NOT for financials: Never approximate revenue, invoice totals, payment amounts
Interview Tip: For COUNT(DISTINCT user_id) on billions of rows, suggest HyperLogLog. For P99 latency, suggest T-Digest. For top-K URLs, suggest Count-Min Sketch. Always mention error bounds and that approximate algorithms are NOT for financial data. Used in Redis (HLL commands), Druid (sketches), ClickHouse (uniqCombined).

15.9 Real-Time Analytics Architectures

Real-time analytics require processing streaming data with low latency. Two main architectures: Lambda (batch + speed layers) and Kappa (stream-only).

Lambda Architecture

graph TB subgraph "Lambda Architecture" DS["Data Source
(Kafka, Kinesis)"] subgraph "Batch Layer (Accuracy)" BL["Batch Processing
(Spark, Flink batch)"] BV["Batch Views
(HDFS, S3)
Complete, accurate
Updated hourly/daily"] end subgraph "Speed Layer (Latency)" SL["Stream Processing
(Flink, Storm)"] SV["Speed Views
(Redis, Cassandra)
Approximate, fast
Real-time"] end subgraph "Serving Layer" SERVE["Merge batch + speed views
(query-time join)"] API["Query API
(return combined result)"] end end DS --> BL --> BV DS --> SL --> SV BV --> SERVE SV --> SERVE SERVE --> API style BV fill:#4ae290 style SV fill:#e24a90 style SERVE fill:#e2c04a

Kappa Architecture

graph TB subgraph "Kappa Architecture (Stream-Only)" DS["Data Source
(Kafka topics)"] SP1["Stream Processing Job v1
(Flink, Kafka Streams)"] SP2["Stream Processing Job v2
(new logic, parallel)"] KS["Kafka: Replayable Log
(retention: weeks/months)
Source of truth"] STORE["Storage
(ClickHouse, Druid)
Single view
Real-time + historical"] API["Query API"] end DS --> KS KS --> SP1 --> STORE KS --> SP2 SP2 -.->|When caught up,
switch over| STORE STORE --> API REPLAY["Reprocess: Deploy v2,
replay Kafka from offset 0,
rebuild state"] KS --> REPLAY style KS fill:#4ae290 style STORE fill:#e2c04a style REPLAY fill:#e24a90

Lambda vs Kappa Comparison

Aspect Lambda Architecture Kappa Architecture
Complexity High (two codebases: batch + stream) Low (single stream processing codebase)
Latency Speed layer: seconds, Batch layer: hours Seconds (stream processing only)
Accuracy Batch layer provides eventual accuracy Single view, accuracy depends on stream processing correctness
Reprocessing Easy (re-run batch job on historical data) Requires replayable log (Kafka), redeploy stream job
Storage Two views: batch (HDFS/S3) + speed (NoSQL) Single view (OLAP database or stream store)
Query Merge batch + speed views at query time Query single view directly
Use Cases ML training (batch) + real-time inference, data lake + live dashboard Event analytics, monitoring, real-time aggregations
Examples Hadoop (batch) + Storm (stream), Spark batch + Flink stream Kafka + Flink/KStreams -> ClickHouse, Kafka + Druid

Modern Trend: Unified Batch + Stream

Modern frameworks like Apache Flink and Spark Structured Streaming support both batch and streaming with the same API, reducing Lambda's complexity.

// Flink: Unified batch + stream processing
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// Stream: unbounded source (Kafka)
DataStream<Event> stream = env.addSource(new FlinkKafkaConsumer<>(...));

// Batch: bounded source (S3 files)
DataStream<Event> batch = env.readFile(new TextInputFormat(...), "s3://...");

// Same processing logic for both
stream.keyBy(e -> e.getUserId())
      .window(TumblingEventTimeWindows.of(Time.hours(1)))
      .aggregate(new CountAggregator())
      .addSink(new ClickHouseSink(...));

// Framework handles watermarks, state, exactly-once for both
Interview Tip: For real-time analytics, prefer Kappa (simpler) unless batch reprocessing is critical (ML training). Mention Kafka as replayable log (source of truth), Flink for stream processing, ClickHouse/Druid for serving. Lambda is legacy; modern systems use unified batch/stream frameworks.

15.10 Decision Framework: Choosing the Right System

No single database fits all analytical needs. Choose based on latency requirements, data volume, query complexity, and operational constraints.

Decision Flowchart

graph TD START["Analytical Workload"] Q1{"Latency requirement?"} Q2{"Data volume?"} Q3{"Query complexity?"} Q4{"Operational preference?"} Q5{"Time-series data?"} Q6{"Real-time ingestion?"} Q7{"High concurrency?"} START --> Q1 Q1 -->|"< 100ms
(user-facing)"| Q7 Q1 -->|"< 1s
(dashboard)"| Q5 Q1 -->|"< 10s
(ad-hoc)"| Q2 Q1 -->|"> 10s OK
(batch)"| Q2 Q7 -->|"Yes
(1000+ QPS)"| PINOT["Apache Pinot
or Druid"] Q7 -->|"No"| Q5 Q5 -->|"Yes
(metrics, logs)"| TSDB["Time-Series DB
InfluxDB, TimescaleDB
Prometheus"] Q5 -->|"No"| Q6 Q6 -->|"Yes
(streaming)"| CLICK["ClickHouse
or Druid"] Q6 -->|"No"| Q2 Q2 -->|"< 1TB"| PG["PostgreSQL
(with columnar extension)"] Q2 -->|"1TB - 100TB"| Q4 Q2 -->|"> 100TB"| CLOUD["Cloud Warehouse
BigQuery, Snowflake"] Q4 -->|"Self-hosted"| Q3 Q4 -->|"Managed/Serverless"| CLOUD Q3 -->|"Simple aggregations"| CLICK Q3 -->|"Complex JOINs"| SNOW["Snowflake
or Redshift"] Q3 -->|"ML integration"| BQ["BigQuery"] style PINOT fill:#4ae290 style TSDB fill:#e2c04a style CLICK fill:#e24a90 style PG fill:#4a90e2 style CLOUD fill:#c04ae2 style SNOW fill:#e2904a style BQ fill:#90e24a

Use Case Recommendations

Use Case Recommended System Rationale
Application metrics (DevOps monitoring) Prometheus + VictoriaMetrics Pull model, service discovery, Grafana integration, alerting
IoT sensor data (millions of devices) InfluxDB or TimescaleDB High write throughput, downsampling, retention policies
Event analytics (clickstream, logs) ClickHouse Fast ingestion, real-time queries, SQL, cost-effective
User-facing dashboards (product analytics) Apache Pinot or Druid Low latency (<100ms), high QPS, pre-aggregated metrics
Ad-hoc queries (data science, BI) BigQuery or Snowflake Zero ops, petabyte scale, integration with BI tools (Tableau, Looker)
Real-time fraud detection Kafka + Flink + ClickHouse Stream processing with complex event patterns, fast writes/queries
Financial reporting (compliance, auditing) Snowflake or Redshift ACID transactions (for updates), time travel, cloning, mature
Log aggregation (centralized logging) ClickHouse or Elasticsearch ClickHouse: cheaper, faster aggregations. ES: full-text search
Data lake analytics (S3/Parquet) BigQuery (external tables) or Athena Query S3 directly without loading, pay per query
High-frequency trading (market data) QuestDB Microsecond latency, zero-GC, specialized for time-series

Cost Considerations

  • Self-Hosted (ClickHouse, TimescaleDB): Lower cost at scale (pay for compute/storage), but operational burden (backups, upgrades, monitoring)
  • Serverless (BigQuery, Athena): Pay per query (good for infrequent queries), zero ops, but unpredictable costs for high QPS
  • Provisioned (Snowflake, Redshift): Pay for uptime (even idle), predictable costs, but waste if underutilized
  • Managed SaaS (Databricks, ClickHouse Cloud): Middle ground (ops handled, but not serverless), typically 2-3x self-hosted cost

Migration Strategy

graph LR subgraph "Phase 1: Proof of Concept" P1["Start with small dataset
(1 week of data)
Evaluate query performance"] end subgraph "Phase 2: Dual Write" P2["Write to OLTP + OLAP
(or CDC with Debezium)
Validate data consistency"] end subgraph "Phase 3: Shadow Traffic" P3["Route read-only queries to OLAP
Compare results with OLTP
Measure latency/accuracy"] end subgraph "Phase 4: Cutover" P4["Migrate dashboards/reports to OLAP
Keep OLTP for transactions
ETL historical data"] end P1 --> P2 --> P3 --> P4 style P2 fill:#4ae290 style P3 fill:#e2c04a style P4 fill:#e24a90
Interview Tip: When asked "design an analytics system", clarify: latency needs, data volume, query types, real-time vs batch. Propose OLTP + OLAP separation. Suggest ETL/CDC tools (Fivetran, Airbyte, Debezium), transformation (dbt), and orchestration (Airflow). For real-time, add Kafka + stream processing. Always mention monitoring (data quality, SLAs).

Summary

Time-series and OLAP databases optimize for analytical workloads with specialized storage, compression, and query execution. Key takeaways:

  • Separate OLTP and OLAP: Different workloads need different storage (row vs columnar)
  • Columnar storage: Reads only needed columns, compresses better, enables vectorized execution
  • Compression: RLE, dictionary, bitmap, delta, Gorilla achieve 10-100x reduction
  • Time-Series DBs: Specialized for metrics/logs with retention policies and downsampling
  • OLAP Systems: ClickHouse (fast, self-hosted), BigQuery/Snowflake (serverless), Druid/Pinot (real-time)
  • Star Schema: Fact table (measures) + dimension tables (attributes), use SCD Type 2 for history
  • Materialized Views: Pre-compute aggregations, trade staleness for speed
  • Approximate Queries: HyperLogLog (COUNT DISTINCT), T-Digest (percentiles), Count-Min Sketch (top-K)
  • Real-Time Architectures: Kappa (stream-only) simpler than Lambda (batch + stream)
  • Choose Based on Needs: Latency, volume, complexity, ops preference