2부 · 데이터를 담는 그릇 (1) 테이블과 데이터 타입
← 1부 · 목차 · 다음: 관계와 외래키 →
1부에서 books 표를 대충 만들어 봤습니다. 이제 표를 제대로 짓습니다. 어떤 칸에 어떤 타입을 줄지, 각 행을 무엇으로 구별할지(기본키), 빈 값은 어떻게 다룰지를 배웁니다. 좋은 표 설계는 나중의 모든 질의를 쉽게 만들고, 나쁜 설계는 두고두고 발목을 잡습니다.
전체 그림에서 지금 위치:
flowchart LR
A["테이블 구조 정하기<br/>CREATE TABLE · 타입 · 기본키"] --> B[관계 정하기]
B --> C[행 추가]
C --> E[묻기]
classDef here fill:#e8d5f2,stroke:#9b59b6,color:#000,stroke-width:3px
classDef dim fill:#eee,stroke:#bbb,color:#888
class A here
class B,C,E dim
2.1 테이블·행·열, 세 단어
관계형 데이터베이스의 데이터는 모두 테이블(table) 에 담깁니다. 테이블은 엑셀 시트와 똑 닮았습니다.
← 열(column) = 한 종류의 정보
┌──────┬──────────┬───────────┬───────┐
│ id │ title │ author │ price │ ← 열 이름과 타입
├──────┼──────────┼───────────┼───────┤
행(row) → │ 1 │ 모비딕 │ 허먼 멜빌 │ 15000 │
│ 2 │ 데미안 │ 헤르만 헤세 │ 12000 │
└──────┴──────────┴───────────┴───────┘
- 열(column). 세로 칸. 하나의 열은 한 종류의 정보를 담습니다(
price는 전부 가격). 열마다 데이터 타입이 정해집니다. - 행(row). 가로 줄. 하나의 행은 하나의 대상을 표현합니다(한 행 = 한 권의 책). 레코드(record) 라고도 부릅니다.
- 테이블(table). 같은 종류의 대상들을 모은 표. 여기선 "책들의 모임".
설계란 결국 "어떤 테이블을 두고, 각 테이블에 어떤 열을, 어떤 타입으로 둘 것인가"를 정하는 일입니다.
2.2 데이터 타입: 칸에 무엇을 담을까
열을 만들 때 "이 칸엔 어떤 종류의 값이 들어온다"를 미리 정합니다. 가격 칸에 "비쌈" 같은 글자가 들어오면 곤란하니까요. SQLite의 타입은 단 5종으로 단순합니다.
| 타입 | 담는 것 | 예 |
|---|---|---|
INTEGER | 정수 | 15000, -3, 0 |
REAL | 소수(실수) | 4.5, 3.14 |
TEXT | 글자(문자열) | '모비딕', 'hana@mail.com' |
BLOB | 이진 데이터(이미지 등 원본 바이트) | (입문에선 거의 안 씀) |
NULL | "값이 없음" | (아래 2.5에서 따로 설명) |
문자열은 항상 작은따옴표로 감쌉니다('모비딕'). 큰따옴표("...")는 SQL에서 다른 용도(열 이름 표시)라 습관을 작은따옴표로 들이세요. 숫자는 따옴표 없이 그대로 씁니다(15000).
SQLite의 특이점(알아 둘 가치 있음). 대부분의 데이터베이스는 타입을 엄격히 강제합니다.INTEGER칸에 글자를 넣으려 하면 거부하죠. 그런데 SQLite는 기본적으로 유연(dynamic typing) 해서,price INTEGER칸에 실수로'비쌈'을 넣어도 받아 줍니다. 편할 때도 있지만 초보에겐 함정입니다 — 잘못된 데이터가 조용히 들어가니까요. 3부에서 배울 제약조건과, SQLite 3.37+의STRICT테이블 기능으로 이를 엄격하게 막을 수 있습니다. 지금은 "타입을 정해도 SQLite는 느슨히 지킨다"만 기억하세요.
날짜와 시간은?
눈치챘겠지만 위 표에 "날짜" 타입이 없습니다. SQLite는 날짜 전용 타입이 없고, 보통 TEXT로 '2026-01-03' 형식(ISO 8601) 으로 저장합니다. 이 형식은 글자 정렬이 곧 날짜 정렬과 같아 편리하고, date()·strftime() 같은 내장 함수로 계산도 됩니다. 우리 서점도 주문일을 이 방식으로 저장합니다.
-- 날짜 비교가 그대로 됨 (글자지만 형식이 정렬과 일치) SELECT * FROM orders WHERE order_date >= '2026-01-01';
2.3 CREATE TABLE 제대로 읽기
이제 books 테이블을 다시, 의미를 알고 만듭니다. 1부의 것보다 칸을 조금 늘렸습니다.
CREATE TABLE books ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, author TEXT, price INTEGER NOT NULL, stock INTEGER NOT NULL DEFAULT 0, published TEXT );
한 줄씩 뜯어 봅니다.
id INTEGER PRIMARY KEY— 정수형이며 이 표의 기본키(각 행을 구별하는 대표 번호). 자세히는 2.4에서.title TEXT NOT NULL— 제목은 글자이고, 반드시 값이 있어야 한다(NOT NULL). 제목 없는 책은 허용 안 함.author TEXT— 저자는 글자이며, 비어 있어도 된다(제약 없음). 저자 미상인 고서가 있을 수 있으니.price INTEGER NOT NULL— 가격은 정수, 필수.stock INTEGER NOT NULL DEFAULT 0— 재고는 정수, 필수이되, 값을 안 주면 기본값 0(DEFAULT 0).published TEXT— 출간일을'2026-01-03'형식의 글자로. 선택 항목.
여기서 NOT NULL, DEFAULT, PRIMARY KEY 같은 추가 규칙을 제약조건(constraint) 이라고 합니다. 표를 지을 때 거는 안전장치인데, 3부에서 본격적으로 다룹니다. 지금은 NOT NULL("빈 값 금지")과 DEFAULT("값 없으면 이걸로")만 알면 충분합니다.
만든 표 확인하기
-- 이 데이터베이스에 어떤 테이블들이 있나? SELECT name FROM sqlite_master WHERE type='table'; -- books 테이블의 구조(열·타입·제약)를 보여 줘 PRAGMA table_info(books);
PRAGMA는 SQLite 고유의 "설정·메타정보 조회" 명령입니다. table_info는 각 열의 이름·타입·NOT NULL 여부·기본값·기본키 여부를 표로 보여 줍니다. DB Browser에서는 왼쪽 Database Structure 탭에서 같은 내용을 클릭으로 볼 수 있습니다.
표를 잘못 만들었다면
DROP TABLE books; -- 표를 통째로 삭제(데이터도 함께 사라짐, 주의!) DROP TABLE IF EXISTS books; -- 없어도 에러 안 나게
연습 중엔 DROP TABLE IF EXISTS books; 후 다시 CREATE하는 패턴을 자주 씁니다. 실데이터가 든 표에는 절대 함부로 쓰지 마세요.
열을 나중에 바꾸려면?ALTER TABLE books ADD COLUMN isbn TEXT;로 열을 추가할 수 있습니다. SQLite는 과거엔 열 삭제·이름변경이 제한적이었지만 근래 버전에서DROP COLUMN·RENAME COLUMN도 지원합니다. 다만 설계 단계에서 한 번에 잘 잡는 게 가장 좋습니다.
2.4 기본키(primary key): 행을 구별하는 이름표
표에 "모비딕"이 두 줄 있다고 합시다(재입고로 같은 책이 두 번 등록되는 실수). 둘을 어떻게 구별할까요? 제목·저자·가격이 같으면 사람도 헷갈립니다. 그래서 각 행에 절대 겹치지 않는 고유 번호를 붙입니다. 이게 기본키(primary key) 입니다.
기본키의 두 가지 약속:
- 유일하다(unique). 같은 값이 두 행에 있을 수 없다.
- 비어 있지 않다(not null). 모든 행이 반드시 값을 가진다.
id INTEGER PRIMARY KEY라고 쓰면 SQLite가 이 둘을 강제합니다. 게다가 SQLite에서 INTEGER PRIMARY KEY는 특별 취급되어, 값을 안 주면 자동으로 1씩 증가하며 번호를 매겨 줍니다(INSERT 때 id를 생략하면 알아서 들어감). 1부에서 id 없이 넣었는데도 1,2,3,4가 붙었던 이유입니다.
INSERT INTO books (title, author, price) VALUES ('데미안', '헤르만 헤세', 12000); -- id를 안 줘도 자동으로 다음 번호가 부여됨 INSERT INTO books (id, title, author, price) VALUES (100, '특별판', '미상', 20000); -- 직접 지정도 가능. 단, 이미 있는 id면 거부됨(유일성 위반)
자연키 vs 대리키. "ISBN(국제표준도서번호)이 책마다 고유하니 그걸 기본키로 쓰면 안 되나?"라고 물을 수 있습니다. 데이터 자체에 있는 고유값을 쓰는 걸 자연키(natural key),id같은 인공 번호를 따로 두는 걸 대리키(surrogate key) 라고 합니다. 실무에선 대개 대리키(id)를 선호합니다. ISBN이 바뀌거나, 형식이 제각각이거나, 처음엔 모를 수 있기 때문입니다. 변하지 않는 단순 정수id가 연결(다음 챕터의 외래키)에도 편합니다. 이 안내서도 모든 테이블에id를 둡니다.
2.5 NULL: "값이 없음"이라는 특별한 상태
author 칸을 비워 둘 수 있게 했습니다. 그렇게 비어 있는 칸의 값이 NULL 입니다. NULL은 초보가 가장 자주 헷갈리는 개념이라 짚고 갑니다.
NULL은 0도 아니고 빈 글자('')도 아닙니다. "아직 모름 / 해당 없음 / 값이 없음"이라는 별도 상태입니다. 재고 0(stock = 0)은 "0권 있다"는 분명한 사실이지만, NULL은 "몇 권인지 모른다"입니다. 둘은 다릅니다.
이 차이가 비교에서 함정을 만듭니다. NULL은 어떤 것과도 =로 비교되지 않습니다 — 심지어 NULL끼리도요.
-- ❌ 의도와 다르게 동작: 저자가 비어 있는 책을 찾으려는 시도 SELECT * FROM books WHERE author = NULL; -- 결과가 항상 비어 있음! -- ✅ 올바른 방법: IS NULL / IS NOT NULL 을 쓴다 SELECT * FROM books WHERE author IS NULL; -- 저자가 없는 책 SELECT * FROM books WHERE author IS NOT NULL; -- 저자가 있는 책
flowchart TB
q["author = NULL 로 비교하면?"]
q --> r["결과는 참도 거짓도 아닌<br/>'알 수 없음(unknown)'<br/>→ 행이 걸러져 안 나옴"]
fix["올바른 방법"] --> ok["IS NULL / IS NOT NULL 사용"]
classDef bad fill:#f5c6cb,stroke:#c0392b,color:#000
classDef good fill:#d5f2e0,stroke:#27ae60,color:#000
class q,r bad
class fix,ok good
NULL은 합계·평균 같은 계산에서도 특별하게 다뤄지는데(보통 무시됨), 그건 5부 집계에서 다시 만납니다. 지금의 교훈은 둘입니다: 빈 값 비교는 =이 아니라 IS NULL로, 그리고 필수 항목엔 NOT NULL을 걸어 애초에 NULL이 안 들어오게 하라.
2.6 정리
- 데이터는 테이블에 담기고, 테이블은 행(한 대상)과 열(한 종류 정보)로 이뤄진다.
- SQLite의 타입은
INTEGER·REAL·TEXT·BLOB·NULL5종. 날짜는 보통TEXT에'2026-01-03'형식으로 둔다. SQLite는 타입을 느슨히 지키므로 제약조건으로 보완한다. CREATE TABLE에서 각 열의 타입과 제약(NOT NULL,DEFAULT등)을 정한다.- 기본키(primary key) 는 각 행을 구별하는 유일·비어있지않은 대표값. SQLite의
INTEGER PRIMARY KEY는 자동 증가까지 해 준다. 대개 인공id(대리키)를 쓴다. - NULL은 "값 없음"이라는 별도 상태. 0이나 ''과 다르며, 비교는 반드시
IS NULL/IS NOT NULL로 한다.
다음 챕터에서 드디어 표 여러 개를 연결합니다. 1부에서 약속한 "책·고객·주문을 쪼개고 잇는다"를 외래키로 실현하고, 서점 스키마를 완성합니다.
직접 해 보기
customers테이블을 직접 설계해CREATE TABLE로 만들어 보세요. 열은 최소한id(기본키),name(필수),email(필수),joined(가입일, 선택)을 두세요.- 위에서 만든
customers에 본인 정보 한 줄을INSERT하되id는 생략해 보세요.SELECT로id가 자동으로 붙었는지 확인하세요. email을 일부러 비운 채(즉NOT NULL을 어긴 채)INSERT를 시도하면 어떤 일이 일어나는지 관찰하세요. 어떤 메시지가 나오나요?PRAGMA table_info(customers);로 방금 만든 표의 구조를 출력해 보세요.
← 1부 · 목차 · 다음: 관계와 외래키 →