dblens 0.0.0-alpha.0.3
dotnet tool install --global dblens --version 0.0.0-alpha.0.3
dotnet new tool-manifest
dotnet tool install --local dblens --version 0.0.0-alpha.0.3
#tool dotnet:?package=dblens&version=0.0.0-alpha.0.3&prerelease
nuke :add-package dblens --version 0.0.0-alpha.0.3
db-lens
A read-only command-line analyzer for Microsoft SQL Server. It ranks queries by what they cost the server, shows the execution plan behind any of them, inventories indexes with their real usage, and reports unused, duplicate, redundant and missing indexes — as structured JSON or as a table. Beyond queries and indexes it reads the server's own diagnostics: wait statistics, live blocking chains, statistics freshness, and the deadlock history SQL Server keeps on its own.
db-lens never writes to your database. Every statement it runs is a hard-coded SELECT over
system views; you cannot pass it SQL to execute. Index changes it suggests are printed as T-SQL for
you to review and run yourself.
There is no LLM inside db-lens. It collects and reasons deterministically, and emits a stable JSON contract — the analysis on top of that is yours to do, by hand or by pointing an agent at the output.
Install
dotnet tool install --global dblens
dblens --help
That needs the .NET 10 SDK; one package covers macOS, Linux and Windows. Later:
dotnet tool update --global dblens.
On a machine with no .NET, take the self-contained binary for your platform from the latest release — one file, nothing to install:
curl -sSL https://github.com/devsmart-pl/db-lens/releases/latest/download/dblens-osx-arm64.tar.gz | tar -xz
./dblens --help
Builds are published for linux-x64, linux-arm64, osx-x64, osx-arm64, win-x64 and
win-arm64.
The binaries are unsigned, so macOS quarantines a downloaded one until you clear the flag:
xattr -d com.apple.quarantine ./dblens.
Quick start
export DBLENS_CONNECTION_STRING='Server=localhost,1433;Database=AppDb;User Id=reader;Password=...;TrustServerCertificate=true'
dblens config test # what can this instance actually answer?
dblens queries --top 20 --order-by cpu # the costliest queries
dblens plan 4711 # the plan behind one of them
dblens indexes suggest # every index finding at once
dblens waits # what the server has been waiting on
dblens blocking # who is blocking whom, right now
dblens deadlocks # recent deadlocks, parsed from system_health
dblens report --format markdown -o report.md
Or save a profile instead of exporting a variable each time:
dblens config set prod 'Server=sql-01;Database=AppDb;Integrated Security=true;TrustServerCertificate=true' --default
dblens queries --profile prod
Requirements
.NET 10 SDK to install as a global tool — or none at all with a self-contained build.
SQL Server 2016 or later, Azure SQL Database, or Azure SQL Managed Instance.
A login with three read permissions:
GRANT VIEW SERVER STATE TO [db-lens-reader]; -- plan cache, index usage, missing-index DMVs, uptime USE [AppDb]; GRANT VIEW DATABASE STATE TO [db-lens-reader]; -- Query Store and database-scoped DMVs GRANT VIEW DEFINITION TO [db-lens-reader]; -- index, column and foreign key definitionsVIEW DEFINITIONis easy to overlook and matters more than it looks: the catalog views only expose objects the login holds some permission on, so without itdblens indexesreturns an empty list rather than an error. db-lens detects that case and says so instead of letting an empty result read as "this database has no indexes".No
db_owner, nodb_ddladmin, no write permission of any kind is needed. Each command degrades to what it can still answer — withoutVIEW SERVER STATEyou keep index definitions and duplicate detection, losing only the usage-dependent findings — and names the missing permission and itsGRANTrather than surfacing a raw SQL error.
Commands
| Command | What it does |
|---|---|
dblens queries |
Rank queries by CPU, duration, reads, writes, executions or memory grant. --analyze also fetches and analyzes each plan. |
dblens query <id> |
One query in full: its text, its aggregate cost, and every plan behind it. |
dblens plan <id> |
The execution plan as an indented operator tree, with findings. --raw-xml -o p.sqlplan writes a file SSMS can open. |
dblens indexes |
Index inventory: definition, size, seeks, scans, lookups, writes, last read. |
dblens indexes unused |
Indexes nothing reads but every write maintains. |
dblens indexes duplicates |
Exact duplicates, and narrower indexes a wider one already covers. |
dblens indexes missing |
What the optimizer asked for, straight from the DMVs. |
dblens indexes suggest |
All index findings, reconciled. -o fixes.sql writes a reviewable script. |
dblens indexes stats |
Statistics the optimizer should no longer trust: stale, never-refreshed, NORECOMPUTE, thin samples. |
dblens waits |
Cumulative wait profile with benign waits filtered out: CPU pressure, IO, locking, memory grants, log writes. |
dblens blocking |
Point-in-time blocking chains, including idle sessions holding open transactions. |
dblens deadlocks |
Deadlock history from the system_health session: victims, statements, contested objects, recurring patterns. |
dblens report |
Everything at once — queries, indexes, statistics, waits, blocking, deadlocks — as JSON or a written Markdown report. --skip-queries / --skip-diagnostics narrow it. |
dblens config <list\|set\|remove\|test> |
Connection profiles, and what the target instance exposes. |
dblens schema [shape] |
JSON Schema of the output, generated from the types that serialize it. |
Run dblens <command> --help for the full filter list — it is the reference, and it is generated
from the same definitions the commands use.
Where the numbers come from
Query statistics have two possible sources, and which one answered changes what the numbers mean:
- Query Store (
sys.query_store_*) — survives restarts and recompiles, keeps history, and gives stablequery_idandplan_idvalues that stay valid between runs. Requires Query Store to be enabled on the database. - Plan cache (
sys.dm_exec_query_stats) — available on every instance withVIEW SERVER STATEand needs no per-database setup, but covers only what is still cached: it resets on restart, on recompile, and under memory pressure.
db-lens picks Query Store when every target database has it enabled, and the plan cache otherwise —
never a mix, because the two are not comparable in one ranking. The source field of every result
says which was used, and --source query-store|plan-cache overrides the choice.
The diagnostic commands have their own sources and scopes:
waitsreadssys.dm_os_wait_stats(instance-wide, since startup). On Azure SQL Database it readssys.dm_db_wait_statsinstead, which sees only that database's waits — the result says so.blockingjoinssys.dm_exec_sessions,sys.dm_exec_requestsand the SQL text DMVs into one snapshot. It starts from sessions rather than requests, so an idle session holding an open transaction — a head blocker with no running request — stays visible. Works everywhere.indexes statsreadssys.statsthroughsys.dm_db_stats_propertiesper database, 2016+ everywhere including Azure SQL Database.deadlocksreads the system_health Extended Events file target, which records every deadlock since 2012 with no setup. Box installs and Managed Instance only; Azure SQL Database exposes no file target and gets an explicit warning instead of an empty-looking result.
Reading the output
Every command returns the same envelope:
{
"tool": "db-lens",
"version": "0.1.0",
"command": "indexes unused",
"collectedAt": "2026-03-14T09:12:00Z",
"server": { "name": "sql-01", "edition": "...", "uptimeDays": 22.4 },
"source": "QueryStore",
"warnings": ["..."],
"data": { }
}
warnings is the field to read first. It carries the things that limit how far the data can be
trusted — and acting on index findings without reading it is how a working index gets dropped:
- Usage counters reset when the instance restarts. Below
--min-uptime-days(7 by default), "never read" only means "not read since Tuesday", and unused-index findings drop toInfo. - An index with no row in
sys.dm_db_index_usage_statshas not been observed at all. That is absence of data, not a measurement of zero. db-lens reports it asno data, never as0, and will not raise such a finding aboveInfo. - Missing-index recommendations ignore write cost. The optimizer's estimate says what a query would have saved, not what the index costs on every insert, update and delete.
Duplicate and redundant findings do not depend on usage counters at all — an index that duplicates another is redundant by its definition — so those stand regardless of uptime.
Using db-lens from a script or an agent
Output defaults to a table on a terminal and JSON when stdout is redirected, so a piped call needs no flags:
dblens indexes suggest | jq '.data.findings[] | select(.severity == "High")'
Points worth knowing when something else consumes the output:
- Field names are stable camelCase, with units in the name (
totalCpuMs,sizeMb,totalLogicalReads). ruleIdvalues are stable across versions, so findings can be referenced, suppressed, or diffed between runs. The families:IDX001–IDX007(indexes),QRY001–QRY013(queries and plans),WAIT001–WAIT006(wait profile),BLK001–BLK003(blocking),STAT001–STAT004(statistics freshness),DLK001–DLK002(deadlocks).- Errors come back in the same format as results, with a stable
error.codeand a non-zero exit code — never a raw SQL exception or a stack trace. - Volume is bounded: every command takes
--top(-t/-n), with a default suited to the data — 25 for queries and waits, 100 for the index inventory, 50 for statistics findings, 20 for deadlocks; the per-command--helpstates it. Query text is truncated at--max-text-length(4000) unless you pass--full-text, and plan XML is not included unless you ask for it. One call will not flood a caller's context. dblens schemaprints the JSON Schema for every shape, generated from the serializing types.
A reasonable path through a problem: report --format json to see everything, then queries --analyze to rank and diagnose, then plan <id> on whatever stands out. When the complaint is
"the server is slow" rather than "this query is slow", start from waits instead — it says which
resource to suspect — and blocking when things are stuck right now. Findings cross-reference each
other: heavy lock waits point at blocking, estimate skew (QRY006) points at indexes stats.
Building
dotnet build
dotnet test
dotnet run --project src/DbLens.Cli -- --help
A single self-contained binary, no runtime needed on the target machine:
dotnet publish src/DbLens.Cli -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true
dotnet publish src/DbLens.Cli -c Release -r win-x64 --self-contained -p:PublishSingleFile=true
Developing against a real server
docker-compose.yml brings up SQL Server 2022, and the seed script creates a database whose
problems are deliberate — duplicate indexes, a redundant one, an index nothing reads, a foreign key
with no index, a heap that needs one, and a varchar column compared against nvarchar parameters:
docker compose up -d
docker exec -i db-lens-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'DbLens!Local1' -C -b -i /dev/stdin < tests/sql/seed-testdb.sql
docker exec -i db-lens-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'DbLens!Local1' -C -b -i /dev/stdin < tests/sql/workload.sql
export DBLENS_CONNECTION_STRING='Server=localhost,14330;Database=DbLensDemo;User Id=sa;Password=DbLens!Local1;TrustServerCertificate=true'
dotnet run --project src/DbLens.Cli -- indexes suggest
Each seeded problem should produce exactly one finding, which is what the analyzer tests assert against — those run on plans captured from this database and need no server.
How it is put together
src/DbLens.Core/
Connection/ opening connections, resolving profiles, probing what the instance supports
Sql/ every statement db-lens runs, as embedded .sql files
Sources/ reading the DMVs into models
Showplan/ parsing ShowplanXML into an operator tree
Analysis/ the rules, as pure functions over collected models
Output/ the JSON envelope and the table, CSV and Markdown renderers
src/DbLens.Cli/ commands, options, and the Spectre.Console table renderer
Two properties are worth preserving when changing it. Every statement lives in Sql/ as a plain
file, so the full set of what db-lens touches can be reviewed by reading one directory. And the
analyzers take collected models and return findings with no I/O in between, so every rule is
testable without a server.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.0-alpha.0.3 | 89 | 8/28/2026 |