Files
sase.tr/docs/analytics-queries.sql
Sase Dev e4b430f69c feat(analytics): enrich query_logs metadata + query_log_insights view
For continuous optimization we need to attribute slow/failed decodes to
the right cause. query_logs.timings jsonb is now a structured decode-meta
blob, not just stage timings:

- wmi: first 3 chars of VIN (per-brand aggregation)
- result_kind: vehicle / pcat_candidates / emex_candidates / unknown / aborted
- cache_source: db_hit / redis_positive / redis_negative / lock_wait / miss
- candidate_pick: pcat / emex / none (when user picks from candidate modal)
- pcat_car_count, emex_candidate_count (cardinality, drives candidate-modal rate)
- pl24_circuit_open, pl24_skipped (CB state at request time)
- vin_api_used, vin_api timing (NHTSA fallback frequency)

Migration 0003 adds a query_log_insights VIEW that flattens these keys
into typed columns, so ad-hoc SQL doesn't need json operators. New meta
keys appear automatically as NULL; the VIEW stays stable.

docs/analytics-queries.sql has 8 starter queries: cache hit ratio,
per-source latency, slowest WMIs, stage breakdowns, CB/abort frequency,
candidate-modal rate, top failing VINs, dedup effectiveness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:04:23 +00:00

114 lines
5.0 KiB
SQL

-- Ad-hoc analytics queries against query_log_insights.
-- All queries default to the last 30 days; adjust the interval as needed.
-- ─── 1. Cache hit ratio (db / redis_positive / redis_negative / lock_wait / miss) ───
SELECT
cache_source,
count(*) AS queries,
round(100.0 * count(*) / sum(count(*)) OVER (), 1) AS pct,
round(avg(response_time_ms)) AS avg_ms,
percentile_cont(0.95) WITHIN GROUP (ORDER BY response_time_ms)::int AS p95_ms
FROM query_log_insights
WHERE created_at > now() - interval '30 days'
GROUP BY cache_source
ORDER BY queries DESC;
-- ─── 2. Per-source latency & success ─────────────────────────────────────────────
SELECT
source,
count(*) AS queries,
round(100.0 * sum((success)::int) / count(*), 1) AS success_pct,
round(avg(response_time_ms)) AS avg_ms,
percentile_cont(0.50) WITHIN GROUP (ORDER BY response_time_ms)::int AS p50_ms,
percentile_cont(0.95) WITHIN GROUP (ORDER BY response_time_ms)::int AS p95_ms,
percentile_cont(0.99) WITHIN GROUP (ORDER BY response_time_ms)::int AS p99_ms,
max(response_time_ms) AS max_ms
FROM query_log_insights
WHERE created_at > now() - interval '30 days'
AND cache_source = 'miss' -- exclude cache hits for a fair upstream comparison
GROUP BY source
ORDER BY queries DESC;
-- ─── 3. Slowest WMIs (which brand prefixes hurt us most) ─────────────────────────
SELECT
wmi,
count(*) AS queries,
round(avg(response_time_ms)) AS avg_ms,
percentile_cont(0.95) WITHIN GROUP (ORDER BY response_time_ms)::int AS p95_ms,
round(100.0 * sum((NOT success)::int) / count(*), 1) AS fail_pct
FROM query_log_insights
WHERE created_at > now() - interval '30 days'
AND cache_source = 'miss'
GROUP BY wmi
HAVING count(*) >= 3
ORDER BY p95_ms DESC
LIMIT 20;
-- ─── 4. Per-stage breakdown when an upstream is to blame ─────────────────────────
SELECT
source,
count(*) AS queries,
round(avg(pcat_ms)) AS avg_pcat_ms,
round(avg(emex_ms)) AS avg_emex_ms,
round(avg(pl24_ms)) AS avg_pl24_ms,
round(avg(vin_api_ms)) AS avg_vin_api_ms,
round(avg(lock_wait_ms)) AS avg_lock_wait_ms
FROM query_log_insights
WHERE created_at > now() - interval '7 days'
AND cache_source = 'miss'
GROUP BY source
ORDER BY queries DESC;
-- ─── 5. Circuit breaker / abort frequency ────────────────────────────────────────
SELECT
date_trunc('hour', created_at) AS hour,
count(*) FILTER (WHERE pl24_circuit_open) AS cb_open_when_called,
count(*) FILTER (WHERE pl24_skipped) AS pl24_skipped_count,
count(*) FILTER (WHERE aborted) AS aborted_count,
count(*) AS total
FROM query_log_insights
WHERE created_at > now() - interval '24 hours'
GROUP BY hour
ORDER BY hour DESC
LIMIT 24;
-- ─── 6. Candidate-modal rate (how often does the user have to disambiguate?) ─────
SELECT
result_kind,
count(*) AS queries,
round(100.0 * count(*) / sum(count(*)) OVER (), 1) AS pct
FROM query_log_insights
WHERE created_at > now() - interval '30 days'
GROUP BY result_kind
ORDER BY queries DESC;
-- ─── 7. Top failing VINs (cluster of retries / unsupported VINs) ─────────────────
SELECT
vin,
wmi,
count(*) AS attempts,
count(*) FILTER (WHERE success) AS ok,
count(*) FILTER (WHERE NOT success) AS fail,
max(response_time_ms) AS worst_ms,
max(created_at) AS last_seen
FROM query_log_insights
WHERE created_at > now() - interval '30 days'
GROUP BY vin, wmi
HAVING count(*) FILTER (WHERE NOT success) >= 2
ORDER BY fail DESC, attempts DESC
LIMIT 30;
-- ─── 8. Dedup effectiveness (lock_wait shows how many requests piggy-backed) ─────
SELECT
date_trunc('day', created_at) AS day,
count(*) FILTER (WHERE cache_source = 'lock_wait') AS dedup_savings,
count(*) FILTER (WHERE cache_source = 'miss') AS actual_decodes,
count(*) FILTER (WHERE cache_source = 'redis_positive') AS redis_hits,
count(*) FILTER (WHERE cache_source = 'redis_negative') AS redis_neg_hits,
count(*) FILTER (WHERE cache_source = 'db_hit') AS db_hits,
count(*) AS total
FROM query_log_insights
WHERE created_at > now() - interval '14 days'
GROUP BY day
ORDER BY day DESC;