지난 초급 3편에서는 UPDATE를 깊게 파 보면서 NULL 채우기, 조인 UPDATE, 그리고 WHERE 없는 UPDATE 사고와 복구까지 경험했습니다. 이번 편의 주인공은 INSERT입니다. 입문 7편에서 기본 문법은 이미 익혔으니, 오늘은 실무에서 정말 자주 쓰는 세 가지 확장 — 다중 행 INSERT, INSERT INTO ... SELECT, SELECT ... INTO — 를 정리하고, IDENTITY 열이 있는 테이블에 INSERT할 때의 주의점까지 챙겨 보겠습니다. 다만 진행 순서는 제목과 살짝 다르게 SELECT INTO부터 시작하겠습니다. 연습장을 먼저 만들어야 마음 놓고 INSERT를 던져 볼 수 있으니까요.
SQL·MES 전체 글 보기 →
SELECT ... INTO — 조회 결과가 그대로 새 테이블이 됩니다
SELECT INTO는 조회 결과를 담은 새 테이블을 그 자리에서 만들어 주는 명령입니다. CREATE TABLE로 그릇을 빚고 INSERT로 채우는 두 단계를 한 문장으로 끝내는 셈이죠. 사실 이 시리즈에서 "연습용 복사본을 만드는 명령"이라고 슬쩍 소개하고 써 왔던 바로 그 문장인데, 오늘 정식으로 배웁니다.
이번 실습의 시나리오는 이렇습니다. BookMart 운영팀이 "2만 원 이상 도서만 모아 프로모션 후보 목록을 만들어 달라"고 요청했다고 해 볼게요. 이제 Book에서 2만 원 이상 도서만 골라 BestBook이라는 새 테이블로 만들어 보겠습니다.
USE BookMart;
SELECT *
INTO BestBook
FROM Book
WHERE Price >= 20000;

메시지 탭에 "(3개 행 적용됨)"이 나왔습니다. 테이블 생성과 데이터 복사가 동시에 끝났다는 뜻입니다. 정말 만들어졌는지 바로 확인해 볼까요?
SELECT * FROM BookMart.dbo.BestBook;
잠깐, 위처럼 길게 쓸 필요 없이 같은 데이터베이스 안이니 짧게 조회하겠습니다.
SELECT * FROM BestBook;

차근차근 SQL(27,000원), 파이썬 업무 자동화(25,000원), 데이터베이스 설계 입문(29,500원) — 2만 원 이상 도서 3권이 그대로 복사됐습니다. 공교롭게도 전부 IT 카테고리네요.
여기서 SELECT INTO의 성질 두 가지를 꼭 기억해 두세요.
첫째, 대상 테이블이 이미 존재하면 실행되지 않습니다. 방금 그 문장을 한 번 더 실행하면 이런 오류가 납니다. 문구는 SQL Server 버전·언어 설정에 따라 조금 다를 수 있습니다.
메시지 2714, 수준 16, 상태 6, 줄 1
데이터베이스에 'BestBook'(이)라는 개체가 이미 있습니다.
둘째, 열 이름·데이터 형식·NULL 허용 여부는 복사되지만, 기본 키, 외래 키, CHECK, DEFAULT 같은 제약조건과 인덱스는 복사되지 않습니다. 원본 Book에서는 BookID가 기본 키라 중복 값이 거부되지만, BestBook에는 아무 제약이 없어서 같은 BookID를 두 번 넣어도 그냥 들어갑니다. "복사본이니까 구조도 똑같겠지"라고 안심하면 안 되는 이유입니다. 참고로 원본 열에 IDENTITY 속성이 있다면 그 속성만은 예외적으로 새 테이블에 따라옵니다.
다중 행 INSERT — VALUES 뒤에 괄호를 쉼표로
입문 7편에서 시드 데이터를 넣을 때 이미 써 본 문법이지만, 정식으로 정리하고 갑니다. VALUES 뒤에 (값들) 괄호를 쉼표로 이어 붙이면 한 문장으로 여러 행이 들어갑니다. 한 문장의 VALUES로는 최대 1,000행까지 넣을 수 있다는 제한도 알아 두세요(그보다 많으면 문장을 나누거나 다음 절의 INSERT SELECT 같은 방법을 씁니다).
프로모션 후보에 아직 Book 테이블에 등록되지 않은 신간 2권을 미리 추가해 달라는 요청이 들어왔다고 하고, BestBook에 넣어 보겠습니다.
INSERT INTO BestBook (BookID, Title, Author, CategoryID, Price, PubDate, Stock)
VALUES
(11, N'실전 쿼리 튜닝', N'정모델', 3, 32000, '2025-06-10', 7),
(12, N'느긋한 캠핑 요리', N'최미라', 4, 21000, '2025-05-28', 12);

"(2개 행 적용됨)" — 성공입니다. 열 목록을 생략하지 않는 습관, nvarchar 열의 N 접두사도 입문 편에서 배운 그대로입니다. 확인 조회는 다음 절의 INSERT까지 마친 뒤 한꺼번에 하겠습니다.
INSERT INTO ... SELECT — 조회 결과를 기존 테이블에 붓기
이번에는 손으로 값을 타이핑하는 대신, 다른 테이블을 조회한 결과를 통째로 밀어 넣어 보겠습니다. INSERT INTO ... SELECT는 VALUES 자리에 SELECT 문이 들어간 형태로, 대상 테이블이 이미 존재해야 한다는 점이 SELECT INTO와 결정적으로 다릅니다.
운영팀이 "2만 원에 조금 못 미치는 도서도 후보에 넣자"고 해서, Book에서 18,000원 이상 2만 원 미만 도서를 BestBook에 편입해 보겠습니다.
INSERT INTO BestBook (BookID, Title, Author, CategoryID, Price, PubDate, Stock)
SELECT BookID, Title, Author, CategoryID, Price, PubDate, Stock
FROM Book
WHERE Price >= 18000 AND Price < 20000;

"(2개 행 적용됨)"이 나왔습니다. INSERT 쪽 열 목록과 SELECT 쪽 열 목록은 개수와 순서, 데이터 형식이 짝이 맞아야 합니다. 그리고 한 가지 주의 — 앞서 본 대로 BestBook에는 기본 키가 없으므로, 이 문장을 실수로 두 번 실행하면 같은 도서가 두 번 들어갑니다. 막아 주는 장치가 없으니 실행 횟수는 스스로 관리해야 합니다.
이제 두 번의 INSERT를 마쳤으니 BestBook의 전체 상태를 확인해 볼까요? 비싼 순서로 정렬해 보겠습니다.
SELECT BookID, Title, Author, Price
FROM BestBook
ORDER BY Price DESC;

처음 복사한 3권 + 다중 행 INSERT 2권 + INSERT SELECT 2권 = 총 7권. 32,000원 신간부터 18,000원 요리책까지 프로모션 후보 목록이 완성됐습니다.
두 방법을 표로 비교하면 이렇습니다.
구분INSERT INTO ... SELECTSELECT ... INTO
| 대상 테이블 | 이미 존재해야 함 | 없어야 함(새로 생성됨) |
| 테이블 구조 | 기존 구조 그대로 | 조회 결과의 열로 자동 결정 |
| 제약조건 | 기존 테이블의 제약이 그대로 적용 | 복사되지 않음(직접 추가 필요) |
| 주 용도 | 집계 결과 적재, 테이블 간 데이터 이동 | 백업·연습용 복사본, 임시 분석 테이블 |
IDENTITY 열이 있는 테이블에 INSERT할 때
BookMart에서 OrderItem의 기본 키 OrderItemID는 입문 6편에서 IDENTITY(1,1)로 만들었습니다. 번호를 SQL Server가 자동으로 채워 주니, INSERT할 때는 열 목록에서 아예 빼는 것이 기본입니다. 그런데 만약 번호를 직접 지정하려고 하면 어떻게 될까요? 실행해 봐도 데이터는 안전하게 거부되니, 한번 시도해 보겠습니다.
INSERT INTO OrderItem (OrderItemID, OrderID, BookID, Qty, UnitPrice)
VALUES (99, 1001, 1, 1, 15800);
역시 문구는 버전·언어 설정에 따라 다를 수 있지만, 이런 오류가 납니다.
메시지 544, 수준 16, 상태 1, 줄 1
IDENTITY_INSERT가 OFF로 설정되면 테이블 'OrderItem'의 ID 열에 명시적 값을 삽입할 수 없습니다.
INSERT가 통째로 거부됐으므로 OrderItem에는 아무 변화도 없습니다. 오류 메시지가 힌트를 주고 있죠 — 정말로 명시적 값을 넣어야 하는 상황(다른 시스템에서 데이터를 이관할 때 등)에서는 SET IDENTITY_INSERT라는 스위치를 잠깐 켰다가 끄면 됩니다.
SET IDENTITY_INSERT OrderItem ON;
-- 이 사이에서만 OrderItemID에 명시적 값을 넣을 수 있습니다 (열 목록 명시 필수)
SET IDENTITY_INSERT OrderItem OFF;
한 세션에서 이 스위치를 ON으로 둘 수 있는 테이블은 한 번에 하나뿐이고, 작업이 끝나면 반드시 OFF로 되돌려야 합니다. 오늘은 원본 데이터를 지키기 위해 실제로 넣지는 않고, "이런 스위치가 있다" 정도만 기억해 두겠습니다.
실습 정리 — BestBook 삭제
BestBook은 오늘 실습을 위해 만든 연습 테이블이니 삭제해서 원상태로 정리하겠습니다.
DROP TABLE BestBook;

"명령이 완료되었습니다."가 나오면 정리 끝입니다. 오늘 원본 테이블에는 손대지 않았고(OrderItem INSERT는 오류로 거부), SELECT COUNT(*) FROM Book;을 실행해 보면 여전히 10 — BookMart는 시드 데이터 그대로입니다.
오늘의 정리
- 다중 행 INSERT는 VALUES 뒤 괄호를 쉼표로 이어 붙이는 방식이고, 한 문장에 최대 1,000행까지 가능합니다.
- INSERT INTO ... SELECT는 조회 결과를 이미 있는 테이블에 붓는 명령 — 열 개수·순서·형식이 짝이 맞아야 합니다.
- SELECT ... INTO는 새 테이블 생성과 복사를 한 번에 처리하지만, 제약조건과 인덱스는 복사되지 않음을 기억하세요(NULL 허용 여부와 IDENTITY 속성은 따라옴).
- IDENTITY 열은 열 목록에서 빼면 자동 번호가 채워지고, 명시적 값이 꼭 필요할 때만 SET IDENTITY_INSERT를 잠깐 켰다가 끕니다.
- 연습 테이블은 DROP TABLE로 정리 — 원본 시드 데이터는 언제나 그대로 유지.
다음 편 예고
INSERT를 자유자재로 다루게 됐으니, 다음 초급 5편에서는 데이터 형식 사이를 오가는 기술 — 형변환 제대로: CAST, CONVERT, 그리고 함정 — 을 다룹니다. 날짜를 원하는 모양으로 출력하는 스타일 코드부터, 문자를 숫자로 바꾸다 터지는 오류와 TRY_CAST까지 알아보겠습니다.
'SQL · MES' 카테고리의 다른 글
| MSSQL SQL 연산자와 CASE 문 — 조건별 값 분류 [MSSQL 22] (0) | 2026.07.16 |
|---|---|
| MSSQL CAST와 CONVERT 차이 — 문자열·숫자·날짜 형변환 [MSSQL 21] (0) | 2026.07.16 |
| MSSQL UPDATE 사용법 — 여러 조건으로 안전하게 수정하기 [MSSQL 19] (0) | 2026.07.16 |
| MSSQL 제약조건 — NOT NULL·UNIQUE·CHECK·DEFAULT [MSSQL 18] (0) | 2026.07.16 |
| MSSQL 테이블 컬럼 추가·삭제·변경하기 — ALTER TABLE 사용법 [MSSQL 17] (0) | 2026.07.16 |