Elasticsearch 입문 16편 Aggregations Metric. sum·avg·min·max·stats·percentiles·cardinality·top_hits·top_metrics.
이 글은 Elasticsearch 입문에서 운영까지 시리즈 38편 중 16편이에요. 12~15편에서 Search API · Full-text · Term-level · Compound 쿼리를 다뤘다면, 이제 검색 결과를 뽑는 것 에서 합치고·평균 내고·줄 세우는 것 으로 한 발 더 들어가는 자리예요. Aggregations — ES 를 검색 엔진 이 아니라 검색 + 분석 양손잡이 엔진 으로 만드는 핵심 기능이에요.
이 글은 Elasticsearch 8.x 공식 docs 의 Metrics aggregations 챕터를 한국어 학습 노트로 풀어쓴 자료예요.
Kibana 의 Dev Tools 콘솔 또는 curl 로 본문 예제를 직접 한 번 던져 보면 본문이 머리에 훨씬 잘 박혀요.
Aggregations 가 ES 를 양손잡이로 만드는 이유
검색 엔진은 맞는 문서를 찾아 주는 게 본업이에요. 그런데 e-commerce·로그·관측·BI 자리에서 정작 자주 필요한 건 "이 카테고리에서 평균 가격이 얼마예요", "오늘 4xx 응답 비율이 어제보다 얼마나 튀었어요", "p95 응답 시간이 SLO 안에 있어요" 같은 숫자 요약 이에요.
전통적으로는 검색은 Elasticsearch·분석은 별도 OLAP (예: ClickHouse·BigQuery) 로 갈라 쓰는 게 보통이었는데, ES 는 한 요청 에 풀텍스트 검색과 집계 결과를 같이 돌려 주는 길을 열어 줬어요. 검색 hits 와 집계 결과가 한 응답에 같이 와요. 화면 한 장에 상품 목록 + 가격대 분포 + 평균 별점 + 브랜드별 카운트 를 동시에 그릴 수 있는 이유.
이게 가능한 건 ES 가 Doc Values (column-oriented 저장) 라는 컬럼 지향 자료구조를 자동으로 만들어 두기 때문이에요. RDBMS 의 GROUP BY 가 row-store 위에서 도는 것과 달리, ES Aggregations 는 column-store 위에서 도는 MPP-스타일 분산 집계 라고 보면 돼요. 분산되어 있는 샤드 각각이 부분 집계 를 만들고, coordinator 노드가 최종 머지 하는 식.
Aggregations 의 3분류 — 이번 글의 자리
ES Aggregations 는 크게 세 종류로 나뉘어요. 시리즈 16~18편에서 한 종류씩 다룹니다.
| 분류 | 하는 일 | 대표 agg | 시리즈 편 |
|---|---|---|---|
| Metric | 숫자 한 개를 계산 (합·평균·분위수 등) | sum · avg · stats · percentiles · cardinality | 16편 (지금) |
| Bucket | 문서를 그룹으로 묶음 (date_histogram · terms · range 등) | terms · date_histogram · range · filters | 17편 |
| Pipeline | 다른 agg 결과 위에서 후처리 (moving_avg · derivative 등) | moving_avg · derivative · cumulative_sum | 18편 |
세 종류는 합쳐서 쓰는 게 정상 이에요. 예 — date_histogram (Bucket) 으로 일별 버킷을 만들고, 그 안에 avg (Metric) 으로 일별 평균을, 다시 그 위에 moving_avg (Pipeline) 로 7일 이동 평균을 얹는 식. Bucket 안에 Metric 이 들어가는 중첩 구조가 ES 집계의 기본 문법.
이번 16편은 숫자 한 개를 계산 하는 Metric 만 집중해서 풀어요. 7가지 핵심 agg + 함정·운영 패턴까지.
sum · avg · min · max — 가장 기본 4총사
sum · avg · min · max 가 Metric agg 의 출발점이에요. 이름 그대로 합·평균·최솟값·최댓값 을 한 숫자로 돌려 줘요.
GET /orders/_search
{
"size": 0,
"aggs": {
"total_revenue": { "sum": { "field": "amount" } },
"avg_order": { "avg": { "field": "amount" } },
"cheapest": { "min": { "field": "amount" } },
"biggest": { "max": { "field": "amount" } }
}
}
"size": 0 을 박는 게 집계 전용 요청 의 표준 패턴이에요. 검색 hits 는 필요 없고 집계만 받으면 되니까, 문서를 0개만 hits 로 넘겨 불필요한 전송 비용을 0으로 잘라요. 응답은 이런 모양.
{
"aggregations": {
"total_revenue": { "value": 12450000 },
"avg_order": { "value": 49800 },
"cheapest": { "value": 1000 },
"biggest": { "value": 980000 }
}
}
value_count — null 빼고 갯수
value_count 는 해당 필드에 값이 있는 문서 갯수 예요. 전체 문서 수가 아니라 그 필드 기준 non-null 카운트. SQL 의 COUNT(field) 와 같은 자리.
"with_price": { "value_count": { "field": "amount" } }
amount 필드가 비어 있는 문서는 카운트에서 빠져요. 이게 일반 _count API 와 차이.
missing — null 채워서 계산
기본적으로 ES Metric agg 는 해당 필드가 없는 문서를 자동으로 무시 해요. null 을 0 으로 치환 하고 싶으면 missing 파라미터를 박아요.
"avg_amount": {
"avg": {
"field": "amount",
"missing": 0
}
}
평균을 0 포함 으로 잡고 싶을 때 — 예 환불된 주문 (amount = null) 도 0 으로 잡아 평균 — 에 쓰는 자리. 다만 분모 정의가 달라지니까 비즈니스 정의를 먼저 못 박고 쓰세요. 환불 = null 무시 가 맞는지 0 으로 계산 이 맞는지에 따라 답이 두 배 정도 차이 나는 일이 잦아요.
stats · extended_stats — 한 번에 여러 통계
stats 는 count · min · max · sum · avg 다섯 가지를 한 번 에 돌려줘요. 같은 필드를 5번 쿼리할 거면 그냥 stats 한 번 이 비용 면에서 압도적이에요.
"price_stats": {
"stats": { "field": "amount" }
}
응답.
{
"price_stats": {
"count": 1247,
"min": 1000,
"max": 980000,
"avg": 49800,
"sum": 62100600
}
}
extended_stats — std deviation 까지
extended_stats 는 stats 의 다섯 개에 sum_of_squares · variance · std_deviation · std_deviation_bounds 같은 분산 통계 까지 더해 줘요. 주문 금액 분포가 얼마나 흩어져 있는가, 응답 시간이 평균 대비 얼마나 들쭉날쭉한가 같은 자리에 핵심.
"price_extended": {
"extended_stats": {
"field": "amount",
"sigma": 2
}
}
sigma: 2 는 평균 ± 2σ 범위 를 같이 돌려달라는 옵션이에요. 응답에 std_deviation_bounds.upper, std_deviation_bounds.lower 가 추가로 박혀요. 통계적으로 95% 신뢰구간 비슷한 자리.
다만 — std deviation 은 정규분포 가정 이 깔린 통계예요. e-commerce 주문 금액 같은 long-tail · right-skewed 분포에는 평균 ± 2σ 가 의미를 잘 못 잡아요. 분위수 (percentiles) 가 더 정직한 답을 줘요.
percentiles · percentile_ranks — T-Digest 의 핵심
응답 시간 분석에서 평균 만 보고 SLO 를 결정하면 큰 사고가 나요. 평균 100ms 인데 p99 = 3,000ms 인 서비스는 99% 사용자에게는 빠르지만 1% 사용자가 30배 느린 화면을 보는 자리거든요. 그래서 운영 모니터링은 p50 · p95 · p99 같은 분위수 가 표준.
"latency_percentiles": {
"percentiles": {
"field": "response_time_ms",
"percents": [50, 95, 99, 99.9]
}
}
응답.
{
"latency_percentiles": {
"values": {
"50.0": 87,
"95.0": 412,
"99.0": 1240,
"99.9": 3800
}
}
}
T-Digest — 분위수 근사값
여기서 중요한 — percentiles 는 정확한 값이 아니라 근사값 이에요. 데이터가 수억 건이면 모든 값을 메모리에 들고 정렬 하는 게 불가능하니까, ES 는 T-Digest 라는 분위수 근사 알고리즘 을 써요. T-Digest 는 중심부 (p1~p99) 정확도는 매우 높고, 극단 꼬리 (p99.9 이상) 는 상대적으로 덜 정확 한 특성이 있어요.
오차를 컨트롤하는 파라미터가 tdigest.compression 이에요. 기본값 100. 숫자를 올리면 메모리 더 쓰고 정확도 ↑, 내리면 메모리 ↓ 정확도 ↓. 운영 모니터링은 기본값 100 이면 충분하고, p99.99 같은 극단 꼬리 가 핵심이면 200~300 으로 올려요.
"latency_p99_precise": {
"percentiles": {
"field": "response_time_ms",
"percents": [99, 99.9, 99.99],
"tdigest": { "compression": 300 }
}
}
percentile_ranks — 역방향 질문
percentile_ranks 는 "이 값이 분포의 몇 % 자리에 해당해요" 를 거꾸로 물어요. "응답 시간 500ms 가 전체 트래픽의 몇 % 자리예요" 같은 질문.
"sla_compliance": {
"percentile_ranks": {
"field": "response_time_ms",
"values": [200, 500, 1000]
}
}
응답 "500ms 가 97.3% 자리예요" 가 나오면 "전체 요청의 97.3% 가 500ms 이내에 응답했다" 와 같은 말. SLO "95% of requests under 500ms" 같은 자리에 그대로 매핑되는 답이에요.
hdr histogram — 대안 알고리즘
T-Digest 외에 HDR (High Dynamic Range) Histogram 이라는 다른 근사 알고리즘도 옵션으로 있어요. 동적 범위가 수 ms ~ 수 분 처럼 매우 넓을 때 유리. 기본은 T-Digest 이고, 지연 시간 모니터링 자리에서만 HDR 를 검토해도 됩니다.
"latency_hdr": {
"percentiles": {
"field": "response_time_ms",
"percents": [50, 95, 99],
"hdr": { "number_of_significant_value_digits": 3 }
}
}
cardinality — HyperLogLog 의 자리
cardinality 는 "고유한 값이 몇 개예요" 를 답해 줘요. SQL 의 COUNT(DISTINCT field). "오늘 방문한 고유 사용자 수", "이 시간 동안 본 고유 IP 수", "이 쿼리로 들어온 고유 상품 종류" 같은 자리.
"unique_users": {
"cardinality": { "field": "user_id" }
}
HyperLogLog — 또 다른 근사
여기도 근사값 이에요. 수억 건 데이터에서 고유 값을 정확히 세려면 모든 값을 메모리 set 에 넣어야 하니까. ES 는 HyperLogLog++ (이하 HLL) 라는 확률적 자료구조 를 써서 적은 메모리로 근사 카운트 를 돌려요.
오차 컨트롤 파라미터가 precision_threshold 예요. 기본 3,000. 의미는 카디널리티가 3,000 이하면 거의 정확, 그보다 크면 점진적으로 오차가 늘어남. 최대값은 40,000.
"unique_users_precise": {
"cardinality": {
"field": "user_id",
"precision_threshold": 40000
}
}
precision_threshold 를 40,000 으로 올리면 카디널리티 40,000 이하까지는 매우 정확 하고 그보다 커도 오차 1~2% 수준 으로 잡아 줘요. 메모리는 threshold × 8 byte 정도 추가로 써요. 40,000 × 8 = 320KB 정도라 비용은 작은 편.
정확한 카운트가 필요하면
비즈니스가 "정확한 고유 사용자 수가 법적으로 필요해요" (예 — 광고 노출 인보이스 청구) 같은 자리면 ES cardinality 만으로는 부족해요. 이런 자리는 Logstash → Kafka → 정확 카운팅 시스템 (예 PG COUNT DISTINCT) 같은 별도 파이프 를 깔아야 합니다.
카디널리티가 낮은 필드는 — terms 가 더 정확
cardinality 는 고유값 수 만 답해요. 어떤 값이 있어요 까지 알고 싶으면 Bucket aggregation 의 terms (17편) 를 써야 해요. 카디널리티가 낮은 필드 (예 상품 카테고리 = 50개) 는 terms 가 정확한 카운트 까지 같이 돌려 주고, 카디널리티가 높은 필드 (예 user_id = 수백만) 는 근사 인 cardinality 가 정답.
value_count · top_hits · top_metrics — 추가 metric agg
value_count — non-null 카운트
이미 위에서 짚었으니 짧게. 그 필드에 값이 있는 문서 갯수 만 답해요. "가격 정보가 있는 상품 갯수" 같은 자리.
top_hits — 버킷 안 대표 문서
top_hits 는 Bucket 안에서 대표 문서 N개 를 그대로 뽑아 줘요. 예 — 각 카테고리 안에서 가장 비싼 상품 1개 를 뽑는 자리. Bucket aggregation 안에 중첩 해서 쓰는 게 보통이라 17편에서 더 자세히.
"by_category": {
"terms": { "field": "category" },
"aggs": {
"top_in_category": {
"top_hits": {
"size": 1,
"sort": [{ "amount": "desc" }]
}
}
}
}
top_metrics — 가벼운 top_hits
top_metrics 는 top_hits 의 경량 버전 이에요. 전체 문서 가 아니라 지정한 필드 몇 개만 뽑아 줘서 메모리·네트워크 비용이 훨씬 낮음. 8.x 부터 권장 — 최근 가격 1개 같은 자리.
"latest_price": {
"top_metrics": {
"metrics": [{ "field": "amount" }],
"sort": { "timestamp": "desc" }
}
}
자주 만나는 사고
사고 1 — cardinality precision_threshold 너무 작아 부정확
원인 — precision_threshold 기본 3,000 으로 둔 채 고유 사용자 수 500만 같은 자리에서 cardinality 를 돌리면, 오차가 5~10% 까지 벌어져서 유료 광고 인보이스 청구액 수억 원 같은 자리에서 사고로 이어져요.
해결 — 카디널리티가 3,000 ~ 수만 이면 precision_threshold: 40000 으로 올리고, 수십만 이상 이면 별도 정확 카운팅 파이프 를 깔아요. ES cardinality 는 대시보드용 근사값 으로만 쓰는 게 안전.
사고 2 — percentiles T-Digest 오차로 SLO 보고가 어긋남
원인 — p99 = 1,200ms 로 ES 가 답했는데, 실제 정확 분포로 계산하면 p99 = 1,500ms 인 경우. 기본 compression: 100 으로는 극단 꼬리 (p99 이상) 오차가 수십 % 까지 벌어질 수 있어요.
해결 — p99 이상이 SLO 의 핵심 이면 tdigest.compression: 200~300 으로 올리거나, HDR Histogram 으로 갈아타요. 그래도 정확 SLO 보고용 이면 원본 latency 를 별도 저장 (예 PostgreSQL TimescaleDB) 하는 이중 파이프가 안전.
사고 3 — sum 으로 keyword 필드 집계 시도
원인 — 가격을 amount 필드를 keyword 로 매핑한 채 sum 을 돌리면 Fielddata is disabled 또는 Field [amount] is of type keyword, which is not supported by sum aggregation 같은 에러가 떠요.
해결 — Metric agg 는 numeric · date · boolean · histogram 필드 위에서만 돌아요. 8편(Mapping Deep) 의 표준 패턴 — 숫자는 long·double·integer·float 로 매핑하고, 식별자 (예 user_id) 만 keyword 로. 이미 keyword 로 박힌 필드는 reindex (5편) 로 매핑을 바꿔야 해요.
사고 4 — Doc Values 비활성으로 집계 실패
원인 — 메모리 절약하려고 doc_values: false 로 매핑한 필드에서 Metric agg 를 돌리면 Can't load fielddata on [...] because fielddata is unsupported on fields of type [keyword] 또는 비슷한 에러.
해결 — Aggregations 는 Doc Values 위에서 동작 해요. 집계 대상 필드는 doc_values 켜 두는 게 기본. 메모리 부담이 정 크면 runtime field (검색 시점 계산) 로 대안 — 다만 응답 시간은 느려져요.
사고 5 — missing 옵션 안 박아 평균이 왜곡
원인 — 주문 amount 가 null 인 환불 주문 30% 가 섞인 인덱스에서 avg: { field: amount } 만 박으면 non-null 70% 만의 평균 이 나와요. 비즈니스는 환불 = 0 원 으로 잡아서 평균 을 원했는데 결과가 30% 더 높게 나오는 사고.
해결 — 비즈니스 정의를 먼저 못 박고 missing: 0 또는 missing 미지정 중 선택. 대시보드에 사용한 정의 도 같이 노출해서 어떤 정의로 계산된 평균인지 항상 명시.
사고 6 — 동일 필드 여러 번 agg 호출
원인 — sum, avg, min, max, count 를 각각 별도 agg 로 5번 박아 같은 doc values 를 5번 스캔. 비용이 5배.
해결 — stats (또는 extended_stats) 한 번이면 같은 답이 한 스캔으로 와요. 운영 대시보드의 최적화 1순위.
사고 7 — top_hits 로 대량 데이터 끌어오기
원인 — top_hits: { size: 1000 } 같이 박아 각 버킷 안에서 문서 1000개씩 끌어오면, 버킷 100개 × 1000개 = 10만 개 문서가 한 응답에 실려 coordinator 노드 OOM.
해결 — top_hits.size 는 대표 1~5개 로 제한하고, 정말 모든 문서가 필요 하면 composite aggregation + pagination 또는 별도 search 요청 으로.
운영 권장 패턴
운영에서 metric agg 를 다룰 때 가장 많이 어긋나는 자리 는 "근사값을 정확값으로 착각" 이에요. cardinality 와 percentiles 두 개는 근사값 이라는 걸 대시보드 라벨에도 명시 하세요. "고유 사용자 수 (근사)" 같은 라벨이 사고를 막아 줍니다.
집계 전용 요청은 size: 0 을 반드시 박아요. 검색 hits 가 필요 없는 자리에 문서 10개 가 같이 따라 오면 네트워크·직렬화 비용 이 의외로 큰 비중. 대시보드처럼 동시 다수 사용자가 같은 집계를 요청 하는 자리에서 효과가 큼.
여러 통계를 같은 필드에서 뽑을 거면 stats 한 번 으로 묶어요. 분산·표준편차까지 필요하면 extended_stats. 별도 agg 를 여러 번 박지 않는 게 첫 최적화.
분위수는 T-Digest 기본 compression: 100 이면 p1~p99 자리는 충분히 정확. p99 이상이 SLO 의 핵심 이면 compression 을 200~300 으로 올리거나 HDR Histogram 으로 가요. 그래도 법적·재무적 정확성 이 핵심이면 원본을 별도 시스템에 저장 하는 이중 파이프 패턴이 안전.
마지막으로, Metric agg 결과는 항상 캐싱 가능 해요. 같은 쿼리·같은 시간 범위·같은 필터면 ES 의 request cache 가 자동으로 캐싱해 줘요. 대시보드 5분마다 갱신 같은 자리에 비용을 0에 가깝게 만들어 줘요. ?request_cache=true 또는 인덱스 setting 으로 활성화.
시험 직전 한 번 더 — 압축 노트
- Aggregations = 집계 + 분석. ES 를 검색 + 분석 양손잡이 로 만드는 핵심.
- 3분류: Metric (16편) · Bucket (17편) · Pipeline (18편). Bucket 안에 Metric 중첩이 표준.
size: 0= 검색 hits 불필요할 때 박는 표준.- sum · avg · min · max = 가장 기본 4총사.
value_count는 non-null 갯수,missing은 null 치환. - stats = count/min/max/sum/avg 다섯을 한 번에. extended_stats = 분산·표준편차까지.
- percentiles = 분위수 근사값. T-Digest 알고리즘,
compression: 100기본, p99 이상이면 200~300. - percentile_ranks = 역방향 — 값 → 분위. SLO 보고 자리.
- cardinality = 고유값 수 근사. HyperLogLog++ 알고리즘,
precision_threshold: 3000기본, 최대 40,000. - top_hits = 버킷 안 대표 문서 N개. top_metrics = 경량 버전, 8.x 권장.
- 사고 7대: cardinality 정밀도 부족·percentiles 꼬리 오차·keyword 에 sum·doc_values: false·missing 누락·동일 필드 중복 agg·top_hits size 폭주.
- 운영 패턴: 근사값 라벨 명시, size: 0 기본, stats 로 묶기, p99 이상은 compression 올리기, request_cache 활용.
- Metric agg 는 Doc Values (column-store) 위에서 동작. Mapping 에서 doc_values: false 켜면 집계 X.
시리즈 다른 편
- 이전 글 = 15편 Compound Queries — bool·dis_max·function_score
- 다음 글 = 17편 Aggregations Bucket — date_histogram·terms·range·filters
- 18편 = Aggregations Pipeline — moving_avg·derivative·cumulative_sum
- 14편 = Term-level Queries — term·terms·range·exists
- 8편 = Mapping Deep — Static·Dynamic·Multi-field·Runtime
- 30편 = Monitoring — Cluster Health·Slow Log·Metricbeat
- 32편 = Spring Data Elasticsearch — Repository·Template·POJO
- 38편 = 시리즈 마무리 — 결정 트리·체크리스트·자격증
한 줄 정리 — Metric Aggregations = 숫자 한 개를 계산 하는 집계. sum·avg·min·max·stats 는 정확, percentiles (T-Digest) 와 cardinality (HyperLogLog) 는 근사값. size: 0 · stats 묶기 · 근사값 라벨 명시 가 운영 3원칙.