CodeKitchen prep
0%
Contest in--:--:--
SQL & Databases Β· lesson 07/13

πŸ›οΈDatabase concepts

⏳ ~15 min

The contest explicitly asks for "database concepts". Expect questions about schema design, indexes, transactions, and the OLTP vs OLAP split, either as MCQs or hidden inside a problem ("design a table so this query is fast").

Keys & constraints

  • Primary key: unique and not null. Use a natural key (email, ISBN) or a surrogate key (auto-increment id, UUID). Warehouses use surrogate keys for dimensions.
  • Foreign key: referential integrity (ON DELETE CASCADE / SET NULL / RESTRICT).
  • UNIQUE, NOT NULL, CHECK (amount >= 0), DEFAULT.
  • Composite key: e.g. PRIMARY KEY (order_id, product_id) on order_items.

Normalization (OLTP) in one table

FormRuleViolation example
1NFAtomic values, no repeating groupsphones = "0912..., 0935..."
2NF1NF + no partial dependency on part of a composite keyorder_items(order_id, product_id, product_name), where product_name depends only on product_id
3NF2NF + no transitive dependency (non-key β†’ non-key)customers(id, zip, city), where city depends on zip
BCNFEvery determinant is a candidate keyRare edge cases of 3NF

Denormalization (OLAP) is the deliberate opposite. It duplicates data to avoid joins at read time, which suits warehouses and wide tables for BI.

Indexes

B-tree (default)

Supports equality, range (< > BETWEEN), ORDER BY, and prefix LIKE 'abc%'. Lookups are O(log n).

Composite (a, b, c) follows the leftmost-prefix rule. It helps a, a,b and a,b,c, but not b alone. Put equality columns first and the range column last.

Others

Hash: equality only. Covering index: contains every column the query needs, so the table isn't touched ("index-only scan"). Partial index (WHERE active). GIN for JSON and full text. Clustered (InnoDB PK): rows are physically stored in PK order.

Indexes don't help when you wrap the column in a function (WHERE YEAR(created_at)=2026; write a range instead), use a leading wildcard (LIKE '%abc'), have low selectivity (status with 3 values), or the query returns a large share of the table. Every index also slows down writes.

-- sargable rewrite
-- BAD:  WHERE DATE(created_at) = '2026-10-23'
-- GOOD: WHERE created_at >= '2026-10-23' AND created_at < '2026-10-24'
CREATE INDEX idx_orders_cust_date ON orders (customer_id, order_date);  -- serves WHERE customer_id=? ORDER BY order_date
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 1 ORDER BY order_date;  -- MySQL/PG: EXPLAIN (ANALYZE)

Transactions: ACID

MeaningHow it's implemented
AtomicityAll or nothingUndo log / rollback
ConsistencyConstraints hold before and afterConstraints, triggers
IsolationConcurrent transactions don't interfere (to a chosen degree)Locks / MVCC
DurabilityCommitted data survives a crashWrite-ahead log (WAL / redo log), fsync

Isolation levels & anomalies

LevelDirty readNon-repeatable readPhantom
Read Uncommittedpossiblepossiblepossible
Read Committed PG defaultβ€”possiblepossible
Repeatable Read MySQL InnoDB defaultβ€”β€”possible (in theory; InnoDB's next-key locks / PG's snapshots largely prevent it)
Serializableβ€”β€”β€”

Lost update: two transactions read-modify-write the same row. Fix it with UPDATE t SET x = x + 1 (atomic), SELECT … FOR UPDATE, or optimistic locking with a version column. Deadlock: two transactions each wait for the other's lock. The database kills one, so retry it, and always lock rows in a consistent order.

MVCC (Postgres, InnoDB, Oracle): readers see a snapshot and don't block writers.

OLTP vs OLAP

OLTP

Many small reads and writes, normalized, row-oriented storage, latency-critical.
Postgres, MySQL, SQL Server.

OLAP

Few huge scans and aggregations, denormalized, columnar storage, compression, MPP.
Snowflake, Redshift, BigQuery, Databricks SQL, ClickHouse, DuckDB.

Dimensional modelling (Kimball)

  • Fact tables hold measurements at a declared grain ("one row per order line"), plus FKs to dimensions. They can be additive (amount), semi-additive (balance), or non-additive (ratio).
  • Dimension tables hold descriptive context: customer, product, date. The star schema has denormalized dimensions; the snowflake schema normalizes them.
  • Slowly Changing Dimensions: Type 0 never changes Β· Type 1 overwrites (no history) Β· Type 2 adds a new row with valid_from / valid_to / is_current (full history) Β· Type 3 adds a "previous value" column Β· Type 4 uses a separate history table.
  • Date dimension: a precomputed calendar (weekday, holiday, fiscal quarter).
-- SCD2: point-in-time join, i.e. which city was the customer in when they ordered?
SELECT f.order_id, d.city
FROM fact_orders f
JOIN dim_customer d
  ON d.customer_id = f.customer_id
 AND f.order_ts >= d.valid_from
 AND (f.order_ts < d.valid_to OR d.valid_to IS NULL);

Scaling a database

  • Partitioning (one database, one table split by range/list/hash, e.g. by month) enables partition pruning and cheap drops of old data.
  • Sharding: data spread across servers by a shard key. Cross-shard joins are hard.
  • Replication: primary β†’ read replicas. Replication lag means eventual consistency for reads.
  • CAP: during a network Partition, choose Consistency or Availability. BASE vs ACID.
  • NoSQL families: key-value (Redis, DynamoDB), document (MongoDB), wide-column (Cassandra), graph (Neo4j), time-series.

How the database runs your query

The optimiser turns SQL into a plan of physical operators and picks the cheapest one based on table statistics (row counts, distinct values, histograms). Stale statistics β†’ bad plans, which is why ANALYZE exists.

Join algorithmHowBest when
Nested loopFor each outer row, look up matches in the inner sideOuter side is small and the inner side has an index on the join key
Hash joinBuild a hash table on the smaller input, probe with the bigger oneEquality joins on large unsorted inputs (the warehouse default)
Sort-merge joinSort both sides on the key, then walk them togetherInputs already sorted (index order), range joins, huge inputs that spill to disk

Reading EXPLAIN: look for full table scans on big tables (type=ALL in MySQL, Seq Scan in Postgres, SCAN in SQLite), the estimated vs actual row counts (EXPLAIN ANALYZE), and sorts or temporary tables (Using filesort, Using temporary) that an index could avoid.

Data types that matter

  • Money: DECIMAL(12, 2), never FLOAT. Or store integer cents.
  • Time: store TIMESTAMP WITH TIME ZONE / UTC, and convert at display time. DATE for calendar days. MySQL TIMESTAMP converts to UTC and is limited to 2038; DATETIME stores the value as given.
  • Strings: VARCHAR(n) vs TEXT; utf8mb4 in MySQL (plain utf8 can't store emoji).
  • IDs: BIGINT auto-increment (compact, ordered) vs UUID (globally unique, no coordination, but random inserts fragment a B-tree; UUIDv7 is time-ordered).
  • Semi-structured: JSON/JSONB columns for flexible attributes; extract and index the fields you filter on.

Views, materialized views, temp tables

Stores data?FreshnessUse for
ViewNo, it's a saved queryAlways currentReuse logic, restrict columns for security
Materialized viewYesAs of the last refreshExpensive aggregates read often (dashboards)
Temp table / CTETemp: yes, for the session Β· CTE: no (usually inlined)β€”Breaking a big transformation into steps

Worked example: design the schema

"A food-delivery app: customers place orders at restaurants; an order has several menu items; a courier delivers it. Analysts need daily revenue per city and average delivery time."

-- OLTP side (3NF)
CREATE TABLE cities      (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE);
CREATE TABLE customers   (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE, city_id INTEGER REFERENCES cities(id));
CREATE TABLE restaurants (id INTEGER PRIMARY KEY, name TEXT NOT NULL, city_id INTEGER NOT NULL REFERENCES cities(id));
CREATE TABLE menu_items  (id INTEGER PRIMARY KEY, restaurant_id INTEGER NOT NULL REFERENCES restaurants(id),
                          name TEXT NOT NULL, price DECIMAL(10, 2) NOT NULL CHECK (price >= 0));
CREATE TABLE orders      (id INTEGER PRIMARY KEY, customer_id INTEGER NOT NULL REFERENCES customers(id),
                          restaurant_id INTEGER NOT NULL REFERENCES restaurants(id), courier_id INTEGER,
                          status TEXT NOT NULL CHECK (status IN ('placed', 'delivered', 'cancelled')),
                          placed_at TIMESTAMP NOT NULL, delivered_at TIMESTAMP);
CREATE TABLE order_lines (order_id INTEGER REFERENCES orders(id), menu_item_id INTEGER REFERENCES menu_items(id),
                          quantity INTEGER NOT NULL CHECK (quantity > 0),
                          unit_price DECIMAL(10, 2) NOT NULL,          -- price AT ORDER TIME, not a join to today's menu
                          PRIMARY KEY (order_id, menu_item_id));
CREATE INDEX idx_orders_restaurant_placed ON orders (restaurant_id, placed_at);

-- OLAP side (star): grain = one row per order line
--   fact_order_lines(date_key, city_key, restaurant_key, customer_key, quantity, revenue, delivery_minutes)
--   dim_date, dim_city, dim_restaurant, dim_customer (SCD2 if the customer's city history matters)

Points interviewers look for: unit_price copied into the order line (prices change; history must not), constraints that encode business rules, an index matching the main access path, a declared grain for the fact table, and knowing which dimension needs history (SCD2).

Quiz Composite index on (country, city, created_at). Which query can NOT use it efficiently?
Quiz Transaction T1 reads a row twice and sees different values because T2 committed in between. That's a…
Quiz In a star schema, the fact table typically holds…
Quiz Which is OLAP-friendly storage?
Quiz Best column type for money amounts?
Quiz Two big unsorted tables, an equality join, and no useful index. The optimiser usually picks…
🏁 --:--