SQL Fundamentals for Senior ML Scientists
Prioritises reasoning over syntax. Know the why, defend the how.
1. WHAT IS A DATABASE?
A database is a file system that understands relationships and enforces consistency — not a folder of CSVs.
Organisation hierarchy:
| |
Why databases exist (ACID):
| Property | Meaning | Without It |
|---|---|---|
| Atomicity | All-or-nothing (no half-writes) | Partial pipeline loads silently corrupt data |
| Consistency | Data always in valid state | Broken constraints, silent bugs |
| Isolation | Concurrent queries don’t conflict | Two writers corrupt same row |
| Durability | Survives crashes | Last 24h of data gone on restart |
Structured vs Unstructured: SQL = rows, columns, schema, fast JOINs. NoSQL = flexible, no native JOINs, eventual consistency. You use SQL because features are relational, ACID matters, and JOINs beat Python merges.
2. TYPES OF SQL
| Type | Purpose | Commands | Your Context |
|---|---|---|---|
| DDL (Definition) | Define schema structure | CREATE, DROP, ALTER TABLE | Written by DevOps via Flyway. Auto-commits — no rollback. |
| DML (Manipulation) | Read/write data | SELECT, INSERT, UPDATE, DELETE | 99% of your work. Can rollback. |
| DCL (Control) | Manage permissions | GRANT, REVOKE | Why “permission denied” errors happen |
| TCL (Transaction) | Transaction lifecycle | BEGIN, COMMIT, ROLLBACK | Wraps ETL pipelines for atomicity |
3. OLTP vs OLAP: Why Vertica Exists
Row-store (OLTP — PostgreSQL): Stores data row-by-row. Fast for single-row lookups, slow for analytics — reads all 50 columns even when you need 3.
Column-store (OLAP — Vertica): Stores data column-by-column. Reads only the columns your query touches.
Why Vertica is 60× faster at 1B rows:
| Mechanism | What It Does | Speedup |
|---|---|---|
| Column storage | Read only needed columns | 8000× less I/O |
| Compression (RLE, dict, bit-pack) | Same-type columns compress 8× | Fits in cache |
| CPU cache-friendly | Sequential column reads = 95% cache hits vs 70% | 10× |
| Vectorisation (SIMD) | Process 10K rows/batch, not 1 row/cycle | 4–8× |
| Projections | Pre-materialised column subsets | 15–50× on hot queries |
| Parallelisation | Auto-distributed across CPUs | 4× on quad-core |
When to use which:
| Workload | Tool | Why |
|---|---|---|
| Transactions, real-time updates | OLTP (PostgreSQL) | Single-row access |
| Analytics, feature engineering, 1B rows | OLAP (Vertica, Snowflake) | Column-scan |
Vertica vs Snowflake: Same column-store model; Vertica is on-premise (fixed cost), Snowflake is cloud-native (pay per compute hour). SQL is 95% identical between them.
4. SCHEMA DESIGN: Why fact_, dim_, agg_
3NF (Normalisation): Eliminate redundancy by splitting tables. One source of truth.
Star Schema: One fact table (events/metrics) + dimension tables (attributes). One JOIN per dimension — fast, predictable.
Snowflake Schema: Like star, but dimensions are further normalised (dimensions join to sub-dimensions). More JOINs, less storage, slower reads.
Your Adform design (dsp.*, tpas.*, train.*):
| Table Type | Purpose | Example |
|---|---|---|
fact_* | Raw events (granular, append-only) | dsp.fact_impressions_full |
dim_* | Attributes (slowly changing) | dsp.dim_placements |
agg_* | Pre-computed aggregations (nightly) | agg_publisher_ctr |
Why separate schemas per domain: DSP team owns dsp.*, fraud team owns tpas.* — no coordination. Different retention policies (RTB = 90 days, fraud = 7 years). Different backup strategies per schema.
Why agg_* tables: Pre-compute once nightly, query 1000× fast. Without them, every training run re-aggregates 1B rows.
5. VIEWS VS TABLES
Core mental model:
- Table: Data physically stored on disk. Pay storage, get fast reads.
- View: Saved query definition. Computed on-the-fly — always fresh, no storage cost.
- Materialized view: Computed once and stored. Refreshed on schedule. Fast reads + acceptable staleness.
Decision:
| Scenario | Choice | Why |
|---|---|---|
| Fact/dimension data you own | Table | Store once, read 1000× |
| Simple filter, queried rarely | View | No storage, always accurate |
| Expensive aggregation, queried frequently | Materialized view | Pre-compute off-peak, read fast |
| Real-time bid data (updates every ms) | Table | Materialized views can’t keep pace |
Your agg_* tables = materialized views refreshed nightly by Flyway. Staleness acceptable (yesterday’s features fine for morning training).
| |
6. EXECUTION SEQUENCE
You write SQL in one order; it executes in a completely different order.
The key trap — WHERE vs HAVING:
| |
Rule: WHERE filters rows (before grouping). HAVING filters groups (after aggregating).
Your real pattern (from Adform queries):
| |
7. INDEXING
Intuition: A book index — jump directly to the relevant page rather than reading every page.
Three types:
| Type | Best For | Used In |
|---|---|---|
| B-Tree | Equality + range queries (user_id = 5, ts > date) | OLTP (PostgreSQL) |
| Hash | Exact match only — no ranges | Rare |
| Columnar | Column-scan analytics — the column is the index | Vertica (RLE, bit-vectors) |
When to index:
| Condition | Index? | Reason |
|---|---|---|
Frequently filtered, high cardinality (user_id, timestamp) | ✅ Yes | Selectivity benefit |
| JOIN column | ✅ Yes | Join condition scanned repeatedly |
| Low cardinality (gender: M/F) | ❌ No | Most queries return most rows anyway |
| Table < 100M rows | ❌ Often no | Full scan is fast enough |
Vertica note: Column encoding replaces traditional indexes. Proper column selection and projections matter more than explicit indexing.
8. ETL & HOW YOUR TABLES GET POPULATED
ETL flow: Extract (raw logs) → Transform (aggregate, clean) → Load (write to Vertica)
Your setup:
Flyway: Versioned SQL migration tool. Tracks which scripts ran, prevents double-execution, enables rollback. Your schema changes (CREATE TABLE, ALTER TABLE) live in versioned Flyway scripts — not ad-hoc DDL.
Nightly aggregation pattern (from your queries):
| |
Why fixed intervals: Predictable availability (features ready by 3 AM), compute-once efficiency, atomic batch load (no partial data).
9. USER-DEFINED FUNCTIONS (UDFs)
Custom logic you define in SQL. Use sparingly.
| Type | Returns | When to Use |
|---|---|---|
| Scalar | Single value per row | Reusable transform (format phone, decode flag) |
| Table-valued | Set of rows | Encapsulate complex multi-row logic |
Use UDFs when: Logic is complex AND reused across 3+ queries AND performance is not critical.
Avoid UDFs when: Called row-by-row on large tables (1M calls = no vectorisation = slow). Inline the logic instead:
10. CONNECTING FROM PYTHON
Connection pooling (always use — opens connections once, reuses them):
Three fetch patterns:
| |
Rule: Always filter in SQL before loading to Python. Never SELECT * then filter in Pandas on 1B rows.
11. SQL vs PANDAS: WHERE TO COMPUTE
| Computation Type | Tool | Why |
|---|---|---|
| Aggregation (1M → 1K rows) | SQL | DB parallelises, compressed, no RAM cost |
| Row-level transforms (1M → 1M rows) | Pandas | Custom logic, ML libraries available |
| Aggregate then enrich | SQL → Pandas | DB does heavy lifting, Pandas does finesse |
Examples from your work:
| |
Default: SQL aggregates. Pandas enriches. Never pull 1M raw rows to Pandas when a GROUP BY gives you 1K.
12. OUTPUT FORMATS
| Format | Use When | Key Trait |
|---|---|---|
| CSV | Sharing with non-technical users, < 100MB | Universal, human-readable; slow I/O, no types |
| Feather | Feature store, repeated Python reads | Sub-second read, native types; Python-only, no compression |
| Parquet | Archive, data lake, 1B+ rows | 4–8× compressed, Spark-native, standardised |
| XLSX | Business reports, stakeholders | Multi-sheet, formatted; 1M row limit |
Default for 1M+ rows: Feather (Python pipeline) or Parquet (archive/distributed).
13. VERTICA & SNOWFLAKE: QUICK REFERENCE
| Property | Vertica | Snowflake |
|---|---|---|
| Deployment | On-premise | Cloud (AWS/Azure/GCP) |
| Cost model | Fixed infrastructure | Pay-per-compute-hour |
| Storage | Column-oriented | Column-oriented |
| Best for | On-prem analytics at scale | Cloud-native, elastic workloads |
| SQL compatibility | Standard + Vertica extensions | Standard + Snowflake extensions |
SQL is 95% identical. Differences surface only in UDF syntax, materialized view refresh commands, and date function variants.
Your stack: Vertica (on-prem, dsp.* / tpas.* / train.* schemas) + Snowflake (cloud reporting). Same query logic, different connection strings.
DECISION CHEAT SHEET
| Question | Answer |
|---|---|
| WHERE vs HAVING? | WHERE = filter rows (before GROUP). HAVING = filter groups (after GROUP). |
| Table vs View vs Mat.View? | Data you own = table. Cheap filter = view. Expensive agg queried often = mat.view. |
| SQL vs Pandas? | Aggregate in SQL. Transform row-by-row in Pandas. |
| Index or not? | High-cardinality filter/JOIN column on >100M rows = yes. Otherwise = no. |
| Vertica vs PostgreSQL? | Analytics at 1B rows = Vertica. Transactions/single-row = PostgreSQL. |
| Which output format? | CSV (share), Feather (fast Python), Parquet (archive), XLSX (business). |