Elasticsearch 입문 16편 — Aggregations Metric (sum·avg·min·max·stats·percentiles·cardinality)

2026-05-19Elasticsearch 입문에서 운영까지

Elasticsearch 입문 16편 Aggregations Metric. sum·avg·min·max·stats·percentiles·cardinality·top_hits·top_metrics.

📚 Elasticsearch 입문에서 운영까지 · 16편 — Aggregations Metric (sum·avg·min·max·stats·percentiles·cardinality)

이 글은 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 — 한 번에 여러 통계

statscount · 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_statsstats 의 다섯 개에 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_hitsBucket 안에서 대표 문서 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_metricstop_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 를 다룰 때 가장 많이 어긋나는 자리"근사값을 정확값으로 착각" 이에요. cardinalitypercentiles 두 개는 근사값 이라는 걸 대시보드 라벨에도 명시 하세요. "고유 사용자 수 (근사)" 같은 라벨이 사고를 막아 줍니다.

집계 전용 요청은 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원칙.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!