학생 한 명의 성적을 찾으려고 Scan에 id = 12093 필터를 붙였다. 결과는 딱 세 건. Query도 세 건을 돌려주니 '둘이 같은 일을 하는 것 아닌가?' 싶다. 결과가 같아도 읽기 시작점은 다르다. 필터가 세 건만 '보이게' 하는 것과 세 건만 '읽게' 하는 것은 별개의 일이다.
Query와 Scan은 어디서부터 읽을까?
StudentScore 테이블의 기본 키가 파티션 키 id(학생 번호)와 정렬 키 subject(과목)로 구성됐다고 하자. 두 학생의 성적을 다음처럼 저장한 작은 예시다.
| id | subject | score |
|---|---|---|
| 12093 | English | 80 |
| 12093 | Science | 92 |
| 12093 | Math | 95 |
| 12103 | English | 100 |
| 12103 | Science | 88 |
| 12103 | Math | 82 |
Query는 파티션 키 id = 12093에서 시작한다. 이 학생의 세 항목을 찾는 데 다른 학생의 성적을 먼저 읽을 이유가 없다. 반면 Scan은 테이블 또는 인덱스를 훑고, FilterExpression을 붙였다면 읽은 뒤에 맞지 않는 항목을 버린다. 아래 그림의 6 read → 3 returned는 이 여섯 행을 한 페이지에서 읽는 설명용 예시다. 실제 읽기 용량 수치를 뜻하지 않는다.
필터를 붙였는데 왜 읽기 비용은 그대로일까?
필터는 결과를 골라서 보내는 단계에 있다. AWS의 Scan 설명에도 필터 유무와 관계없이 같은 데이터를 읽었다면 읽기 용량 소비가 같다고 명시돼 있다. ProjectionExpression으로 돌려받는 속성만 줄여도, 이미 읽은 항목의 비용까지 줄어들지는 않는다.
작은 예시에서는 Scan이 여섯 항목을 평가해 세 항목을 반환하고, 키로 좁힌 Query는 해당 학생의 세 항목을 대상으로 한다. 하지만 6 대 3을 곧바로 RCU가 두 배라는 계산으로 바꾸면 안 된다. 항목 크기, 일관성 읽기, 페이지와 최소 과금 단위가 실제 소비량에 영향을 준다. 실제 테이블에서는 응답의 ScannedCount(평가한 항목 수), Count(반환한 항목 수), ConsumedCapacity를 함께 확인한다.
한 번의 Scan은 최대 1MB를 읽고 필터링한다. 첫 페이지에 일치 항목이 없어 Count가 0이어도, LastEvaluatedKey가 있으면 뒤에 데이터가 남아 있을 수 있다. 빈 결과 한 번만 보고 '이 학생은 없다'고 결론 내리면 성적표보다 먼저 식은땀이 난다.
학생 번호 조회를 Query로 바꾸려면?
테이블이 위의 키 구조로 이미 만들어져 있다면, 학생 번호는 FilterExpression이 아니라 키 조건에 넣는다. 다음 두 명령은 같은 조건을 요청하지만 접근 방식은 다르다. 실제 AWS 계정에서 실행한 출력이 아니라, 해당 테이블이 있을 때 비교해 볼 명령 예시다.
aws dynamodb query \
--table-name StudentScore \
--key-condition-expression 'id = :student' \
--expression-attribute-values '{":student":{"N":"12093"}}' \
--return-consumed-capacity TOTAL
aws dynamodb scan \
--table-name StudentScore \
--filter-expression 'id = :student' \
--expression-attribute-values '{":student":{"N":"12093"}}' \
--return-consumed-capacity TOTALAWS의 Query 키 조건 설명에 따르면 파티션 키의 이름과 단일 값을 반드시 지정해야 한다. 정렬 키 subject는 필요할 때 조건을 더할 수 있다. 반대로 'Math 과목 학생을 점수순으로 찾기'가 자주 필요한 요구라면 id만으로는 시작점을 잡을 수 없다. 이때는 subject를 파티션 키, score를 정렬 키로 하는 GSI를 설계해 그 인덱스를 Query할 수 있다. 대신 인덱스의 추가 쓰기·저장 비용과 최종 일관성 읽기만 지원한다는 점도 함께 판단해야 한다.
Scan은 언제 써도 될까?
Scan을 무조건 금지할 필요는 없다. 테이블 전체를 살펴야 하는 관리·일괄 작업이라면 의도에 맞을 수 있다. 다만 사용자 요청마다 Scan + 필터를 반복하는 조회 API라면, 데이터가 커질수록 읽지 않아도 될 항목까지 평가한다. 이런 경우 먼저 어떤 값으로 조회할지 적고, 그 값이 테이블이나 인덱스의 파티션 키인지 확인하는 편이 낫다.
전체 조회가 정말 필요하다면 한 페이지의 Count만 보지 말고 LastEvaluatedKey로 끝까지 넘기는지, 소비 용량과 다른 요청에 미치는 영향을 점검한다. 작은 개발용 테이블에서 빠르게 끝났다는 사실은 운영 테이블의 비용·지연 시간을 보증하지 않는다.
핵심 요약
Query는 파티션 키 값에서 읽기를 시작하고, Scan은 항목을 읽은 뒤 필터로 반환 결과를 줄인다. 돌아온 세 건이 같아도 읽은 범위는 다를 수 있다. FilterExpression을 성능 최적화로 착각하지 말고 ScannedCount, Count, 실제 소비 용량과 페이지 처리를 확인하자. 조회 요구가 반복된다면 필터를 더 붙이기 전에 키와 GSI 설계를 다시 보는 것이 순서다.
조건이 적용되는 단계가 결과에 미치는 영향을 더 보고 싶다면 SQL WHERE와 HAVING 차이를 이어 읽을 수 있다. SQL의 집계 조건과 DynamoDB의 읽기 방식은 서로 다른 문제이므로 그대로 대응시키지는 말자.

