pg-perf-check 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ recursive-include pg_perf_check/sql *.sql
2
+ include README.md
3
+ include LICENSE
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: pg-perf-check
3
+ Version: 0.1.0
4
+ Summary: Diagnose why a PostgreSQL database is slow: 24 read-only threshold checks plus 7 SQL files, each finding carrying its evidence, likely impact and a concrete action
5
+ Author: duke5am
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/duke5am/pg-perf-check
8
+ Project-URL: Issues, https://github.com/duke5am/pg-perf-check/issues
9
+ Keywords: postgres,postgresql,performance,diagnostics,tuning,bloat,autovacuum,indexes,slow-queries,locks,dba
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Database
12
+ Classifier: Topic :: System :: Monitoring
13
+ Classifier: Topic :: Software Development :: Quality Assurance
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: psycopg
18
+ Requires-Dist: psycopg[binary]>=3.1; extra == "psycopg"
19
+ Provides-Extra: psycopg2
20
+ Requires-Dist: psycopg2-binary>=2.9; extra == "psycopg2"
21
+ Dynamic: license-file
22
+
23
+ # pg-perf-check
24
+
25
+ [![PyPI](https://img.shields.io/pypi/v/pg-perf-check)](https://pypi.org/project/pg-perf-check/)
26
+
27
+ Answer **"why is this database slow?"** with a scripted diagnostic report instead
28
+ of guesswork. 24 threshold checks plus 7 read-only SQL files, each answering one
29
+ question.
30
+
31
+ ```bash
32
+ pip install pg-perf-check[psycopg] # from PyPI, Python 3.9+
33
+ pg-perf-check --database-url "$DATABASE_URL"
34
+ pg-perf-check --list-checks
35
+ pg-perf-check --self-test # no database needed
36
+
37
+ # or straight from a clone, no install:
38
+ pip install psycopg2-binary # or: apt install python3-psycopg2
39
+ python3 run_diagnostics.py --database-url "$DATABASE_URL"
40
+ python3 run_diagnostics.py --list-checks
41
+ python3 run_diagnostics.py --self-test # no database needed
42
+ ```
43
+
44
+ Installing without the extra pulls no dependencies at all — the driver is optional
45
+ and imported lazily, because the tool accepts either `psycopg` (v3) or `psycopg2`
46
+ and every no-database mode has to work on an interpreter with neither. The extras
47
+ are `[psycopg]` and `[psycopg2]`.
48
+
49
+ ```
50
+ Connected to mydb on 127.0.0.1/5433 (PostgreSQL 17.11)
51
+ Running 24 checks and the sql/ files...
52
+
53
+ connections CONNECTIONS
54
+ 84 of 100 connection slots in use, 12 idle in transaction
55
+ ...
56
+ Suggested action:
57
+ Put a connection pooler in transaction mode in front of PostgreSQL and cap
58
+ the pool at roughly (2 x CPU cores) + effective_spindle_count. Do not raise
59
+ max_connections as the first move: it multiplies memory use per backend.
60
+ Source: sql/05-connections-and-locks.sql (Q5.0)
61
+ ```
62
+
63
+ ## Everything here is read-only
64
+
65
+ No check writes, alters or drops anything, and none holds a lock beyond a normal
66
+ `SELECT`. Each SQL file says so in its header so you can confirm before pointing
67
+ it at production.
68
+
69
+ ## What it checks
70
+
71
+ **24 threshold checks** across connections, idle-in-transaction sessions, lock
72
+ blocking chains, long-running queries, the oldest `xmin` horizon, replication
73
+ slots pinning `xmin`, table and index bloat, unused and duplicate indexes, cache
74
+ hit ratios, autovacuum health, and more — each with the evidence, the likely
75
+ impact and a concrete action.
76
+
77
+ **7 SQL files**, each answering one question and safe to run alone:
78
+
79
+ | File | Question |
80
+ |---|---|
81
+ | `01-table-sizes-and-bloat` | what is big, and what is dead weight? |
82
+ | `02-unused-and-missing-indexes` | which indexes earn their keep, and what is missing? |
83
+ | `03-slow-queries` | where is the time actually going? |
84
+ | `04-index-usage` | scan counts, index sizes, redundancy |
85
+ | `05-connections-and-locks` | who is waiting on whom? |
86
+ | `06-cache-hit-and-io` | is it reading from cache or from disk? |
87
+ | `07-vacuum-and-autovacuum` | is bloat accumulating, and why? |
88
+
89
+ ## Verified against a real PostgreSQL 17.11
90
+
91
+ **All 24 checks were executed against a live PostgreSQL 17.11 server, and the
92
+ whole report was produced end to end from that server.** Where
93
+ `pg_stat_statements` is unavailable, the four extension-dependent checks are
94
+ reported as *skipped* with the two-step install instructions, rather than failing
95
+ or silently reporting nothing.
96
+
97
+ The 7 SQL files are executed by the runner too, which prints a per-file result
98
+ line so you can see the state of your own server version. Exactly what that
99
+ showed, and what was not verified:
100
+
101
+ * `01`, `02`, `04`, `05`, `06` and `07` ran every statement cleanly on 17.11.
102
+ `07-vacuum-and-autovacuum.sql` reports **10/10 statements OK**.
103
+ * Two statements in `07-vacuum-and-autovacuum.sql` did **not**, and are fixed:
104
+ Q7.6 named `pg_stat_progress_vacuum` columns that PostgreSQL 17 replaced —
105
+ `max_dead_tuples` / `num_dead_tuples` became `max_dead_tuple_bytes`,
106
+ `dead_tuple_bytes` and `num_dead_item_ids`, a rename *and* a change of unit, so
107
+ naming either set breaks the other branch. Q7.6 now reads those fields out of
108
+ the row's JSON form (`to_jsonb(p) ->> '…'`), so it parses on every supported
109
+ version and reports which branch you are on in a `dead_store_units` column.
110
+ Q7.7 selected `datname` from `pg_stat_user_tables`, a column that has never
111
+ existed on **any** release — that view is already scoped to the current
112
+ database — and now selects `schemaname` / `relname`. Q7.6's 17+ branch was
113
+ verified by catching a live throttled `VACUUM` (`dead_store_units = bytes`,
114
+ `dead_store_capacity = 67108864`, i.e. the default 64 MiB
115
+ `maintenance_work_mem`); Q7.7 was verified directly. **Not verified:** the
116
+ pre-17 branch of Q7.6, because no PostgreSQL 16-or-older server was available.
117
+ * Two check-output defects were found during that same verification and are also
118
+ fixed: `shared_buffers_low` reported `approximately 0 MB` for a stock 128 MB
119
+ `shared_buffers` because the parser ignored `pg_settings.unit` (`setting = 16384,
120
+ unit = 8kB`) — it now prints `approximately 128 MB` — and `autovacuum_throttled`
121
+ printed `vacuum_cost_page_miss = None, vacuum_cost_page_dirty = None` because the
122
+ settings query selected neither; it now prints `2` and `20`.
123
+ * `03-slow-queries.sql` needs the `pg_stat_statements` extension. On a server
124
+ without it, seven of its eight statements fail with *relation
125
+ "pg_stat_statements" does not exist*, and the runner reports exactly that
126
+ alongside the install instructions. That is a missing extension on the server,
127
+ not a broken statement, and it is the only remaining failure the runner prints
128
+ on a stock 17.11 server without that extension.
129
+
130
+ ## The advice it gives about its own output
131
+
132
+ Worth quoting, because it is the part most tooling gets wrong:
133
+
134
+ > Change ONE thing, then re-run this script and compare. Two changes at once means
135
+ > you cannot tell which one helped. […] Changing a setting is not the same as
136
+ > fixing a query.
137
+
138
+ Counters are cumulative since the last statistics reset, so the counter-based
139
+ findings (unused indexes, sequential scans, cache ratios) only mean something
140
+ after a full business cycle.
141
+
142
+ ## What it is not
143
+
144
+ It diagnoses; it does not tune for you. It cannot know your workload, so it cannot
145
+ tell you whether a missing index is worth the write cost. `EXPLAIN-GUIDE.md`
146
+ covers reading plans; designing an index is your call.
147
+
148
+ Results also depend on your statistics being current — on a server where `ANALYZE`
149
+ has not run recently, estimated row counts will mislead you.
150
+
151
+ ## Requirements
152
+
153
+ Python 3.9+ (the package declares `>=3.9`; the code itself runs on 3.8),
154
+ PostgreSQL 12+, and a Postgres driver — `psycopg` (v3) or `psycopg2`, either one,
155
+ installed explicitly or via `pip install pg-perf-check[psycopg]`. With no driver
156
+ and no database, `--help`, `--list-checks` and `--self-test` still work; a
157
+ connection attempt without a driver exits 2 with the install command to run.
158
+
159
+ ## The full pack
160
+
161
+ The paid kit adds the two playbooks this repository does not ship —
162
+ `INDEXING-PLAYBOOK.md` (how to design an index the planner actually uses, with
163
+ page counts and WAL volumes measured on a live PostgreSQL 17.11 server rather than
164
+ asserted) and `REMEDIATION-PLAYBOOK.md` (one section per check: the threshold that
165
+ fired, what the counter really counts, the false positives to rule out, the SQL
166
+ with the guards that stop it being an outage, and how to verify the fix worked) —
167
+ plus the CI harness that turns the `--json` report into a baseline comparison,
168
+ failing a build only on findings that are new or worse.
169
+
170
+ <!-- RELATED:START -->
171
+
172
+ ## Related tools
173
+
174
+ - **[pg-restore-drill](https://github.com/duke5am/pg-restore-drill)** — Prove your PostgreSQL backup actually restores: a scripted point-in-time recovery drill with a measured RPO/RTO report and a negative control.
175
+ *(if you were searching for "test postgres backup restore")*
176
+ - **[postgres-migration-safety-lint](https://github.com/duke5am/postgres-migration-safety-lint)** — Lint SQL migrations before they run: finds statements that take an ACCESS EXCLUSIVE lock, rewrite a table, or destroy data, and gives the safe rewrite.
177
+ *(if you were searching for "postgres migration lock")*
178
+ - **[rls-policy-tester](https://github.com/duke5am/rls-policy-tester)** — Prove user A cannot read user B's rows in Postgres or Supabase with row level security, including negative controls that fail on a missing policy.
179
+ *(if you were searching for "supabase rls test")*
180
+
181
+ All 28 tools in this set, grouped by what they check: **[dev-tools-index](https://duke5am.github.io/dev-tools-index/)**
182
+
183
+ If you arrived here searching for one of these, this is the tool: **postgres performance tuning queries** · **find missing indexes postgres** · **table bloat check postgresql** · **why is my postgres query slow**
184
+
185
+ <!-- RELATED:END -->
186
+
187
+ → **[Postgres Performance Toolkit](https://duke5am.gumroad.com/l/05-postgres-perf-toolkit)** — $29 on Gumroad <!-- GUMROAD-LINK -->
@@ -0,0 +1,165 @@
1
+ # pg-perf-check
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/pg-perf-check)](https://pypi.org/project/pg-perf-check/)
4
+
5
+ Answer **"why is this database slow?"** with a scripted diagnostic report instead
6
+ of guesswork. 24 threshold checks plus 7 read-only SQL files, each answering one
7
+ question.
8
+
9
+ ```bash
10
+ pip install pg-perf-check[psycopg] # from PyPI, Python 3.9+
11
+ pg-perf-check --database-url "$DATABASE_URL"
12
+ pg-perf-check --list-checks
13
+ pg-perf-check --self-test # no database needed
14
+
15
+ # or straight from a clone, no install:
16
+ pip install psycopg2-binary # or: apt install python3-psycopg2
17
+ python3 run_diagnostics.py --database-url "$DATABASE_URL"
18
+ python3 run_diagnostics.py --list-checks
19
+ python3 run_diagnostics.py --self-test # no database needed
20
+ ```
21
+
22
+ Installing without the extra pulls no dependencies at all — the driver is optional
23
+ and imported lazily, because the tool accepts either `psycopg` (v3) or `psycopg2`
24
+ and every no-database mode has to work on an interpreter with neither. The extras
25
+ are `[psycopg]` and `[psycopg2]`.
26
+
27
+ ```
28
+ Connected to mydb on 127.0.0.1/5433 (PostgreSQL 17.11)
29
+ Running 24 checks and the sql/ files...
30
+
31
+ connections CONNECTIONS
32
+ 84 of 100 connection slots in use, 12 idle in transaction
33
+ ...
34
+ Suggested action:
35
+ Put a connection pooler in transaction mode in front of PostgreSQL and cap
36
+ the pool at roughly (2 x CPU cores) + effective_spindle_count. Do not raise
37
+ max_connections as the first move: it multiplies memory use per backend.
38
+ Source: sql/05-connections-and-locks.sql (Q5.0)
39
+ ```
40
+
41
+ ## Everything here is read-only
42
+
43
+ No check writes, alters or drops anything, and none holds a lock beyond a normal
44
+ `SELECT`. Each SQL file says so in its header so you can confirm before pointing
45
+ it at production.
46
+
47
+ ## What it checks
48
+
49
+ **24 threshold checks** across connections, idle-in-transaction sessions, lock
50
+ blocking chains, long-running queries, the oldest `xmin` horizon, replication
51
+ slots pinning `xmin`, table and index bloat, unused and duplicate indexes, cache
52
+ hit ratios, autovacuum health, and more — each with the evidence, the likely
53
+ impact and a concrete action.
54
+
55
+ **7 SQL files**, each answering one question and safe to run alone:
56
+
57
+ | File | Question |
58
+ |---|---|
59
+ | `01-table-sizes-and-bloat` | what is big, and what is dead weight? |
60
+ | `02-unused-and-missing-indexes` | which indexes earn their keep, and what is missing? |
61
+ | `03-slow-queries` | where is the time actually going? |
62
+ | `04-index-usage` | scan counts, index sizes, redundancy |
63
+ | `05-connections-and-locks` | who is waiting on whom? |
64
+ | `06-cache-hit-and-io` | is it reading from cache or from disk? |
65
+ | `07-vacuum-and-autovacuum` | is bloat accumulating, and why? |
66
+
67
+ ## Verified against a real PostgreSQL 17.11
68
+
69
+ **All 24 checks were executed against a live PostgreSQL 17.11 server, and the
70
+ whole report was produced end to end from that server.** Where
71
+ `pg_stat_statements` is unavailable, the four extension-dependent checks are
72
+ reported as *skipped* with the two-step install instructions, rather than failing
73
+ or silently reporting nothing.
74
+
75
+ The 7 SQL files are executed by the runner too, which prints a per-file result
76
+ line so you can see the state of your own server version. Exactly what that
77
+ showed, and what was not verified:
78
+
79
+ * `01`, `02`, `04`, `05`, `06` and `07` ran every statement cleanly on 17.11.
80
+ `07-vacuum-and-autovacuum.sql` reports **10/10 statements OK**.
81
+ * Two statements in `07-vacuum-and-autovacuum.sql` did **not**, and are fixed:
82
+ Q7.6 named `pg_stat_progress_vacuum` columns that PostgreSQL 17 replaced —
83
+ `max_dead_tuples` / `num_dead_tuples` became `max_dead_tuple_bytes`,
84
+ `dead_tuple_bytes` and `num_dead_item_ids`, a rename *and* a change of unit, so
85
+ naming either set breaks the other branch. Q7.6 now reads those fields out of
86
+ the row's JSON form (`to_jsonb(p) ->> '…'`), so it parses on every supported
87
+ version and reports which branch you are on in a `dead_store_units` column.
88
+ Q7.7 selected `datname` from `pg_stat_user_tables`, a column that has never
89
+ existed on **any** release — that view is already scoped to the current
90
+ database — and now selects `schemaname` / `relname`. Q7.6's 17+ branch was
91
+ verified by catching a live throttled `VACUUM` (`dead_store_units = bytes`,
92
+ `dead_store_capacity = 67108864`, i.e. the default 64 MiB
93
+ `maintenance_work_mem`); Q7.7 was verified directly. **Not verified:** the
94
+ pre-17 branch of Q7.6, because no PostgreSQL 16-or-older server was available.
95
+ * Two check-output defects were found during that same verification and are also
96
+ fixed: `shared_buffers_low` reported `approximately 0 MB` for a stock 128 MB
97
+ `shared_buffers` because the parser ignored `pg_settings.unit` (`setting = 16384,
98
+ unit = 8kB`) — it now prints `approximately 128 MB` — and `autovacuum_throttled`
99
+ printed `vacuum_cost_page_miss = None, vacuum_cost_page_dirty = None` because the
100
+ settings query selected neither; it now prints `2` and `20`.
101
+ * `03-slow-queries.sql` needs the `pg_stat_statements` extension. On a server
102
+ without it, seven of its eight statements fail with *relation
103
+ "pg_stat_statements" does not exist*, and the runner reports exactly that
104
+ alongside the install instructions. That is a missing extension on the server,
105
+ not a broken statement, and it is the only remaining failure the runner prints
106
+ on a stock 17.11 server without that extension.
107
+
108
+ ## The advice it gives about its own output
109
+
110
+ Worth quoting, because it is the part most tooling gets wrong:
111
+
112
+ > Change ONE thing, then re-run this script and compare. Two changes at once means
113
+ > you cannot tell which one helped. […] Changing a setting is not the same as
114
+ > fixing a query.
115
+
116
+ Counters are cumulative since the last statistics reset, so the counter-based
117
+ findings (unused indexes, sequential scans, cache ratios) only mean something
118
+ after a full business cycle.
119
+
120
+ ## What it is not
121
+
122
+ It diagnoses; it does not tune for you. It cannot know your workload, so it cannot
123
+ tell you whether a missing index is worth the write cost. `EXPLAIN-GUIDE.md`
124
+ covers reading plans; designing an index is your call.
125
+
126
+ Results also depend on your statistics being current — on a server where `ANALYZE`
127
+ has not run recently, estimated row counts will mislead you.
128
+
129
+ ## Requirements
130
+
131
+ Python 3.9+ (the package declares `>=3.9`; the code itself runs on 3.8),
132
+ PostgreSQL 12+, and a Postgres driver — `psycopg` (v3) or `psycopg2`, either one,
133
+ installed explicitly or via `pip install pg-perf-check[psycopg]`. With no driver
134
+ and no database, `--help`, `--list-checks` and `--self-test` still work; a
135
+ connection attempt without a driver exits 2 with the install command to run.
136
+
137
+ ## The full pack
138
+
139
+ The paid kit adds the two playbooks this repository does not ship —
140
+ `INDEXING-PLAYBOOK.md` (how to design an index the planner actually uses, with
141
+ page counts and WAL volumes measured on a live PostgreSQL 17.11 server rather than
142
+ asserted) and `REMEDIATION-PLAYBOOK.md` (one section per check: the threshold that
143
+ fired, what the counter really counts, the false positives to rule out, the SQL
144
+ with the guards that stop it being an outage, and how to verify the fix worked) —
145
+ plus the CI harness that turns the `--json` report into a baseline comparison,
146
+ failing a build only on findings that are new or worse.
147
+
148
+ <!-- RELATED:START -->
149
+
150
+ ## Related tools
151
+
152
+ - **[pg-restore-drill](https://github.com/duke5am/pg-restore-drill)** — Prove your PostgreSQL backup actually restores: a scripted point-in-time recovery drill with a measured RPO/RTO report and a negative control.
153
+ *(if you were searching for "test postgres backup restore")*
154
+ - **[postgres-migration-safety-lint](https://github.com/duke5am/postgres-migration-safety-lint)** — Lint SQL migrations before they run: finds statements that take an ACCESS EXCLUSIVE lock, rewrite a table, or destroy data, and gives the safe rewrite.
155
+ *(if you were searching for "postgres migration lock")*
156
+ - **[rls-policy-tester](https://github.com/duke5am/rls-policy-tester)** — Prove user A cannot read user B's rows in Postgres or Supabase with row level security, including negative controls that fail on a missing policy.
157
+ *(if you were searching for "supabase rls test")*
158
+
159
+ All 28 tools in this set, grouped by what they check: **[dev-tools-index](https://duke5am.github.io/dev-tools-index/)**
160
+
161
+ If you arrived here searching for one of these, this is the tool: **postgres performance tuning queries** · **find missing indexes postgres** · **table bloat check postgresql** · **why is my postgres query slow**
162
+
163
+ <!-- RELATED:END -->
164
+
165
+ → **[Postgres Performance Toolkit](https://duke5am.gumroad.com/l/05-postgres-perf-toolkit)** — $29 on Gumroad <!-- GUMROAD-LINK -->
@@ -0,0 +1,9 @@
1
+ """pg_perf_check -- PostgreSQL performance diagnostics.
2
+
3
+ The implementation lives in :mod:`pg_perf_check.cli`; the `pg-perf-check`
4
+ console script and the repository-root `run_diagnostics.py` wrapper both call
5
+ ``pg_perf_check.cli.main``, so there is one copy of the code, not two.
6
+
7
+ The read-only SQL files the report cites are package data in
8
+ ``pg_perf_check/sql/``.
9
+ """