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) 에 담깁니다. 테이블은 엑셀 시트와 똑 닮았습니다.

CODE
                    ← 열(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() 같은 내장 함수로 계산도 됩니다. 우리 서점도 주문일을 이 방식으로 저장합니다.

SQL
-- 날짜 비교가 그대로 됨 (글자지만 형식이 정렬과 일치)
SELECT * FROM orders WHERE order_date >= '2026-01-01';

2.3 CREATE TABLE 제대로 읽기

이제 books 테이블을 다시, 의미를 알고 만듭니다. 1부의 것보다 칸을 조금 늘렸습니다.

SQL
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("값 없으면 이걸로")만 알면 충분합니다.

만든 표 확인하기

SQL
-- 이 데이터베이스에 어떤 테이블들이 있나?
SELECT name FROM sqlite_master WHERE type='table';

-- books 테이블의 구조(열·타입·제약)를 보여 줘
PRAGMA table_info(books);

PRAGMA는 SQLite 고유의 "설정·메타정보 조회" 명령입니다. table_info는 각 열의 이름·타입·NOT NULL 여부·기본값·기본키 여부를 표로 보여 줍니다. DB Browser에서는 왼쪽 Database Structure 탭에서 같은 내용을 클릭으로 볼 수 있습니다.

표를 잘못 만들었다면

SQL
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) 입니다.

기본키의 두 가지 약속:

  1. 유일하다(unique). 같은 값이 두 행에 있을 수 없다.
  2. 비어 있지 않다(not null). 모든 행이 반드시 값을 가진다.

id INTEGER PRIMARY KEY라고 쓰면 SQLite가 이 둘을 강제합니다. 게다가 SQLite에서 INTEGER PRIMARY KEY는 특별 취급되어, 값을 안 주면 자동으로 1씩 증가하며 번호를 매겨 줍니다(INSERTid를 생략하면 알아서 들어감). 1부에서 id 없이 넣었는데도 1,2,3,4가 붙었던 이유입니다.

SQL
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끼리도요.

SQL
-- ❌ 의도와 다르게 동작: 저자가 비어 있는 책을 찾으려는 시도
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·NULL 5종. 날짜는 보통 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부에서 약속한 "책·고객·주문을 쪼개고 잇는다"를 외래키로 실현하고, 서점 스키마를 완성합니다.

직접 해 보기

  1. customers 테이블을 직접 설계해 CREATE TABLE로 만들어 보세요. 열은 최소한 id(기본키), name(필수), email(필수), joined(가입일, 선택)을 두세요.
  2. 위에서 만든 customers에 본인 정보 한 줄을 INSERT하되 id는 생략해 보세요. SELECTid가 자동으로 붙었는지 확인하세요.
  3. email을 일부러 비운 채(즉 NOT NULL을 어긴 채) INSERT를 시도하면 어떤 일이 일어나는지 관찰하세요. 어떤 메시지가 나오나요?
  4. PRAGMA table_info(customers);로 방금 만든 표의 구조를 출력해 보세요.
← 1부 · 목차 · 다음: 관계와 외래키 →