DynamoDB 키 조건 표현식
키 조건 표현식(key condition expression) 은 Query에 넘기는
KeyConditionExpression입니다 — 요청에서 DynamoDB가 아이템을 찾는 데 쓰는 유일한
부분이죠. 나머지(필터, 프로젝션)는 읽기가 이미 계측된 후에 실행됩니다.
DynamoDB에서 키 조건 표현식이란 무엇인가요?
키 조건 표현식은 Query에 넘기는 KeyConditionExpression으로, DynamoDB가 어떤 아이템을 읽을지 알려줍니다. 는 반드시 동등 비교(PK = :v)여야 하고, 는 =, <, <=, >, >=, BETWEEN, 또는 begins_with 중 하나의 범위 연산자를 받습니다. 필터와 달리, 무엇이 읽히고 과금되는지를 결정하는 부분입니다.
- 파티션 키는 반드시 동등 비교입니다.
PK = :v, 그뿐 — 범위도,begins_with도,IN도 없습니다. DynamoDB가 그것을 해시해 한 파티션을 찾습니다. - 정렬 키는 범위 연산자를 받습니다.
=,<,<=,>,>=,BETWEEN, 또는begins_with— 여기가 아이템 컬렉션을 잘라 내는 곳입니다. - 필터가 아닙니다. 키 조건은 무엇이 읽히고 과금되는지를 결정하고,
FilterExpression은 읽기에 값을 치른 후 결과를 다듬을 뿐입니다. - 정렬 키는 바이트 순서입니다. 범위 연산자는 사전식으로 비교하므로, 정렬 키 문자열을 어떻게 형식화하느냐 가 곧 쿼리 능력입니다.
파티션 키가 동등 비교에 묶이는 이유
DynamoDB는 파티션 키를 물리 파티션으로 해시해 아이템을 저장합니다. 해시는 범위가 아니라 하나의 위치를 주므로 — 가로질러 스캔할 게 없습니다.
그래서 PK > :v나 begins_with(PK, :v)가 단호히 거부됩니다. 엔진은 테이블 전체를
읽지 않고는 "키가 X로 시작하는 모든 파티션"에 답할 수 없는데, 그게 바로 그것이
피하도록 만들어진 Scan입니다.
SQL에서 왔다면 이게 거꾸로 느껴집니다. Postgres에서 WHERE id LIKE 'order%'는
사소하니까요. DynamoDB에서 파티션 키는 검색 가능한 컬럼이 아니라 주소입니다.
정렬 키가 능력이 사는 곳
한 파티션 안에서 아이템은 정렬 키로 정렬되어 저장됩니다. 그 순서가 범위 연산자가 활용하는 것입니다 — DynamoDB가 한 위치로 탐색해 앞으로 읽어 나가죠.
| 연산자 | 읽는 것 | 용도 |
|---|---|---|
SK = :v | 정확한 아이템 하나 | 키로 지목한 특정 자식 |
SK < / <= / > / >= :v | 한쪽이 열린 슬라이스 | "이 지점 이후 전부" |
SK BETWEEN :a AND :b | 닫힌 범위(양끝 포함) | 한정된 구간 — 날짜 범위 |
begins_with(SK, :p) | 접두사 슬라이스 | PK 아래의 한 유형이나 계층 |
키에는 LIKE도, CONTAINS도, ENDS_WITH도 없습니다. 부분 문자열과 접미사 매칭은
바이트 순서가 아니라 전체 읽기를 강요할 테니 — 설계상, API가 허락하지 않습니다.
그것들은 이미 값을 치른 FilterExpression에 삽니다. (AWS: 키 조건
표현식)
실전 예제: 채팅 앱의 메시지
채널 기반 채팅을 짓는다고 합시다. 하나의 테이블, 채널로 파티셔닝되고, 메시지 시간으로 정렬됩니다. 독자적인 키 스키마:
- 파티션 키
ChannelRef—CH#{channelId} - 정렬 키
PostedAt— ISO-8601 타임스탬프,MSG#2026-06-23T14:05:00Z
MSG# 접두사는 메시지 행을 정렬 가능하게 유지하고, 같은 채널 아래 함께 둘 수 있는
다른 행 유형(고정 설정, 멤버십)과 구별되게 합니다.
채널의 최신 메시지 불러오기. 파티션 키만, 최신순:
KeyConditionExpression: ChannelRef = :ch
ExpressionAttributeValues: { ":ch": "CH#general" }
ScanIndexForward: falseScanIndexForward: false는 정렬된 컬렉션을 거꾸로 훑습니다 — 클라이언트 측 정렬
없이 "가장 최근 먼저"를 얻는 저렴한 방법이죠.
begins_with로 특정 날. 타임스탬프가 정렬 키이고 텍스트로 저장되므로, 날짜
접두사가 깔끔한 슬라이스입니다:
KeyConditionExpression ChannelRef = :ch AND begins_with(PostedAt, :day)
:ch "CH#general"
:day "MSG#2026-06-23"
이것은 2026-06-23의 모든 메시지를, 그리고 그것만 읽습니다 — DynamoDB가 접두사로 탐색하고 그 끝에서 벗어나면 멈춥니다. 이것이 작동하는 건 오직 접두사가 바이트로 정렬된 문자열의 진정한 왼쪽 앵커이기 때문입니다.
BETWEEN으로 정밀한 구간. "14:00 시간대의 메시지"에는 접두사보다 포함 범위가
낫습니다:
KeyConditionExpression ChannelRef = :ch AND PostedAt BETWEEN :lo AND :hi
:ch "CH#general"
:lo "MSG#2026-06-23T14:00:00Z"
:hi "MSG#2026-06-23T14:59:59Z"
BETWEEN은 양 경계를 포함하므로, 끝점을 신중히 고르세요 — 여기서 off-by-one은
경계 메시지를 조용히 떨어뜨리거나 두 배로 셉니다.
DynamoDB 표현식 빌더에서 이 표현식 중 무엇이든
ExpressionAttributeValues 맵을 채운 채로 조립하고 복사할 수 있습니다 —
begins_with와 BETWEEN 문법을 처음부터 맞게 잡는 데 유용합니다.
DynoTable에서 직접 보기
같은 키 조건을 실제 채널 파티션에 실행하고, 소비된 용량이 라이브로 갱신되는 걸 지켜보세요. 그래야 컬렉션 전체가 아니라 슬라이스를 읽고 있음을 확인할 수 있습니다.
함정: 키 조건을 필터와 혼동하기
비싼 실수는 키의 일을 하려고 FilterExpression에 손을 뻗는 것입니다.
KeyConditionExpression: ChannelRef = :ch
FilterExpression: begins_with(PostedAt, :day)이건 위의 begins_with 키 조건과 같아 보이고 같은 행을 돌려줍니다 — 하지만 채널
파티션 전체를 먼저 읽고, 그날 밖의 모든 것을 버립니다. 전체 읽기에 과금됩니다.
필터는 결코 읽기 비용을 줄이지 않습니다. DynamoDB가 아이템을 계측한 후에 실행되니까요 —
필터된 Scan과 같은 함정이죠. 술어가 키 조건에 들어갈 수
있다면, 거기에 속합니다.
해결은 상류에 있습니다. 액세스 패턴을 하나의 PK 동등 비교 더하기 정렬 키 범위로 표현할 수 없다면, 그건 모델링 신호입니다. 정렬 키를 다시 빚거나, 그 패턴에 맞춰 키잉된 인덱스를 추가하세요 — 키를 어떻게 배치할지는 GSI vs LSI와 싱글 테이블 디자인을 보세요.
함정과 다음 단계
- 파티션 키는 항상
=입니다. 범위는, 절대로, 안 됩니다. 파티션을 가로지르는 범위가 필요하다면, 단일Query를 벗어난 것입니다. - 쿼리당 정렬 키 조건 하나. 두 정렬 키 술어를
AND할 수 없습니다.BETWEEN또는begins_with를 고르되, 둘 다는 안 됩니다. - 예약어는 별칭이 필요합니다.
Timestamp나Name으로 이름 지은 키는ExpressionAttributeNames(#ts)를 써야 합니다 — 안 그러면 쿼리가 오류납니다. (AWS: 예약어) BETWEEN은 포함적입니다. 양 끝점이 일치하므로 — 그에 맞춰 경계를 설계하세요.
표현식 빌더에서 키 조건을 초안 잡은 뒤, DynoTable을 써보세요. 여러분 테이블에 실행하고 그것이 실제로 소비하는 용량을 지켜보세요.