Four advanced features that come up in senior SQL interviews and real data pipelines: UPSERT (insert or update in one statement), MERGE, LATERAL joins for "top N per row" queries, and materialized views that store a query's result for fast reads.

UPSERT: Insert or Update

What is UPSERT?

UPSERT = INSERT if the row does not exist, UPDATE if it does.

Extremely common in real systems: syncing data, updating counters, idempotent writes.

Different databases have different syntax — know all three for interviews.

MySQL: INSERT ... ON DUPLICATE KEY UPDATE

-- MySQL UPSERT: if primary key/unique key conflicts, update instead
INSERT INTO product_inventory (product_id, stock, last_updated)
VALUES (101, 50, NOW())
ON DUPLICATE KEY UPDATE
    stock        = stock + VALUES(stock),  -- add to existing stock (MySQL 8.0.20+ prefers a row alias: VALUES (...) AS new ... stock = stock + new.stock)
    last_updated = NOW();

-- Insert or update user login stats
INSERT INTO user_stats (user_id, login_count, last_login)
VALUES (42, 1, NOW())
ON DUPLICATE KEY UPDATE
    login_count = login_count + 1,
    last_login  = NOW();

-- Batch upsert (very efficient)
INSERT INTO prices (product_id, price, updated_at) VALUES
    (1, 999.00, NOW()),
    (2, 1499.00, NOW()),
    (3, 599.00, NOW())
ON DUPLICATE KEY UPDATE
    price = VALUES(price), updated_at = NOW();

PostgreSQL: INSERT ... ON CONFLICT

-- PostgreSQL UPSERT
INSERT INTO product_inventory (product_id, stock)
VALUES (101, 50)
ON CONFLICT (product_id) DO UPDATE
    SET stock = product_inventory.stock + EXCLUDED.stock,
        last_updated = NOW();
-- EXCLUDED refers to the row that was attempted to be inserted

-- Do nothing on conflict (ignore duplicates)
INSERT INTO events (event_id, event_type, user_id)
VALUES ('evt_123', 'click', 42)
ON CONFLICT (event_id) DO NOTHING;

SQL Server / Oracle: MERGE Statement

-- MERGE (SQL Server / Oracle) — the most powerful upsert
MERGE INTO dim_customers AS target
USING (SELECT 101 AS customer_id, 'Hyderabad' AS city) AS source
ON (target.customer_id = source.customer_id)

WHEN MATCHED THEN
    UPDATE SET target.city = source.city, target.updated_at = GETDATE()

WHEN NOT MATCHED BY TARGET THEN
    INSERT (customer_id, city, created_at)
    VALUES (source.customer_id, source.city, GETDATE())

WHEN NOT MATCHED BY SOURCE THEN
    UPDATE SET target.is_active = 0;  -- deactivate removed records

-- MERGE is extremely useful for ETL: full sync from staging to production
Advertisement

LATERAL Joins and CROSS APPLY

What is a LATERAL Join?

A LATERAL join allows the right-side subquery to REFERENCE columns from the left side.

In a regular subquery in FROM, you cannot reference outer table columns.

LATERAL removes this restriction — like a correlated subquery but in the FROM clause.

SQL Server / Oracle: CROSS APPLY and OUTER APPLY

PostgreSQL: LATERAL keyword

MySQL 8.0+: LATERAL keyword

Use case: "For each customer, get their top 3 orders" — cannot be done with regular JOIN.

-- PostgreSQL / MySQL 8: LATERAL
-- For each customer, get their 3 most recent orders
SELECT c.customer_id, c.name,
       o.order_id, o.amount, o.order_date
FROM customers c
CROSS JOIN LATERAL (
    SELECT order_id, amount, order_date
    FROM orders
    WHERE customer_id = c.customer_id   -- ← references outer table column!
    ORDER BY order_date DESC
    LIMIT 3
) o;

-- Without LATERAL, you cannot use c.customer_id inside the FROM subquery:
-- This FAILS:
SELECT c.name, o.amount FROM customers c
JOIN (SELECT amount FROM orders WHERE customer_id = c.customer_id LIMIT 3) o ON TRUE;
-- ERROR: column "c.customer_id" does not exist (c is not in scope)

-- SQL Server: CROSS APPLY (equivalent to LATERAL)
SELECT c.customer_id, c.name, o.order_id, o.amount
FROM customers c
CROSS APPLY (
    SELECT TOP 3 order_id, amount FROM orders
    WHERE customer_id = c.customer_id
    ORDER BY order_date DESC
) o;

-- OUTER APPLY = LEFT LATERAL JOIN (includes customers with no orders)
SELECT c.name, o.order_id
FROM customers c
OUTER APPLY (
    SELECT TOP 1 order_id FROM orders
    WHERE customer_id = c.customer_id
    ORDER BY order_date DESC
) o;
-- o.order_id = NULL for customers with no orders
LATERAL vs Correlated Subquery vs Window Function

Correlated subquery in SELECT: returns ONE scalar value per row. Cannot return multiple rows.

LATERAL join: can return MULTIPLE rows per outer row. More flexible.

Window function: does not reduce rows but adds a computed column. Cannot LIMIT per partition.

"Get top 3 products per category" — use LATERAL or ROW_NUMBER window function.

ROW_NUMBER approach is usually preferred in MySQL as LATERAL was added only in 8.0.

Materialized Views

Regular View vs Materialized View

Regular View: Saved SQL query. Runs the query EVERY time you query the view. No storage.

Materialized View: Saves the QUERY RESULT physically to disk. Fast reads. Must be refreshed.

Analogy: Regular view = opening a spreadsheet formula every time.

Materialized view = pre-calculated and saved result — instant lookup.

FeatureRegular ViewMaterialized View
Data storageNone — query reruns each timePhysical storage (like a table)
Query speedDepends on base query complexityVery fast (pre-computed)
Data freshnessAlways current (live)Stale until refreshed
Storage costZeroSame as query result size
Indexable?NoYes — can add indexes on MV
Use caseSimplify complex queriesExpensive aggregations, reporting dashboards
DB supportAll RDBMSPostgreSQL, Oracle, SQL Server, Redshift, BigQuery
MySQL support?Yes (regular views)NO native support — must simulate manually

Materialized Views in PostgreSQL

-- Create a materialized view: pre-compute monthly revenue
CREATE MATERIALIZED VIEW mv_monthly_revenue AS
SELECT
    DATE_TRUNC('month', order_date) AS month,
    COUNT(DISTINCT order_id)         AS total_orders,
    COUNT(DISTINCT customer_id)      AS unique_customers,
    SUM(amount)                      AS revenue,
    AVG(amount)                      AS avg_order_value
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;

-- Add index on the MV for faster filtering
CREATE INDEX idx_mv_month ON mv_monthly_revenue(month);

-- Query the MV — extremely fast (reads pre-computed data)
SELECT * FROM mv_monthly_revenue WHERE month >= '2025-01-01';

-- Refresh the MV (re-run underlying query, update stored data)
REFRESH MATERIALIZED VIEW mv_monthly_revenue;

-- Refresh WITHOUT locking (allows reads during refresh) — PostgreSQL 9.4+
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_revenue;
-- CONCURRENTLY requires a UNIQUE index on the MV
CREATE UNIQUE INDEX idx_mv_month_uniq ON mv_monthly_revenue(month);

-- Drop MV
DROP MATERIALIZED VIEW IF EXISTS mv_monthly_revenue;

Simulating Materialized Views in MySQL

-- MySQL has no native MV. Simulate with a regular table + scheduled refresh.

-- Step 1: Create a summary table (acts as MV)
CREATE TABLE mv_monthly_revenue (
    month          DATE PRIMARY KEY,
    total_orders   INT,
    unique_customers INT,
    revenue        DECIMAL(14,2),
    avg_order_value DECIMAL(10,2),
    refreshed_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Step 2: Refresh procedure (truncate + re-insert)
CREATE PROCEDURE refresh_monthly_revenue()
BEGIN
    TRUNCATE TABLE mv_monthly_revenue;

    INSERT INTO mv_monthly_revenue
        (month, total_orders, unique_customers, revenue, avg_order_value, refreshed_at)
    SELECT
        DATE_FORMAT(order_date, '%Y-%m-01'),
        COUNT(DISTINCT order_id),
        COUNT(DISTINCT customer_id),
        SUM(amount),
        AVG(amount),
        NOW()
    FROM orders
    WHERE status = 'completed'
    GROUP BY DATE_FORMAT(order_date, '%Y-%m-01');
END;

-- Step 3: Schedule via MySQL Event Scheduler (runs every day at 2 AM)
CREATE EVENT ev_refresh_revenue
ON SCHEDULE EVERY 1 DAY STARTS '2025-01-01 02:00:00'
DO CALL refresh_monthly_revenue();

When to Use Materialized Views — Interview Answer

Perfect Interview Answer

"When would you use a materialized view over a regular view?"

1. EXPENSIVE AGGREGATIONS: When the underlying query takes seconds/minutes

(e.g., SUM revenue across 100M orders) and is queried hundreds of times per day.

2. DASHBOARDS & REPORTING: Business dashboards that refresh hourly — users do not

need second-by-second accuracy, and query speed matters more than freshness.

3. CROSS-DATABASE JOINS: When you need to pre-join data from multiple sources.

4. DATA PIPELINES: Intermediate aggregation layers in ETL pipelines.

Trade-off: "The main cost is data staleness. You must decide acceptable refresh frequency.

For real-time data, a regular view or streaming solution (Kafka + KSQL) is better."

FAQs

How do you write an UPSERT in MySQL?

INSERT … ON DUPLICATE KEY UPDATE col = …, which updates the existing row when the insert hits a primary or unique key.

What is the PostgreSQL equivalent of ON DUPLICATE KEY UPDATE?

INSERT … ON CONFLICT (key) DO UPDATE SET col = EXCLUDED.col, or DO NOTHING to skip duplicates.

What is the difference between a view and a materialized view?

A view stores only the query and runs it each time; a materialized view stores the result and must be refreshed, trading freshness for speed.