Menu

SQLite ROWID 완벽 정리: INTEGER PRIMARY KEY와 WITHOUT ROWID

SQLite의 숨은 ROWID가 정확히 뭔지, INTEGER PRIMARY KEY로 어떻게 별칭이 되는지, 그리고 WITHOUT ROWID 테이블은 언제 써야 하는지 정리합니다.

이 페이지에는 실행 가능한 에디터가 있습니다 — 편집하고 실행하면 결과를 바로 볼 수 있습니다.

모든 테이블에는 숨겨진 컬럼이 하나 있다

SQLite에서 평범한 테이블을 만들면, 직접 선언하지도 않은 컬럼이 이미 하나 따라붙어 있습니다.

rowid 컬럼은 진짜로 존재합니다. SQLite는 일반 테이블의 모든 행에 이 값을 자동으로 부여하는데, 우리가 요청했든 안 했든 상관없이 항상 붙습니다. 64비트 부호 있는 정수이고, 테이블 안에서 고유하며, SQLite가 B-tree 저장 구조에서 행을 찾을 때 실제로 사용하는 키입니다. 테이블의 등뼈, 즉 나머지 모든 데이터를 정렬해 주는 색인이라고 보면 됩니다.

평소에는 잘 보이지 않습니다. SELECT *로는 나오지 않거든요. 직접 이름을 적어서 호출해야 합니다.

ROWID에는 별칭이 세 개 있다

다른 데이터베이스용 SQL에서도 rowid라는 이름이 자주 쓰이다 보니, SQLite는 같은 컬럼을 가리키는 세 가지 이름을 모두 받아 줍니다:

rowid, oid, _rowid_ 이 셋은 모두 같은 숨겨진 컬럼을 가리킵니다. 만약 이 이름들 중 하나로 실제 컬럼을 선언했다면 직접 만든 컬럼이 우선이고 별칭은 더 이상 쓸 수 없게 되는데, 주의할 점은 딱 이거 하나입니다. 평소 코드에서는 그냥 rowid라고 쓰면 됩니다.

INTEGER PRIMARY KEY가 바로 마법의 키워드

다른 데이터베이스를 쓰다 SQLite로 넘어온 사람들이 가장 많이 헷갈려 하는 부분입니다. 컬럼을 정확히 INTEGER PRIMARY KEY로 선언하면, 그 컬럼은 별도로 저장되지 않고 그 자체가 rowid가 됩니다.

rowidid는 사실 같은 컬럼을 두 가지 이름으로 부르는 것뿐입니다. id 값을 빼고 INSERT하면 SQLite가 알아서 정수 값을 골라 채워 넣습니다(보통은 현재 최대 rowid + 1). 그래서 SQLite에서 자동 증가 기본키를 만들 때 가장 효율적인 방법이 바로 INTEGER PRIMARY KEY입니다. 별도 컬럼도, 별도 인덱스도 필요 없이 rowid 그 자체를 그대로 활용하니까요.

여기서 철자가 정확해야 합니다. INT PRIMARY KEYINTEGER PRIMARY KEY다릅니다. 같은 정수 타입처럼 보여도 SQLite에서는 INTINTEGER가 전혀 다르게 동작하거든요:

테이블 a에서는 idrowid가 동일한 값을 가리킵니다. 반면 테이블 b에서는 id가 평범한 컬럼일 뿐이고, rowid는 별도로 숨겨진 정수 컬럼으로 존재합니다. 더 골치 아픈 건 b.idINSERT 시점에 자동으로 채워지지 않아서, 직접 값을 넣어주기 전까지는 NULL이라는 점이죠. 별칭(alias) 동작을 그대로 활용하고 싶다면 INTEGER PRIMARY KEY라는 정확한 표현을 써주는 게 좋습니다.

INSERT 후 rowid 가져오기 — last_insert_rowid()

INSERT 직후에는 방금 부여된 rowid 값이 궁금해질 때가 많습니다. 보통 자식 테이블의 행을 이 rowid에 연결하기 위해서죠. SQLite에서는 last_insert_rowid() 함수로 간단히 확인할 수 있습니다.

이 함수는 현재 연결에서 가장 최근에 성공한 INSERT의 rowid를 돌려줍니다. 대부분의 데이터베이스 드라이버는 같은 값을 cursor.lastrowid 같은 속성으로 노출합니다. 뒤에서 다룰 RETURNING 절을 쓰면 INSERT 문 자체에서 바로 받아올 수도 있습니다.

ROWID는 영구적이지 않습니다

행이 존재하는 동안 rowid는 변하지 않지만, 평생 따라다니는 식별자는 아닙니다. VACUUM을 실행하면 rowid가 다시 매겨질 수 있고, 행을 삭제하면 그 번호가 나중에 들어오는 INSERT에서 재사용될 수 있습니다.

새 행이 예전 rowid를 다시 쓸지 말지는 SQLite 버전이나 상황에 따라 달라집니다. 핵심은 rowid가 영원히 고유하다고 믿으면 안 된다는 점이죠. 삭제하거나 VACUUM을 돌리거나 데이터를 내보내도 살아남는 식별자가 필요하다면, 직접 INTEGER PRIMARY KEY 컬럼을 선언해서 값을 그 행에 고정시키세요. 한 번 쓴 값이 절대 재활용되지 않고 단조 증가해야 한다면 AUTOINCREMENT 키워드도 함께 고려해 볼 만합니다.

WITHOUT ROWID 테이블 사용법

rowid 자체가 부담스러울 때도 있습니다. 진짜 키가 정수가 아닌 경우가 대표적이죠. 예를 들어 도시 이름을 기본키로 쓰는 테이블을 만들면 내부적으로 두 가지 구조가 생깁니다. rowid를 위한 B-트리 하나, 그리고 기본키를 강제하기 위해 name 컬럼에 걸린 별도 인덱스 하나죠. WITHOUT ROWID를 쓰면 이 둘을 하나로 합칠 수 있습니다.

이제 name이 실제 저장 키 역할을 합니다. name으로 조회할 때 한 단계 우회 과정이 사라지고, 테이블 크기도 작아집니다. 다만 다음과 같은 트레이드오프가 있습니다:

  • rowid, oid, _rowid_가 없습니다 — 이 컬럼들 자체가 존재하지 않습니다.
  • 이 테이블에 INSERT를 해도 last_insert_rowid() 값이 갱신되지 않습니다.
  • 증분(incremental) BLOB I/O와 일부 복제 기능을 쓸 수 없습니다.
  • 테이블에 PRIMARY KEY반드시 선언되어 있어야 합니다.

WITHOUT ROWID는 기본 선택지가 아니라, 의도적으로 적용하는 최적화입니다. 기본키가 정수가 아니면서 테이블이 크거나 쓰기 부하가 많을 때 꺼내 쓰면 좋습니다. 정수 키를 쓰는 평범한 테이블이라면 일반 rowid 구조만으로도 이미 충분히 최적입니다.

핵심만 정리한 멘탈 모델

꼭 필요한 것만 추려보면 이렇습니다:

  • 일반적인 SQLite 테이블에는 rowid라는 숨겨진 64비트 정수 키가 항상 들어 있습니다.
  • INTEGER PRIMARY KEY(정확히 이 표기)로 선언하면 해당 컬럼이 rowid의 별칭이 됩니다.
  • 방금 할당된 값을 읽고 싶다면 last_insert_rowid()를 사용하세요.
  • rowid는 삭제 이후 재사용될 수 있고, VACUUM을 거치면 번호가 다시 매겨질 수 있습니다.
  • WITHOUT ROWID 테이블은 숨겨진 키를 없애고 선언한 기본키를 그대로 사용합니다 — 정수가 아닌 키에는 유용하지만, 일부 기능은 포기해야 합니다.

대부분의 경우 rowid를 의식할 일은 거의 없습니다. id INTEGER PRIMARY KEY라고 선언해두면 번호 매기기는 SQLite가 알아서 해주니까요. 동작 원리를 알아둬야 할 때는 저장 구조를 튜닝하거나, 기존 스키마를 분석하거나, INT PRIMARY KEYINTEGER PRIMARY KEY가 왜 다르게 동작하는지 의문이 생길 때입니다.

다음 주제: NOT NULL과 DEFAULT

행의 식별자 문제가 정리됐으니, 그다음은 나머지 컬럼들에 적절한 값이 들어가도록 보장하는 단계입니다. NOT NULLDEFAULT가 바로 이 역할을 도맡는 두 가지 절(clause)이며, 다음 글에서 자세히 다룹니다.

자주 묻는 질문

SQLite의 ROWID는 정확히 뭔가요?

일반적인 SQLite 테이블에는 직접 선언하지 않아도 rowid라는 64비트 부호 있는 정수 컬럼이 숨어 있습니다. 각 행을 고유하게 식별하는 값이고, 내부적으로는 B-tree 저장 구조의 실제 키로 쓰입니다. 선언한 적이 없어도 SELECT rowid, * FROM t처럼 직접 조회할 수 있어요.

ROWID와 PRIMARY KEY는 뭐가 다른가요?

rowid는 항상 존재하고, PRIMARY KEY는 사용자가 직접 선언하는 겁니다. 특별한 케이스가 하나 있는데, 컬럼을 INTEGER PRIMARY KEY로 선언하면 그 컬럼이 별도 컬럼이 아니라 rowid의 별칭(alias)이 됩니다. 반대로 텍스트 기본키, 복합 기본키, 그리고 INTEGER가 아닌 INT PRIMARY KEY는 별칭이 되지 않고 rowid와는 별개로 저장됩니다.

WITHOUT ROWID는 어떤 역할을 하나요?

WITHOUT ROWID를 붙이면 SQLite가 숨겨진 rowid를 만들지 않고, 선언한 PRIMARY KEY를 실제 저장 키로 사용합니다. 정수가 아닌 키를 쓰는 테이블에서는 공간을 아끼고 조회 속도를 올릴 수 있죠. 다만 last_insert_rowid(), 증분 BLOB I/O 같은 일부 기능을 못 쓰게 되니, 기본값처럼 무작정 쓰지 말고 필요할 때만 적용하세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기