조회부터 익혀야 하는 이유
PostgreSQL을 배운다는 것은 결국 데이터를 정확히 읽고, 원하는 형태로 꺼내는 능력을 기르는 일에서 시작된다. 테이블을 잘 설계해도 조회 조건을 잘못 쓰면 결과는 틀어진다. 인덱스를 만들어도 어떤 조건으로 데이터를 찾는지 모르면 성능을 판단하기 어렵다.
이 장에서는 SELECT 문을 중심으로 SQL의 기본 골격을 익힌다. 예제는 쇼핑몰의 고객 목록을 조회하는 상황으로 진행한다. psql에서 shop 데이터베이스에 접속한 뒤, 먼저 테이블 구조를 확인해 보자.
d customers이 장에서는 customers 테이블에 다음과 같은 열이 있다고 가정한다.
id 고객 ID
name 고객 이름
email 이메일
grade 고객 등급
point 보유 포인트
joined_at 가입 일시
last_login_at 마지막 로그인 일시
is_active 활성 고객 여부
phone 전화번호실제 실습 테이블의 열 이름이 다르다면 예제의 열 이름만 바꾸어 실행하면 된다.
AI 생성 이미지
SELECT와 FROM: 어떤 열을, 어느 테이블에서 읽을 것인가
가장 기본적인 조회문은 다음 모양이다.
SELECT name, email
FROM customers;SELECT는 결과에 표시할 열을 고른다. FROM은 데이터를 읽을 테이블을 지정한다. 위 쿼리는 customers 테이블에서 name, email 두 열만 보여 달라는 뜻이다.
모든 열을 보고 싶을 때는 *를 사용할 수 있다.
SELECT *
FROM customers;학습 중에는 SELECT *가 편하다. 하지만 실무 코드에서는 필요한 열만 적는 습관이 좋다. 이유는 분명하다. 테이블에 열이 많아질수록 불필요한 데이터를 더 읽고, 애플리케이션으로 더 많이 전송하기 때문이다. 또한 나중에 테이블에 민감한 열이 추가되었을 때 의도치 않게 노출될 수 있다.
처음에는 다음처럼 작은 조회부터 시작하자.
SELECT id, name, grade, point
FROM customers;이 쿼리의 질문은 간단하다.
어느 테이블에서 읽는가?
customers어떤 값을 보여 주는가?
id,name,grade,point조건은 있는가? 아직 없다
SQL을 읽을 때는 이 세 가지를 먼저 확인하면 된다.
WHERE: 필요한 행만 남기기
WHERE는 조회할 행을 걸러낸다. 예를 들어 활성 고객만 보고 싶다면 다음과 같이 쓴다.
SELECT id, name, email, is_active
FROM customers
WHERE is_active = true;WHERE 뒤에는 각 행이 만족해야 할 조건을 적는다. 조건이 참인 행만 결과에 남는다.
포인트가 10,000점 이상인 고객은 다음처럼 찾는다.
SELECT id, name, point
FROM customers
WHERE point >= 10000;고객 등급이 VIP인 고객은 문자열 비교를 사용한다.
SELECT id, name, grade
FROM customers
WHERE grade = 'VIP';SQL에서 문자열은 작은따옴표로 감싼다. 큰따옴표는 문자열이 아니라 테이블명이나 열 이름 같은 식별자를 감쌀 때 사용한다. 따라서 일반 값 비교에는 작은따옴표를 쓰는 습관을 들여야 한다.
조건은 AND, OR, NOT으로 조합할 수 있다.
SELECT id, name, grade, point
FROM customers
WHERE grade = 'VIP'
AND point >= 10000;이 쿼리는 등급이 VIP이면서 포인트가 10,000점 이상인 고객만 조회한다. 조건이 길어질 때는 줄을 나누어 쓰면 읽기 쉽다. SQL은 줄바꿈보다 키워드와 구분 기호를 기준으로 해석된다.
OR를 사용할 때는 괄호를 아끼지 말자.
SELECT id, name, grade, point, is_active
FROM customers
WHERE is_active = true
AND (grade = 'VIP' OR point >= 50000);괄호가 없으면 AND와 OR의 우선순위 때문에 의도와 다른 결과가 나올 수 있다. 조건이 섞이면 괄호로 의미를 드러내는 편이 안전하다.
ORDER BY와 LIMIT: 결과의 순서와 개수 정하기
테이블에 저장된 행에는 사용자가 기대하는 자연스러운 순서가 없다. id가 증가하는 순서로 보일 때도 있지만, 그것을 보장된 결과로 믿으면 안 된다. 정렬이 필요하면 반드시 ORDER BY를 써야 한다.
최근 가입한 고객부터 보고 싶다면 다음처럼 작성한다.
SELECT id, name, joined_at
FROM customers
ORDER BY joined_at DESC;DESC는 내림차순이다. 날짜·시간에서는 최신 값이 먼저 나온다. 반대로 오래된 가입자부터 보려면 ASC를 쓴다. ASC는 기본값이므로 생략할 수 있다.
SELECT id, name, joined_at
FROM customers
ORDER BY joined_at ASC;정렬 기준은 여러 개를 사용할 수 있다.
SELECT id, name, grade, point
FROM customers
ORDER BY grade ASC, point DESC;이 쿼리는 등급으로 먼저 정렬하고, 같은 등급 안에서는 포인트가 높은 고객부터 보여 준다.
목록 화면에서는 전체 데이터를 한 번에 보여 주지 않는다. 보통 상위 몇 개만 가져오거나 페이지 단위로 나누어 가져온다. 이때 LIMIT을 사용한다.
SELECT id, name, point
FROM customers
ORDER BY point DESC
LIMIT 10;포인트가 높은 고객 10명을 조회하는 쿼리다. 여기서 중요한 점은 ORDER BY와 LIMIT을 함께 써야 의미가 분명해진다는 것이다. LIMIT 10만 쓰면 "어떤 기준의 10명인지" 알 수 없다.
SQL을 작성하는 순서는 보통 다음과 같다.
SELECT 표시할 열
FROM 읽을 테이블
WHERE 행 조건
ORDER BY 정렬 기준
LIMIT 개수;하지만 PostgreSQL이 의미를 처리하는 흐름은 대략 FROM에서 대상을 정하고, WHERE로 행을 거른 뒤, SELECT로 표시할 값을 만들고, ORDER BY로 정렬한 다음, LIMIT으로 개수를 제한한다고 이해하면 된다. 이 순서를 알면 오류를 줄일 수 있다.
값 비교의 기본: 문자열, 숫자, 날짜, 불리언, NULL
숫자 비교는 수학에서 쓰는 기호와 비슷하다.
SELECT id, name, point
FROM customers
WHERE point > 0;자주 쓰는 비교 연산자는 다음과 같다.
= 같다
<> 같지 않다
> 크다
>= 크거나 같다
< 작다
<= 작거나 같다문자열은 작은따옴표로 비교한다.
SELECT id, name, grade
FROM customers
WHERE grade <> 'BASIC';날짜와 시간도 문자열처럼 작은따옴표로 적지만, PostgreSQL은 문맥에 따라 날짜·시간 값으로 해석한다.
SELECT id, name, joined_at
FROM customers
WHERE joined_at >= '2026-01-01';시간까지 비교하려면 다음처럼 쓸 수 있다.
SELECT id, name, last_login_at
FROM customers
WHERE last_login_at >= '2026-05-01 00:00:00';날짜·시간을 다룰 때는 저장된 열의 타입을 확인해야 한다. date는 날짜만 저장하고, timestamp는 날짜와 시간을 저장한다. 시간대까지 다루는 timestamptz는 입력값과 표시값이 세션 시간대의 영향을 받을 수 있다. 지금은 "비교할 값의 의미가 열 타입과 맞아야 한다"는 원칙만 기억하자.
불리언 값은 true, false로 비교한다.
SELECT id, name, is_active
FROM customers
WHERE is_active = false;PostgreSQL에서는 불리언 열 자체를 조건으로 쓸 수도 있다.
SELECT id, name, is_active
FROM customers
WHERE is_active;위 쿼리는 is_active = true와 같은 의미다. 초보 단계에서는 명시적으로 = true를 써도 좋다. 팀 코드에서는 한 가지 방식으로 통일하는 편이 읽기 쉽다.
마지막으로 NULL을 주의해야 한다. NULL은 0도 아니고 빈 문자열도 아니다. "값이 없음" 또는 "알 수 없음"에 가깝다. 그래서 다음 쿼리는 원하는 대로 동작하지 않는다.
SELECT id, name, phone
FROM customers
WHERE phone = NULL;NULL은 =로 비교하지 않는다. 반드시 IS NULL을 사용한다.
SELECT id, name, phone
FROM customers
WHERE phone IS NULL;값이 있는 행만 찾고 싶다면 IS NOT NULL을 쓴다.
SELECT id, name, phone
FROM customers
WHERE phone IS NOT NULL;NULL 때문에 결과가 달라지는 일은 실무에서 자주 발생한다. 조건이 맞는데 행이 빠진 것처럼 보이면, 먼저 해당 열에 NULL이 있는지 확인하자.
검색 화면에서 자주 쓰는 조건
관리자 화면에서 고객을 검색한다고 생각해 보자. 검색 화면에는 이름, 이메일, 등급, 가입일, 전화번호 등록 여부 같은 조건이 있을 수 있다. 이 요구사항은 SQL 조건으로 자연스럽게 바뀐다.
이름에 특정 글자가 포함된 고객을 찾을 때는 LIKE를 사용한다.
SELECT id, name, email
FROM customers
WHERE name LIKE '%민%';%는 글자 수와 관계없이 아무 문자열이나 올 수 있다는 뜻이다. 따라서 '%민%'은 앞뒤에 무엇이 있든 이름 안에 민이 포함된 값을 찾는다.
이메일 도메인이 example.com인 고객은 다음처럼 찾을 수 있다.
SELECT id, name, email
FROM customers
WHERE email LIKE '%@example.com';LIKE는 대소문자를 구분한다. 영문 검색에서 대소문자를 구분하지 않고 찾고 싶다면 PostgreSQL의 ILIKE를 사용할 수 있다.
SELECT id, name, email
FROM customers
WHERE email ILIKE '%gmail.com';여러 등급 중 하나에 해당하는 고객은 IN이 적합하다.
SELECT id, name, grade
FROM customers
WHERE grade IN ('VIP', 'GOLD');이 쿼리는 grade = 'VIP' OR grade = 'GOLD'와 같은 의미지만, 값 목록이 길어질수록 IN이 더 읽기 쉽다.
가입일이 특정 기간에 들어오는 고객은 BETWEEN으로 표현할 수 있다.
SELECT id, name, joined_at
FROM customers
WHERE joined_at BETWEEN '2026-01-01' AND '2026-01-31';BETWEEN은 양쪽 끝값을 포함한다. 다만 joined_at이 시간까지 저장하는 열이라면 주의가 필요하다. 위 조건은 '2026-01-31 00:00:00'까지 포함하는 의미가 될 수 있어, 1월 31일 낮에 가입한 고객이 빠질 수 있다. 시간까지 저장된 열에서는 다음 방식이 더 안전하다.
SELECT id, name, joined_at
FROM customers
WHERE joined_at >= '2026-01-01'
AND joined_at < '2026-02-01';시작 시점은 포함하고, 다음 달 시작 전까지 조회하는 방식이다. 로그, 주문, 결제처럼 시간이 중요한 데이터에서 자주 쓰는 패턴이다.
전화번호가 등록되지 않은 고객을 찾는 조건은 다음과 같다.
SELECT id, name, phone
FROM customers
WHERE phone IS NULL;검색 화면 요구사항을 SQL로 바꾸면 다음처럼 정리할 수 있다.
이름 포함 검색 → LIKE 또는 ILIKE
여러 등급 선택 → IN
기간 검색 → >= 와 <, 또는 BETWEEN
미입력 값 검색 → IS NULL
입력된 값만 검색 → IS NOT NULL조건을 외우기보다 "사용자가 어떤 목록을 보고 싶은가"를 먼저 문장으로 쓰고, 그 문장을 SQL 조건으로 바꾸는 연습을 하자.
별칭과 계산식: 결과를 읽기 좋은 형태로 만들기
조회 결과의 열 이름을 바꾸고 싶을 때는 별칭을 사용한다.
SELECT
id AS customer_id,
name AS customer_name,
point AS current_point
FROM customers;AS는 결과 열의 이름을 지정한다. AS를 생략할 수도 있지만, 처음에는 적어 주는 편이 의도가 분명하다.
계산식도 SELECT에 넣을 수 있다. 예를 들어 포인트를 1,000점 단위 쿠폰 수로 환산해 보자.
SELECT
id,
name,
point,
point / 1000 AS coupon_count
FROM customers;정수끼리 나누면 소수점 이하가 잘릴 수 있다. 정확한 비율 계산이 필요하면 형 변환을 사용한다.
SELECT
id,
name,
point,
point::numeric / 1000 AS coupon_count
FROM customers;point::numeric은 point 값을 numeric 타입으로 바꾸라는 뜻이다. PostgreSQL에서는 값::타입 형태의 형 변환을 자주 볼 수 있다. 표준 SQL 방식으로는 CAST(point AS numeric)도 사용할 수 있다.
SELECT
id,
name,
CAST(point AS numeric) / 1000 AS coupon_count
FROM customers;문자열을 합쳐 표시할 때는 concat 함수를 사용할 수 있다.
SELECT
id,
concat(name, ' <', email, '>') AS display_name
FROM customers;NULL을 다른 값으로 바꾸어 표시할 때는 coalesce가 유용하다.
SELECT
id,
name,
coalesce(phone, '미등록') AS phone_display
FROM customers;coalesce는 앞에서부터 값을 확인해 NULL이 아닌 첫 번째 값을 반환한다. 전화번호가 있으면 전화번호를 보여 주고, 없으면 '미등록'을 보여 준다.
날짜·시간 함수도 기본 조회에서 자주 쓴다.
SELECT
id,
name,
joined_at,
now() AS queried_at
FROM customers
LIMIT 5;now()는 현재 트랜잭션의 시각을 반환한다. 지금은 "함수 호출 결과도 조회 결과의 열처럼 보여 줄 수 있다"는 점을 이해하면 충분하다.
한 번에 읽는 SELECT 문
이제 고객 목록 화면을 위한 쿼리를 하나로 묶어 보자. 요구사항은 다음과 같다.
활성 고객만 조회한다.
등급은
VIP또는GOLD만 본다.2026년에 가입한 고객을 대상으로 한다.
이메일에
gmail.com이 포함된 고객을 찾는다.포인트가 높은 순서로 정렬한다.
상위 20명만 가져온다.
전화번호가 없으면
미등록으로 표시한다.
SQL은 다음과 같다.
SELECT
id,
name,
email,
grade,
point,
coalesce(phone, '미등록') AS phone_display,
joined_at
FROM customers
WHERE is_active = true
AND grade IN ('VIP', 'GOLD')
AND joined_at >= '2026-01-01'
AND joined_at < '2027-01-01'
AND email ILIKE '%gmail.com%'
ORDER BY point DESC, joined_at DESC
LIMIT 20;길어 보이지만 구조는 단순하다.
SELECT: 화면에 보여 줄 열을 정한다.FROM: 고객 테이블에서 읽는다.WHERE: 필요한 고객만 남긴다.ORDER BY: 포인트와 가입일 기준으로 정렬한다.LIMIT: 20명만 가져온다.
복잡한 SQL도 기본 골격은 여기에서 크게 벗어나지 않는다. 나중에 조인, 집계, 윈도 함수가 추가되어도 "어디서 읽고, 어떤 행을 남기고, 어떤 값을 보여 주며, 어떤 순서로 제한하는가"를 따라가면 된다.
실행 후 확인하는 습관
SQL은 실행됐다고 끝이 아니다. 결과가 맞는지 확인해야 한다. 특히 처음 쿼리를 작성할 때는 조건을 한 번에 많이 넣지 말고 단계적으로 좁히는 편이 안전하다.
예를 들어 다음 순서로 확인할 수 있다.
-- 1단계: 전체 고객 일부 확인
SELECT id, name, grade, point
FROM customers
LIMIT 10;
-- 2단계: 활성 고객만 확인
SELECT id, name, grade, point
FROM customers
WHERE is_active = true
LIMIT 10;
-- 3단계: 등급 조건 추가
SELECT id, name, grade, point
FROM customers
WHERE is_active = true
AND grade IN ('VIP', 'GOLD')
LIMIT 10;
-- 4단계: 정렬과 개수 제한 추가
SELECT id, name, grade, point
FROM customers
WHERE is_active = true
AND grade IN ('VIP', 'GOLD')
ORDER BY point DESC
LIMIT 10;각 단계에서 행 수와 값의 범위를 눈으로 확인하자. 결과가 비어 있으면 조건이 너무 좁은지, 비교 값의 대소문자나 날짜 범위가 맞는지, NULL 때문에 제외된 값이 있는지 점검한다.
이 장의 핵심은 문법을 많이 외우는 것이 아니다. SELECT, FROM, WHERE, ORDER BY, LIMIT의 역할을 알고, 값의 타입에 맞게 비교하고, 결과를 필요한 형태로 가공하는 흐름을 익히는 것이다. 이 흐름이 익숙해지면 다음 장에서 테이블을 설계할 때도 "나중에 어떤 방식으로 조회할 것인가"를 함께 생각할 수 있다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.