QueryLens
QueryLens reads PostgreSQL and MySQL slow query logs offline and tells you which query shapes cost the most database time, with transparent, heuristic hints about common anti-patterns.
Why it exists
A slow query log holds thousands of near-identical entries that differ only in their literal values. What you want to know is which shapes of query consume the most time and which have obvious problems. QueryLens answers that from a plain log file: no database connection, no EXPLAIN, no paid APM, and no runtime dependencies beyond the Python standard library. Its reports contain normalized queries only, so you can paste them into a pull request or a chat without leaking the data that was in your logs.
Features
- Parses PostgreSQL (
log_min_duration_statement, stderr format, anylog_line_prefix) and MySQL / MariaDB slow query logs, including multi-line statements, and auto-detects which one you gave it. - Groups equivalent queries with a tokenizer-based fingerprint: literals, placeholders and comments are removed, and
IN (...)(including row-value lists), multi-rowVALUESand PostgreSQLARRAY[...]literal lists collapse. - Reports count, total, mean, p50, p95 and max time, share of total time, and (MySQL) rows examined per row sent.
- Eight heuristic hints (
QL001toQL008) that state their own limitations. - Text, JSON (stable, versioned schema) and Markdown reports.
- CI gates:
--fail-on-hintand--fail-over-mswith documented exit codes. - Reads files,
.gzfiles (detected by content) and stdin; streams line by line. - Malformed entries are counted and skipped, never fatal.
- Zero runtime dependencies; Python 3.10 to 3.14.
Live demo
Try it in your browser: query-lens.netlify.app. The web playground runs this exact Python package in your browser through Pyodide, so your log is analyzed locally and never uploaded. It offers one-click sample logs, file upload or paste, search, sorting and hint filtering, per-pattern details, and the CLI's own text, Markdown and JSON reports. Demo limits: logs up to 5 MB, plain text only (no .gz or stdin), up to 500 patterns shown, and an analysis is stopped after 60 seconds. The CLI remains the primary product.
Installation
From PyPI (after the first upload — see RELEASING.md):
pipx install slowlens # or: pip install slowlens
slowlens --version
QueryLens is the product name. On PyPI, the import package and CLI are slowlens
(pip install slowlens → run slowlens). The name querylens is already taken by an
unrelated project. The GitHub repository remains query-lens.
From a clone (development or before the package is on PyPI):
git clone https://github.com/yagnang-vyas/query-lens.git
cd query-lens
pipx install . # or: python -m venv .venv && . .venv/bin/activate && pip install .
slowlens --version
Maintainer steps for GitHub Releases and PyPI Trusted Publishing are in RELEASING.md. The live demo stays on Netlify; see web/README.md.
Enabling slow query logs
PostgreSQL (postgresql.conf; reload the server afterwards). Log any statement slower than 200 ms to the standard error log:
log_min_duration_statement = 200 # milliseconds; 0 logs everything, -1 disables
logging_collector = on # if you want the output in a file
QueryLens finds the LOG: duration: ... ms statement: ... message wherever it sits on the line, so the default log_line_prefix and custom ones both work. Use the stderr log format (not csvlog or jsonlog).
MySQL / MariaDB (my.cnf, or SET GLOBAL at runtime):
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 0.5 # seconds
Usage
# Top 10 shapes by total time; the format is auto-detected
slowlens examples/postgres-sample.log
# Top 5 by 95th percentile, as Markdown for a pull request comment
slowlens --top 5 --sort p95 --output markdown examples/mysql-slow-sample.log
# Machine-readable output, ignoring anything faster than 50 ms
slowlens --output json --min-duration 50 examples/postgres-sample.log
# Several files, compressed files and stdin
slowlens postgresql-2026-10-24.log postgresql-2026-10-25.log.gz
zcat mysql-slow.log.gz | slowlens --format mysql -
Using it as a CI gate
# Fail the build if any query shape has SELECT * (QL001) or a leading wildcard LIKE (QL003),
# or if any shape has a p95 above 750 ms.
slowlens --fail-on-hint QL001,QL003 --fail-over-ms 750 slow.log
The report is printed as usual, followed by a one-line reason on stderr, and the exit code is 1. Fail conditions are evaluated on all fingerprints, not only the --top subset.
Sample output
Produced by running slowlens --top 5 examples/postgres-sample.log on the synthetic sample log in this repository:
QueryLens 0.1.0 | format: postgres | files: examples/postgres-sample.log
entries parsed: 73 | malformed: 1 | duration-only: 1 | skipped: 34 | filtered: 0
total time: 19513.555 ms | unique fingerprints: 10 | showing top 5 by total
# ID Count Total ms Mean ms P95 ms Max ms Share % Hints
1 e7949fc430c3 6 6872.643 1145.440 1449.955 1449.955 35.22 QL004
select id, title from articles where published = true order by id limit ? offset ?
2 d236be3dd0c4 8 4148.673 518.584 838.875 838.875 21.26 QL003
select id, name, price from products where name ilike ? and active = true
3 4beddc297df7 2 3103.438 1551.719 1768.626 1768.626 15.90 QL002
update sessions set expires_at = now(), revoked = true
4 002b0a2998f8 9 1761.588 195.732 265.755 265.755 9.03 QL006
select id, email from users where lower(email) = ?
5 25060abf2d0e 14 1713.947 122.425 217.071 217.071 8.78 QL001
select * from orders where customer_id = ? order by created_at desc
Hints (heuristics, may be wrong; see README):
QL001 SELECT *: Heuristic: the select list contains * or table.*, which reads every column and
breaks covering indexes. It cannot know whether you really need all columns.
QL002 Unfiltered write: Heuristic: an UPDATE or DELETE has no top-level WHERE clause and touches
every row. It is sometimes intentional (maintenance jobs) and statements starting with WITH
are not inspected.
QL003 Leading-wildcard LIKE: Heuristic: LIKE/ILIKE with a literal pattern starting with %, which
usually prevents B-tree index use. Patterns built at runtime (CONCAT, ||, parameters) are not
detected.
QL004 Large OFFSET: Heuristic: a literal OFFSET (or MySQL LIMIT offset, count) above 1000; the
database must read and discard all skipped rows. Offsets passed as parameters are not
detected.
QL006 Function on filtered column: Heuristic: a column is wrapped in LOWER, UPPER, DATE, CAST,
COALESCE or EXTRACT inside a WHERE/AND/OR comparison, which can prevent plain index use. An
expression index may make it fine.
Hint reference
Hints are heuristics on query text (and, for QL007, on log counters). They know nothing about your schema, indexes, table sizes or query plans, so treat them as prompts to investigate, not verdicts.
| ID | Title | Triggers when | Known limits |
|---|---|---|---|
| QL001 | SELECT * | the select list contains * or t.* |
count(*), a * b and exists (select * ...) are ignored; may be harmless on narrow tables |
| QL002 | Unfiltered write | UPDATE / DELETE without a top-level WHERE |
statements starting with WITH are not inspected; may be intentional |
| QL003 | Leading-wildcard LIKE | LIKE / ILIKE with a literal pattern starting with % |
patterns built at runtime (CONCAT, ||, parameters) are not seen |
| QL004 | Large OFFSET | a literal OFFSET above 1000, or MySQL LIMIT offset, count with offset above 1000 |
offsets passed as parameters are not seen |
| QL005 | Random ordering | ORDER BY RAND() / RANDOM() |
none known beyond the usual heuristic caveats |
| QL006 | Function on filtered column | WHERE / AND / OR comparing LOWER, UPPER, DATE, CAST, COALESCE or EXTRACT of a column |
an expression index makes this fine |
| QL007 | High rows examined | MySQL only: total Rows_examined / total Rows_sent above 1000 for the fingerprint |
ratio comes from log counters; undefined (no hint) when no rows were sent |
| QL008 | Unbounded SELECT | a non-aggregate SELECT ... FROM with no WHERE, LIMIT or GROUP BY |
fine for small lookup tables; statements starting with WITH are not inspected |
A fingerprint carries a hint if any of its entries triggered it.
JSON output schema
--output json prints one JSON document (schema_version 1). Millisecond values are rounded to 3 decimals and percentages to 2 at output time; key order is fixed, so output is deterministic.
{
"schema_version": 1,
"tool": {"name": "slowlens", "version": "0.1.0"},
"input": {
"format": "postgres",
"files": ["examples/postgres-sample.log"],
"entries": 0, "malformed": 0, "duration_only": 0,
"skipped_phase": 0, "filtered_out": 0
},
"summary": {"total_ms": 0.0, "unique_fingerprints": 0, "sort": "total", "top": 10},
"fingerprints": [
{
"id": "a1b2c3d4e5f6", "query": "select ...",
"count": 0, "total_ms": 0.0, "mean_ms": 0.0, "p50_ms": 0.0,
"p95_ms": 0.0, "max_ms": 0.0, "share_pct": 0.0,
"rows_sent_total": null, "rows_examined_total": null,
"examined_per_sent": null, "hints": ["QL001"]
}
],
"hints": {"QL001": {"title": "SELECT *", "description": "..."}}
}
input.entriesis the number of entries parsed;filtered_outof them were dropped by--min-durationbefore aggregation.skipped_phasecounts PostgreSQLparse/binddurations,auto_explainplan lines and MySQL administrator commands.fingerprintsholds the--topsubset in ranked order;summary.unique_fingerprintsandsummary.total_mscover all of them.rows_*andexamined_per_sentarenullfor PostgreSQL and when a ratio is undefined.examined_per_sentis rounded to 2 decimals.hintsis the legend for the hint IDs that appear infingerprints.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | A --fail-on-hint or --fail-over-ms condition triggered (the report is still printed) |
| 2 | Usage or input error: bad arguments, missing or unreadable file, undetectable format, or zero entries parsed |
| 130 | Interrupted with Ctrl-C |
| 141 | The output pipe was closed early (for example slowlens big.log | head); QueryLens stops quietly, the same code a shell reports for a process killed by SIGPIPE |
Errors are printed as slowlens: error: <message> with no traceback. Messages name files and counts, never log content.
How it works
- Input. Files are opened lazily and read line by line. gzip is detected from the first two bytes, text is decoded as UTF-8 with replacement characters for invalid bytes, and CRLF is normalized.
- Parsing. A small state machine per engine builds entries from multi-line statements. PostgreSQL
parseandbinddurations are skipped so a prepared statement is not counted three times. - Fingerprinting. A single-pass tokenizer removes comments, replaces strings (including
'', backslash escapes and$tag$quoting), numbers, hex literals and placeholders ($1,?,:name) with?, lowercases keywords and identifiers, collapses whitespace and trailing semicolons, and turnsIN (?, ?, ...)and multi-rowVALUESinto(?+). The fingerprint ID is the first 12 hex characters of the SHA-1 of the normalized text. Normalizing twice gives the same result. - Statistics. Durations are grouped per fingerprint. Percentiles use the nearest-rank method: for
nsamples sorted ascending, the p-th percentile is the value at rankceil(p / 100 * n)(1-based). For 20 samples, p95 is the 19th value. Share is a fingerprint's total time over the total time of all kept entries. Rankings sort descending by the chosen key and break ties by fingerprint ID. - Hints. Rules run on the token stream of each raw query and keep only a boolean; the report never sees raw SQL.
Privacy
Logs may contain personal data and secrets. QueryLens never prints raw query text, literal values, or PostgreSQL DETAIL: parameters: lines (those lines are dropped by the parser). Reports show normalized fingerprints with every literal replaced by ?. Error messages never echo log content. A test checks a fixture with fake e-mail addresses, API-key-like strings and parameter lines against all three report formats.
Normalization is not anonymization: table, column and other identifiers are kept, and so is anything the tokenizer cannot recognise as a literal. Review a report before sharing it outside your team.
Limitations
- Normalization is tokenizer based, not a SQL parser; exotic syntax can group imperfectly.
- Hints are heuristics (see the table above). They can be wrong in both directions.
- PostgreSQL support covers the stderr format only: no
csvlog,jsonlogorpg_stat_statements. - Statements are timed by their
duration:message;log_statementlines without a duration are not entries. - All durations of a fingerprint are held in memory to compute exact percentiles, so memory grows with the number of entries, not with file size.
- Auto-detection looks at the first 200 lines of the first input only; use
--formatfor odd files. Mixed-format inputs are not supported. - No database access,
EXPLAINoutput or index suggestions.
Prior art
QueryLens stands on the shoulders of pt-query-digest from Percona Toolkit (the reference for MySQL slow log digests) and pgBadger (a full-featured PostgreSQL log analyzer). If you need histograms, per-user breakdowns or HTML dashboards, use them. QueryLens differs by having zero dependencies, privacy-safe output by default, a stable JSON schema for automation, CI exit codes, and both engines in one small tool.
Development
python -m venv .venv
. .venv/bin/activate
make install # editable install with ruff, mypy, pytest, pytest-cov
make check # ruff, ruff format --check, mypy --strict, pytest with >= 90% branch coverage
make format # apply ruff formatting and safe fixes
make dist # build sdist/wheel and run twine check (does not upload)
JSON golden files for the sample logs live in tests/golden/. After an intentional output change, regenerate them and review the diff:
UPDATE_GOLDEN=1 pytest tests/integration
Test fixtures are synthetic; see tests/fixtures/README.md. The design and acceptance criteria are in SPEC.md, contribution ground rules in AGENTS.md.
Roadmap
Ideas for later versions, none of them promised: PostgreSQL csvlog / jsonlog input, pg_stat_statements snapshots, a baseline comparison mode for CI (fail on regressions rather than absolute thresholds), and configurable hint thresholds.
License
MIT © 2026 Yagnang Vyas
Metadata
Release files for slowlens 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| slowlens-0.1.0.tar.gz | 64.1 kB | With provenance | Details for slowlens-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| slowlens-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for slowlens-0.1.0-py3-none-any.whl |
Total release size: 99.2 kB
Release files / slowlens-0.1.0.tar.gz
| Download URL | slowlens-0.1.0.tar.gz |
|---|---|
| Size | 64.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e6d2a0b694ae90f9eb9a6e44d9062c20b5ca388c6bce32861225f75688e0a214
|
|
BLAKE2b-256 checksum How to use checksums |
e1db8cc782af1a9ba91175240d5d7773f0ca4a6a43f63178ea723fc14cbea12c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logRelease files / slowlens-0.1.0-py3-none-any.whl
| Download URL | slowlens-0.1.0-py3-none-any.whl |
|---|---|
| Size | 35.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2e41d1f1f925423ef1897a8eed39cc9dd0526810c684a4f3b558198742d4e7f3
|
|
BLAKE2b-256 checksum How to use checksums |
4afe8f9800c8781af68df8092fcead2e17c6598f7ce0e0f7c71cf346fb7a6bf6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logSecurity for slowlens 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for slowlens-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.