제조 데이터 · 설비 자동화

현장에서 얻은 데이터를
쓸 수 있는 정보로 정리합니다.

PLC, Python, SQL, Tableau와 예지보전을 실제 설비기술 업무의 흐름 안에서 기록합니다.

MSSQL 쿼리 디버깅 — 오류와 잘못된 결과 점검 순서 [MSSQL 28]

지난 11편에서는 서브쿼리를 SELECT절과 FROM절까지 확장하며 서브쿼리 3부작을 마무리했습니다. 마지막 편의 주제는 조금 특별합니다. 새 문법을 배우는 대신, 빨간 오류 메시지를 읽고 스스로 고치는 힘을 기릅니다. 그리고 초급 1~11편에서 배운 내용을 골고루 섞은 종합 연습문제 10선으로 시리즈를 마무리하겠습니다. 준비되셨다면, 일부러 틀린 쿼리부터 실행해 볼까요?

시리즈 안내 · 이 글은 MSSQL 28입니다.
SQL·MES 전체 글 보기 →

오류 메시지, 겁내지 말고 읽어 봅시다

SSMS의 빨간 오류 메시지는 사실 꽤 친절한 안내문입니다. 구조만 알면 절반은 해결된 셈입니다.

  • 메시지 번호: 오류의 종류를 나타내는 고유 번호입니다(예: 207 = 잘못된 열 이름). 검색할 때 유용합니다.
  • 수준(Severity): 심각도입니다. 우리가 만나는 문법·이름 오류는 대부분 수준 15~16입니다.
  • 줄(Line): 오류가 난 위치입니다. 쿼리 창에서 해당 줄부터 살펴보면 됩니다.

참고로 이 글에 인용한 오류 문구는 SQL Server 버전과 언어 설정에 따라 조금씩 다를 수 있습니다. 번호와 구조는 같으니 흐름 위주로 봐 주세요.

자주 만나는 오류를 다섯 가지로 정리하면 이렇습니다.

유형대표 증상원인 한 줄

1. 열 이름 오타 메시지 207 "열 이름이 잘못되었습니다" Titel처럼 철자 실수, 또는 다른 테이블의 열 이름 사용
2. 별칭을 WHERE에서 사용 역시 메시지 207 WHERE가 SELECT보다 먼저 처리되어 별칭을 아직 모름
3. GROUP BY 규칙 위반 메시지 8120 SELECT에 집계 함수도 GROUP BY 대상도 아닌 열이 있음
4. 문자열에 큰따옴표 메시지 207 (문자열이 열 이름으로 해석됨) 문자열은 작은따옴표, 한글 문자열은 N'...'
5. 조인 조건 누락 오류 없음, 행 수 폭발(카티전 곱) ON 절 누락 또는 옛날식 콤마 조인

1번 오타는 금방 이해되실 겁니다. 예를 들어 SELECT Titel FROM Book;을 실행하면 이렇게 나옵니다.

메시지 207, 수준 16, 상태 1, 줄 1
열 이름 'Titel'이(가) 잘못되었습니다.

4번 따옴표도 짧게 보고 가겠습니다. WHERE City = "서울"처럼 큰따옴표를 쓰면 SQL Server는 "서울"을 문자열이 아니라 열 이름으로 해석합니다. 그래서 "열 이름 '서울'이(가) 잘못되었습니다"라는, 처음 보면 어리둥절한 오류가 납니다. 문자열은 반드시 작은따옴표('서울'), nvarchar 열이라면 N'서울'이 정답입니다.

이제 나머지 세 유형(2·3·5번)을 "잘못된 쿼리 → 원인 → 고친 쿼리" 순서로 하나씩 훈련해 보겠습니다.

훈련 1 — 별칭을 WHERE에서 쓰면 왜 안 될까요

3,000원 할인 행사를 가정하고, 할인 후 가격이 22,000원 이상인 도서를 찾아보겠습니다. 아래 쿼리, 어디가 문제일까요?

SELECT Title, Price, Price - 3000 AS SalePrice
FROM Book
WHERE SalePrice >= 22000;

실행하면 이런 오류가 납니다.

메시지 207, 수준 16, 상태 1, 줄 3
열 이름 'SalePrice'이(가) 잘못되었습니다.

분명히 SELECT에서 SalePrice라는 별칭을 만들었는데 왜 모른다고 할까요? 비밀은 논리적 처리 순서에 있습니다. SQL Server는 쿼리를 쓰인 순서가 아니라 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY 순서로 처리합니다. WHERE를 평가하는 시점에는 SELECT가 아직 실행되지 않아 별칭이 존재하지 않는 것입니다. 반대로 ORDER BY는 SELECT 이후에 처리되므로 별칭을 쓸 수 있습니다.

원인을 알았으니 WHERE에는 별칭 대신 원래 식을 넣어 고쳐 보겠습니다.

SELECT Title, Price, Price - 3000 AS SalePrice
FROM Book
WHERE Price - 3000 >= 22000
ORDER BY SalePrice DESC;   -- ORDER BY에서는 별칭 사용 가능

결과는 세 권입니다.

TitlePriceSalePrice

데이터베이스 설계 입문 29500 26500
차근차근 SQL 27000 24000
파이썬 업무 자동화 25000 22000

훈련 2 — GROUP BY 규칙 위반

이번에는 카테고리별 도서 수를 세면서 제목도 같이 보고 싶었던 쿼리입니다.

SELECT CategoryID, Title, COUNT(*) AS BookCount
FROM Book
GROUP BY CategoryID;
메시지 8120, 수준 16, 상태 1, 줄 1
'Book.Title' 열이 집계 함수나 GROUP BY 절에 없으므로 SELECT 목록에서 사용할 수 없습니다.

원인은 초급 7편에서 다룬 그 규칙입니다. GROUP BY를 쓰면 결과는 그룹당 한 행인데, 카테고리 하나에는 도서 제목이 여러 개라서 어느 제목을 보여줘야 할지 SQL Server가 결정할 수 없습니다. 그래서 SELECT에는 ① GROUP BY에 있는 열이나 ② 집계 함수만 올 수 있습니다. 제목 대신 그룹을 요약하는 집계로 고쳐서 실행해 보겠습니다.

SELECT CategoryID, COUNT(*) AS BookCount, SUM(Price) AS PriceSum
FROM Book
GROUP BY CategoryID
ORDER BY CategoryID;

카테고리 다섯 그룹이 한 행씩, 도서 수와 가격 합계로 깔끔하게 요약되었습니다. 만약 "그룹당 대표 한 행"이 정말 필요하다면 초급 9편의 상관 서브쿼리가 답이 됩니다(잠시 뒤 연습문제 4번에서 다시 만납니다).

훈련 3 — 조인 조건 누락, 오류 없이 틀리는 무서운 실수

마지막 유형이 가장 무섭습니다. 오류가 나지 않기 때문입니다. 회원별 주문 목록을 뽑으려던 쿼리입니다.

SELECT m.Name, o.OrderID
FROM Member AS m, Orders AS o;   -- 옛날식 콤마 조인, 조인 조건 없음

실행하면 빨간 메시지 대신 결과 그리드가 멀쩡히 나옵니다. 그런데 행 수가 이상합니다. 회원 8명 × 주문 10건 = 80행. 모든 회원과 모든 주문을 기계적으로 짝지은 카티전 곱(Cartesian Product)입니다. "김민준"이 남의 주문 1002번과도 짝지어진, 완전히 틀린 결과인데 오류는 없습니다. 실무에서 집계 금액이 몇 배로 뻥튀기되는 사고가 대부분 이 패턴입니다. 결과가 나왔다고 안심하지 말고, 행 수가 예상과 맞는지 꼭 확인하는 습관이 필요합니다.

ON 절이 있는 표준 INNER JOIN으로 고쳐서 실행해 보겠습니다.

SELECT m.Name, o.OrderID, o.Status
FROM Member AS m
    INNER JOIN Orders AS o ON o.MemberID = m.MemberID
ORDER BY o.OrderID;

이번에는 정확히 10행 — 주문 건수와 일치합니다. 참고로 INNER JOIN 구문에서 ON을 빼먹으면 구문 오류가 나서 바로 알 수 있으니, 콤마 조인 대신 항상 JOIN ... ON 표준 구문을 쓰시는 것을 권합니다.

종합 연습문제 10선

이제 초급 시리즈 전체를 훑는 연습문제입니다. 괄호에 관련 편을 적어 두었으니 막히면 해당 편을 복습해 보세요. 정답은 바로 아래 "정답과 해설"에 모아 두었습니다 — 먼저 힌트만 보고 직접 풀어 보시길 권합니다. 문제 7~9는 원본을 건드리지 않도록 연습용 복사 테이블 BookPractice에서 진행합니다.

문제 1. (초급 6편) Book 테이블에서 Price가 25,000원 이상이면 '고가', 17,000원 이상이면 '중가', 그 미만이면 '저가'로 표시하는 PriceBand 열을 만들어 Title, Price와 함께 가격 내림차순으로 조회하세요.

  • 힌트: 검색 CASE는 WHEN을 위에서부터 순서대로 평가합니다.

문제 2. (초급 5편) BookID가 3 이하인 도서의 Title과, PubDate를 'yyyymmdd' 형식의 8자리 문자로 바꾼 열을 조회하세요.

  • 힌트: CONVERT의 스타일 코드 112.

문제 3. (초급 7편) 회원의 도시(City)와 주문 상태(Status) 조합별 주문 건수를 구하세요. 도시, 상태 순으로 정렬합니다.

  • 힌트: Member와 Orders를 조인한 뒤 GROUP BY에 열을 두 개 넣습니다.

문제 4. (초급 9편) 각 카테고리에서 가장 비싼 도서의 CategoryID, Title, Price를 조회하세요.

  • 힌트: 상관 서브쿼리 — 바깥 행의 CategoryID를 안쪽 MAX 계산에서 참조합니다.

문제 5. (초급 10편) 주문 이력이 전혀 없는 회원의 이름을 조회하세요.

  • 힌트: NOT EXISTS. (NOT IN을 쓴다면 NULL 함정을 떠올려 보세요.)

문제 6. (초급 11편) 전체 회원 이름과 각자의 주문 건수를 조회하세요. 주문이 없는 회원은 0으로 나와야 합니다.

  • 힌트: SELECT절의 스칼라 서브쿼리가 가장 간단합니다.

문제 7. (초급 1·4편) Book 테이블을 BookPractice라는 연습용 테이블로 복사한 뒤, NULL을 허용하는 Memo nvarchar(50) 열을 추가하세요.

  • 힌트: SELECT ... INTO로 복사, ALTER TABLE ... ADD로 열 추가.

문제 8. (초급 3편) BookPractice에서 Stock이 NULL인 행을 0으로 UPDATE하세요. 몇 행이 바뀌어야 할까요?

  • 힌트: NULL 비교는 = 이 아니라 IS NULL입니다.

문제 9. (초급 2편) BookPractice에 "Price는 0보다 커야 한다"는 CHECK 제약을 추가하고, 가격이 -1,000원인 행을 일부러 INSERT해서 제약이 막아 주는지 확인하세요.

  • 힌트: ALTER TABLE ... ADD CONSTRAINT ... CHECK. 오류가 나면 성공입니다.

문제 10. (종합) 취소된 주문을 제외하고, 회원별 총 주문 금액이 50,000원 이상인 회원의 이름과 총액을 총액 내림차순으로 조회하세요.

  • 힌트: 3개 테이블 조인 + WHERE(집계 전에 취소 제거) + GROUP BY + HAVING(집계 후 필터).

정답과 해설

정답 1.

SELECT Title, Price,
       CASE WHEN Price >= 25000 THEN N'고가'
            WHEN Price >= 17000 THEN N'중가'
            ELSE N'저가'
       END AS PriceBand
FROM Book
ORDER BY Price DESC;

10행이 반환되며 고가 3권(29,500·27,000·25,000원), 중가 3권(19,800·18,000·17,500원), 저가 4권(16,500·15,800·15,000·14,000원)으로 나뉩니다. 두 번째 WHEN에 "25,000 미만" 조건을 따로 쓰지 않아도 되는 이유는, 첫 WHEN을 통과하지 못한 행만 내려오기 때문입니다.

정답 2.

SELECT Title, CONVERT(char(8), PubDate, 112) AS PubDate8
FROM Book
WHERE BookID <= 3;

TitlePubDate8

바람의 도서관 20230520
아침의 습관 20240110
차근차근 SQL 20240315

정답 3.

SELECT m.City, o.Status, COUNT(*) AS OrderCount
FROM Member AS m
    INNER JOIN Orders AS o ON o.MemberID = m.MemberID
GROUP BY m.City, o.Status
ORDER BY m.City, o.Status;

CityStatusOrderCount

대전 배송중 1
부산 배송완료 2
부산 취소 1
서울 배송완료 4
서울 배송중 1
서울 주문접수 1

도시 4곳 × 상태 4종 = 16조합이 아니라, 실제 주문이 있는 6개 조합만 행이 됩니다. 주문이 없는 광주는 아예 나오지 않는다는 점도 확인해 보세요.

정답 4. 대표 문제로 실행 화면까지 확인해 보겠습니다.

SELECT b.CategoryID, b.Title, b.Price
FROM Book AS b
WHERE b.Price = (SELECT MAX(b2.Price)
                 FROM Book AS b2
                 WHERE b2.CategoryID = b.CategoryID)
ORDER BY b.CategoryID;

카테고리 5개마다 최고가 도서가 정확히 한 권씩, 총 5행입니다. 안쪽 서브쿼리가 바깥 행의 b.CategoryID를 참조하므로, 바깥 행이 바뀔 때마다 그 카테고리의 최고가를 다시 계산하는 것이 상관 서브쿼리의 핵심이었습니다.

정답 5.

SELECT m.Name
FROM Member AS m
WHERE NOT EXISTS (SELECT 1
                  FROM Orders AS o
                  WHERE o.MemberID = m.MemberID);

최수아, 윤지호 — 2행입니다. Orders의 MemberID에는 NULL이 없어 이번에는 NOT IN을 써도 같은 결과지만, NULL이 하나라도 섞이면 NOT IN은 빈 결과를 돌려준다는 것이 초급 10편의 교훈이었습니다.

정답 6.

SELECT m.Name,
       (SELECT COUNT(*)
        FROM Orders AS o
        WHERE o.MemberID = m.MemberID) AS OrderCount
FROM Member AS m
ORDER BY m.MemberID;

8행이 반환됩니다: 김민준 3, 이서연 2, 박지훈 1, 최수아 0, 정다은 2, 강하늘 1, 윤지호 0, 임세라 1. 주문이 없는 회원도 COUNT(*)가 0을 돌려주므로 목록에서 빠지지 않습니다.

정답 7. 이제 연습용 복사 테이블 차례입니다. 두 문장을 차례로 실행해 보겠습니다.

SELECT * INTO BookPractice FROM Book;
•
ALTER TABLE BookPractice ADD Memo nvarchar(50) NULL;

첫 문장은 메시지 탭에 "(10개 행 적용됨)", 두 번째는 "명령이 완료되었습니다."가 나오면 성공입니다. SELECT * FROM BookPractice;로 확인하면 기존 10행의 Memo 열은 모두 NULL로 채워져 있습니다 — 새로 추가한 열에는 아직 아무 값도 넣지 않았기 때문입니다.

정답 8.

UPDATE BookPractice
SET Stock = 0
WHERE Stock IS NULL;

메시지는 "(1개 행 적용됨)"입니다. 시드 데이터에서 Stock이 NULL인 도서는 주말 베이킹 한 권뿐이니까요. 만약 적용 행 수가 예상과 다르다면 WHERE 조건부터 의심하는 습관, 초급 3편에서 강조했던 그대로입니다.

정답 9.

ALTER TABLE BookPractice
ADD CONSTRAINT CK_BookPractice_Price CHECK (Price > 0);
•
INSERT INTO BookPractice (BookID, Title, Author, CategoryID, Price, PubDate, Stock)
VALUES (99, N'말도 안 되는 책', N'아무개', 1, -1000, '2025-01-01', 0);

INSERT에서 이런 오류가 나면 성공입니다.

메시지 547, 수준 16, 상태 0, 줄 4
INSERT 문이 CHECK 제약 조건 "CK_BookPractice_Price"과(와) 충돌했습니다.

제약조건이 잘못된 데이터를 문 앞에서 막아 준 것입니다. 오류가 항상 나쁜 소식은 아니라는 좋은 예입니다.

연습이 끝났으니 복사 테이블을 정리하겠습니다.

DROP TABLE BookPractice;

"명령이 완료되었습니다."가 나오면 정리 완료입니다. 문제 7~9의 변경은 전부 BookPractice에서만 이루어졌으므로, 원본 BookMart는 이번 편에서 단 한 번도 바뀌지 않았습니다. 시드 데이터 그대로입니다.

정답 10. 마지막 종합 문제도 실행 화면으로 확인해 보겠습니다.

SELECT m.Name, SUM(oi.Qty * oi.UnitPrice) AS TotalAmount
FROM Member AS m
    INNER JOIN Orders AS o ON o.MemberID = m.MemberID
    INNER JOIN OrderItem AS oi ON oi.OrderID = o.OrderID
WHERE o.Status <> N'취소'
GROUP BY m.Name
HAVING SUM(oi.Qty * oi.UnitPrice) >= 50000
ORDER BY TotalAmount DESC;

정다은 111,000원, 김민준 95,800원 — 2행입니다. 취소를 WHERE에서 걸렀기 때문에 이서연의 취소 주문 17,500원은 집계에 아예 들어가지 않았고, 그 결과 이서연은 총액 33,000원으로 명단에서 빠집니다. "집계 전 행 필터는 WHERE, 집계 후 그룹 필터는 HAVING" — 이 한 줄이 초급 7편과 이번 문제의 핵심입니다.

초급 시리즈 총정리 — 12편에서 배운 것

편제목핵심 한 줄

1 ALTER TABLE 운영 중인 테이블의 열 추가·수정·삭제, sp_rename
2 제약조건 완전정복 DEFAULT·CHECK·UNIQUE로 데이터 품질 지키기
3 UPDATE 심화 NULL 채우기, 조인 UPDATE, WHERE 없는 UPDATE 사고와 복구
4 INSERT 여러 방법 다중 행 INSERT, INSERT SELECT, SELECT INTO
5 형변환 제대로 CAST/CONVERT 스타일 코드, TRY_CAST로 안전하게
6 연산자와 CASE 우선순위(AND가 OR보다 먼저), CASE로 구간 나누기
7 GROUP BY 심화 다중 열 그룹핑, HAVING 실전, SELECT 열 규칙
8 서브쿼리 기본기 스칼라·IN 서브쿼리, 안쪽부터 바깥으로
9 상관 서브쿼리 바깥 행을 참조해 행마다 다시 계산
10 IN vs EXISTS 상호 변환, NOT IN의 NULL 함정
11 서브쿼리는 어디까지 SELECT절 스칼라 서브쿼리, FROM절 파생 테이블
12 오류 찾기 훈련 오류 5유형 진단법, 종합 연습문제 10선

두 시리즈를 돌아보며, 그리고 다음 단계

입문 16편에서 우리는 SQL Server 설치부터 시작해 BookMart라는 작은 서점을 직접 지었습니다. 테이블을 설계하고(PK/FK), 데이터를 넣고 고치고(INSERT/UPDATE/DELETE), SELECT·JOIN·GROUP BY로 원하는 답을 꺼내고, 마지막에는 백업으로 데이터를 지키는 법까지 익혔습니다. 이어진 초급 12편에서는 같은 BookMart를 무대로 테이블을 운영 중에 바꾸고(ALTER·제약조건), 형변환과 CASE로 데이터를 다듬고, 서브쿼리 3부작으로 "쿼리 속 쿼리"를 자유롭게 다루게 되었습니다. 합쳐서 28편 — 이제 여러분은 웬만한 실무 조회 요청 앞에서 손이 먼저 움직이는 단계에 와 있습니다.

다음 단계로는 중급 주제 세 가지를 추천합니다.

  • 윈도우 함수: ROW_NUMBER, RANK, SUM OVER처럼 "그룹으로 접지 않고" 행마다 순위·누적을 계산하는 도구입니다. 상관 서브쿼리로 힘들게 풀던 문제가 한 줄로 끝나는 경험을 하시게 됩니다.
  • 트랜잭션: BEGIN TRAN/COMMIT/ROLLBACK으로 여러 문장을 "전부 성공 아니면 전부 취소"로 묶는 기술입니다. 초급 3편의 UPDATE 사고 같은 상황에서 진짜 안전벨트가 됩니다.
  • 실행 계획: 같은 결과를 내는 쿼리라도 속도는 하늘과 땅 차이입니다. SSMS의 실행 계획을 읽으며 인덱스가 실제로 쓰이는지 확인하는 순간, 성능 튜닝의 문이 열립니다.

오늘의 정리

  • 오류 메시지는 번호·수준·줄 순서로 읽기 — 줄 번호가 위치를, 번호가 원인을 알려 줍니다.
  • 자주 나는 오류 5유형: 열 이름 오타, WHERE의 별칭 사용, GROUP BY 규칙 위반, 문자열 큰따옴표, 조인 조건 누락(카티전 곱).
  • 논리 처리 순서는 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY — 별칭은 ORDER BY에서만 사용 가능합니다.
  • 조인 조건 누락은 오류 없이 조용히 틀리므로, 결과 행 수가 예상과 맞는지 확인하는 습관이 필요합니다.
  • 파괴적인 연습은 복사 테이블(BookPractice)에서 하고 끝나면 DROP — 원본은 항상 깨끗하게 유지합니다.

공식 참고 자료

시리즈를 마치며

입문 1편의 "데이터베이스가 뭐길래"에서 시작해 여기까지 함께 달려오신 여러분, 정말 수고 많으셨습니다. 이제 오류 메시지가 빨갛게 떠도 당황하지 않고, 줄 번호를 찾고, 원인을 좁히고, 고친 쿼리를 다시 실행하는 자신을 발견하실 겁니다. 그 침착함이야말로 28편 동안 쌓은 가장 큰 자산입니다. BookMart는 언제든 다시 열려 있으니, 막히는 날엔 필요한 편을 펼쳐 복습해 보세요. 중급 주제에서 다시 만나 뵙기를 기대합니다. 감사합니다!