Skip to content

DB Object Design

Tables

orders — Orders (status, amount, date)

order_items — Order items (qty, unit price)

products — Products (name, price, stock, brand)

customers — Customers (grade, points, channel)

categories — Categories (parent-child hierarchy)

reviews — Reviews (rating, content)

inventory_transactions — Inventory (type, quantity)

staff — Staff (dept, role, manager)

payments — Payments (method, amount, status)

Concepts

CREATE VIEW, DROP VIEW, CREATE TRIGGER, AFTER/BEFORE, INSERT/UPDATE/DELETE, OLD/NEW, Audit Logging

1. Create monthly sales summary views.

View name: v_monthly_revenue. Columns: Year Month, Number of Orders, Total Sales, Average Order Amount. Excludes canceled/returned orders.

Hint 1: Define the view as CREATE VIEW v_monthly_revenue AS SELECT .... You can use GROUP BY inside a view.

Answer
CREATE VIEW v_monthly_revenue AS
SELECT
    SUBSTR(ordered_at, 1, 7)   AS year_month,
    COUNT(*)                    AS order_count,
    ROUND(SUM(total_amount), 0) AS total_revenue,
    ROUND(AVG(total_amount), 0) AS avg_order_value
FROM orders
WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
GROUP BY SUBSTR(ordered_at, 1, 7);

-- 뷰 사용
SELECT *
FROM v_monthly_revenue
WHERE year_month LIKE '2024%'
ORDER BY year_month;

2. Create a customer dashboard view

View name: v_customer_dashboard. Includes basic information for each customer, total number of orders, total purchase amount, last order date, and number of reviews.

Hint 1: Linking orders and reviews with LEFT JOIN may cause double counting issues. It is safer to aggregate each by subquery or CTE and then join.

Answer
CREATE VIEW v_customer_dashboard AS
WITH order_stats AS (
    SELECT
        customer_id,
        COUNT(*)              AS order_count,
        ROUND(SUM(total_amount), 0) AS total_spent,
        MAX(ordered_at)       AS last_order_date
    FROM orders
    WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY customer_id
),
review_stats AS (
    SELECT
        customer_id,
        COUNT(*) AS review_count
    FROM reviews
    GROUP BY customer_id
)
SELECT
    c.id              AS customer_id,
    c.name,
    c.email,
    c.grade,
    c.created_at      AS signup_date,
    COALESCE(os.order_count, 0)   AS order_count,
    COALESCE(os.total_spent, 0)   AS total_spent,
    os.last_order_date,
    COALESCE(rs.review_count, 0)  AS review_count
FROM customers AS c
LEFT JOIN order_stats  AS os ON c.id = os.customer_id
LEFT JOIN review_stats AS rs ON c.id = rs.customer_id;

-- VIP 고객 조회
SELECT *
FROM v_customer_dashboard
WHERE grade = 'VIP'
ORDER BY total_spent DESC
LIMIT 10;

3. Create product performance views and use them to analyze sales efficiency relative to inventory.

View name: v_product_performance. Includes total sales, revenue, current inventory, and average rating for each product.

Hint 1: When creating a view, also include products without reviews as LEFT JOIN. When querying a view, you can add derived columns such as "Inventory Turnover" (units_sold / stock_qty).

Answer
CREATE VIEW v_product_performance AS
SELECT
    p.id              AS product_id,
    p.name            AS product_name,
    cat.name          AS category,
    p.price,
    p.stock_qty,
    COALESCE(SUM(oi.quantity), 0) AS total_sold,
    COALESCE(ROUND(SUM(oi.quantity * oi.unit_price), 0), 0) AS total_revenue,
    COALESCE(r.review_count, 0)   AS review_count,
    r.avg_rating
FROM products AS p
INNER JOIN categories AS cat ON p.category_id = cat.id
LEFT JOIN order_items AS oi ON p.id = oi.product_id
LEFT JOIN orders AS o ON oi.order_id = o.id
    AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
LEFT JOIN (
    SELECT
        product_id,
        COUNT(*) AS review_count,
        ROUND(AVG(rating), 2) AS avg_rating
    FROM reviews
    GROUP BY product_id
) AS r ON p.id = r.product_id
WHERE p.is_active = 1
GROUP BY p.id, p.name, cat.name, p.price, p.stock_qty,
         r.review_count, r.avg_rating;

-- 재고 대비 판매 효율 분석
SELECT
    product_name,
    category,
    total_sold,
    stock_qty,
    CASE
        WHEN stock_qty > 0
        THEN ROUND(1.0 * total_sold / stock_qty, 2)
        ELSE NULL
    END AS turnover_ratio,
    total_revenue,
    avg_rating
FROM v_product_performance
WHERE total_sold > 0
ORDER BY turnover_ratio DESC
LIMIT 10;

4. Nested query using views — Find year-on-year growth rate in monthly sales view.

Self-join the v_monthly_revenue created earlier.

Hint 1: It references v_monthly_revenue twice. JOIN condition: The "month part" of the current month is the same as the "month part" of the previous year, and the year is 1 different. Extract the month part with SUBSTR(year_month, 6, 2) and the year with SUBSTR(year_month, 1, 4).

Answer
SELECT
    curr.year_month,
    curr.total_revenue      AS current_revenue,
    prev.total_revenue      AS prev_year_revenue,
    curr.total_revenue - prev.total_revenue AS diff,
    ROUND(100.0 * (curr.total_revenue - prev.total_revenue)
        / prev.total_revenue, 1) AS yoy_growth_pct
FROM v_monthly_revenue AS curr
INNER JOIN v_monthly_revenue AS prev
    ON SUBSTR(curr.year_month, 6, 2) = SUBSTR(prev.year_month, 6, 2)
   AND CAST(SUBSTR(curr.year_month, 1, 4) AS INTEGER)
     = CAST(SUBSTR(prev.year_month, 1, 4) AS INTEGER) + 1
WHERE curr.year_month LIKE '2024%'
ORDER BY curr.year_month;

5. Check the list of all views that currently exist in the DB.

It uses SQLite's system catalog.

Hint 1: In SQLite, all DB object information is stored in the sqlite_master table. Filter by type = 'view'.

Answer
SELECT
    name,
    type,
    sql
FROM sqlite_master
WHERE type = 'view'
ORDER BY name;

Result (top 7 of 18 rows)

name type sql
v_cart_abandonment view CREATE VIEW v_cart_abandonment AS

SEL... | | v_category_tree | view | CREATE VIEW v_category_tree AS WITH R... | | v_coupon_effectiveness | view | CREATE VIEW v_coupon_effectiveness AS... | | v_customer_rfm | view | CREATE VIEW v_customer_rfm AS WITH rf... | | v_customer_summary | view | CREATE VIEW v_customer_summary AS SEL... | | v_daily_orders | view | CREATE VIEW v_daily_orders AS SELECT ... | | v_hourly_pattern | view | CREATE VIEW v_hourly_pattern AS SELEC... |


6. Drop the view and recreate it (DROP + CREATE).

Regenerate the v_monthly_revenue view by adding a column for increase/decrease compared to the previous month.

Hint 1: SQLite does not support ALTER VIEW, so delete and recreate it as DROP VIEW IF EXISTS. You can use the window function LAG inside a view definition.

Answer
DROP VIEW IF EXISTS v_monthly_revenue;

CREATE VIEW v_monthly_revenue AS
WITH base AS (
    SELECT
        SUBSTR(ordered_at, 1, 7)   AS year_month,
        COUNT(*)                    AS order_count,
        ROUND(SUM(total_amount), 0) AS total_revenue,
        ROUND(AVG(total_amount), 0) AS avg_order_value
    FROM orders
    WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY SUBSTR(ordered_at, 1, 7)
)
SELECT
    year_month,
    order_count,
    total_revenue,
    avg_order_value,
    LAG(total_revenue, 1) OVER (ORDER BY year_month) AS prev_month_revenue,
    ROUND(100.0 * (total_revenue - LAG(total_revenue, 1) OVER (ORDER BY year_month))
        / LAG(total_revenue, 1) OVER (ORDER BY year_month), 1) AS mom_growth_pct
FROM base;

-- 개선된 뷰 확인
SELECT *
FROM v_monthly_revenue
WHERE year_month LIKE '2024%'
ORDER BY year_month;

7. Simulate the Materialized View concept in SQLite.

A regular view executes the query every time, but a “materialized view” stores the results in a table. Save statistics for each product as a table and implement an update pattern.

Hint 1: Save the results to a table with CREATE TABLE mv_product_stats AS SELECT .... On update, run DROP TABLE IF EXISTS + CREATE TABLE AS again. Since SQLite does not support native MATERIALIZED VIEW, this pattern is an alternative.

Answer
-- 1. 구체화된 뷰 생성 (테이블로 저장)
DROP TABLE IF EXISTS mv_product_stats;

CREATE TABLE mv_product_stats AS
SELECT
    p.id              AS product_id,
    p.name            AS product_name,
    cat.name          AS category,
    COALESCE(SUM(oi.quantity), 0) AS total_sold,
    COALESCE(ROUND(SUM(oi.quantity * oi.unit_price), 0), 0) AS total_revenue,
    (SELECT COUNT(*) FROM reviews AS r WHERE r.product_id = p.id) AS review_count,
    (SELECT ROUND(AVG(rating), 2) FROM reviews AS r WHERE r.product_id = p.id) AS avg_rating,
    DATETIME('now') AS refreshed_at
FROM products AS p
INNER JOIN categories AS cat ON p.category_id = cat.id
LEFT JOIN order_items AS oi ON p.id = oi.product_id
LEFT JOIN orders AS o ON oi.order_id = o.id
    AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
WHERE p.is_active = 1
GROUP BY p.id, p.name, cat.name;

-- 2. 구체화된 뷰 사용 (인덱스 생성 가능 — 일반 뷰와의 차이점)
CREATE INDEX idx_mv_product_stats_revenue ON mv_product_stats(total_revenue);

SELECT *
FROM mv_product_stats
ORDER BY total_revenue DESC
LIMIT 5;

8. Check the list of triggers that currently exist in the DB.

Displays the trigger name, associated table, and definition (SQL).

Hint 1: Filter from sqlite_master to type = 'trigger'. The tbl_name column is the table to which the trigger is attached.

Answer
SELECT
    name       AS trigger_name,
    tbl_name   AS table_name,
    sql        AS definition
FROM sqlite_master
WHERE type = 'trigger'
ORDER BY tbl_name, name;

Result (5 rows)

trigger_name table_name definition
trg_customers_updated_at customers CREATE TRIGGER trg_customers_updated_...
trg_orders_updated_at orders CREATE TRIGGER trg_orders_updated_at

... | | trg_product_price_history | products | CREATE TRIGGER trg_product_price_hist... | | trg_products_updated_at | products | CREATE TRIGGER trg_products_updated_a... | | trg_reviews_updated_at | reviews | CREATE TRIGGER trg_reviews_updated_at... |


9. Create order status change audit log tables and triggers.

Automatically logs changes to status in the orders table.

Hint 1: First, create the audit log table as CREATE TABLE, to CREATE TRIGGER ... AFTER UPDATE OF status ON orders WHEN OLD.status != NEW.status Record the status before and after the change. OLD is the value before the change, and NEW is the value after the change.

Answer
-- 1. 감사 로그 테이블 생성
CREATE TABLE IF NOT EXISTS order_status_log (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    order_id    INTEGER NOT NULL,
    old_status  TEXT NOT NULL,
    new_status  TEXT NOT NULL,
    changed_at  TEXT NOT NULL DEFAULT (DATETIME('now'))
);

-- 2. 트리거 생성
CREATE TRIGGER trg_order_status_change
AFTER UPDATE OF status ON orders
WHEN OLD.status != NEW.status
BEGIN
    INSERT INTO order_status_log (order_id, old_status, new_status)
    VALUES (NEW.id, OLD.status, NEW.status);
END;

-- 3. 테스트: 주문 상태 변경
UPDATE orders SET status = 'shipped' WHERE id = 1 AND status = 'preparing';

-- 4. 로그 확인
SELECT * FROM order_status_log ORDER BY id DESC LIMIT 5;

10. Create automatic inventory deduction triggers.

When a new row is inserted in order_items, stock_qty for that product is automatically deducted.

Hint 1: Use CREATE TRIGGER ... AFTER INSERT ON order_items. UPDATE products SET stock_qty = stock_qty - NEW.quantity WHERE id = NEW.product_id;

Answer
CREATE TRIGGER trg_stock_deduct
AFTER INSERT ON order_items
BEGIN
    UPDATE products
    SET stock_qty = stock_qty - NEW.quantity,
        updated_at = DATETIME('now')
    WHERE id = NEW.product_id;
END;

-- 테스트: 현재 재고 확인
SELECT id, name, stock_qty FROM products WHERE id = 1;

-- order_items에 삽입 (실제 운영에서는 주문 프로세스에서 발생)
-- INSERT INTO order_items (order_id, product_id, quantity, unit_price, discount_amount, subtotal)
-- VALUES (1, 1, 2, 2987500, 0, 5975000);

-- 재고가 2 감소했는지 확인
-- SELECT id, name, stock_qty FROM products WHERE id = 1;

11. Create automatic point accumulation triggers when you write a review.

When a new review is inserted into reviews, you will earn 500 points for that customer. Applies only to purchase reviews verified by the WHEN NEW.is_verified = 1 condition.

Hint 1: You need to perform two actions: 1. Increment point_balance in table customers 2. Insert accrual record into point_transactions table Multiple SQL statements can be executed in the trigger body.

Answer
CREATE TRIGGER trg_review_point
AFTER INSERT ON reviews
WHEN NEW.is_verified = 1
BEGIN
    -- 1. 포인트 잔액 증가
    UPDATE customers
    SET point_balance = point_balance + 500,
        updated_at = DATETIME('now')
    WHERE id = NEW.customer_id;

    -- 2. 포인트 이력 기록
    INSERT INTO point_transactions (
        customer_id, order_id, type, reason, amount,
        balance_after, expires_at, created_at
    )
    VALUES (
        NEW.customer_id,
        NEW.order_id,
        'earn',
        'review',
        500,
        (SELECT point_balance FROM customers WHERE id = NEW.customer_id),
        DATE('now', '+1 year'),
        DATETIME('now')
    );
END;

-- 트리거 확인
SELECT name, sql
FROM sqlite_master
WHERE type = 'trigger' AND name = 'trg_review_point';

12. Create a trigger to automatically record product price change history.

When price of products is changed, the history is automatically recorded in the product_prices table. Updates ended_at in the old price record to the current time and inserts a new price record.

Hint 1: Use AFTER UPDATE OF price ON products WHEN OLD.price != NEW.price. Step 1: Update ended_at of an existing record (WHERE product_id = NEW.id AND ended_at IS NULL). Step 2: Insert a new record.

Answer
CREATE TRIGGER trg_price_history
AFTER UPDATE OF price ON products
WHEN OLD.price != NEW.price
BEGIN
    -- 1. 이전 가격 레코드 종료
    UPDATE product_prices
    SET ended_at = DATETIME('now')
    WHERE product_id = NEW.id
      AND ended_at IS NULL;

    -- 2. 새 가격 레코드 생성
    INSERT INTO product_prices (product_id, price, started_at, change_reason)
    VALUES (NEW.id, NEW.price, DATETIME('now'), 'price_drop');
END;

-- 테스트: 가격 변경
-- UPDATE products SET price = 2800000, updated_at = DATETIME('now') WHERE id = 1;

-- 이력 확인
SELECT *
FROM product_prices
WHERE product_id = 1
ORDER BY started_at DESC
LIMIT 3;

13. Create a delete protection trigger.

Confirmed orders (status = 'confirmed' or 'delivered') cannot be deleted. When attempting to delete, an error occurs with RAISE(ABORT, ...).

Hint 1: Use BEFORE DELETE ON orders. In condition WHEN OLD.status IN ('confirmed', 'delivered') Run RAISE(ABORT, 'Confirmed orders cannot be deleted.').

Answer
CREATE TRIGGER trg_prevent_order_delete
BEFORE DELETE ON orders
WHEN OLD.status IN ('confirmed', 'delivered')
BEGIN
    SELECT RAISE(ABORT, '확정/배송 완료된 주문은 삭제할 수 없습니다.');
END;

-- 테스트: 확정된 주문 삭제 시도
-- DELETE FROM orders WHERE id = 100 AND status = 'confirmed';
-- 결과: Error: 확정/배송 완료된 주문은 삭제할 수 없습니다.

-- 트리거 확인
SELECT name, sql
FROM sqlite_master
WHERE type = 'trigger' AND name = 'trg_prevent_order_delete';

14. Practice trigger deletion and conditional regeneration.

Delete the existing trg_stock_deduct trigger and recreate it with an improved version that raises an error when out of stock.

Hint 1: Delete with DROP TRIGGER IF EXISTS trg_stock_deduct;. The improved trigger uses BEFORE INSERT, If inventory is low, insertion itself is prevented with RAISE(ABORT, ...).

Answer
-- 1. 기존 트리거 삭제
DROP TRIGGER IF EXISTS trg_stock_deduct;

-- 2. 재고 검증 트리거 (BEFORE)
CREATE TRIGGER trg_stock_check
BEFORE INSERT ON order_items
WHEN (SELECT stock_qty FROM products WHERE id = NEW.product_id) < NEW.quantity
BEGIN
    SELECT RAISE(ABORT, '재고가 부족합니다.');
END;

-- 3. 재고 차감 트리거 (AFTER)
CREATE TRIGGER trg_stock_deduct
AFTER INSERT ON order_items
BEGIN
    UPDATE products
    SET stock_qty = stock_qty - NEW.quantity,
        updated_at = DATETIME('now')
    WHERE id = NEW.product_id;

    -- 재고 변동 이력 기록
    INSERT INTO inventory_transactions (
        product_id, type, quantity, reference_id, notes, created_at
    )
    VALUES (
        NEW.product_id, 'outbound', -NEW.quantity,
        NEW.order_id, 'order_deduction', DATETIME('now')
    );
END;

-- 트리거 확인
SELECT name, tbl_name, sql
FROM sqlite_master
WHERE type = 'trigger'
  AND name IN ('trg_stock_check', 'trg_stock_deduct')
ORDER BY name;

15. Check out stored procedure-like functionality in SQLite.

Count the number of all tables, views, triggers, and indexes in the current DB from the system catalog.

Hint 1: Returns COUNT(*) from sqlite_master by type. You can know the number of each type with GROUP BY type.

Answer
SELECT
    type,
    COUNT(*) AS object_count
FROM sqlite_master
WHERE type IN ('table', 'view', 'trigger', 'index')
GROUP BY type
ORDER BY
    CASE type
        WHEN 'table' THEN 1
        WHEN 'view' THEN 2
        WHEN 'index' THEN 3
        WHEN 'trigger' THEN 4
    END;

Result (4 rows)

type object_count
table 32
view 18
index 73
trigger 5

16. Design a customer rating renewal process.

Customer levels are recalculated based on annual purchase amount: VIP (5 million won or more), GOLD (3 to 5 million won), SILVER (1 to 3 million won), BRONZE (less than 1 million won).

Hint 1: Use CREATE PROCEDURE in MySQL and CREATE OR REPLACE PROCEDURE in PostgreSQL. Update each customer's rating using a cursor or the UPDATE ... FROM pattern.

Answer
DELIMITER //

    CREATE PROCEDURE sp_update_customer_grades(IN p_year INT)
    BEGIN
        -- 1. 연간 구매 금액 기반 등급 갱신
        UPDATE customers AS c
        INNER JOIN (
            SELECT
                customer_id,
                SUM(total_amount) AS annual_spent
            FROM orders
            WHERE YEAR(ordered_at) = p_year
              AND status NOT IN ('cancelled', 'returned', 'return_requested')
            GROUP BY customer_id
        ) AS s ON c.id = s.customer_id
        SET c.grade = CASE
            WHEN s.annual_spent >= 5000000 THEN 'VIP'
            WHEN s.annual_spent >= 3000000 THEN 'GOLD'
            WHEN s.annual_spent >= 1000000 THEN 'SILVER'
            ELSE 'BRONZE'
        END,
        c.updated_at = NOW();

        -- 2. 등급 변경 이력 기록
        INSERT INTO customer_grade_history (customer_id, old_grade, new_grade, changed_at, reason)
        SELECT
            c.id, cgh.prev_grade, c.grade, NOW(), 'yearly_review'
        FROM customers AS c
        INNER JOIN (
            SELECT customer_id, grade AS prev_grade
            FROM customer_grade_history
            WHERE (customer_id, changed_at) IN (
                SELECT customer_id, MAX(changed_at)
                FROM customer_grade_history
                GROUP BY customer_id
            )
        ) AS cgh ON c.id = cgh.customer_id
        WHERE c.grade != cgh.prev_grade;

        SELECT ROW_COUNT() AS updated_count;
    END //

    DELIMITER ;

    -- 실행
    CALL sp_update_customer_grades(2024);

CREATE OR REPLACE PROCEDURE sp_update_customer_grades(p_year INT)
    LANGUAGE plpgsql
    AS $$
    DECLARE
        v_updated INT;
    BEGIN
        -- 1. 연간 구매 금액 기반 등급 갱신
        UPDATE customers AS c
        SET grade = CASE
            WHEN s.annual_spent >= 5000000 THEN 'VIP'
            WHEN s.annual_spent >= 3000000 THEN 'GOLD'
            WHEN s.annual_spent >= 1000000 THEN 'SILVER'
            ELSE 'BRONZE'
        END,
        updated_at = NOW()
        FROM (
            SELECT
                customer_id,
                SUM(total_amount) AS annual_spent
            FROM orders
            WHERE EXTRACT(YEAR FROM ordered_at) = p_year
              AND status NOT IN ('cancelled', 'returned', 'return_requested')
            GROUP BY customer_id
        ) AS s
        WHERE c.id = s.customer_id;

        GET DIAGNOSTICS v_updated = ROW_COUNT;
        RAISE NOTICE '갱신된 고객 수: %', v_updated;
    END;
    $$;

    -- 실행
    CALL sp_update_customer_grades(2024);

17. Design a month-end settlement procedure.

Calculates a sales summary for a specific month and stores it in a settlement table.

Hint 1: It takes the year and month (p_year_month) as a parameter. Aggregate order/payment data and INSERT settlement records. An error occurs for months in which settlement has already been completed.

Answer
DELIMITER //

    CREATE PROCEDURE sp_monthly_settlement(IN p_year_month VARCHAR(7))
    BEGIN
        DECLARE v_exists INT;

        -- 중복 정산 검사
        SELECT COUNT(*) INTO v_exists
        FROM monthly_settlements
        WHERE year_month = p_year_month;

        IF v_exists > 0 THEN
            SIGNAL SQLSTATE '45000'
                SET MESSAGE_TEXT = '이미 정산이 완료된 월입니다.';
        END IF;

        -- 정산 데이터 삽입
        INSERT INTO monthly_settlements (
            year_month, order_count, total_revenue,
            total_discount, net_revenue, settled_at
        )
        SELECT
            p_year_month,
            COUNT(*),
            SUM(total_amount),
            SUM(discount_amount),
            SUM(total_amount) - SUM(discount_amount),
            NOW()
        FROM orders
        WHERE DATE_FORMAT(ordered_at, '%Y-%m') = p_year_month
          AND status NOT IN ('cancelled', 'returned', 'return_requested');

        SELECT * FROM monthly_settlements WHERE year_month = p_year_month;
    END //

    DELIMITER ;

    CALL sp_monthly_settlement('2024-12');

CREATE OR REPLACE PROCEDURE sp_monthly_settlement(p_year_month TEXT)
    LANGUAGE plpgsql
    AS $$
    BEGIN
        -- 중복 정산 검사
        IF EXISTS (SELECT 1 FROM monthly_settlements WHERE year_month = p_year_month) THEN
            RAISE EXCEPTION '이미 정산이 완료된 월입니다: %', p_year_month;
        END IF;

        -- 정산 데이터 삽입
        INSERT INTO monthly_settlements (
            year_month, order_count, total_revenue,
            total_discount, net_revenue, settled_at
        )
        SELECT
            p_year_month,
            COUNT(*),
            SUM(total_amount),
            SUM(discount_amount),
            SUM(total_amount) - SUM(discount_amount),
            NOW()
        FROM orders
        WHERE TO_CHAR(ordered_at, 'YYYY-MM') = p_year_month
          AND status NOT IN ('cancelled', 'returned', 'return_requested');

        RAISE NOTICE '정산 완료: %', p_year_month;
    END;
    $$;

    CALL sp_monthly_settlement('2024-12');

18. Design inventory replenishment procedures.

Find products that are below the safety stock (threshold) and create an automatic order request.

Hint 1: It takes the safety stock standard (e.g. 20 units) as a parameter. Search for out-of-stock products and INSERT them into the ordering table. Replenishment quantity is calculated as “average monthly sales x 2”.

Answer
DELIMITER //

    CREATE PROCEDURE sp_restock_check(IN p_threshold INT)
    BEGIN
        -- 재고 부족 상품 + 권장 발주 수량
        SELECT
            p.id            AS product_id,
            p.name          AS product_name,
            p.stock_qty     AS current_stock,
            p_threshold     AS safety_stock,
            COALESCE(ROUND(AVG(monthly_sold) * 2), 50) AS recommended_qty,
            s.company_name  AS supplier
        FROM products AS p
        INNER JOIN suppliers AS s ON p.supplier_id = s.id
        LEFT JOIN (
            SELECT
                oi.product_id,
                DATE_FORMAT(o.ordered_at, '%Y-%m') AS ym,
                SUM(oi.quantity) AS monthly_sold
            FROM order_items AS oi
            INNER JOIN orders AS o ON oi.order_id = o.id
            WHERE o.ordered_at >= DATE_SUB(NOW(), INTERVAL 6 MONTH)
              AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
            GROUP BY oi.product_id, DATE_FORMAT(o.ordered_at, '%Y-%m')
        ) AS ms ON p.id = ms.product_id
        WHERE p.is_active = 1
          AND p.stock_qty <= p_threshold
        GROUP BY p.id, p.name, p.stock_qty, s.company_name
        ORDER BY p.stock_qty ASC;
    END //

    DELIMITER ;

    CALL sp_restock_check(20);

CREATE OR REPLACE PROCEDURE sp_restock_check(p_threshold INT)
    LANGUAGE plpgsql
    AS $$
    DECLARE
        rec RECORD;
    BEGIN
        FOR rec IN
            SELECT
                p.id AS product_id,
                p.name AS product_name,
                p.stock_qty AS current_stock,
                COALESCE(ROUND(AVG(ms.monthly_sold) * 2), 50) AS recommended_qty,
                s.company_name AS supplier
            FROM products AS p
            INNER JOIN suppliers AS s ON p.supplier_id = s.id
            LEFT JOIN (
                SELECT
                    oi.product_id,
                    TO_CHAR(o.ordered_at, 'YYYY-MM') AS ym,
                    SUM(oi.quantity) AS monthly_sold
                FROM order_items AS oi
                INNER JOIN orders AS o ON oi.order_id = o.id
                WHERE o.ordered_at >= NOW() - INTERVAL '6 months'
                  AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
                GROUP BY oi.product_id, TO_CHAR(o.ordered_at, 'YYYY-MM')
            ) AS ms ON p.id = ms.product_id
            WHERE p.is_active = 1
              AND p.stock_qty <= p_threshold
            GROUP BY p.id, p.name, p.stock_qty, s.company_name
            ORDER BY p.stock_qty ASC
        LOOP
            RAISE NOTICE '발주 필요: % (재고: %, 권장: %)',
                rec.product_name, rec.current_stock, rec.recommended_qty;
        END LOOP;
    END;
    $$;

    CALL sp_restock_check(20);

19. Design a customer activity summary function.

Takes a specific customer ID and returns a comprehensive activity summary (orders/reviews/points/CS) for that customer as JSON.

Hint 1: Use CREATE FUNCTION ... RETURNS JSON in MySQL and RETURNS JSONB in PostgreSQL. Each indicator is obtained as a subquery and combined into JSON_OBJECT/jsonb_build_object.

Answer
DELIMITER //

    CREATE FUNCTION fn_customer_summary(p_customer_id INT)
    RETURNS JSON
    DETERMINISTIC
    READS SQL DATA
    BEGIN
        DECLARE v_result JSON;

        SELECT JSON_OBJECT(
            'customer_id', c.id,
            'name', c.name,
            'grade', c.grade,
            'order_count', COALESCE(os.cnt, 0),
            'total_spent', COALESCE(os.total, 0),
            'review_count', COALESCE(rs.cnt, 0),
            'avg_rating_given', COALESCE(rs.avg_r, 0),
            'point_balance', c.point_balance,
            'complaint_count', COALESCE(cs.cnt, 0)
        ) INTO v_result
        FROM customers AS c
        LEFT JOIN (
            SELECT customer_id, COUNT(*) AS cnt, SUM(total_amount) AS total
            FROM orders
            WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
            GROUP BY customer_id
        ) AS os ON c.id = os.customer_id
        LEFT JOIN (
            SELECT customer_id, COUNT(*) AS cnt, AVG(rating) AS avg_r
            FROM reviews GROUP BY customer_id
        ) AS rs ON c.id = rs.customer_id
        LEFT JOIN (
            SELECT customer_id, COUNT(*) AS cnt
            FROM complaints GROUP BY customer_id
        ) AS cs ON c.id = cs.customer_id
        WHERE c.id = p_customer_id;

        RETURN v_result;
    END //

    DELIMITER ;

    SELECT fn_customer_summary(1);

CREATE OR REPLACE FUNCTION fn_customer_summary(p_customer_id INT)
    RETURNS JSONB
    LANGUAGE plpgsql
    STABLE
    AS $$
    DECLARE
        v_result JSONB;
    BEGIN
        SELECT jsonb_build_object(
            'customer_id', c.id,
            'name', c.name,
            'grade', c.grade,
            'order_count', COALESCE(os.cnt, 0),
            'total_spent', COALESCE(os.total, 0),
            'review_count', COALESCE(rs.cnt, 0),
            'avg_rating_given', COALESCE(ROUND(rs.avg_r, 2), 0),
            'point_balance', c.point_balance,
            'complaint_count', COALESCE(cs.cnt, 0)
        ) INTO v_result
        FROM customers AS c
        LEFT JOIN (
            SELECT customer_id, COUNT(*) AS cnt, SUM(total_amount) AS total
            FROM orders
            WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
            GROUP BY customer_id
        ) AS os ON c.id = os.customer_id
        LEFT JOIN (
            SELECT customer_id, COUNT(*) AS cnt, AVG(rating) AS avg_r
            FROM reviews GROUP BY customer_id
        ) AS rs ON c.id = rs.customer_id
        LEFT JOIN (
            SELECT customer_id, COUNT(*) AS cnt
            FROM complaints GROUP BY customer_id
        ) AS cs ON c.id = cs.customer_id
        WHERE c.id = p_customer_id;

        RETURN v_result;
    END;
    $$;

    SELECT fn_customer_summary(1);

20. Design transaction-based order processing procedures.

The entire process of order creation (Create order record -> Create order item -> Deduct inventory -> Create payment -> Earn points) Encapsulate it in one procedure. If there is an intermediate failure, there is a full rollback.

Hint 1: Use START TRANSACTION ... COMMIT in MySQL and BEGIN ... COMMIT in PostgreSQL. If an error occurs, cancel entirely with ROLLBACK. Utilizes MySQL's DECLARE ... HANDLER FOR SQLEXCEPTION and PostgreSQL's EXCEPTION WHEN blocks.

Answer
DELIMITER //

    CREATE PROCEDURE sp_create_order(
        IN p_customer_id INT,
        IN p_product_id INT,
        IN p_quantity INT,
        IN p_payment_method VARCHAR(20)
    )
    BEGIN
        DECLARE v_price REAL;
        DECLARE v_stock INT;
        DECLARE v_order_id INT;
        DECLARE v_total REAL;
        DECLARE v_order_number VARCHAR(20);

        DECLARE EXIT HANDLER FOR SQLEXCEPTION
        BEGIN
            ROLLBACK;
            SIGNAL SQLSTATE '45000'
                SET MESSAGE_TEXT = '주문 처리 중 오류가 발생했습니다.';
        END;

        START TRANSACTION;

        -- 1. 상품 정보 & 재고 확인
        SELECT price, stock_qty INTO v_price, v_stock
        FROM products WHERE id = p_product_id FOR UPDATE;

        IF v_stock < p_quantity THEN
            SIGNAL SQLSTATE '45000'
                SET MESSAGE_TEXT = '재고가 부족합니다.';
        END IF;

        -- 2. 주문 생성
        SET v_total = v_price * p_quantity;
        SET v_order_number = CONCAT('ORD-',
            DATE_FORMAT(NOW(), '%Y%m%d'), '-',
            LPAD(FLOOR(RAND() * 99999), 5, '0'));

        INSERT INTO orders (
            order_number, customer_id, address_id, status,
            total_amount, discount_amount, shipping_fee,
            ordered_at, created_at, updated_at
        ) VALUES (
            v_order_number, p_customer_id,
            (SELECT id FROM customer_addresses
             WHERE customer_id = p_customer_id AND is_default = 1 LIMIT 1),
            'paid', v_total, 0,
            CASE WHEN v_total >= 50000 THEN 0 ELSE 3000 END,
            NOW(), NOW(), NOW()
        );

        SET v_order_id = LAST_INSERT_ID();

        -- 3. 주문 항목 생성
        INSERT INTO order_items (order_id, product_id, quantity, unit_price, subtotal)
        VALUES (v_order_id, p_product_id, p_quantity, v_price, v_total);

        -- 4. 재고 차감
        UPDATE products
        SET stock_qty = stock_qty - p_quantity, updated_at = NOW()
        WHERE id = p_product_id;

        -- 5. 결제 생성
        INSERT INTO payments (order_id, method, amount, status, paid_at, created_at)
        VALUES (v_order_id, p_payment_method, v_total, 'completed', NOW(), NOW());

        COMMIT;

        SELECT v_order_id AS order_id, v_order_number AS order_number,
               v_total AS total_amount, '주문 완료' AS message;
    END //

    DELIMITER ;

    CALL sp_create_order(1, 10, 2, 'card');

CREATE OR REPLACE PROCEDURE sp_create_order(
        p_customer_id INT,
        p_product_id INT,
        p_quantity INT,
        p_payment_method TEXT
    )
    LANGUAGE plpgsql
    AS $$
    DECLARE
        v_price NUMERIC;
        v_stock INT;
        v_order_id INT;
        v_total NUMERIC;
        v_order_number TEXT;
    BEGIN
        -- 1. 상품 정보 & 재고 확인 (행 잠금)
        SELECT price, stock_qty INTO v_price, v_stock
        FROM products WHERE id = p_product_id FOR UPDATE;

        IF v_stock < p_quantity THEN
            RAISE EXCEPTION '재고가 부족합니다. (현재: %, 요청: %)', v_stock, p_quantity;
        END IF;

        -- 2. 주문 생성
        v_total := v_price * p_quantity;
        v_order_number := 'ORD-' || TO_CHAR(NOW(), 'YYYYMMDD') || '-'
            || LPAD(FLOOR(RANDOM() * 99999)::TEXT, 5, '0');

        INSERT INTO orders (
            order_number, customer_id, address_id, status,
            total_amount, discount_amount, shipping_fee,
            ordered_at, created_at, updated_at
        ) VALUES (
            v_order_number, p_customer_id,
            (SELECT id FROM customer_addresses
             WHERE customer_id = p_customer_id AND is_default = TRUE LIMIT 1),
            'paid', v_total, 0,
            CASE WHEN v_total >= 50000 THEN 0 ELSE 3000 END,
            NOW(), NOW(), NOW()
        )
        RETURNING id INTO v_order_id;

        -- 3. 주문 항목 생성
        INSERT INTO order_items (order_id, product_id, quantity, unit_price, subtotal)
        VALUES (v_order_id, p_product_id, p_quantity, v_price, v_total);

        -- 4. 재고 차감
        UPDATE products
        SET stock_qty = stock_qty - p_quantity, updated_at = NOW()
        WHERE id = p_product_id;

        -- 5. 결제 생성
        INSERT INTO payments (order_id, method, amount, status, paid_at, created_at)
        VALUES (v_order_id, p_payment_method, v_total, 'completed', NOW(), NOW());

        RAISE NOTICE '주문 완료: % (금액: %)', v_order_number, v_total;
    END;
    $$;

    CALL sp_create_order(1, 10, 2, 'card');