PromQL Advanced: Aggregations, Subqueries, Join Operations

เมื่อเข้าใจ PromQL ระดับพื้นฐานแล้ว ขั้นต่อไปคือการใช้ความสามารถที่ลึกขึ้น เช่น subquery, vector matching (join ข้าม metric), group operator และเทคนิคจัดการ high-cardinality ที่จะเปลี่ยนการเขียน query ธรรมดาให้ตอบโจทย์งานจริงที่ซับซ้อนได้ บทความนี้จะลงรายละเอียดเรื่องเหล่านี้พร้อมตัวอย่างที่ใช้งานได้จริง

PromQL ขั้นสูงไม่ได้แปลว่า syntax ยากขึ้น แต่คือการใช้ความสามารถที่มีอยู่ให้ได้ประสิทธิภาพและความแม่นยำสูงสุด ตั้งแต่การรวมข้อมูลข้าม metric ต่างตัว การคำนวณ rate จากผลรวมของ query ซ้อน ไปจนถึงการสร้าง view ระดับ service ด้วยการ join

Subquery

Subquery คือการเอา query ที่คืน instant vector มาทำเป็น range vector ในตัวมัน — syntax คือ [range:resolution] หลัง expression ใช้เมื่อต้องการคำนวณ rate ของ query ที่มี aggregation อยู่แล้ว:

# max ของ rate ใน 1 ชั่วโมง โดย sample ทุก 1 นาที
max_over_time(
  sum(rate(http_requests_total[5m]))[1h:1m]
)

# avg ของ 95th percentile latency ใน 24 ชั่วโมง
avg_over_time(
  histogram_quantile(0.95,
    sum by (le)(rate(http_request_duration_seconds_bucket[5m]))
  )[1d:5m]
)

# quantile over time
quantile_over_time(0.99, up[1h:])

Subquery แพงกว่า query ธรรมดา — Prometheus ต้องประมวลผล inner query หลายครั้งตาม resolution ใช้เท่าที่จำเป็นและหลีกเลี่ยงใน alert rule ที่รันบ่อย

Vector Matching พื้นฐาน

เมื่อ operator ทำงานกับ 2 instant vector เช่น A / B Prometheus จะจับคู่ series โดย matching labels ตาม default คือ “ทุก label ต้องตรงกัน”:

# one-to-one matching (default) — ต้องมี label ทุกตัวตรงกัน
http_errors_total / http_requests_total

# ignoring — ไม่สนใจ label ที่ระบุ
http_errors_total / ignoring(code) http_requests_total

# on — ใช้เฉพาะ label ที่ระบุในการจับคู่
http_errors_total / on(service, instance) http_requests_total

Many-to-One / One-to-Many

บางกรณี series ทางซ้ายมีหลายตัวที่ match กับ series ทางขวาตัวเดียว ต้องใช้ group_left หรือ group_right:

# labels เพิ่มเข้ามาจาก instance_info (metadata metric)
# หา pod ที่รัน version เก่า
kube_pod_info
* on(pod) group_left(node, host_ip)
  kube_node_info

# คำนวณ cost ต่อ pod โดยเอา cost_per_hour (per node) มารวม
container_cpu_usage_seconds_total
* on(node) group_left
  node_cost_per_hour

Group operator: group_left(<labels>) บอกว่าด้านซ้ายเป็น “many” และจะดึง label จากด้านขวามาเพิ่มให้ series ซ้าย สลับด้านได้ด้วย group_right

Error Budget และ SLO

ตัวอย่างการใช้ vector matching เพื่อคำนวณ error budget จาก SLO 99.9%:

# error rate 5m
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))

# error budget remaining (SLO 99.9%, window 30 วัน)
1 - (
  (sum(increase(http_requests_total{status=~"5.."}[30d]))
   / sum(increase(http_requests_total[30d])))
  / (1 - 0.999)
)

# burn rate alert — spend budget 2% ใน 1 ชั่วโมง = crash trajectory
(
  sum(rate(http_requests_total{status=~"5.."}[1h]))
  / sum(rate(http_requests_total[1h]))
) > (14.4 * (1 - 0.999))

จัดการ High Cardinality

High cardinality คือ label combination จำนวนมหาศาลที่ทำให้ Prometheus ช้าและกิน memory เทคนิคการรับมือ:

  • ใช้ label_replace() จัดกลุ่ม label ให้น้อยลงก่อน aggregate
  • Drop label ที่ไม่จำเป็นด้วย metric_relabel_configs ในระดับ scrape config
  • ใช้ topk() / bottomk() แทนการดึงทุก series
  • ตั้ง sample_limit และ target_limit กันระเบิดโดยไม่ตั้งใจ
  • ตรวจสอบ cardinality ด้วย count({__name__=~".+"}) หรือ topk(10, count by (__name__)({__name__=~".+"}))

label_replace และ label_join

# เปลี่ยน label pod ที่มี suffix hash ให้เหลือชื่อ deployment
label_replace(
  kube_pod_info,
  "deployment",      # new label
  "$1",               # replacement
  "pod",              # source label
  "(.+)-[a-f0-9]+-.*" # regex capture group
)

# join 2 labels ให้เป็น label ใหม่
label_join(
  up,
  "target",          # new label
  ":",                # separator
  "instance", "job"   # source labels
)

Aggregation ขั้นสูง

# count_values — นับจำนวน series ตามค่า value
# เช่น มี pod กี่ตัวที่ใช้ memory ในช่วงเดียวกัน
count_values("memory_gb", round(container_memory_usage_bytes / 1e9))

# quantile over groups
quantile by (job) (0.99, rate(http_requests_total[5m]))

# group — ลดขนาดโดยรวม series
group by (service) (up)

Histogram และ Heatmap

Histogram metric ต้องใช้ histogram_quantile() กับ rate ของ bucket — รวมกลุ่มผิดจะได้ผลลัพธ์ผิดทันที:

# ถูก — aggregate ที่ le
histogram_quantile(0.95,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

# ผิด — aggregate ก่อน histogram_quantile โดยไม่รักษา le
histogram_quantile(0.95,
  sum(rate(http_request_duration_seconds_bucket[5m])))

# หลาย percentile พร้อมกันด้วย label manipulation
histogram_quantile(0.50, sum by (le,job) (rate(http_request_duration_seconds_bucket[5m])))
or
histogram_quantile(0.95, sum by (le,job) (rate(http_request_duration_seconds_bucket[5m])))
or
histogram_quantile(0.99, sum by (le,job) (rate(http_request_duration_seconds_bucket[5m])))

Predict และ Trend Analysis

# คาดการณ์ว่า disk จะเต็มเมื่อไหร่ — linear regression 1 ชั่วโมง
predict_linear(node_filesystem_free_bytes[1h], 4 * 3600) < 0

# derivative — อัตราเปลี่ยนต่อวินาที (ใช้กับ gauge)
deriv(node_memory_MemAvailable_bytes[5m])

# smoothing ด้วย avg_over_time
avg_over_time(rate(http_requests_total[5m])[30m:])

Performance Tips

  • ทำ query selectivity สูงสุดก่อน ลด time series ที่ต้องประมวลผลตั้งแต่ต้น
  • ใช้ rate() ก่อน sum() — ไม่ reverse
  • หลีกเลี่ยง subquery ใน alert rule ที่รันทุก 30s
  • Pre-aggregate ด้วย recording rule สำหรับ query ที่ใช้ซ้ำ
  • Instrument prometheus_engine_query_duration_seconds เพื่อติดตาม query ที่ช้า
  • ระวัง __name__=~".+" ที่กระทบทุก metric — ใช้เฉพาะตอน explore

สรุป

PromQL ขั้นสูงเปิดประตูสู่การวิเคราะห์ที่ลึกและแม่นยำขึ้น ตั้งแต่ subquery ที่คำนวณ query ซ้อน vector matching ที่ join ข้าม metric ไปจนถึง label manipulation และการคาดการณ์ด้วย linear regression — เทคนิคเหล่านี้คือตัวแยกระหว่างผู้ใช้ Prometheus มือใหม่กับผู้ที่ใช้ได้อย่างเชี่ยวชาญ

Query ที่ซับซ้อนมักส่งผลกับ performance ของ server ดังนั้นควรตรวจสอบ query_duration และ optimize ด้วย recording rule เมื่อจำเป็น เพื่อให้ dashboard และ alert ทำงานได้เสถียรในระยะยาว