Database concepts
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
| Form | Rule | Violation example |
|---|---|---|
| 1NF | Atomic values, no repeating groups | phones = "0912..., 0935..." |
| 2NF | 1NF + no partial dependency on part of a composite key | order_items(order_id, product_id, product_name), where product_name depends only on product_id |
| 3NF | 2NF + no transitive dependency (non-key β non-key) | customers(id, zip, city), where city depends on zip |
| BCNF | Every determinant is a candidate key | Rare 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
| Meaning | How it's implemented | |
|---|---|---|
| Atomicity | All or nothing | Undo log / rollback |
| Consistency | Constraints hold before and after | Constraints, triggers |
| Isolation | Concurrent transactions don't interfere (to a chosen degree) | Locks / MVCC |
| Durability | Committed data survives a crash | Write-ahead log (WAL / redo log), fsync |
Isolation levels & anomalies
| Level | Dirty read | Non-repeatable read | Phantom |
|---|---|---|---|
| Read Uncommitted | possible | possible | possible |
| Read Committed PG default | β | possible | possible |
| 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 algorithm | How | Best when |
|---|---|---|
| Nested loop | For each outer row, look up matches in the inner side | Outer side is small and the inner side has an index on the join key |
| Hash join | Build a hash table on the smaller input, probe with the bigger one | Equality joins on large unsorted inputs (the warehouse default) |
| Sort-merge join | Sort both sides on the key, then walk them together | Inputs 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.DATEfor calendar days. MySQLTIMESTAMPconverts to UTC and is limited to 2038;DATETIMEstores the value as given. - Strings:
VARCHAR(n)vsTEXT; utf8mb4 in MySQL (plainutf8can't store emoji). - IDs:
BIGINTauto-increment (compact, ordered) vs UUID (globally unique, no coordination, but random inserts fragment a B-tree; UUIDv7 is time-ordered). - Semi-structured:
JSON/JSONBcolumns for flexible attributes; extract and index the fields you filter on.
Views, materialized views, temp tables
| Stores data? | Freshness | Use for | |
|---|---|---|---|
| View | No, it's a saved query | Always current | Reuse logic, restrict columns for security |
| Materialized view | Yes | As of the last refresh | Expensive aggregates read often (dashboards) |
| Temp table / CTE | Temp: 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).