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>
114 lines
5.0 KiB
SQL
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;
|