Keyword search breaks down at scale—it can’t answer aggregation questions. This post explains Observe’s SQL-based log search: field parsing, common query templates, and the partitioning and columnar storage that keep aggregations fast. Turn “searching logs” into “asking questions.”
When something breaks, the first instinct is to search logs: type a keyword, pick a time range, page through results. That flow is fine at low volume. At scale it hits a wall.
The root cause: logs are being treated as text, not data. Full-text search is great at finding, but it can’t answer “how many,” “how it’s distributed,” or “what the trend is.”
Observe makes log search SQL-based for exactly this reason—so logs can be what they actually are: timestamped, structured records you query like a table.
SQL over logs only works if logs have fields. The Observe collector does two things at ingestion:
{"level":"ERROR","service":"pay","cost_ms":1523} becomes level, service, and cost_ms.172.16.0.10 - - [12/Sep/2026:10:23:41 +0800] "GET /api/order?id=123 HTTP/1.1" 500 812
One grok template extracts client_ip, method, uri, status, and body_bytes, after which you can aggregate by status or uri.
Once fields exist, a search turns from ERROR AND pay AND cost_ms > 1000 into:
SELECT host, COUNT(*) AS cnt
FROM logs
WHERE level = 'ERROR'
AND service = 'pay'
AND cost_ms > 1000
AND ts BETWEEN '2026-09-12 10:00:00' AND '2026-09-12 11:00:00'
GROUP BY host
ORDER BY cnt DESC
A few templates that cover most on-call scenarios.
Error rate trend
SELECT DATE_FORMAT(ts, '%Y-%m-%d %H:%i') AS minute,
SUM(level = 'ERROR') / COUNT(*) AS err_rate
FROM logs
WHERE service = 'order' AND ts > NOW() - INTERVAL 1 HOUR
GROUP BY minute
Top 10 slow requests
SELECT trace_id, uri, cost_ms
FROM logs
WHERE cost_ms > 2000
ORDER BY cost_ms DESC
LIMIT 10
Per-region stats
SELECT region, COUNT(*) AS total, AVG(cost_ms) AS avg_cost
FROM logs
WHERE ts > NOW() - INTERVAL 1 DAY
GROUP BY region
Latency percentiles (finding the long tail)
SELECT region,
PERCENTILE(cost_ms, 0.5) AS p50,
PERCENTILE(cost_ms, 0.99) AS p99
FROM logs
WHERE service = 'pay' AND ts > NOW() - INTERVAL 1 HOUR
GROUP BY region
Queries can be saved as views and attached to alert rules for one-click reuse. Common queries become shared views the whole team calls up instead of rewriting every time.
The biggest worry with SQL over logs is whether aggregation will drag the system down. Observe handles this in three layers.
ts predicate is pushed down as partition pruning.COUNT(*) never decompresses full rows.In practice, on a billion-row store, a single-table GROUP BY aggregation runs with a P95 under 2 seconds. A full-table scan with no time filter is blocked by the optimizer with a “please add a time range” warning.
SQL search isn’t an island. It connects to two other capabilities.
trace_id link straight into the trace view, so you can see the full call chain around that line.GROUP BY results over dumping tens of thousands of raw lines.For the people on call, the value is simple: troubleshooting goes from “searching for a log line” to “asking a question and getting an answer.”