{
  "assessmentTests": {
    "sql_test": {
      "name": "SQL 테스트",
      "questions": [
        {
          "question": "테이블에 nullable column인 age가 있습니다. SELECT * FROM users WHERE age = NULL을 실행하면, age 값이 NULL인 행이 여러 개 있어도 몇 행이 반환될까요?",
          "options": [
            {
              "icon": "",
              "label": "age가 NULL인 모든 행 — =는 다른 값과 마찬가지로 NULL도 매치합니다"
            },
            {
              "icon": "",
              "label": "0행 — =로 무엇을 NULL과 비교하든 결과는 항상 UNKNOWN이고 절대 TRUE가 될 수 없어서 어떤 행도 매치되지 않습니다. 대신 IS NULL을 사용해야 합니다"
            },
            {
              "icon": "",
              "label": "구문 오류 — NULL은 = 오른쪽에 올 수 없습니다"
            },
            {
              "icon": "",
              "label": "테이블의 모든 행 — NULL 비교는 기본적으로 TRUE로 처리됩니다"
            }
          ]
        },
        {
          "question": "SELECT DISTINCT department, role FROM employees를 실행합니다. DISTINCT는 무엇의 중복을 제거할까요?",
          "options": [
            {
              "icon": "",
              "label": "department와 role의 조합 전체 — 두 column이 모두 다른 행과 정확히 일치할 때만 그 행이 제거됩니다"
            },
            {
              "icon": "",
              "label": "department 값의 중복만 제거하고, role은 모두 그대로 유지됩니다"
            },
            {
              "icon": "",
              "label": "role 값의 중복만 제거하고, department는 모두 그대로 유지됩니다"
            },
            {
              "icon": "",
              "label": "아무것도 제거하지 않습니다 — DISTINCT는 컬럼 하나에만 적용할 수 있습니다"
            }
          ]
        },
        {
          "question": "column인 name을 WHERE name LIKE 'A_'로 필터링합니다. 'A', 'Al', 'Ana', 'Ally' 중 어떤 값이 매치될까요?",
          "options": [
            {
              "icon": "",
              "label": "'A' — 밑줄(_)은 선택 사항이며 0개 이상의 문자와 매치됩니다"
            },
            {
              "icon": "",
              "label": "'Al' — 밑줄(_)은 정확히 한 글자와 매치되므로, LIKE 'A_'는 A로 시작하는 두 글자짜리 문자열과 매치됩니다"
            },
            {
              "icon": "",
              "label": "'Ana'와 'Ally' — 밑줄(_)은 뒤에 몇 글자가 오든 상관없이 매치됩니다"
            },
            {
              "icon": "",
              "label": "네 값 모두 매치됩니다 — LIKE는 문자열 길이를 무시합니다"
            }
          ]
        },
        {
          "question": "price column이 WHERE price BETWEEN 10 AND 20으로 필터링됩니다. price가 정확히 10이거나 20인 행도 포함될까요?",
          "options": [
            {
              "icon": "",
              "label": "아니요 — BETWEEN은 양쪽 끝점을 모두 제외합니다"
            },
            {
              "icon": "",
              "label": "예 — BETWEEN은 양쪽 끝을 포함합니다. price >= 10 AND price <= 20과 동일합니다"
            },
            {
              "icon": "",
              "label": "10만 포함됩니다; 상한은 제외됩니다"
            },
            {
              "icon": "",
              "label": "20만 포함됩니다; 하한은 제외됩니다"
            }
          ]
        },
        {
          "question": "orders 테이블이 10행을 가지고 3행이 NULL shipped_date를 가집니다. SELECT COUNT(*) FROM orders와 SELECT COUNT(shipped_date) FROM orders는 각각 무엇을 반환할까요?",
          "options": [
            {
              "icon": "",
              "label": "10, 그 다음 10 — COUNT는 NULL 여부와 관계없이 항상 모든 행을 셉니다"
            },
            {
              "icon": "",
              "label": "7, 그 다음 7 — 두 형식 모두 NULL 행을 건너뜁니다"
            },
            {
              "icon": "",
              "label": "10, 그 다음 7 — COUNT(*)는 모든 행을 세고, COUNT(column)는 해당 column이 NULL이 아닌 행만 셉니다"
            },
            {
              "icon": "",
              "label": "10, 그 다음 3 — COUNT(column)는 NULL 값의 개수만 셉니다"
            }
          ]
        },
        {
          "question": "SELECT name, salary FROM employees ORDER BY 2 DESC를 실행합니다. 숫자 2는 무엇을 의미할까요?",
          "options": [
            {
              "icon": "",
              "label": "구문 오류 — ORDER BY는 column 이름만 허용하고 숫자는 허용하지 않습니다"
            },
            {
              "icon": "",
              "label": "결과의 두 번째 행"
            },
            {
              "icon": "",
              "label": "2라는 literal 값으로, tiebreaker로 사용됩니다"
            },
            {
              "icon": "",
              "label": "SELECT list의 두 번째 column인 salary — ORDER BY는 column의 위치 번호를 축약형으로 받아들입니다"
            }
          ]
        },
        {
          "question": "standard SQL에서 'Ana' || ' ' || 'Lee'는 무엇으로 평가될까요?",
          "options": [
            {
              "icon": "",
              "label": "구문 오류 — SQL에는 concatenation 연산자가 없습니다"
            },
            {
              "icon": "",
              "label": "3 — ||는 boolean OR로 처리되어 개수를 반환합니다"
            },
            {
              "icon": "",
              "label": "'AnaLee' — ||는 공백을 유지하지 않고 연결합니다"
            },
            {
              "icon": "",
              "label": "'Ana Lee' — ||는 standard SQL의 string concatenation 연산자입니다"
            }
          ]
        },
        {
          "question": "WHERE status IN ('open', 'pending', 'review')와 정확히 동등한 것은 어느 것일까요?",
          "options": [
            {
              "icon": "",
              "label": "WHERE status = 'open' AND status = 'pending' AND status = 'review'"
            },
            {
              "icon": "",
              "label": "WHERE status != 'open' OR status != 'pending' OR status != 'review'"
            },
            {
              "icon": "",
              "label": "WHERE status = 'open' OR status = 'pending' OR status = 'review'"
            },
            {
              "icon": "",
              "label": "WHERE status LIKE 'open,pending,review'"
            }
          ]
        },
        {
          "question": "customers 테이블에 100행이 있고, 그중 20행은 한 번도 order를 하지 않았습니다. SELECT c.id FROM customers c INNER JOIN orders o ON c.id = o.customer_id를 실행합니다. order가 없는 20명의 customers가 결과에 나타날까요?",
          "options": [
            {
              "icon": "",
              "label": "예, 한 번씩 나타나고 o.id는 NULL로 표시됩니다"
            },
            {
              "icon": "",
              "label": "예, 하지만 WHERE 절에도 나타날 때만 그렇습니다"
            },
            {
              "icon": "",
              "label": "아니요 — INNER JOIN은 두 테이블 모두에서 매칭되는 행만 반환하므로, order가 하나도 없는 customers는 결과에서 완전히 제외됩니다"
            },
            {
              "icon": "",
              "label": "예, orders의 column 수만큼 중복되어 나타납니다"
            }
          ]
        },
        {
          "question": "orders가 있든 없든 모든 customer를 원해서 SELECT c.id, o.total FROM customers c LEFT JOIN orders o ON c.id = o.customer_id WHERE o.total > 100을 작성합니다. 이 쿼리도 order가 0건인 customers를 반환할까요?",
          "options": [
            {
              "icon": "",
              "label": "아니요 — WHERE 절에서 o.total을 필터링하면 LEFT JOIN이 매칭되지 않은 customers를 위해 만든 NULL 행이 버려지므로, 결국 INNER JOIN처럼 동작합니다"
            },
            {
              "icon": "",
              "label": "예 — LEFT JOIN은 이후에 무엇이 오든 항상 customers의 모든 행을 보존합니다"
            },
            {
              "icon": "",
              "label": "예, order가 없는 customers는 o.total이 0으로 표시됩니다"
            },
            {
              "icon": "",
              "label": "아니요 — WHERE 절이 추가되면 LEFT JOIN은 자동으로 RIGHT JOIN으로 바뀝니다"
            }
          ]
        },
        {
          "question": "employees 테이블에는 id column과, 다른 행의 id를 가리키는 manager_id column이 있습니다. 각 employee 옆에 그 manager의 이름을 나열하기 위해 테이블을 자기 자신과 join합니다: SELECT e.name, m.name FROM employees e JOIN employees m ON e.manager_id = m.id. 이 패턴을 무엇이라고 부를까요?",
          "options": [
            {
              "icon": "",
              "label": "cross join — 모든 employee가 모든 manager와 매칭됩니다"
            },
            {
              "icon": "",
              "label": "recursive join — 전체 관리 체계(management chain)를 가져옵니다"
            },
            {
              "icon": "",
              "label": "self-join — 같은 테이블을 서로 다른 두 alias를 사용해 자기 자신과 join합니다"
            },
            {
              "icon": "",
              "label": "이것은 유효하지 않은 SQL입니다 — 테이블은 자기 자신과 join할 수 없습니다"
            }
          ]
        },
        {
          "question": "같은 column을 가진 두 SELECT 쿼리가 UNION으로 결합됩니다. 두 쿼리가 똑같은 행을 하나씩 반환한다면, 최종 결과에는 그 행이 몇 개나 나타날까요?",
          "options": [
            {
              "icon": "",
              "label": "1행 — UNION은 결합된 결과에서 중복 행을 제거합니다. UNION ALL이었다면 두 복사본이 모두 남았을 것입니다"
            },
            {
              "icon": "",
              "label": "2행 — UNION은 두 쿼리의 모든 행을 그대로 유지합니다"
            },
            {
              "icon": "",
              "label": "0행 — UNION은 두 쿼리 모두에 나타나는 행을 제거합니다"
            },
            {
              "icon": "",
              "label": "어느 쿼리가 그 행을 먼저 나열했는지에 따라 달라집니다"
            }
          ]
        },
        {
          "question": "sizes 테이블이 3행을 가지고 colors 테이블이 4행을 가집니다. SELECT * FROM sizes CROSS JOIN colors는 몇 행을 반환할까요?",
          "options": [
            {
              "icon": "",
              "label": "7 — CROSS JOIN은 row counts를 더합니다"
            },
            {
              "icon": "",
              "label": "12 — CROSS JOIN은 두 테이블의 모든 행 조합을 반환합니다 (3 x 4)"
            },
            {
              "icon": "",
              "label": "3 — CROSS JOIN은 첫 번째 테이블의 행마다 한 행을 반환합니다"
            },
            {
              "icon": "",
              "label": "0 — CROSS JOIN은 ON 조건이 필요하며, 없으면 아무것도 반환하지 않습니다"
            }
          ]
        },
        {
          "question": "orders 테이블은 order #100에 대해 1행을 가지고, order_items 테이블은 order #100에 대해 3행(라인 아이템당 1행)을 가집니다. SELECT o.id, o.total FROM orders o JOIN order_items i ON o.id = i.order_id WHERE o.id = 100을 실행합니다. order #100에 대해 몇 행이 반환될까요?",
          "options": [
            {
              "icon": "",
              "label": "3 — join은 매칭되는 order_items 행마다 출력 행을 하나씩 만들어내므로, order #100의 단일 행이 라인 아이템 수만큼 반복됩니다"
            },
            {
              "icon": "",
              "label": "1 — orders에는 order #100에 대한 행이 하나뿐이므로, join이 그보다 많은 행을 만들어낼 수 없습니다"
            },
            {
              "icon": "",
              "label": "4 — order에 대한 1행에 라인 아이템당 1행씩을 더한 것입니다"
            },
            {
              "icon": "",
              "label": "0 — unique하지 않은 key로 1행짜리 테이블을 3행짜리 테이블과 join하면 실패합니다"
            }
          ]
        },
        {
          "question": "SELECT c.name, o.id FROM customers c RIGHT JOIN orders o ON c.id = o.customer_id와 동등한 것은?",
          "options": [
            {
              "icon": "",
              "label": "SELECT c.name, o.id FROM customers c INNER JOIN orders o ON c.id = o.customer_id"
            },
            {
              "icon": "",
              "label": "SELECT c.name, o.id FROM orders o LEFT JOIN customers c ON c.id = o.customer_id — 테이블 순서를 바꾸고 RIGHT JOIN 대신 LEFT JOIN을 쓰면 동등합니다"
            },
            {
              "icon": "",
              "label": "SELECT c.name, o.id FROM customers c LEFT JOIN orders o ON c.id = o.customer_id"
            },
            {
              "icon": "",
              "label": "SELECT c.name, o.id FROM orders o CROSS JOIN customers c"
            }
          ]
        },
        {
          "question": "standard SQL에서 SELECT department, name, AVG(salary) FROM employees GROUP BY department를 실행합니다. 이것은 유효한 쿼리일까요?",
          "options": [
            {
              "icon": "",
              "label": "예 — department가 SELECT에서 첫 번째로 나열되어 있으므로 GROUP BY에는 department만 포함하면 됩니다"
            },
            {
              "icon": "",
              "label": "아니요 — name이 SELECT에 있지만 집계되지도, GROUP BY에 나열되지도 않았습니다. standard SQL은 집계되지 않은 모든 selected column이 GROUP BY 절에 나타나야 합니다"
            },
            {
              "icon": "",
              "label": "예 — SQL이 department마다 임의의 name 값을 하나씩 자동으로 골라줍니다"
            },
            {
              "icon": "",
              "label": "아니요 — AVG()는 같은 쿼리에서 GROUP BY와 함께 사용할 수 없습니다"
            }
          ]
        },
        {
          "question": "average salary가 80000을 초과하는 departments를 원합니다. grouping 이후에 AVG(salary) 같은 집계값을 필터링하는 절은 WHERE일까요, HAVING일까요?",
          "options": [
            {
              "icon": "",
              "label": "WHERE — HAVING은 UNION과만 사용됩니다"
            },
            {
              "icon": "",
              "label": "aggregate 함수와 함께라면 둘 다 동일하게 작동합니다"
            },
            {
              "icon": "",
              "label": "둘 다 아닙니다 — aggregate 필터링에는 subquery가 필요합니다"
            },
            {
              "icon": "",
              "label": "HAVING — WHERE는 grouping이 일어나기 전에 개별 행을 필터링하고, HAVING은 집계 이후에 그룹을 필터링합니다"
            }
          ]
        },
        {
          "question": "SELECT salary * 1.1 AS new_salary FROM employees WHERE new_salary > 50000을 작성합니다. 이 쿼리는 실행될까요?",
          "options": [
            {
              "icon": "",
              "label": "예 — SELECT에서 정의된 aliases는 같은 쿼리의 WHERE에서 항상 사용할 수 있습니다"
            },
            {
              "icon": "",
              "label": "예, 하지만 숫자형 alias에만 해당합니다"
            },
            {
              "icon": "",
              "label": "아니요 — WHERE로 필터링하는 쿼리 안에서는 AS를 사용할 수 없습니다"
            },
            {
              "icon": "",
              "label": "아니요 — WHERE는 SELECT가 new_salary라는 alias를 할당하기 전에 평가되므로, 그 시점에는 아직 alias가 존재하지 않습니다"
            }
          ]
        },
        {
          "question": "SELECT name FROM employees e WHERE salary > (SELECT AVG(salary) FROM employees WHERE department = e.department). 이 쿼리를 correlated subquery라고 부르는 이유는 무엇일까요?",
          "options": [
            {
              "icon": "",
              "label": "JOIN 대신 WHERE 절을 사용하기 때문입니다"
            },
            {
              "icon": "",
              "label": "inner subquery가 outer query의 e.department를 참조하므로, outer query가 검토하는 모든 행마다 다시 평가되어야 하기 때문입니다"
            },
            {
              "icon": "",
              "label": "둘 이상의 column을 반환하기 때문입니다"
            },
            {
              "icon": "",
              "label": "outer query가 시작되기 전에 정확히 한 번만 실행되기 때문입니다"
            }
          ]
        },
        {
          "question": "서브쿼리 SELECT manager_id FROM employees는 실제 id들과 함께 일부 NULL 값도 반환합니다. SELECT name FROM employees WHERE id NOT IN (SELECT manager_id FROM employees)를 실행하면 무슨 일이 일어날까요?",
          "options": [
            {
              "icon": "",
              "label": "manager가 아닌 모든 employees를 반환합니다 — NULL은 무시됩니다"
            },
            {
              "icon": "",
              "label": "0행을 반환합니다 — NOT IN 목록에 NULL이 하나라도 있으면 모든 비교가 UNKNOWN이 되어, 어떤 행도 조건을 만족할 수 없습니다"
            },
            {
              "icon": "",
              "label": "오류가 발생합니다 — NOT IN은 subquery와 함께 사용할 수 없습니다"
            },
            {
              "icon": "",
              "label": "모든 employees를 반환합니다 — NULL이 wildcard처럼 취급되기 때문입니다"
            }
          ]
        },
        {
          "question": "SELECT name FROM customers c WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id)는 무엇을 반환할까요?",
          "options": [
            {
              "icon": "",
              "label": "orders에 최소 1개의 행이 있는 모든 customers — EXISTS는 subquery가 행을 반환하는지만 확인할 뿐, 그 값이 무엇인지는 보지 않습니다"
            },
            {
              "icon": "",
              "label": "모든 customers — SELECT 1은 항상 true를 반환하기 때문입니다"
            },
            {
              "icon": "",
              "label": "오류 — subquery가 column 이름 대신 숫자를 선택했기 때문입니다"
            },
            {
              "icon": "",
              "label": "order를 정확히 1건만 가진 customers만"
            }
          ]
        },
        {
          "question": "SELECT name, (SELECT order_id FROM orders WHERE customer_id = c.id) AS last_order FROM customers c를 작성했는데, 특정 customer가 orders에 3개의 행을 가지고 있습니다. 이 쿼리를 실행하면 어떻게 될까요?",
          "options": [
            {
              "icon": "",
              "label": "가장 먼저 매칭된 order_id를 반환하고, 나머지 두 개는 조용히 무시합니다"
            },
            {
              "icon": "",
              "label": "세 개의 order_id를 모두 comma로 구분된 목록으로 반환합니다"
            },
            {
              "icon": "",
              "label": "그 customer에 대해 order당 1행씩, 총 3행을 반환합니다"
            },
            {
              "icon": "",
              "label": "실행 시점에 오류가 발생합니다 — SELECT list의 scalar subquery는 최대 1행만 반환할 수 있는데, 이 subquery는 3행을 반환하기 때문입니다"
            }
          ]
        },
        {
          "question": "4행이 최고 score로 동점입니다. RANK() ORDER BY score DESC를 사용하면 이 네 행은 모두 rank 1을 받습니다. 그다음 행은 어떤 rank를 받을까요?",
          "options": [
            {
              "icon": "",
              "label": "5 — RANK()는 이어지는 순위 전에 동점 행 개수만큼 gap을 남깁니다"
            },
            {
              "icon": "",
              "label": "2 — RANK()는 동점 이후 항상 정확히 1씩 증가합니다"
            },
            {
              "icon": "",
              "label": "1 — 그 뒤에 오는 모든 행도 rank 1을 받습니다"
            },
            {
              "icon": "",
              "label": "4 — RANK()는 동점 개수부터 다시 세기 시작합니다"
            }
          ]
        },
        {
          "question": "4행이 최고 score로 동점입니다. DENSE_RANK() ORDER BY score DESC를 사용하면 이 네 행은 모두 rank 1을 받습니다. 그다음 행은 어떤 rank를 받을까요?",
          "options": [
            {
              "icon": "",
              "label": "5 — DENSE_RANK()는 RANK()처럼 정확히 작동합니다"
            },
            {
              "icon": "",
              "label": "1 — DENSE_RANK()는 남은 모든 행에 같은 rank를 할당합니다"
            },
            {
              "icon": "",
              "label": "2 — DENSE_RANK()는 gap을 남기지 않아서, 다음으로 구별되는 값은 항상 바로 다음 연속 rank를 받습니다"
            },
            {
              "icon": "",
              "label": "3 — DENSE_RANK()는 동점 그룹당 rank를 하나씩 건너뜁니다"
            }
          ]
        },
        {
          "question": "SUM(amount) OVER (PARTITION BY region ORDER BY amount)가 쿼리에 추가됩니다. 여기서 PARTITION BY region은 무엇을 할까요?",
          "options": [
            {
              "icon": "",
              "label": "전체 결과에 걸쳐 누적하는 대신, 각 region별로 running sum을 따로 다시 시작합니다"
            },
            {
              "icon": "",
              "label": "결과를 단일 region으로 필터링합니다"
            },
            {
              "icon": "",
              "label": "GROUP BY처럼 행들을 region당 하나로 묶고 접습니다"
            },
            {
              "icon": "",
              "label": "합산하기 전에 region을 알파벳 순으로 정렬합니다"
            }
          ]
        },
        {
          "question": "ROW_NUMBER() OVER (ORDER BY score DESC)가 5개의 행에 적용되고, 그중 2개는 score가 정확히 동점입니다. 두 행이 같은 row number를 받을 수 있을까요?",
          "options": [
            {
              "icon": "",
              "label": "예 — 동점인 행은 항상 같은 row number를 공유합니다"
            },
            {
              "icon": "",
              "label": "PARTITION BY도 함께 사용할 때만 그렇습니다"
            },
            {
              "icon": "",
              "label": "동점이 첫 번째 위치인지 마지막 위치인지에 따라 달라집니다"
            },
            {
              "icon": "",
              "label": "아니요 — ROW_NUMBER()는 값이 동점이더라도 모든 행에 고유하고 엄격하게 증가하는 정수를 부여합니다"
            }
          ]
        },
        {
          "question": "WITH high_earners AS (SELECT * FROM employees WHERE salary > 100000) SELECT department, COUNT(*) FROM high_earners GROUP BY department. high_earners는 무엇일까요?",
          "options": [
            {
              "icon": "",
              "label": "common table expression (CTE) — 이름이 붙은 임시 result set으로, 나머지 쿼리에서 테이블처럼 참조할 수 있습니다"
            },
            {
              "icon": "",
              "label": "데이터베이스에 생성된 permanent table입니다"
            },
            {
              "icon": "",
              "label": "쿼리가 끝난 후에도 유지되는 view입니다"
            },
            {
              "icon": "",
              "label": "따로 호출해야 하는 stored procedure입니다"
            }
          ]
        },
        {
          "question": "column이 PRIMARY KEY로 선언됩니다. 그 column이 NULL인 행을 insert할 수 있을까요?",
          "options": [
            {
              "icon": "",
              "label": "예 — PRIMARY KEY는 고유성(uniqueness)만 강제할 뿐, NULL 여부는 강제하지 않습니다"
            },
            {
              "icon": "",
              "label": "예, 하지만 UNIQUE 제약과 마찬가지로 NULL 행은 하나만 허용됩니다"
            },
            {
              "icon": "",
              "label": "아니요 — PRIMARY KEY column은 암묵적으로 NOT NULL이므로, 여기에 NULL을 insert하면 거부됩니다"
            },
            {
              "icon": "",
              "label": "그 column에 default 값이 있는지 여부에 따라 달라집니다"
            }
          ]
        },
        {
          "question": "열려 있는 transaction 안에서 UPDATE를 실행했지만 아직 COMMIT을 하지 않았습니다. 데이터베이스에 별도로 연결된 두 번째 connection에서는 이 update가 보일까요?",
          "options": [
            {
              "icon": "",
              "label": "예 — 모든 connection이 모든 write를 실행되는 즉시 봅니다"
            },
            {
              "icon": "",
              "label": "예, 하지만 두 번째 connection도 transaction을 열었을 때만 그렇습니다"
            },
            {
              "icon": "",
              "label": "어느 테이블이 업데이트되었는지에 따라서만 달라집니다"
            },
            {
              "icon": "",
              "label": "아니요 — 커밋되지 않은 변경은 그것을 만든 transaction 안에서만 보이며, COMMIT이 그 변경을 영구적으로 만들고 다른 사람에게 보이게 할 때까지는 보이지 않습니다"
            }
          ]
        },
        {
          "question": "products.category_id에는 categories.id를 참조하는 FOREIGN KEY constraint가 있습니다. ON DELETE 규칙이 지정되지 않은 상태에서, 여전히 products가 가리키고 있는 categories의 행을 DELETE하려고 하면 무슨 일이 일어날까요?",
          "options": [
            {
              "icon": "",
              "label": "category row가 삭제되고, 매칭되는 products.category_id 값들은 자동으로 NULL로 설정됩니다"
            },
            {
              "icon": "",
              "label": "category row가 삭제되고, 그것을 참조하던 모든 product도 함께 삭제됩니다"
            },
            {
              "icon": "",
              "label": "DELETE가 거부됩니다 — 기본 foreign key 동작은 의존하는 행이 여전히 가리키고 있는 참조된 행을 삭제하지 못하도록 막습니다"
            },
            {
              "icon": "",
              "label": "DELETE는 조용히 성공하고, products의 category_id는 더 이상 존재하지 않는 category를 계속 가리키게 됩니다"
            }
          ]
        }
      ],
      "optionOrderVersion": "e0466e547cfd8a45",
      "results": {
        "expert": {
          "recommendation": "이제 도구와 리뷰에서의 이득이 있습니다: 쿼리 플랜 읽기와 의도하지 않은 풀 테이블 스캔 발견, 그룹화와 파티셔닝 전략의 성능상 영향, 다른 사람의 쿼리에서 N+1 또는 팬아웃 문제 정확히 찾기. 역할로 선발되고 있다면, SQL 문법을 열거하기보다 이런 버그를 찾고 고친 사례를 설명하세요 — 문법 지식이 아니라 추론 능력을 보여줍니다.",
          "name": "전문가",
          "desc": "모든 섹션에서 최고점을 받았습니다 — 필터링/NULL, JOIN, 집계, 윈도우 함수/제약/트랜잭션. 실제로는 낯선 쿼리를 받았을 때 그것이 어떤 결과를 반환할 것인지 그리고 왜 그렇게 되는지 설명할 수 있다는 뜻입니다. 구문만으로는 아니라 실제로 훨씬 더 어렵고 가치 있는 기술입니다. 이 수준에서는 순수 쿼리 로직이 당신의 작업의 제한 요소인 경우가 거의 없습니다. 보통 제약은 스키마 설계나 인덱스 전략입니다."
        },
        "intermediate": {
          "recommendation": "JOIN과 집계의 상호작용에 집중하세요: 왜 LEFT JOIN 이후 집계가 행을 복제하는지, 집계 함수에서 NULL을 어떻게 처리하는지, HAVING 절과 WHERE 절의 차이. 그다음 CASE 식 안의 조건부 집계, 윈도우 함수의 PARTITION과 ORDER BY가 결과에 미치는 영향.",
          "name": "중급자",
          "desc": "일상적인 쿼리를 편하게 작성하고 읽으며, JOIN, 기본 집계, 간단한 WHERE 절을 잘 이해합니다. 기존 데이터베이스에서 루틴 유지보수나 작은 기능 추가 쿼리를 작성할 때 느려질 일은 없을 것입니다. 여기서 고급 수준까지의 격차는 주로 여러 SQL 기능이 함께 작용할 때의 상호작용입니다. 예를 들어, LEFT JOIN과 집계가 만나면서 팬아웃(fan-out) 문제가 생기는 경우, 서브쿼리의 NULL이 전체 결과를 바꾸는 경우, GROUP BY와 윈도우 함수의 미묘한 차이 같은 것들입니다. 이런 종류의 버그는 경력 많은 리뷰어라면 3초에 지적하지만, 다른 사람들은 30분을 들여도 찾지 못합니다."
        },
        "beginner": {
          "recommendation": "이 세 가지부터 시작하세요 (이 순서대로): WHERE 절에서 NULL 비교의 동작 방식, INNER JOIN과 LEFT JOIN의 차이 (그리고 WHERE 절이 이를 어떻게 바꾸는지), NOT IN 이 NULL과 함께하면 왜 항상 거짓인지. 모든 주요 SQL 문서와 대부분의 고급 튜토리얼이 이 세 가지를 명시적으로 다루고 있습니다.",
          "name": "초급자",
          "desc": "기본적인 SQL 쿼리를 따라 읽을 수 있지만, 놓친 문제들은 SQL이 조용히 행동하는 방식 때문입니다. 예를 들어, NULL 값과 비교하면 항상 거짓을 반환하지만 오류도 발생하지 않는 것, LEFT JOIN이 WHERE 절의 필터 때문에 INNER JOIN처럼 동작하는 것, NOT IN에 NULL이 포함되면 결과가 항상 비어있는 것 같은 것들입니다. 이것들은 쿼리를 못 읽어서가 아니라 데이터베이스가 실제로 하는 일과 읽기만 한 것의 차이를 모르기 때문입니다. 이런 것들은 코드 리뷰에서 10초에 지적되지만, 모르면 한 시간을 낭비하게 됩니다."
        },
        "advanced": {
          "recommendation": "인덱스와 성능에 차단되지 않는 범위 안에서 데이터 무결성 부분으로 나아가세요: 제약 조건 (UNIQUE, CHECK, FOREIGN KEY)이 실제로 어떤 동작을 강제하고 어떻게 쿼리 성능에 영향을 주는지, 트랜잭션 격리 수준의 실제 의미 (READ UNCOMMITTED부터 SERIALIZABLE까지), 잠금과 데드락이 일어나는 조건. SQL Server와 PostgreSQL 문서의 심층 분석 섹션들이 이 주제들에 좋은 다음 단계입니다.",
          "name": "고급자",
          "desc": "대부분의 채용공고가 '강한 SQL'이라고 말할 때 의미하는 수준입니다. 이 수준에서는 쿼리를 읽고 정확히 예측할 수 있고, 팬아웃과 팬인을 직관적으로 이해하며, NULL이 어디서 나타나는지 알 수 있습니다. 또한 윈도우 함수, CTE, 트랜잭션 격리 수준 같은 고급 기능들도 자신 있게 사용할 수 있습니다. 이 등급과 최상위 등급을 구분하는 것은 방어적 작업입니다. 즉, 제약 조건(UNIQUE, FOREIGN KEY), 트리거, 잠금 전략이 관련되었을 때 두 가지 이상의 올바른 접근 방식 중 어느 것이 원하지 않는 동작을 할 수 있는지 아는 것입니다."
        }
      },
      "desc": "필터링과 NULL 의미론, JOIN, 집계, 윈도우 함수와 제약 조건에 관한 30개의 시나리오 문제입니다. 당신의 SQL 실력이 채용공고에서 말하는 '강한 SQL' 능력에 부합하는지 확인하세요.",
      "recommendation": "당신의 SQL 기술 프로필"
    }
  }
}
