query-exporter turns scheduled SQL queries into Prometheus metrics
Export Prometheus metrics from SQL queries
At a glance
- What is it?
- query-exporter is a Prometheus exporter that runs SQL on a schedule and publishes the results as metrics, connecting through SQLAlchemy to PostgreSQL, MySQL, Oracle and SQL Server. A single query can update several metrics across several databases, and the whole thing is a YAML file plus one command.
- Who is it for?
- Adopt query-exporter when the number you need to graph lives in a query rather than in a counter the database already exposes, and when you want that number on a schedule without writing an exporter yourself. Do not adopt it for the metrics a database already publishes, since those have dedicated exporters and cost less to run.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 14 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem it solves
query-exporter is a Prometheus exporter that allows collecting metrics from database queries, at specified time intervals.
That is the whole proposition, and it exists because a large share of the things people want to graph in a database are not counters the database publishes. The number of rows in a table that meets a condition, the ratio of two aggregates, the value of a field in a configuration table, the age of the oldest unprocessed job. None of those are in the database's own metrics endpoint, and each one would otherwise need a custom exporter written, packaged, deployed and maintained.
So this exporter is a generic bridge. You write the SQL, you say which metrics the result populates, and the exporter handles the scheduling, the metric lifecycle and the exposition.
The mechanics have two properties that matter. It uses SQLAlchemy to connect to different database engines, including PostgreSQL, MySQL, Oracle and Microsoft SQL Server, so the same configuration works across four databases. And each query can be run on multiple databases and update multiple metrics, so a single query can feed a set of metrics and a set of metric definitions can be fed by several databases.
The application is run simply as `query-exporter`, which looks for a `config.yaml` configuration file in the current directory, containing the definitions of the databases to connect and the queries to perform to update metrics.
The configuration file can be overridden by passing the `--config` option or by setting the `QE_CONFIG` environment variable. And the option can be provided multiple times to pass partial configuration files, with the resulting configuration being the merge of the content of each top-level section, `databases`, `metrics` and `queries`.
That merging behaviour is the answer to a real deployment problem. A base configuration with the databases and the metrics can be shared, and a per-environment or per-team file can add or override queries without duplicating everything.
The project is Python, licensed GPL-3.0-OR-later, maintained by Alberto Donato, with the last push on 2026-10-09.
The three sections of the configuration
The sample configuration in the README is the clearest description of the model, and it has three top-level sections.
The `databases` section defines the connections, each with a DSN and optionally some SQL to run at connect time and a set of labels. The `metrics` section defines the metrics, each with a type, a description and type-specific options. The `queries` section defines the queries, each with an interval, the databases to run against, the metrics to update and the SQL.
Here is the metrics and queries part of the sample:
metrics:
metric1:
type: gauge
description: A sample gauge
metric2:
type: summary
description: A sample summary
labels: [l1, l2]
expiration: 24h
metric3:
type: histogram
description: A sample histogram
buckets: [10, 20, 50, 100, 1000]
metric4:
type: enum
description: A sample enum
states: [foo, bar, baz]
queries:
query1:
interval: 5
databases: [db1]
metrics: [metric1]
sql: SELECT random() / 1000000000000000 AS metric1
query2:
interval: 20
timeout: 0.5
databases: [db1, db2]
metrics: [metric2, metric3]
sql: |Four metric types are supported. A `gauge` is a single value. A `summary` declares a set of label names and can carry an `expiration`, which is how a value that is no longer produced stops being exported rather than lingering in Prometheus until it goes stale. A `histogram` declares explicit `buckets`. An `enum` declares a list of `states`, which is how a value that should be one of a small fixed set, like a status column, is exported without becoming a label cardinality problem.
That enum type is the one worth understanding, because it solves a real Prometheus problem. Exporting a status column as a labelled metric would create one time series per distinct value seen, which is a cardinality trap. Declaring the allowed states up front keeps the series count fixed.
The label names on a metric, such as `l1` and `l2` on metric2, are the columns the query result is expected to contain. So the contract between the query and the metric is the column names, and a mismatch between them is a configuration error rather than a silent omission.
Labels on databases, and how a result row becomes a sample
The databases section carries labels too, and that is the part of the model that is easy to miss and easy to get wrong.
In the sample, `db1` and `db2` are both SQLite connections with different labels, `region: us1` and `region: us2`, and a shared `app: app1`. The other options on a database are the DSN and a `connect-sql` list, which in the sample sets SQLite pragmas such as the application id and the auto vacuum mode.
The reason database labels exist is that one query can run against several databases. Query2 in the sample runs against `db1` and `db2` and updates `metric2` and `metric3`. Without database labels, the results from the two databases would collide on the same metric and one would overwrite the other. With the labels attached, each database's result becomes its own series, distinguished by the region label while sharing the app label.
So there are two levels of labels. The database labels are static and describe where the value came from. The metric labels are dynamic and come from columns in the query result, which is how a single query against a table with many rows produces many series, one per row, distinguished by the label values.
The sample query3 illustrates the dynamic case. Its SQL selects a value from a union of three literal rows and orders by random with a limit of one, and it updates metric3 and metric4, the histogram and the enum. The union includes both metric4 and metric3 aliases, which is how one result set can feed metrics of different types.
The remaining option worth understanding is `timeout`. Query2 sets `timeout: 0.5`, and that is the safety valve for the exporter's biggest operational risk, which is a slow query holding a worker and, in the worst case, piling up against the database. The scheduling dependency is `apscheduler`, which is what runs the interval and cron triggers.
Scheduling, and the option surface
Queries are triggered either by an interval in seconds or by a cron-style schedule, and the sample shows both. Query1 has `interval: 5`, meaning it runs every five seconds, while query3 has `schedule: "*/5 * * * *"`, the same cadence expressed as a cron expression.
The distinction matters more than it looks. An interval is relative to when the query last finished, so a slow query pushes the next run back, and a cron schedule is fixed to the wall clock, so several queries with the same cron expression all fire at the same instant. For a query that takes ten seconds and an interval of five, the interval approach means the database sees a query every ten seconds rather than every five, which is the safer behaviour.
The exporter options are all settable three ways, through command line switches, through environment variables or through a `.env` file, and the README gives the full table. The defaults are sensible for a container: the host to bind is `localhost`, the port is 9560, the metrics path is `/metrics`, the log level is `info` with `critical`, `error`, `warning`, `info` and `debug` available, and the log format is `plain` or `json`.
Two options are about the exporter's own footprint. `--process-stats` includes process statistics in the metrics, off by default, which is the kind of thing you want in a container where the process is the unit of work. And `--check-only` only checks the configuration without running the exporter, which is the option to use in a deployment pipeline or a pre-commit check, and it is much cheaper than starting the thing and watching it fail.
Three options handle TLS. `--ssl-private-key`, `--ssl-public-key` and `--ssl-ca` take full paths to the private key, the public key and the CA certificate, so the exporter can serve HTTPS itself rather than being assumed to sit behind a terminating proxy.
The environment variable names follow a consistent `QE_` prefix, and the dotenv path defaults to `$PWD/.env`.
The configuration file can be given more than once, and the merge happens per top-level section, so you can keep databases and metrics in one file and queries in another.
Packaging, and what it looks like in a repository
The packaging tells you how the project is maintained and what it expects of its users.
The build backend is setuptools with `setuptools.build_meta`, the project name is `query-exporter` with the description Export Prometheus metrics generated from SQL queries, and the readme is the reStructuredText README with the content type declared as `text/x-rst`. The version is dynamic, so it is derived at build time rather than hardcoded in the manifest.
Python 3.12 is the floor, and the classifiers list 3.12 through 3.15, which places the project firmly in the current era. The license is GPL-3.0-OR-later with the license file declared, and the maintainer is Alberto Donato.
The runtime dependencies are the interesting list. `sqlalchemy>=2` for the database layer, `prometheus-aioexporter>=3.1` for the exporter machinery, `prometheus-client` for the metric types, `pydantic` for configuration validation, `pyyaml` for the config file, `apscheduler` for the scheduling, `aiohttp` for the web server, and `structlog` for the structured logging that the `--log-format json` option produces.
The combination of pydantic and structlog with aiohttp and an async Prometheus exporter tells you the whole application is asyncio-based, which is the right shape for an exporter that polls several databases concurrently on short intervals.
The repository layout matches that: `query_exporter/` for the package, `tests/` and `integration_tests/` for the test suites, `docs/` for the documentation including the configuration file format reference, `examples/` with an `oracle-stats/` and a `postgresql-stats/` example, a `Dockerfile` and a `.dockerignore`, a `pyproject.toml`, a `uv.lock` and a `CHANGES.rst`.
The presence of two worked examples is the most useful thing for a new user, because they show the configuration for a real database rather than the SQLite toy in the README.
There are no published GitHub releases, so the tags and the changelog in `CHANGES.rst` are the version history, and the last push was on 2026-10-09.
When a database exporter is the better answer
There is a real alternative and it is the obvious one for a large class of metrics.
Most databases, and the services around them, already publish their own internal counters through a metrics endpoint. A database's own exporter exposes connection counts, transaction rates, replication lag, cache hit ratios and buffer usage, and it does so with no query against the data and no load on the query path.
That is a fundamentally different approach. A database exporter reads counters the engine maintains anyway, so it costs almost nothing and cannot slow a query. query-exporter runs a SQL statement on a schedule, so every value it produces is the cost of a query, and the values it can produce are the ones you can express as a SELECT.
So the decision is whether the number you want is a property of the engine or a property of the data. Replication lag is the first, and there is no reason to write a query for it. The number of accounts in an unusual state is the second, and there is nothing else that will give it to you.
There is a second trade-off, which is the query you write and its cost. The exporter polls on the interval you set, so a query that scans a large table on a five second interval is a load problem you introduced. The `timeout` option bounds how long a query may run, and the interval and cron distinction determines whether a slow query backs off or piles up.
A third point is coupling. query-exporter is GPL licensed, so a distributed modification carries obligations, and it needs Python 3.12 or newer, which may not match the rest of your monitoring stack.
Editorial conclusion
Adopt query-exporter when the number you need to graph lives in a query rather than in a counter the database already exposes, and when you want that number on a schedule without writing an exporter yourself. Do not adopt it for the metrics a database already publishes, since those have dedicated exporters and cost less to run. Verify first that your database driver is available in the environment, that the queries you write are cheap enough to run on the interval you set, since the exporter polls the database directly, that the label columns in your result set match the labels declared on the metric, and that the version you take matches the release you read, with the last push on 2026-10-09 and the package requiring Python 3.12 or newer.
Frequently asked questions
What does query-exporter do?
It is a Prometheus exporter that collects metrics from database queries at specified time intervals. It uses SQLAlchemy to connect to PostgreSQL, MySQL, Oracle and Microsoft SQL Server, and each query can be run on multiple databases and update multiple metrics.
How do I configure query-exporter?
With a `config.yaml` file in the current directory, found automatically when you run `query-exporter`. You can override the location with the `--config` option or the `QE_CONFIG` environment variable, and you can pass the option multiple times to supply partial files, with the resulting configuration merging the content of each top-level `databases`, `metrics` and `queries` section.
Which metric types does query-exporter support?
Four: `gauge`, `summary` with label names and an optional `expiration`, `histogram` with explicit `buckets`, and `enum` with a list of `states`. The enum type keeps the number of exported series fixed for a value drawn from a small set, such as a status column.
What are the query-exporter command line options?
They can be set through switches, environment variables or a `.env` file: `--host`/`QE_HOST` defaulting to localhost, `--port`/`QE_PORT` defaulting to 9560, `--metrics-path` defaulting to /metrics, `--log-level` defaulting to info with plain or json `--log-format`, `--process-stats` defaulting to false, `--ssl-private-key`, `--ssl-public-key` and `--ssl-ca` for TLS, and `--check-only` to validate the configuration without running the exporter.
How are queries scheduled in query-exporter?
Either with an `interval` in seconds or with a cron-style `schedule`, and a query can also set a `timeout` in seconds. The scheduling is handled by the APScheduler dependency, and the sample configuration shows both forms.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/albertodonato-query-exporter)Was this useful?