← Back to blog
Product 4 min read 炬鲸团队

SQL-Style Log Search: Query Your Logs Like a Database

Keyword search falls short when you need aggregation. JUJING OBSERVE ships SQL-like log querying—SELECT, WHERE, GROUP BY, and piped processing—with automatic field parsing and no pre-built indexes. This post walks through real queries for finding slow endpoints and tracing user sessions.

Most teams still use the log search box as a glorified keyword filter. That's fine for "any errors in the last few minutes," but it falls apart the moment you need to answer "which endpoint is slowest" or "what exactly happened during this user's request." Keyword search is great at finding needles, not at telling you how many needles there are and where they cluster.

JUJING OBSERVE ships a SQL-like query language in its log search: SELECT, WHERE, GROUP BY, ORDER BY, plus piped processing. Fields are parsed out of your logs automatically — no pre-built indexes, no writing a separate regex for every log shape your services emit.

Start with three queries

The simplest filter: all 5xx errors in the last five minutes.

SELECT * FROM logs
WHERE status >= 500
  AND time > now() - 5m

Group by endpoint to find the ten slowest over the past hour:

SELECT api, count(*) AS cnt, p95(latency) AS p95
FROM logs
WHERE time > now() - 1h
GROUP BY api
ORDER BY p95 DESC
LIMIT 10

p95() is a built-in aggregate. latency comes from a numeric field parsed automatically — if your log line contains a key-value pair like latency=182, the platform turns it into a numeric column you can aggregate, normalized to milliseconds. No schema to declare, no mapping file to maintain.

Here's how you reconstruct one user's full path through the system:

SELECT trace_id, min(time) AS start_ts, max(time) AS end_ts
FROM logs
WHERE user_id = 'u_88213'
  AND time > now() - 30m
GROUP BY trace_id
ORDER BY start_ts

This is the kind of question that's painful to answer with grep — the user's activity is scattered across a dozen services and thousands of lines — but trivial once trace_id and user_id are first-class columns.

Piped queries: narrow down step by step

Unlike writing one full SQL statement, pipes match how you actually debug. Chain operations with |; each step's output feeds the next:

FROM logs
| WHERE status >= 400
| SELECT api, status, count(*) GROUP BY api, status
| ORDER BY count(*) DESC
| LIMIT 20

The real advantage: you can delete the later steps to inspect intermediate results without knowing the final answer up front. Start with a broad filter, look at what comes back, then tighten. It turns log analysis from a "compose a perfect query" exercise into a "follow the data" loop.

Field extraction: turn unstructured logs into columns

Not every log is JSON. Say your Nginx access log looks like this:

192.168.1.10 - - [13/Sep/2026:10:02:11] "POST /api/order 200" 0.182

An extraction expression splits the meaningful segments into fields:

FROM logs
| PARSE 'ip - - [date] "method path status" latency'

After that you can WHERE latency > 1 or group by path. Save extraction rules as templates and they apply automatically to matching log shapes, so the whole team shares one definition instead of each person maintaining their own regex with slightly different edge cases.

Recommendations

  • Save high-frequency queries as views, and reference those views in alert rules so investigation and alerting use the same logic. Nothing is worse than an alert firing on a condition you can't reproduce in the search UI.
  • Turn aggregation results into dashboard panels with one click; on-call engineers should start from the board and drill down, not hand-type a query at 3am.
  • Keep field names in lower_snake_case and match the keys your code actually logs, to cut down on remapping and back-and-forth.
  • Search hits the hot tier by default; older data lives in the archive tier. Specify a time range explicitly and the query spans both tiers automatically — no manual switching required.