SQL 데이터 타입은 열마다 어떤 종류의 값이 들어갈지 정해 두는 규칙입니다. 포인트 열은 정수만, 가입일 열은 날짜만 받게 해 두면 잘못된 값이 처음부터 들어오지 못하는데요, 여기에 「값이 없음」을 뜻하는 NULL 까지 알면 표를 읽을 때 헷갈릴 일이 크게 줄어듭니다. 저도 SQL 을 처음 배우면서 「빈칸이면 그냥 0 아닌가?」라고 생각했는데요, 이번 글에서는 숫자·문자·날짜·불리언 타입을 고르는 기준과 NULL 이 0·빈 문자열과 어떻게 다른지 예시 표로 하나씩 짚어 보겠습니다. 타입 이름과 쿼리는 BigQuery(GoogleSQL) 문법으로 적었습니다. 지난 글: 데이터베이스 정규화: 왜 표를 여러 개로 나눌까
SQL 기초 시리즈 4/12 · 전체 커리큘럼
오늘 배울 개념
1회에서 스키마를 빈 주문서 양식에 비유했는데요, 데이터 타입은 그 양식의 칸마다 정해 둔 입력 형식입니다. 주문일 칸에는 날짜만, 수량 칸에는 숫자만, 포장 여부 칸에는 예·아니오 체크만 하게 되어 있죠. 엑셀은 한 셀에 아무 값이나 넣을 수 있어서 숫자가 텍스트로 저장돼 합계에서 빠지기도 하는데요, BigQuery 를 비롯한 대부분의 데이터베이스는 열마다 타입을 정해 두고 맞지 않는 값을 받지 않습니다(SQLite처럼 타입을 느슨하게 다루는 제품도 있습니다). 그리고 양식의 어떤 칸은 비어 있을 수 있는데요, SQL 에서는 이 「아직 모름, 해당 없음」 상태를 NULL 이라는 특별한 표시로 나타냅니다.
숫자·문자·날짜·불리언, 어떤 타입이 있을까요?
BigQuery 데이터 타입 문서에는 타입이 꽤 많은데요, 처음에는 아래 여섯 묶음이면 충분합니다.
| 묶음 | BigQuery 타입 | 들어가는 값 | 마케팅 데이터 예 |
|---|---|---|---|
| 정수 | INT64 | 소수점 없는 수 | 클릭 수, 주문 수량 |
| 정확한 소수 | NUMERIC, BIGNUMERIC | 자릿수가 정해진 소수, 계산이 정확 | 광고비, 결제 금액 |
| 근사 소수 | FLOAT64 | 아주 크거나 작은 수, 계산에 오차 가능 | 측정값, 비율 계산 |
| 문자 | STRING, STRING(L) | 글자. L 은 최대 글자 수 | 캠페인 이름, 전화번호 |
| 날짜·시간 | DATE, DATETIME, TIMESTAMP | 날짜, 날짜+시각 | 가입일, 주문 시각 |
| 불리언 | BOOL | 참·거짓 | 수신 동의 여부 |
고르는 기준은 공식 문서에 이렇게 나와 있습니다.
- 정수: BigQuery 의 정수 타입은
INT64하나이고,INT·INTEGER·BIGINT같은 이름은 모두INT64의 별칭이라고 문서에 적혀 있습니다. 범위가 약 ±922경이라 클릭 수가 아무리 쌓여도 모자랄 일이 거의 없죠. - 소수:
NUMERIC은 전체 38자리, 소수점 아래 9자리까지 정확하게 담고,NUMERIC(12, 2)처럼 자릿수를 정할 수도 있습니다. 반면FLOAT64는 문서 표현대로 「근삿값」이라 금액처럼 딱 맞아야 하는 값에는 맞지 않습니다. - 문자:
STRING은 길이를 정하지 않고 쓸 수 있고,STRING(13)처럼 최대 글자 수를 정하면 그보다 긴 값이 들어올 때 오류(OUT_OF_RANGE)가 납니다. 길이는 바이트가 아니라 글자 수로 셉니다. 다른 제품에서 보이는VARCHAR(n)·CHAR(n)은 BigQuery 에서는STRING으로 씁니다. - 날짜·시간:
DATE는 시간대 없는 날짜,DATETIME은 시간대 없는 날짜와 시각,TIMESTAMP는 시간대와 상관없는 절대 시점입니다. 날짜는DATE '2026-11-10'처럼 적습니다.
개념 그림: 열마다 정해 둔 칸에 맞는 값만 들어갑니다
예시로 이해하기: 타입이 왜 중요할까요?
지난 글들의 문구점 회원 표에 포인트, 전화번호, 문자 수신 동의 열을 붙여 보겠습니다(가상 데이터). 표를 만들 때는 이렇게 열마다 타입을 적습니다.
CREATE TABLE shop.members (
member_id INT64 NOT NULL,
name STRING(20) NOT NULL,
joined_on DATE NOT NULL,
points INT64,
phone STRING(13),
sms_ok BOOL,
PRIMARY KEY (member_id) NOT ENFORCED
);BigQuery 는 표 이름 앞에 데이터셋 이름(shop)을 붙여 적습니다. DDL 문서에 따르면 NOT NULL 을 붙인 열은 비워 둘 수 없는 열(REQUIRED)이 되고, 기본 키는 NOT ENFORCED 를 붙여 선언만 합니다. 2회에서 본 기본 키 규칙을 BigQuery 가 대신 지켜 주지는 않는다는 뜻이라, 중복이 없는지는 직접 확인해야 합니다.
members
| member_id | name | joined_on | points | phone | sms_ok |
|---|---|---|---|---|---|
| 1 | 김하늘 | 2026-01-15 | 1200 | 010-0000-0001 | TRUE |
| 2 | 이도윤 | 2026-03-02 | 0 | (빈 문자열 '') | FALSE |
| 3 | 박서연 | 2026-05-20 | NULL | NULL | TRUE |
| 4 | 최지우 | 2026-07-08 | 300 | 010-0000-0004 | NULL |
| 5 | 정민준 | 2026-09-11 | NULL | 010-0000-0005 | FALSE |
타입을 정해 두면 무엇이 달라지는지 이 표를 BigQuery 쿼리 안에 그대로 넣어 직접 돌려 봤습니다.
- 숫자를 글자로 두면 순서가 틀립니다: 포인트를 문자로 바꿔 정렬하면
0,1200,300순서가 되고'300' > '1200'이true로 나옵니다. 글자는 첫 글자부터 비교하기 때문인데요,INT64로 두면 300 이 1200 보다 작다고 바르게 판단합니다. - 없는 날짜를 막아 줍니다:
DATE '2026-02-30'은 「Invalid DATE literal」 오류로 거부됐고, 두 날짜 사이 날 수는DATE_DIFF(DATE '2026-11-10', DATE '2026-09-11', DAY)로 구하면 60이 나옵니다. BigQuery 에서는 날짜끼리 빼는 대신 DATE_DIFF 함수를 씁니다. - 금액은 근사 소수에 두지 않습니다:
FLOAT64로 0.1 과 0.2 를 더하면0.30000000000000004가 되어 0.3 과 같지 않다고 나오지만,NUMERIC '0.1' + NUMERIC '0.2'는NUMERIC '0.3'과 정확히 같다고 나옵니다. - 길이 제한이 실수를 걸러 줍니다:
STRING(13)으로 정한 변수에 17글자를 넣자 「STRING(13) has maximum length 13 but got a value with length 17」 오류가 났습니다.
NULL 은 0·빈 문자열과 무엇이 다를까요?
위 표에서 이도윤 님의 포인트는 0, 박서연 님의 포인트는 NULL 입니다. 둘 다 「없다」처럼 보이지만 뜻이 다른데요, MySQL 문서는 전화번호 열을 예로 들어 NULL 은 「번호를 모른다」, 빈 문자열 '' 은 「전화가 없다는 것을 안다」로 읽을 수 있다고 설명합니다.
| 값 | 뜻 | IS NULL | 계산에 넣으면 |
|---|---|---|---|
0 | 0 이라는 값이 있다 | 거짓 | 0 으로 계산 |
'' (빈 문자열) | 글자 수 0 인 값이 있다 | 거짓 | 빈 글자로 계산 |
NULL | 값이 없다, 모른다 | 참 | 대부분 결과도 NULL |
BigQuery 데이터 타입 문서는 현재 모든 데이터 타입이 NULL 을 가질 수 있다고 적고 있는데요, NOT NULL 을 붙이지 않은 열이라면 어느 타입이든 비어 있을 수 있으니 생각보다 자주 보이죠.
NULL 이 계산에 끼면 결과도 달라지는데요, 박서연 님의 포인트에 100 을 더하면 NULL 이고, 집계 함수는 NULL 을 건너뜁니다. BigQuery 집계 함수 문서에 따르면 COUNT(*) 는 모든 행 수를, COUNT(열) 은 NULL 이 아닌 행 수를 세고, SUM·AVG 는 NULL 을 빼고 계산합니다.
SELECT COUNT(*), COUNT(points), SUM(points), AVG(points)
FROM shop.members;| COUNT(*) | COUNT(points) | SUM(points) | AVG(points) |
|---|---|---|---|
| 5 | 3 | 1500 | 500.0 |
평균은 1500 을 5명이 아니라 포인트가 있는 3명으로 나눈 500 입니다(BigQuery 는 AVG 결과를 소수 타입으로 돌려줘서 500.0 으로 보입니다). NULL 을 0 으로 보고 싶다면 AVG(COALESCE(points, 0)) 으로 바꿔서 평균을 내면 300 이 되는데요, 조건 표현식 문서는 COALESCE 가 NULL 이 아닌 첫 번째 값을 돌려준다고 설명합니다.
개념 그림: 0 과 빈 문자열은 「있는 값」이고, NULL 은 「모르는 값」입니다
3값 논리: NULL 이 낀 조건은 왜 결과에서 사라질까요?
엑셀에서 조건을 따지면 TRUE 아니면 FALSE 가 나오죠. 그런데 BigQuery 연산자 문서는 논리 연산자가 참·거짓에 NULL(알 수 없음)을 더한 3값 논리를 쓴다고 설명합니다.
| a | b | a AND b | a OR b |
|---|---|---|---|
| TRUE | NULL | NULL | TRUE |
| FALSE | NULL | FALSE | NULL |
| NULL | NULL | NULL | NULL |
같은 문서에 따르면 비교 연산자는 NULL 이 하나라도 들어가면 NULL 을 돌려줘서 7 = NULL 도, 7 <> NULL 도 결과가 NULL 입니다. 그리고 쿼리 문법 문서는 WHERE 절이 조건이 NULL 이나 FALSE 로 계산된 행을 버린다고 적고 있죠.
그래서 회원 표에 「수신 동의한 회원」(sms_ok = TRUE)을 물으면 김하늘·박서연 2명, 「동의하지 않은 회원」(sms_ok = FALSE)을 물으면 이도윤·정민준 2명이 나오고, 동의 여부가 NULL 인 최지우 님은 어느 쪽에도 나오지 않습니다.
NULL 을 찾을 때는 = NULL 이 아니라 IS NULL 을 씁니다. BigQuery 에서 phone = NULL 을 실행해 보니 아예 「Operands of = cannot be literal NULL」 오류가 나서 실수를 바로 알려 주더라고요. 연산자 문서에 따르면 IS NULL·IS TRUE 같은 IS 연산자는 NULL 이 들어와도 NULL 을 돌려주지 않고 참이나 거짓만 돌려줍니다.
SELECT name FROM shop.members WHERE phone = NULL; -- 오류
SELECT name FROM shop.members WHERE phone IS NULL; -- 박서연
SELECT name FROM shop.members WHERE phone = ''; -- 이도윤
개념 그림: 조건이 NULL 로 계산된 행은 참도 거짓도 아니라서 결과에서 빠집니다
연습 문제
1. (쉬움) 광고 성과 표에 아래 열을 만들려고 합니다. 각각 어떤 타입이 맞을까요? 캠페인 이름, 집행일, 클릭 수, 광고비(원 단위, 소수점 둘째 자리까지), 진행 중 여부
정답 보기
캠페인 이름은 STRING(최대 길이를 정하려면 STRING(100) 처럼), 집행일은 DATE, 클릭 수는 INT64, 광고비는 NUMERIC(12, 2) 처럼 자릿수를 정한 NUMERIC, 진행 중 여부는 BOOL 입니다. 광고비를 FLOAT64 에 두면 계산 오차가 생길 수 있어 피합니다.
2. (보통) 위 members 표에서 SELECT COUNT(phone) FROM shop.members; 의 결과는 몇일까요? 이도윤 님의 빈 문자열은 세어질까요?
정답 보기
4입니다. COUNT(열) 은 NULL 이 아닌 값만 세는데, 전화번호가 NULL 인 사람은 박서연 님 한 명뿐입니다. 이도윤 님의 '' 은 NULL 이 아니라 「글자 수 0 인 값」이라서 세어집니다.
3. (보통) WHERE points < 500 으로 「포인트가 500 미만인 회원」을 찾으면 누가 나올까요? 포인트를 모르는 회원까지 함께 보고 싶다면 조건을 어떻게 바꿀까요?
정답 보기
이도윤(0)·최지우(300) 2명만 나옵니다. 박서연·정민준 님은 포인트가 NULL 이라 NULL < 500 이 NULL 로 계산되어 빠집니다. 함께 보려면 WHERE points < 500 OR points IS NULL 로 쓰면 되고, 결과는 이도윤·박서연·최지우·정민준 4명입니다.
헷갈리기 쉬운 것
NOT을 붙여도 NULL 은 돌아오지 않습니다:WHERE NOT sms_ok도 이도윤·정민준 2명뿐이고 최지우 님은 빠집니다. 연산자 문서의 표대로NOT NULL도 NULL 이기 때문인데요, 「동의가 확인되지 않은 회원」 전체를 보려면WHERE sms_ok IS NOT TRUE를 쓰면 3명이 나옵니다.IS NOT TRUE는 NULL 이 들어와도 참·거짓만 돌려줍니다.- 빈 문자열을 NULL 로 보는 제품도 있습니다: Oracle 문서는 현재 길이 0 인 문자 값을 NULL 로 다룬다고 적으면서도, 앞으로 바뀔 수 있으니 빈 문자열과 NULL 을 같게 다루지 말라고 권합니다. NULL 의 정렬 위치도 제품마다 다른데요, BigQuery 는 오름차순에서 NULL 을 맨 앞에, 내림차순에서 맨 뒤에 두고
NULLS FIRST·NULLS LAST로 바꿀 수 있습니다(쿼리 문법 문서). 다른 제품으로 옮기면 순서가 달라질 수 있다는 점만 기억해 두세요. - 합계가 0 이 아니라 NULL 로 나올 수 있습니다: 집계 함수 문서에 따르면
SUM·AVG는 조건에 맞는 행이 하나도 없거나 값이 전부 NULL 이면 0 이 아닌 NULL 을 돌려줍니다. 실제로 포인트가 99999 넘는 회원의 합계를 구하니 NULL 이 나왔습니다. 리포트에서 빈칸 대신 0 을 보여 주고 싶다면COALESCE(SUM(...), 0)처럼 감싸 둡니다.
마치며
SQL 데이터 타입은 열마다 들어갈 값의 종류를 정해 잘못된 값을 막아 주는 장치이고, NULL 은 0 이나 빈 문자열과 달리 「모르는 값」이라 계산과 조건에서 따로 움직입니다. 표를 볼 때 「이 열은 어떤 타입이고, 비어 있으면 NULL 일까?」를 먼저 물어보시면 뒤에 배울 쿼리 결과가 왜 그렇게 나오는지 훨씬 쉽게 읽힐 거예요. 저도 시리즈를 따라가며 공부하고 있으니 전체 커리큘럼을 참고해 주세요. 다음 글에서는 SQL 이 어떤 언어인지와 명령어 종류, 쿼리가 실행되는 순서를 다룹니다.
자주 묻는 질문
- SQL 데이터 타입은 왜 정해야 하나요?
- 열마다 들어갈 값의 종류를 정해 두면 맞지 않는 값을 저장 단계에서 거부할 수 있고, 숫자는 숫자로, 날짜는 날짜로 비교·계산할 수 있기 때문입니다. 포인트를 문자로 저장하면 「300」이 「1200」보다 크다고 나오는 것처럼, 타입이 틀리면 정렬과 비교 결과가 달라집니다.
- NULL 과 0, 빈 문자열은 어떻게 다른가요?
- 0 과 빈 문자열은 「0」, 「글자 수 0」이라는 값이 실제로 있는 것이고, NULL 은 값이 없거나 모른다는 표시입니다. 그래서 IS NULL 로 찾을 때 0 과 빈 문자열은 나오지 않고, NULL 이 계산에 끼면 결과도 대부분 NULL 이 됩니다. 다만 Oracle 처럼 빈 문자열을 NULL 로 다루는 제품도 있습니다.
- WHERE 컬럼 = NULL 은 왜 쓰면 안 되나요?
- SQL 에서 NULL 과의 비교는 참도 거짓도 아닌 NULL(알 수 없음)이 되고, WHERE 절은 조건이 참인 행만 남기기 때문에 원하는 행이 나오지 않습니다. BigQuery 는
= NULL처럼 NULL 을 직접 비교하면 아예 오류를 냅니다. NULL 인 행을 찾으려면 IS NULL, NULL 이 아닌 행을 찾으려면 IS NOT NULL 을 씁니다.
관련 글 더 보기
- 데이터베이스 정규화: 왜 표를 여러 개로 나눌까 - SQL 기초 시리즈 3회
- 관계형 데이터베이스 구조: 테이블·행·열·스키마 - SQL 기초 시리즈 1회, 스키마와 열
- 엑셀 숫자 인식 안됨 | 텍스트로 저장된 숫자를 숫자로 바꾸는 법 - 엑셀에서 타입이 어긋날 때 생기는 문제
