🏠 CLICKHOUSE
HOME

🧠 ClickHouse Fundamentals

🎯 What is ClickHouse?

ClickHouse is an open-source, column-oriented OLAP database management system designed for real-time analytics. Originally developed by Yandex in 2016 to power Yandex.Metrica (the world's second-largest web analytics platform), it processes billions of rows per second with sub-second query latency and full SQL support. It's known for being one of the fastest analytical databases in the world.

Why ClickHouse Matters

Speed: Billions of rows/second. Columnar: Optimized for analytics. SQL: Full SQL support. Scale: Petabytes. Simple: Single binary. Real-time: Sub-second queries.

πŸ“Œ Columnar OLAPπŸ“Œ Sub-Second QueriesπŸ“Œ Full SQLπŸ“Œ Real-Time
# ClickHouse β€” the big picture # The fastest open-source columnar OLAP database. # CONCEPTUAL OVERVIEW print(""" === ClickHouse === What it is: - Column-oriented OLAP database - Real-time analytics - Full SQL support - Vectorized execution - Single binary - Open source (Apache 2.0) Created by: - Yandex (2016) - Yandex.Metrica - Open source (2016) - ClickHouse Inc. (2021) Key features: - MergeTree engine - Vectorized execution - Columnar storage - Compression - Materialized views - Distributed tables - Replication - Real-time ingestion What it's NOT: - Not OLTP - Not for high-frequency updates - Not a general-purpose RDBMS - Not for small data What it IS: - Real-time analytics - Log analysis - Observability - Clickstream - Business intelligence - Time-series """) # WHY IT MATTERS print("=== Why It Matters ===") print(""" Challenges: - Billions of rows - Real-time analytics - Sub-second queries - Full SQL needed - Simple operations - Cost-effective ClickHouse solves: - Blazing speed - Full SQL - Simple ops - Compression - Scalability - Open source """) # REAL-WORLD print("=== Real-World Scale ===") print(""" Companies using ClickHouse: - Yandex (original) - Uber - Cloudflare - eBay - Cisco - Spotify - Netflix - Alibaba Scale examples: - Yandex.Metrica: trillions of rows - Cloudflare: 20M+ requests/sec - Uber: petabyte-scale logs - eBay: 100B+ rows """)

πŸ—οΈ Architecture

🎯 Architecture

ClickHouse uses a shared-nothing, MPP (Massively Parallel Processing) architecture. Each node is independent; data is distributed across shards, and queries are parallelized across nodes.

Architecture

Single binary: No dependencies. Shared-nothing: Independent nodes. MPP: Parallel queries. Sharding: Data distributed. Replication: Replica per shard. ZooKeeper: Coordination (optional).

πŸ—οΈ Shared-NothingπŸ—οΈ MPPπŸ—οΈ ShardingπŸ—οΈ Replication
# ClickHouse Architecture print("=== Architecture Diagram ===") print(""" β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Clients β”‚ β”‚ (SQL, HTTP, JDBC, ClickHouse CLI) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Distributed Table β”‚ β”‚ (Logical view over shards) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Shard 1 β”‚ β”‚ Shard 2 β”‚ β”‚ Shard 3 β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚Rep 1β”‚ β”‚ β”‚ β”‚Rep 1β”‚ β”‚ β”‚ β”‚Rep 1β”‚ β”‚ β”‚ β”‚Rep 2β”‚ β”‚ β”‚ β”‚Rep 2β”‚ β”‚ β”‚ β”‚Rep 2β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ZooKeeper / ClickHouse Keeper β”‚ β”‚ (Coordination, optional) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ """) print("\n=== Components ===") print(""" 1. SERVER - Single binary - No dependencies - Multi-threaded - Vectorized execution 2. SHARD - Horizontal partition - Subset of data - Independent - Scaled out 3. REPLICA - Copy of shard - High availability - Read scaling - ReplicatedMergeTree 4. DISTRIBUTED TABLE - Logical view - Spans shards - Query fan-out - Result merge 5. ZOOKEEPER / KEEPER - Optional - Coordination - Replication metadata - ClickHouse Keeper (Raft) 6. CLIENT - clickhouse-client - HTTP interface - JDBC/ODBC - Native protocol """) print("\n=== Single Node vs Cluster ===") print(""" Single Node: - Dev, small workloads - Simple setup - Single binary - No coordination Cluster: - Production - Sharding + replication - MPP parallelism - ZooKeeper/Keeper - Distributed tables """) print("\n=== Configuration ===") print(""" # config.xml information /var/log/clickhouse-server/clickhouse-server.log 0.0.0.0 8123 9000 /var/lib/clickhouse/ users.xml """) print("\n=== Cluster Config ===") print(""" ch19000 ch29000 ch39000 ch49000 """)

🌲 MergeTree

🎯 The Core Engine

MergeTree is ClickHouse's flagship table engine. Data is stored in sorted parts that are asynchronously merged in the background. It's the foundation for most ClickHouse tables.

MergeTree

Sorted: By primary key. Parts: Immutable chunks. Merges: Background. Index: Sparse primary. Compression: Columnar. TTL: Data lifecycle.

🌲 Sorted🌲 Parts🌲 Merges🌲 Sparse Index
# MergeTree print("=== What is MergeTree? ===") print(""" MergeTree: - Primary table engine - Data stored in parts - Parts sorted by primary key - Async background merges - Sparse primary index - Columnar storage - Compression Key concepts: - Part: Immutable file - Granule: 8192 rows (default) - Primary key: Sort order - ORDER BY: Required - PARTITION BY: Optional - TTL: Data expiration Properties: - Fast range scans - Fast aggregations - High insert throughput - Background merges - Data immutability """) print("\n=== Basic Example ===") print(""" CREATE TABLE events ( timestamp DateTime, user_id UInt64, event_type String, amount Float64 ) ENGINE = MergeTree() PARTITION BY toYYYYMM(timestamp) ORDER BY (timestamp, user_id) TTL timestamp + INTERVAL 90 DAY SETTINGS index_granularity = 8192; """) print("\n=== Part Structure ===") print(""" Part: - Immutable directory - Data columns (bin) - Primary index (idx) - Marks (mrk) - Metadata (txt) - Checksums Naming: partition_min_block_max_block_level Example: 202401_1_1_0/ β”œβ”€β”€ timestamp.bin β”œβ”€β”€ timestamp.mrk β”œβ”€β”€ user_id.bin β”œβ”€β”€ user_id.mrk β”œβ”€β”€ event_type.bin β”œβ”€β”€ event_type.mrk β”œβ”€β”€ amount.bin β”œβ”€β”€ amount.mrk β”œβ”€β”€ primary.idx β”œβ”€β”€ count.txt └── checksums.txt """) print("\n=== Primary Key ===") print(""" Primary key: - Sort order of data - Sparse index (per granule) - Not unique (unlike RDBMS) - Prefix of ORDER BY Example: ORDER BY (timestamp, user_id) Primary key: (timestamp, user_id) Query: WHERE timestamp = '2024-01-01' β†’ Uses primary index β†’ Fast Query: WHERE user_id = 123 β†’ No primary index β†’ Full scan Rule: - Put most-filtered columns first - Low cardinality? Maybe partition - High cardinality? Consider """) print("\n=== Granules ===") print(""" Granule: - 8192 rows (default) - Unit of index - Unit of read - Configurable index_granularity: - Default: 8192 - Rows per index entry - Trade-off Smaller granules: - More index - More memory - Faster lookup Larger granules: - Less index - Less memory - Slower lookup """) print("\n=== Merges ===") print(""" Background merges: - Combine small parts - Maintain sort order - Merge indexes - Free resources Merge: - Automatic - Background - Configurable - Non-blocking Benefits: - Fewer parts - Faster queries - Compressed Trigger: - Parts count - Time interval - Manual OPTIMIZE """) print("\n=== TTL ===") print(""" TTL: - Time-to-live - Auto-expire data - Column-level or table-level - Efficient Example: TTL timestamp + INTERVAL 90 DAY Column-level: TTL timestamp + INTERVAL 30 DAY DELETE, timestamp + INTERVAL 7 DAY TO DISK 'cold' Actions: - DELETE - TO DISK - TO VOLUME - GROUP BY (aggregate) """)

πŸ”§ Table Engines

🎯 Engines for Every Use Case

ClickHouse has many table engines. MergeTree family is the core; others serve specific purposes like aggregation, replication, or distributed queries.

Engines

MergeTree: Core. Replacing: Dedup. Summing: Pre-agg. Aggregating: Merge states. Distributed: Cluster. Kafka: Streaming.

πŸ”§ MergeTreeπŸ”§ ReplacingπŸ”§ SummingπŸ”§ Distributed
# Table Engines print("=== MergeTree Family ===") print(""" 1. MergeTree - Base engine - No dedup - Raw data 2. ReplacingMergeTree - Dedup by ORDER BY key - Keeps latest - Eventual (on merge) ENGINE = ReplacingMergeTree(version) 3. SummingMergeTree - Sum numeric columns - By ORDER BY key - Eventual (on merge) ENGINE = SummingMergeTree((amount)) 4. AggregatingMergeTree - Merge aggregate states - For materialized views ENGINE = AggregatingMergeTree() 5. CollapsingMergeTree - Collapse sign (+1/-1) - For updates ENGINE = CollapsingMergeTree(sign) 6. VersionedCollapsingMergeTree - Order-independent - Uses version column """) print("\n=== Other Engines ===") print(""" Distributed: - Distributed table - Spans shards - Query fan-out Kafka: - Streaming ingestion - From Kafka topics - No storage itself Buffer: - In-memory buffer - Batch inserts Memory: - In-memory storage - Fast, volatile Log: - Simple append-only - For small tables Null: - Discard data - For testing File: - Read external files - CSV, Parquet URL: - Read from URL - HTTP Join: - Pre-joined data - Fast lookups Dictionary: - External dictionaries - Fast key-value View: - Virtual table - No storage MaterializedView: - Pre-computed - Triggered on insert """) print("\n=== Example: ReplacingMergeTree ===") print(""" -- Create CREATE TABLE users ( user_id UInt64, name String, email String, updated_at DateTime ) ENGINE = ReplacingMergeTree(updated_at) ORDER BY user_id; -- Insert INSERT INTO users VALUES (1, 'Alice', 'a@x.com', now()); INSERT INTO users VALUES (1, 'Alice Smith', 'a@x.com', now()); -- Query (may show duplicates until merge) SELECT * FROM users; -- Force merge OPTIMIZE TABLE users FINAL; -- Query (deduplicated) SELECT * FROM users; -- Query with FINAL (dedup on read) SELECT * FROM users FINAL; """) print("\n=== Example: SummingMergeTree ===") print(""" -- Create CREATE TABLE daily_stats ( date Date, user_id UInt64, pageviews UInt64, revenue Float64 ) ENGINE = SummingMergeTree((pageviews, revenue)) ORDER BY (date, user_id); -- Insert INSERT INTO daily_stats VALUES ('2024-01-01', 1, 10, 100.5); INSERT INTO daily_stats VALUES ('2024-01-01', 1, 5, 50.25); -- Force merge OPTIMIZE TABLE daily_stats FINAL; -- Query (summed) SELECT * FROM daily_stats; -- 2024-01-01, 1, 15, 150.75 """) print("\n=== Engine Comparison ===") print(f"{'Engine':<30} {'Purpose':<35}") print("-" * 70) print(f"{'MergeTree':<30} {'Raw data, sorted':<35}") print(f"{'ReplacingMergeTree':<30} {'Dedup by key':<35}") print(f"{'SummingMergeTree':<30} {'Sum by key':<35}") print(f"{'AggregatingMergeTree':<30} {'Aggregate states':<35}") print(f"{'CollapsingMergeTree':<30} {'Update patterns':<35}") print(f"{'Distributed':<30} {'Span shards':<35}") print(f"{'Kafka':<30} {'Streaming ingestion':<35}") print(f"{'Buffer':<30} {'Batch inserts':<35}") print(f"{'Memory':<30} {'Fast in-memory':<35}") print(f"{'Join':<30} {'Pre-joined lookups':<35}")

πŸ“Š Data Types

🎯 Rich Type System

ClickHouse has a rich type system optimized for analytics: numeric types, strings, dates, arrays, maps, tuples, and specialized types like AggregateFunction for materialized views.

Data Types

Numeric: UInt8-256, Int8-256, Float32/64. String: Variable length. FixedString: Fixed. Date/Time: Date, DateTime. Array: Lists. Map: Key-value.

πŸ“Š NumericπŸ“Š StringπŸ“Š Date/TimeπŸ“Š Array
# Data Types print("=== Numeric Types ===") print(""" Unsigned integers: UInt8, UInt16, UInt32, UInt64, UInt128, UInt256 Signed integers: Int8, Int16, Int32, Int64, Int128, Int256 Floats: Float32, Float64 Decimals: Decimal(P, S) β€” precision, scale Special: Bool (alias for UInt8) """) print("\n=== String Types ===") print(""" String: - Variable length - UTF-8 - Any bytes FixedString(N): - Fixed length N - Padded with null bytes Example: name String, uuid FixedString(16) """) print("\n=== Date/Time Types ===") print(""" Date: - 2 bytes - Days since 1970 - Range: 1970-2149 Date32: - 4 bytes - Wider range DateTime: - 4 bytes - Seconds since 1970 - Timezone configurable DateTime64: - 8 bytes - Sub-second precision - Configurable precision Example: timestamp DateTime('UTC'), precise DateTime64(3, 'UTC') """) print("\n=== Complex Types ===") print(""" Array(T): - Lists of type T - Any type tags Array(String) Map(K, V): - Key-value pairs attributes Map(String, String) Tuple(T1, T2, ...): - Fixed-size tuples coordinates Tuple(Float64, Float64) Nested: - Special array of structs Nested( name String, value Float64 ) """) print("\n=== Special Types ===") print(""" Nullable(T): - Adds NULL support - Performance cost - Use sparingly LowCardinality(T): - Dictionary encoding - For low cardinality strings - Big performance gain AggregateFunction: - Aggregate states - For materialized views - Type-specific SimpleAggregateFunction: - Simple aggregates - Merged on read Enum: - Enumerated values Enum('a' = 1, 'b' = 2) UUID: - 16 bytes - Unique identifiers """) print("\n=== Example ===") print(""" CREATE TABLE events ( timestamp DateTime('UTC'), user_id UInt64, session_id UUID, event_type LowCardinality(String), country LowCardinality(String), tags Array(String), metadata Map(String, String), amount Decimal(18, 2), is_mobile Bool, score Nullable(Float64) ) ENGINE = MergeTree() ORDER BY (timestamp, user_id); """) print("\n=== Type Comparison ===") print(f"{'Type':<25} {'Size':<15} {'Use Case':<30}") print("-" * 70) print(f"{'UInt64':<25} {'8 bytes':<15} {'IDs, counters':<30}") print(f"{'Int32':<25} {'4 bytes':<15} {'Signed numbers':<30}") print(f"{'Float64':<25} {'8 bytes':<15} {'Measurements':<30}") print(f"{'Decimal':<25} {'varies':<15} {'Money':<30}") print(f"{'String':<25} {'varies':<15} {'Text':<30}") print(f"{'Date':<25} {'2 bytes':<15} {'Dates':<30}") print(f"{'DateTime':<25} {'4 bytes':<15} {'Timestamps':<30}") print(f"{'Array':<25} {'varies':<15} {'Lists':<30}") print(f"{'Map':<25} {'varies':<15} {'Key-value':<30}") print(f"{'LowCardinality':<25} {'varies':<15} {'Enums, categories':<30}")

πŸ” SQL Querying

🎯 Full SQL, Extended

ClickHouse supports full SQL with extensions: array functions, higher-order functions, window functions, and specialized aggregations. It's ANSI SQL-compatible but adds powerful analytic extensions.

SQL Querying

SELECT: Full. JOIN: All types. GROUP BY: Extended. Window: Full. Arrays: Powerful. Approximate: Fast.

πŸ” SELECTπŸ” JOINπŸ” WindowπŸ” Arrays
# SQL Querying print("=== Basic SELECT ===") print(""" SELECT * FROM events LIMIT 10; SELECT event_type, COUNT(*) as count, SUM(amount) as revenue FROM events WHERE timestamp >= '2024-01-01' GROUP BY event_type ORDER BY count DESC LIMIT 10; """) print("\n=== Advanced Aggregations ===") print(""" -- Approximate distinct SELECT uniq(user_id) FROM events; -- Approximate quantile SELECT quantile(0.95)(latency) FROM events; -- Quantiles SELECT quantiles(0.5, 0.9, 0.99)(latency) FROM events; -- Top K SELECT topK(10)(page) FROM events; -- Histogram SELECT histogram(10)(latency) FROM events; -- Group by with filters SELECT event_type, countIf(amount > 100) as high_value, sumIf(amount, country = 'US') as us_revenue FROM events GROUP BY event_type; """) print("\n=== Array Functions ===") print(""" -- Array column SELECT tags FROM events; -- Array length SELECT length(tags) FROM events; -- Array contains SELECT has(tags, 'important') FROM events; -- Array filter SELECT arrayFilter(x -> x > 10, values) FROM events; -- Array map SELECT arrayMap(x -> x * 2, values) FROM events; -- Array join (explode) SELECT event_id, tag FROM events ARRAY JOIN tags AS tag; -- Group array SELECT user_id, groupArray(event_type) as events FROM events GROUP BY user_id; """) print("\n=== Higher-Order Functions ===") print(""" -- Lambda functions SELECT arrayMap(x -> x * 2, [1, 2, 3]); -- [2, 4, 6] SELECT arrayFilter(x -> x > 2, [1, 2, 3, 4]); -- [3, 4] SELECT arrayReduce('sum', [1, 2, 3]); -- 6 -- Complex SELECT user_id, arraySum(arrayMap(x -> x * price, items)) as total FROM orders; """) print("\n=== Window Functions ===") print(""" SELECT user_id, timestamp, amount, SUM(amount) OVER ( PARTITION BY user_id ORDER BY timestamp ) as running_total, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY timestamp ) as row_num FROM events; """) print("\n=== JOINs ===") print(""" -- All standard JOINs SELECT * FROM a JOIN b ON a.id = b.id; SELECT * FROM a LEFT JOIN b ON a.id = b.id; SELECT * FROM a RIGHT JOIN b ON a.id = b.id; SELECT * FROM a FULL JOIN b ON a.id = b.id; -- ASOF JOIN (time-series) SELECT * FROM trades t ASOF JOIN quotes q ON t.symbol = q.symbol AND t.time >= q.time; -- Cross join SELECT * FROM a CROSS JOIN b; """) print("\n=== EXPLAIN ===") print(""" EXPLAIN SELECT * FROM events WHERE user_id = 123; -- Types: EXPLAIN AST ... EXPLAIN SYNTAX ... EXPLAIN PLAN ... EXPLAIN PIPELINE ... EXPLAIN ESTIMATE ... -- Example output: Expression Aggregating Expression Filter ReadFromMergeTree """) print("\n=== Query Optimization ===") print(""" 1. Use PREWHERE for filter 2. Use PARTITION BY for time 3. ORDER BY for primary key 4. Approximate algorithms 5. Use FINAL sparingly 6. Avoid SELECT * 7. Use LowCardinality 8. Sample data """)

πŸ“¦ Materialized Views

🎯 Pre-Computed Aggregations

ClickHouse materialized views are insert triggers that transform data as it's written. Unlike traditional MVs, they only process new data β€” they're incremental by design.

Materialized Views

Insert trigger: Fires on insert. Incremental: New data only. Target table: Stores results. Aggregating: Use AggregatingMergeTree. Fast: Pre-computed.

πŸ“¦ Insert TriggerπŸ“¦ IncrementalπŸ“¦ AggregatingπŸ“¦ Fast
# Materialized Views print("=== What is a Materialized View? ===") print(""" Materialized View (ClickHouse): - Insert trigger - Transforms data on insert - Stores in target table - Incremental (new data only) - NOT a snapshot Unlike traditional MVs: - Not refreshed - Not a cache - Just an insert pipeline - Data flows through """) print("\n=== Basic Example ===") print(""" -- Source table CREATE TABLE events ( timestamp DateTime, user_id UInt64, event_type String, amount Float64 ) ENGINE = MergeTree() ORDER BY timestamp; -- Target table CREATE TABLE events_by_type ( event_type String, count UInt64, revenue Float64 ) ENGINE = SummingMergeTree((count, revenue)) ORDER BY event_type; -- Materialized view CREATE MATERIALIZED VIEW mv_events_by_type TO events_by_type AS SELECT event_type, count() as count, sum(amount) as revenue FROM events GROUP BY event_type; -- Insert into source INSERT INTO events VALUES (now(), 1, 'click', 10), (now(), 2, 'view', 0), (now(), 3, 'click', 20); -- Query target SELECT * FROM events_by_type; """) print("\n=== AggregatingMergeTree Pattern ===") print(""" -- Source CREATE TABLE raw_events (...) ENGINE = MergeTree() ORDER BY timestamp; -- Aggregating target CREATE TABLE agg_events ( date Date, event_type String, unique_users AggregateFunction(uniq, UInt64), total_amount AggregateFunction(sum, Float64) ) ENGINE = AggregatingMergeTree() ORDER BY (date, event_type); -- Materialized view CREATE MATERIALIZED VIEW mv_agg TO agg_events AS SELECT toDate(timestamp) as date, event_type, uniqState(user_id) as unique_users, sumState(amount) as total_amount FROM raw_events GROUP BY date, event_type; -- Query (merge states) SELECT date, event_type, uniqMerge(unique_users) as users, sumMerge(total_amount) as revenue FROM agg_events GROUP BY date, event_type; """) print("\n=== Live Updates ===") print(""" Materialized views are incremental: - Only process NEW inserts - Don't reprocess old data - Fast and cheap For real-time dashboards: - Query MV + raw - MV: historical - Raw: recent - UNION Example: SELECT * FROM agg_events UNION ALL SELECT toDate(timestamp), event_type, uniq(user_id), sum(amount) FROM raw_events WHERE timestamp > (SELECT max(date) FROM agg_events) GROUP BY 1, 2; """) print("\n=== Refreshable MVs ===") print(""" New feature (2024): - REFRESH EVERY interval - Full refresh - Or APPEND Example: CREATE MATERIALIZED VIEW mv REFRESH EVERY 1 HOUR AS SELECT ...; CREATE MATERIALIZED VIEW mv REFRESH EVERY 1 HOUR APPEND AS SELECT ...; """) print("\n=== Best Practices ===") print(""" 1. Use AggregatingMergeTree - For aggregations - State functions 2. Use LowCardinality - Dimension columns 3. Partition target - By date usually 4. Order by group keys - Query performance 5. Don't reprocess - Insert-only - Plan for backfill 6. Monitor - MV lag - Target size 7. Test - Insert data - Verify target """)

πŸ“₯ Ingestion

🎯 Getting Data In

ClickHouse supports many ingestion methods: SQL inserts, HTTP, Kafka, S3, batch files, and streaming. It's optimized for high-throughput inserts in large batches.

Ingestion

INSERT: SQL. HTTP: API. Kafka: Streaming. S3: Batch. File: CSV, Parquet. Buffer: Batching.

πŸ“₯ INSERTπŸ“₯ KafkaπŸ“₯ S3πŸ“₯ HTTP
# Ingestion print("=== INSERT (SQL) ===") print(""" -- Values INSERT INTO events VALUES (now(), 1, 'click', 10), (now(), 2, 'view', 0); -- From SELECT INSERT INTO events SELECT * FROM source_table; -- From file INSERT INTO events SELECT * FROM file('data.csv', 'CSV'); -- From S3 INSERT INTO events SELECT * FROM s3('s3://bucket/data/*.parquet', 'Parquet'); -- From URL INSERT INTO events SELECT * FROM url('http://example.com/data.csv'); """) print("\n=== HTTP Interface ===") print(""" # Insert via HTTP curl -X POST 'http://localhost:8123/' \\ --data-binary 'INSERT INTO events FORMAT JSONEachRow {"timestamp": "2024-01-01 00:00:00", "user_id": 1, "amount": 10}' # Query via HTTP curl 'http://localhost:8123/?query=SELECT%20count()%20FROM%20events' # With auth curl -u user:pass 'http://localhost:8123/?query=...' """) print("\n=== Kafka Engine ===") print(""" -- Kafka table CREATE TABLE events_queue ( timestamp DateTime, user_id UInt64, amount Float64 ) ENGINE = Kafka SETTINGS kafka_broker_list = 'localhost:9092', kafka_topic_list = 'events', kafka_group_name = 'clickhouse', kafka_format = 'JSONEachRow'; -- Materialized view to destination CREATE TABLE events (...) ENGINE = MergeTree() ORDER BY timestamp; CREATE MATERIALIZED VIEW events_mv TO events AS SELECT * FROM events_queue; """) print("\n=== S3 Ingestion ===") print(""" -- Read from S3 SELECT * FROM s3( 's3://bucket/data/*.parquet', 'access_key', 'secret_key', 'Parquet' ); -- Insert into table INSERT INTO events SELECT * FROM s3('s3://bucket/data/*.parquet', 'Parquet'); -- With schema SELECT * FROM s3( 's3://bucket/data/*.csv', 'CSV', 'timestamp DateTime, user_id UInt64, amount Float64' ); """) print("\n=== File Ingestion ===") print(""" -- CSV INSERT INTO events SELECT * FROM file('data.csv', 'CSV'); -- Parquet INSERT INTO events SELECT * FROM file('data.parquet', 'Parquet'); -- JSON INSERT INTO events SELECT * FROM file('data.json', 'JSONEachRow'); -- TSV INSERT INTO events SELECT * FROM file('data.tsv', 'TSV'); """) print("\n=== Bulk Insert Best Practices ===") print(""" 1. Batch inserts - 10K-100K rows per batch - Fewer parts - Faster 2. Avoid small inserts - Creates many parts - Background merges - Overhead 3. Use async_insert - ClickHouse batches - Configurable 4. Use Buffer engine - In-memory buffer - Flush to target 5. Parallel inserts - Multiple clients - Higher throughput 6. Monitor parts count - Alert on high - Prevent problems """) print("\n=== Async Inserts ===") print(""" -- Enable SET async_insert = 1; -- Configure SET async_insert_max_data_size = 10000000; -- 10MB SET async_insert_busy_timeout_ms = 200; -- Or per query INSERT INTO events SETTINGS async_insert = 1 VALUES (...); Benefits: - Auto batching - Fewer parts - Higher throughput """) print("\n=== Ingestion Comparison ===") print(f"{'Method':<20} {'Best For':<35}") print("-" * 55) print(f"{'INSERT':<20} {'Simple, ad-hoc':<35}") print(f"{'HTTP':<20} {'Apps, APIs':<35}") print(f"{'Kafka':<20} {'Streaming':<35}") print(f"{'S3':<20} {'Batch, data lake':<35}") print(f"{'File':<20} {'Local, small':<35}") print(f"{'Buffer':<20} {'High frequency':<35}")

πŸš€ Performance

🎯 Blazing Fast

ClickHouse's speed comes from vectorized execution, columnar storage, compression, and parallel processing. It processes billions of rows per second.

Performance

Vectorized: SIMD. Columnar: Less I/O. Compressed: Faster reads. Parallel: All cores. Sparse index: Fast lookup. Approximate: Faster.

πŸš€ VectorizedπŸš€ ColumnarπŸš€ ParallelπŸš€ Compression
# Performance print("=== Vectorized Execution ===") print(""" Vectorized: - Process data in batches - SIMD instructions - CPU cache friendly - 100x faster than row-by-row Example: Traditional: for row in rows: result = row.a + row.b Vectorized: result = vector_a + vector_b Modern CPU processes 4-8 values per instruction """) print("\n=== Columnar Storage ===") print(""" Columnar: - Column stored together - Only read needed columns - Better compression - Less I/O Query: SELECT avg(price) FROM sales; Row: read all columns Columnar: read only price I/O: 10x less """) print("\n=== Compression ===") print(""" Codecs: - LZ4 (default) - ZSTD (better) - Delta - DoubleDelta - Gorilla - T64 Example: timestamp DateTime CODEC(DoubleDelta) price Float64 CODEC(Gorilla) name String CODEC(ZSTD(3)) Savings: - 5-20x compression - Faster reads - Less storage """) print("\n=== Index Granules ===") print(""" index_granularity: - 8192 rows per granule - Sparse index - Fast lookups Smaller: - More index - Faster lookup - More memory Larger: - Less index - Slower lookup - Less memory Default: 8192 """) print("\n=== Query Optimization ===") print(""" 1. Use PREWHERE SELECT * FROM events PREWHERE user_id = 123 WHERE timestamp > '2024-01-01' PREWHERE reads fewer columns first β†’ Faster 2. Use partition pruning WHERE timestamp >= '2024-01-01' β†’ Skip old partitions 3. Use primary key ORDER BY (timestamp, user_id) WHERE timestamp = '2024-01-01' β†’ Fast 4. Use approximate uniq() instead of count(distinct) quantile() instead of exact β†’ 10x faster 5. Avoid FINAL SELECT * FROM table FINAL β†’ Slower Use ReplacingMergeTree carefully 6. Use materialized views β†’ Pre-computed """) print("\n=== Benchmark ===") print(""" ClickHouse benchmark: - 100M rows - Simple aggregation Results: - ClickHouse: 0.05s - PostgreSQL: 10s - MySQL: 30s - MongoDB: 100s Speedup: 200-2000x """) print("\n=== Scaling ===") print(""" Vertical: - More CPU - More RAM - Faster disk Horizontal: - Add shards - Add replicas - Distributed tables Example: 1 node: 1M rows/sec 10 nodes: 10M rows/sec 100 nodes: 100M rows/sec Linear scaling """) print("\n=== Resource Usage ===") print(f"{'Resource':<20} {'Usage':<35}") print("-" * 55) print(f"{'CPU':<20} {'All cores':<35}") print(f"{'RAM':<20} {'Query + cache':<35}") print(f"{'Disk':<20} {'Compressed data':<35}") print(f"{'Network':<20} {'Shuffle for joins':<35}")

🚒 Deployment

🎯 Running ClickHouse

ClickHouse can be deployed on a single node, a cluster, or managed services. It's simple to install (single binary) but needs planning for production.

Deployment

Single binary: Easy. Docker: Container. Cluster: Sharded. Cloud: Managed. Kubernetes: Operator. ClickHouse Cloud: Official.

🚒 Single🚒 Docker🚒 Cluster🚒 Cloud
# Deployment print("=== Single Node ===") print(""" Install: # Ubuntu/Debian sudo apt-get install clickhouse-server clickhouse-client # Or download binary curl https://clickhouse.com/ | sh ./clickhouse server # Docker docker run -d --name clickhouse \\ -p 8123:8123 -p 9000:9000 \\ clickhouse/clickhouse-server Config: /etc/clickhouse-server/config.xml /etc/clickhouse-server/users.xml Start: sudo systemctl start clickhouse-server clickhouse-client """) print("\n=== Docker Compose ===") print(""" version: '3.8' services: clickhouse: image: clickhouse/clickhouse-server:latest ports: - "8123:8123" - "9000:9000" volumes: - ./data:/var/lib/clickhouse - ./config:/etc/clickhouse-server/config.d ulimits: nofile: soft: 262144 hard: 262144 environment: CLICKHOUSE_USER: admin CLICKHOUSE_PASSWORD: secret """) print("\n=== Cluster Setup ===") print(""" 3-node cluster: - 2 shards - 2 replicas per shard - ZooKeeper/Keeper for coordination Config: ch19000 ch29000 ch39000 ch49000 01 ch1 zk12181 zk22181 zk32181 """) print("\n=== Distributed Tables ===") print(""" -- Local tables on each shard CREATE TABLE events_local ON CLUSTER prod_cluster ( timestamp DateTime, user_id UInt64, amount Float64 ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}') ORDER BY timestamp; -- Distributed table CREATE TABLE events ON CLUSTER prod_cluster AS events_local ENGINE = Distributed('prod_cluster', 'default', 'events_local', rand()); -- Query distributed table SELECT count() FROM events; -- Insert distributed INSERT INTO events VALUES (...); """) print("\n=== ClickHouse Cloud ===") print(""" ClickHouse Cloud: - Managed service - Auto-scaling - Serverless - Pay per use - AWS, GCP, Azure Features: - Automatic backups - Multi-AZ - Encryption - Monitoring - IAM integration Console: console.clickhouse.cloud """) print("\n=== Kubernetes ===") print(""" ClickHouse Operator: - Kubernetes-native - CRDs - Automated cluster Example: apiVersion: clickhouse.altinity.com/v1 kind: ClickHouseInstallation metadata: name: ch-cluster spec: configuration: clusters: - name: cluster layout: shardsCount: 2 replicasCount: 2 """) print("\n=== Sizing ===") print(f"{'Workload':<20} {'CPU':<10} {'RAM':<10} {'Disk':<15}") print("-" * 55) print(f"{'Dev':<20} {'2':<10} {'8 GB':<10} {'100 GB':<15}") print(f"{'Small':<20} {'8':<10} {'32 GB':<10} {'1 TB':<15}") print(f"{'Medium':<20} {'32':<10} {'128 GB':<10} {'10 TB':<15}") print(f"{'Large':<20} {'64+':<10} {'256+ GB':<10} {'100+ TB':<15}") print("\n=== Best Practices ===") print(""" 1. Use SSD for data 2. Separate data and log 3. Ample RAM 4. 10G+ network 5. Replication 6. Backup 7. Monitoring 8. Test failover """)

πŸ’Ό Use Cases

🎯 Where ClickHouse Excels

ClickHouse is ideal for real-time analytical workloads: log analytics, observability, clickstream, IoT, BI, and time-series data.

Use Cases

Logs: Log analytics. Observability: Metrics, traces. Clickstream: User behavior. IoT: Sensor data. BI: Analytics. Real-time: Dashboards.

πŸ’Ό LogsπŸ’Ό ObservabilityπŸ’Ό ClickstreamπŸ’Ό IoT
# Use Cases print("=== Log Analytics ===") print(""" Use case: - Application logs - Server logs - Security logs - Audit logs Why ClickHouse: - High ingestion rate - Fast queries - Full-text search (via tokens) - Compression Example: - Cloudflare: 20M+ req/sec - Uber: PB-scale logs - Netflix: streaming logs """) print("\n=== Observability ===") print(""" Use case: - Metrics - Traces - Logs - APM Why ClickHouse: - Time-series - Real-time - Aggregations - Fast queries Tools: - Grafana - Uber M3 - Signoz - Uptrace """) print("\n=== Clickstream Analytics ===") print(""" Use case: - User behavior - Page views - Funnels - Sessions Why ClickHouse: - High cardinality - Session analysis - Funnel queries - Fast Example: - Yandex.Metrica - eBay - Spotify """) print("\n=== IoT and Time-Series ===") print(""" Use case: - Sensor data - Device telemetry - Monitoring Why ClickHouse: - Time-series - Compression - Fast aggregations - TTL Example: - Smart meters - Industrial IoT - Fleet tracking """) print("\n=== Business Intelligence ===") print(""" Use case: - Dashboards - Reports - Analytics Why ClickHouse: - SQL - Fast - Connects to BI tools Tools: - Grafana - Metabase - Superset - Tableau """) print("\n=== Real-World Examples ===") print(""" 1. Yandex.Metrica - Trillions of rows - Millions of queries/day - Sub-second 2. Cloudflare - 20M+ requests/sec - Real-time dashboards - DDoS detection 3. Uber - PB-scale logs - Observability - Real-time analytics 4. eBay - 100B+ rows - Real-time monitoring - Business analytics 5. Netflix - Streaming logs - Observability - Analytics 6. Spotify - User analytics - Real-time metrics """) print("\n=== When to Use ClickHouse ===") print(""" USE CLICKHOUSE: βœ“ Real-time analytics βœ“ Log analysis βœ“ Observability βœ“ Clickstream βœ“ IoT / time-series βœ“ Business intelligence βœ“ Sub-second queries βœ“ Full SQL βœ“ Simple operations AVOID: βœ— OLTP (use Postgres) βœ— High-frequency updates (use ReplacingMergeTree) βœ— Full-text search (use Elasticsearch) βœ— Complex joins (limit) βœ— Small data (use SQLite) βœ— Key-value (use Redis) """)

βš–οΈ vs Alternatives

🎯 ClickHouse vs the Competition

ClickHouse competes with Druid, Pinot, Snowflake, BigQuery, and traditional warehouses. Each has trade-offs.

Comparison

ClickHouse: Batch speed, SQL. Druid: Streaming, concurrency. Pinot: User-facing. Snowflake: Managed. BigQuery: Serverless. Redshift: AWS.

βš–οΈ Druidβš–οΈ Pinotβš–οΈ Snowflakeβš–οΈ BigQuery
Aspect ClickHouse Druid Pinot
OriginYandexMetamarketsLinkedIn
ArchitectureMonolithicMicroservicesMicroservices
Primary UseBatch analyticsReal-time analyticsUser-facing
SQLFullPartialPartial
JoinsFullLimitedLimited
StreamingKafka (table engine)Kafka, Kinesis (native)Kafka (native)
RollupMaterialized viewsBuilt-in (default)Pre-aggregation
ConcurrencyHighVery highVery high
UpdatesAsync mutationsNoUpserts (CDC)
Ops ComplexityLowHighHigh
StorageLocal + S3Deep storageDeep storage
Best ForBatch speed, SQLReal-time, concurrencyUser-facing analytics
# ClickHouse vs Snowflake/BigQuery print("=== ClickHouse vs Snowflake ===") print(f"{'Aspect':<25} {'ClickHouse':<25} {'Snowflake':<25}") print("-" * 75) print(f"{'Type':<25} {'Self-managed':<25} {'Managed cloud':<25}") print(f"{'Cost':<25} {'Low (self-host)':<25} {'Pay-per-use':<25}") print(f"{'Setup':<25} {'Simple':<25} {'Easy':<25}") print(f"{'Performance':<25} {'Very fast':<25} {'Fast':<25}") print(f"{'SQL':<25} {'Full + extensions':<25} {'Full':<25}") print(f"{'Concurrency':<25} {'High':<25} {'High':<25}") print(f"{'Open source':<25} {'Yes':<25} {'No':<25}") print(f"{'Lock-in':<25} {'None':<25} {'High':<25}") print("\n=== ClickHouse vs BigQuery ===") print(f"{'Aspect':<25} {'ClickHouse':<25} {'BigQuery':<25}") print("-" * 75) print(f"{'Type':<25} {'Self-managed':<25} {'Serverless':<25}") print(f"{'Cost':<25} {'Fixed':<25} {'Per query':<25}") print(f"{'Setup':<25} {'You install':<25} {'No setup':<25}") print(f"{'Performance':<25} {'Very fast':<25} {'Very fast':<25}") print(f"{'SQL':<25} {'Full':<25} {'Full':<25}") print(f"{'Concurrency':<25} {'High':<25} {'Very high':<25}") print(f"{'Open source':<25} {'Yes':<25} {'No':<25}") print(f"{'Lock-in':<25} {'None':<25} {'High':<25}") print("\n=== When to Use Which ===") print(""" USE CLICKHOUSE: βœ“ Self-hosted βœ“ Cost-sensitive βœ“ Real-time analytics βœ“ Batch + streaming βœ“ Full SQL βœ“ No lock-in USE DRUID: βœ“ Real-time streaming βœ“ High concurrency βœ“ Rollup pre-aggregation βœ“ User-facing USE PINOT: βœ“ User-facing analytics βœ“ High concurrency βœ“ LinkedIn scale USE SNOWFLAKE/BIGQUERY: βœ“ Managed βœ“ Serverless βœ“ Enterprise βœ“ Multi-cloud """) print("\n=== Decision Flow ===") print(""" Start β”‚ β”œβ”€ Managed? ──Yes──→ Snowflake/BigQuery β”‚ β”‚ β”‚ No β”‚ ↓ β”œβ”€ Streaming? ──Yes──→ Druid β”‚ β”‚ β”‚ No β”‚ ↓ β”œβ”€ User-facing? ──Yes──→ Pinot β”‚ β”‚ β”‚ No β”‚ ↓ └─ Batch analytics? ──Yes──→ ClickHouse """)

❓ FAQ

🎯 Frequently Asked Questions

Answers to common questions about ClickHouse.

What is ClickHouse?

ClickHouse is an open-source, column-oriented OLAP database management system designed for real-time analytics. Originally developed by Yandex in 2016, it processes billions of rows per second with sub-second query latency and full SQL support.

What is MergeTree in ClickHouse?

MergeTree is ClickHouse's primary table engine family. Data is stored in sorted parts that are asynchronously merged in the background. MergeTree variants (ReplacingMergeTree, SummingMergeTree, AggregatingMergeTree) provide different merge behaviors for various use cases.

What is the difference between ClickHouse and Druid?

ClickHouse is a monolithic, full-SQL analytical database optimized for raw batch query speed. Druid is a microservices-based real-time analytics database optimized for streaming ingestion, high concurrency, and pre-aggregation (rollup). ClickHouse is simpler to operate and has broader SQL support; Druid excels at user-facing analytics with streaming data.

Does ClickHouse support updates and deletes?

Yes, but they are asynchronous and heavyweight. ClickHouse supports UPDATE and DELETE via mutations (ALTER TABLE ... UPDATE/DELETE), plus lightweight deletes (DELETE FROM). For high-frequency updates, use ReplacingMergeTree or CollapsingMergeTree patterns.

When should I use ClickHouse?

Use ClickHouse for real-time analytical workloads: log analytics, observability, clickstream analysis, IoT, business intelligence, and time-series data. It excels when you need sub-second query latency on billions of rows with full SQL support and simple operations.

Is ClickHouse free?

Yes. ClickHouse is open source under Apache 2.0. There's also ClickHouse Cloud (managed service) with a free trial, and enterprise support from ClickHouse Inc.

How does ClickHouse achieve such high performance?

ClickHouse uses vectorized execution (SIMD), columnar storage, compression (LZ4, ZSTD), sparse primary indexes, and parallel processing across all CPU cores. It's designed from scratch for analytical workloads.

What is the difference between ClickHouse and PostgreSQL?

PostgreSQL is a general-purpose relational database optimized for OLTP (transactions). ClickHouse is a specialized OLAP database optimized for analytics. PostgreSQL is better for transactions, updates, and complex joins; ClickHouse is 100-1000x faster for analytical queries on large datasets.

Does ClickHouse support JOINs?

Yes. ClickHouse supports all standard JOIN types (INNER, LEFT, RIGHT, FULL, CROSS) plus ASOF JOIN for time-series. For best performance, use smaller tables on the right side (broadcast) and consider dictionary or denormalization for high-cardinality joins.

What are ClickHouse materialized views?

ClickHouse materialized views are insert triggers that transform data as it's written. Unlike traditional MVs, they only process new data β€” they're incremental by design. Use AggregatingMergeTree for aggregations and combine MV with raw data for real-time dashboards.

# Quick Reference print("=== Quick Answers ===") print(""" Q: What is ClickHouse? A: Fastest open-source columnar OLAP database. Q: Free? A: Yes β€” Apache 2.0. Q: Sub-second queries? A: Yes β€” designed for it. Q: Streaming? A: Kafka, Kinesis (table engines). Q: Updates? A: Async mutations, ReplacingMergeTree. Q: Joins? A: Full SQL support. Q: SQL? A: Full β€” ANSI + extensions. Q: Alternative? A: Druid, Pinot, Snowflake, BigQuery. Q: Best for? A: Logs, observability, clickstream, BI. Q: Managed? A: ClickHouse Cloud. """)

πŸš€ Future

🎯 Where ClickHouse Is Going

The future is cloud-native, AI-augmented, serverless, and more integrated with the modern data stack.

Future Directions

Cloud: Managed. AI: Auto-optimization. Serverless: ClickHouse Cloud. Lakehouse: Iceberg. Real-time: Streaming. Open: Standards.

πŸš€ CloudπŸš€ AIπŸš€ ServerlessπŸš€ Lakehouse
# Future of ClickHouse print("=== 1. Cloud-First ===") print(""" Trend: ClickHouse Cloud Features: - Managed service - Auto-scaling - Serverless - Pay per use - Multi-cloud Benefits: - No ops - Elastic - Cost-effective Future: - Serverless default - Managed standard """) print("\n=== 2. AI-Augmented ===") print(""" Trend: AI in ClickHouse Features: - Query optimization - Auto-indexing - Anomaly detection - Natural language queries Future: - Self-tuning - Predictive - AI-native """) print("\n=== 3. Lakehouse Integration ===") print(""" Trend: ClickHouse + Lakehouse Features: - Iceberg tables - Delta Lake - Hudi - S3 native Future: - Read from lake - Write to lake - Unified analytics """) print("\n=== 4. Real-Time ===") print(""" Trend: Streaming-first Features: - Kafka - Kinesis - Pulsar - Async inserts - Real-time MVs Future: - Streaming-native - Real-time default """) print("\n=== 5. Performance ===") print(""" Trend: Even faster Features: - New codecs - Vectorization - SIMD - GPU (experimental) - Better compression Future: - 10x faster? - Lower cost - More efficient """) print("\n=== 6. Open Standards ===") print(""" Trend: Open wins Standards: - SQL - Iceberg - OpenTelemetry - OpenLineage Future: - Interoperability - No lock-in - Open ecosystem """) # TIMELINE print("\n=== Timeline ===") print(""" Past: 2016: ClickHouse open sourced 2017: Yandex.Metrica 2019: Growing adoption 2021: ClickHouse Inc. 2022: Cloud launch 2024: Mature Present: 2025: Rapid growth 2026: AI features Future: 2030: Serverless? 2035: AI-native? 2040: New paradigms? """) print("\n=== Final Thoughts ===") print(""" ClickHouse is: - Fastest OLAP database - Full SQL - Columnar - Open source - Easy to operate The future is: - Cloud-first - AI-augmented - Serverless - Lakehouse - Real-time "Blazing fast analytics at scale." Keep exploring. Keep building. """)