pg_reports 0.5.3 → 0.6.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/README.md +12 -4
- data/app/controllers/pg_reports/dashboard_controller.rb +6 -2
- data/app/views/layouts/pg_reports/application.html.erb +70 -61
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +53 -1
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +31 -11
- data/app/views/pg_reports/dashboard/index.html.erb +80 -9
- data/app/views/pg_reports/dashboard/show.html.erb +6 -2
- data/config/locales/en.yml +109 -0
- data/config/locales/ru.yml +81 -0
- data/config/locales/uk.yml +126 -0
- data/lib/pg_reports/compatibility.rb +63 -0
- data/lib/pg_reports/configuration.rb +2 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +36 -0
- data/lib/pg_reports/definitions/indexes/fk_without_indexes.yml +30 -0
- data/lib/pg_reports/definitions/indexes/index_correlation.yml +31 -0
- data/lib/pg_reports/definitions/indexes/inefficient_indexes.yml +45 -0
- data/lib/pg_reports/definitions/queries/temp_file_queries.yml +39 -0
- data/lib/pg_reports/definitions/system/wraparound_risk.yml +31 -0
- data/lib/pg_reports/definitions/tables/tables_without_pk.yml +28 -0
- data/lib/pg_reports/engine.rb +6 -0
- data/lib/pg_reports/modules/indexes.rb +3 -0
- data/lib/pg_reports/modules/queries.rb +1 -0
- data/lib/pg_reports/modules/system.rb +27 -0
- data/lib/pg_reports/modules/tables.rb +1 -0
- data/lib/pg_reports/query_monitor.rb +139 -42
- data/lib/pg_reports/sql/indexes/fk_without_indexes.sql +23 -0
- data/lib/pg_reports/sql/indexes/index_correlation.sql +27 -0
- data/lib/pg_reports/sql/indexes/inefficient_indexes.sql +22 -0
- data/lib/pg_reports/sql/queries/temp_file_queries.sql +16 -0
- data/lib/pg_reports/sql/system/checkpoint_stats.sql +20 -0
- data/lib/pg_reports/sql/system/checkpoint_stats_legacy.sql +19 -0
- data/lib/pg_reports/sql/system/wraparound_risk.sql +21 -0
- data/lib/pg_reports/sql/tables/tables_without_pk.sql +20 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +5 -0
- metadata +16 -1
data/config/locales/uk.yml
CHANGED
|
@@ -51,6 +51,16 @@ uk:
|
|
|
51
51
|
- "Збільшення shared_buffers допомагає, але є межа ефективності (зазвичай 25% RAM)."
|
|
52
52
|
- "Covering indexes (INCLUDE) дозволяють виконувати index-only scan без звернення до таблиці."
|
|
53
53
|
|
|
54
|
+
temp_file_queries:
|
|
55
|
+
title: "Temp File Queries"
|
|
56
|
+
what: "Запити, що скидають проміжні результати у тимчасові файли на диск."
|
|
57
|
+
how: "Аналізує temp_blks_written та temp_blks_read з pg_stat_statements. Тимчасові файли створюються, коли work_mem недостатній для сортування, хешування або матеріалізації."
|
|
58
|
+
nuances:
|
|
59
|
+
- "Запис у тимчасові файли на порядки повільніший за операції в пам'яті."
|
|
60
|
+
- "Збільшення work_mem може усунути тимчасові файли, але впливає на всі сесії — використовуйте SET LOCAL для конкретних запитів."
|
|
61
|
+
- "Часті причини: великі сортування (ORDER BY), hash join на великих таблицях, DISTINCT на багатьох рядках, складні CTE."
|
|
62
|
+
- "Розгляньте додавання індексів для усунення сортувань або переписування запитів для зменшення проміжних результатів."
|
|
63
|
+
|
|
54
64
|
all_queries:
|
|
55
65
|
title: "All Queries"
|
|
56
66
|
what: "Повна статистика по всіх запитах з pg_stat_statements."
|
|
@@ -107,6 +117,17 @@ uk:
|
|
|
107
117
|
- "idx_tup_read — рядки прочитані з індексу."
|
|
108
118
|
- "idx_tup_fetch — рядки отримані з таблиці після index scan (для non-covering indexes)."
|
|
109
119
|
|
|
120
|
+
inefficient_indexes:
|
|
121
|
+
title: "Inefficient Indexes"
|
|
122
|
+
what: "Індекси, які використовуються, але сканують значно більше записів, ніж реально витягують."
|
|
123
|
+
how: "Порівнює idx_tup_read (записів прочитано з індексу) та idx_tup_fetch (рядків витягнуто з heap) з pg_stat_user_indexes. Високе співвідношення означає, що порядок колонок індексу не відповідає предикатам запиту, змушуючи PostgreSQL сканувати великі діапазони індексу."
|
|
124
|
+
nuances:
|
|
125
|
+
- "Співвідношення read/fetch >10 означає, що індекс читає в 10+ разів більше записів, ніж повертає — явна ознака неправильного порядку колонок."
|
|
126
|
+
- "Для складених індексів (a, b, c) запити, що фільтрують по (b) або (c) без (a), не можуть виконати точний seek і змушені сканувати широкі діапазони."
|
|
127
|
+
- "Рішення: створити цільовий індекс з колонками, що відповідають найбільш селективним WHERE-предикатам у першу чергу."
|
|
128
|
+
- "Використовуйте EXPLAIN ANALYZE для підтвердження, що index scan читає надлишкову кількість записів, перш ніж вносити зміни."
|
|
129
|
+
- "Index-only scans (де idx_tup_fetch = 0) виключені — вони вказують на ефективно працюючі covering indexes."
|
|
130
|
+
|
|
110
131
|
bloated_indexes:
|
|
111
132
|
title: "Bloated Indexes"
|
|
112
133
|
what: "Індекси з високим рівнем bloat (роздування) через мертві кортежі."
|
|
@@ -117,6 +138,27 @@ uk:
|
|
|
117
138
|
- "pg_repack дозволяє перебудувати індекси без блокувань."
|
|
118
139
|
- "Bloat >30-50% — привід для дій."
|
|
119
140
|
|
|
141
|
+
fk_without_indexes:
|
|
142
|
+
title: "FK Without Indexes"
|
|
143
|
+
what: "Зовнішні ключі на дочірніх таблицях без підтримуючого індексу."
|
|
144
|
+
how: "Порівнює pg_constraint (зовнішні ключі) з pg_index для пошуку FK-колонок без відповідного ведучого індексу."
|
|
145
|
+
nuances:
|
|
146
|
+
- "Без індексу DELETE або UPDATE батьківської таблиці викликає sequential scan дочірньої таблиці."
|
|
147
|
+
- "На великих дочірніх таблицях це може спричинити серйозну конкуренцію блокувань та сповільнити каскадні операції."
|
|
148
|
+
- "Індекс повинен мати FK-колонку як ведучу (першу) для ефективності."
|
|
149
|
+
- "Маленькі таблиці (< 10K рядків) можуть не виграти від додавання індексу — PostgreSQL віддасть перевагу seq scan."
|
|
150
|
+
|
|
151
|
+
index_correlation:
|
|
152
|
+
title: "Index Correlation"
|
|
153
|
+
what: "Індекси, де фізичний порядок рядків погано відповідає порядку індексу."
|
|
154
|
+
how: "Читає pg_stats.correlation для ведучих колонок індексів. Кореляція близько 0 означає випадковий фізичний порядок відносно індексу, що спричиняє надлишковий random I/O при range scan."
|
|
155
|
+
nuances:
|
|
156
|
+
- "Кореляція від -1 до 1. Значення близько 0 — випадковий порядок; близько 1 або -1 — впорядкований."
|
|
157
|
+
- "Низька кореляція переважно впливає на range scan (BETWEEN, >, <) та ORDER BY — точкові пошуки зачіпаються менше."
|
|
158
|
+
- "CLUSTER table USING index фізично переупорядковує рядки, але блокує таблицю та не підтримується автоматично."
|
|
159
|
+
- "Для append-only таблиць з timestamp колонками кореляція природно висока — дій не потрібно."
|
|
160
|
+
- "Показані лише таблиці > 10MB з > 100 index scan для зниження шуму."
|
|
161
|
+
|
|
120
162
|
index_sizes:
|
|
121
163
|
title: "Index Sizes"
|
|
122
164
|
what: "Розміри індексів на диску."
|
|
@@ -183,6 +225,16 @@ uk:
|
|
|
183
225
|
- "Високий seq_tup_read/seq_scan = багато рядків за один scan = можливо норма."
|
|
184
226
|
- "Низький seq_tup_read/seq_scan = багато scan маленьких об'ємів = можливо N+1."
|
|
185
227
|
|
|
228
|
+
tables_without_pk:
|
|
229
|
+
title: "Tables Without Primary Keys"
|
|
230
|
+
what: "Таблиці без первинного ключа."
|
|
231
|
+
how: "Перевіряє pg_index на відсутність indisprimary записів для користувацьких таблиць."
|
|
232
|
+
nuances:
|
|
233
|
+
- "Логічна реплікація потребує первинний ключ або REPLICA IDENTITY для ідентифікації рядків."
|
|
234
|
+
- "Без PK операції UPDATE та DELETE потребують іншого способу унікальної ідентифікації рядків, часто призводячи до повних сканів таблиці."
|
|
235
|
+
- "Join-таблиці (many-to-many) часто навмисно без PK — розгляньте додавання складеного PK."
|
|
236
|
+
- "Деякі ORM (наприклад, Rails) передбачають наявність 'id' первинного ключа — таблиці без нього можуть спричинити помилки фреймворку."
|
|
237
|
+
|
|
186
238
|
recently_modified:
|
|
187
239
|
title: "Recently Modified"
|
|
188
240
|
what: "Таблиці з недавньою активністю INSERT/UPDATE/DELETE."
|
|
@@ -249,6 +301,46 @@ uk:
|
|
|
249
301
|
- "Занадто багато idle = застосунок не закриває з'єднання або пул занадто великий."
|
|
250
302
|
- "idle_session_timeout (PostgreSQL 14+) може автоматично закривати idle з'єднання."
|
|
251
303
|
|
|
304
|
+
pool_usage:
|
|
305
|
+
title: "Використання пулу з'єднань"
|
|
306
|
+
what: "Поточне використання пулу з'єднань з показниками активних, idle та доступних з'єднань по базах даних."
|
|
307
|
+
how: "Аналіз станів з'єднань в pg_stat_activity та порівняння з лімітом max_connections."
|
|
308
|
+
nuances:
|
|
309
|
+
- "Утилізація вище 70% означає наближення до ліміту — розгляньте масштабування."
|
|
310
|
+
- "Idle in transaction з'єднання витрачають ресурси та блокують VACUUM."
|
|
311
|
+
- "max_connections — глобальне налаштування для всієї БД, а не для кожної бази окремо."
|
|
312
|
+
- "Connection pooler'и (PgBouncer/pgpool) дозволяють мати більше з'єднань на рівні застосунку."
|
|
313
|
+
|
|
314
|
+
pool_wait_times:
|
|
315
|
+
title: "Аналіз часу очікування"
|
|
316
|
+
what: "Запити, що очікують ресурсів (блокування, I/O, мережа)."
|
|
317
|
+
how: "Аналіз wait_event та wait_event_type з pg_stat_activity для не-idle з'єднань."
|
|
318
|
+
nuances:
|
|
319
|
+
- "ClientRead очікування = повільний клієнт не встигає споживати дані."
|
|
320
|
+
- "Lock очікування = конкуренція між конкурентними запитами."
|
|
321
|
+
- "IO очікування = проблеми продуктивності диска або недостатній кеш."
|
|
322
|
+
- "Очікування понад 60 секунд критичні та потребують негайного розслідування."
|
|
323
|
+
|
|
324
|
+
pool_saturation:
|
|
325
|
+
title: "Попередження про насичення пулу"
|
|
326
|
+
what: "Загальні метрики здоров'я пулу з'єднань з попередженнями про насичення та рекомендаціями."
|
|
327
|
+
how: "Розрахунок відсотків утилізації для total, active, idle та проблемних з'єднань."
|
|
328
|
+
nuances:
|
|
329
|
+
- "Постійна утилізація вище 70% = необхідність налаштування пулу або масштабування."
|
|
330
|
+
- "Висока кількість idle in transaction = проблеми обробки транзакцій застосунком."
|
|
331
|
+
- "superuser_reserved_connections зменшують доступну ємність пулу."
|
|
332
|
+
- "Відстежуйте тренди — різкі стрибки можуть вказувати на витоки з'єднань."
|
|
333
|
+
|
|
334
|
+
connection_churn:
|
|
335
|
+
title: "Аналіз оборотності з'єднань"
|
|
336
|
+
what: "Аналіз патернів життєвого циклу з'єднань для виявлення надлишкового churn (часте підключення/відключення)."
|
|
337
|
+
how: "Вивчення віку з'єднань для ідентифікації короткоживучих з'єднань та розрахунку churn rate по застосунках."
|
|
338
|
+
nuances:
|
|
339
|
+
- "З'єднання молодші 10 секунд = короткоживучі."
|
|
340
|
+
- "Churn rate вище 50% = відсутність або неправильне налаштування connection pooling."
|
|
341
|
+
- "Багато коротких з'єднань = збільшене навантаження на CPU та автентифікацію."
|
|
342
|
+
- "Веб-застосунки повинні підтримувати пул з'єднань, а не створювати з'єднання на кожен запит."
|
|
343
|
+
|
|
252
344
|
# === SYSTEM ===
|
|
253
345
|
database_sizes:
|
|
254
346
|
title: "Database Sizes"
|
|
@@ -286,6 +378,28 @@ uk:
|
|
|
286
378
|
- "Корисно для швидкої оцінки стану бази."
|
|
287
379
|
- "Порівнюйте з baseline для виявлення аномалій."
|
|
288
380
|
|
|
381
|
+
wraparound_risk:
|
|
382
|
+
title: "Wraparound Risk"
|
|
383
|
+
what: "Близькість віку Transaction ID до ліміту wraparound у 2 мільярди."
|
|
384
|
+
how: "Читає age(datfrozenxid) з pg_database. Коли значення наближається до 2^31 (~2.1 млрд), PostgreSQL зупиниться для запобігання пошкодження даних."
|
|
385
|
+
nuances:
|
|
386
|
+
- "autovacuum_freeze_max_age (за замовчуванням 200M) автоматично запускає агресивний anti-wraparound VACUUM."
|
|
387
|
+
- "Якщо age перевищує freeze_max_age, anti-wraparound VACUUM вже повинен працювати — якщо ні, розслідуйте причину."
|
|
388
|
+
- "Довготривалі транзакції не дають VACUUM просунути frozen XID — моніторте idle-in-transaction."
|
|
389
|
+
- "В екстрених випадках запустіть VACUUM FREEZE на найбільших/найстаріших таблицях першими."
|
|
390
|
+
- "Pct > 50% — попередження; > 75% — критично, потребує негайних дій."
|
|
391
|
+
|
|
392
|
+
checkpoint_stats:
|
|
393
|
+
title: "Checkpoint Stats"
|
|
394
|
+
what: "Частота чекпоінтів та метрики продуктивності background writer."
|
|
395
|
+
how: "Дані з pg_stat_bgwriter показують кількість чекпоінтів, тайминги та розподіл запису буферів."
|
|
396
|
+
nuances:
|
|
397
|
+
- "checkpoints_timed = заплановані чекпоінти (нормально); checkpoints_req = вимушені чекпоінти (під навантаженням)."
|
|
398
|
+
- "Високий requested_pct означає, що WAL заповнюється до checkpoint_timeout — збільште max_wal_size."
|
|
399
|
+
- "buffers_backend > 0 означає, що бекенди самі пишуть брудні буфери — збільште shared_buffers або активність bgwriter."
|
|
400
|
+
- "bgwriter_stops (maxwritten_clean) > 0 означає, що bgwriter досяг ліміту за раунд — збільште bgwriter_lru_maxpages."
|
|
401
|
+
- "Статистика накопичується з моменту stats_reset — порівнюйте за періоди для осмисленого аналізу."
|
|
402
|
+
|
|
289
403
|
cache_stats:
|
|
290
404
|
title: "Cache Stats"
|
|
291
405
|
what: "Статистика кешування бази даних."
|
|
@@ -316,8 +430,20 @@ uk:
|
|
|
316
430
|
low_cache_hit: "Низький cache hit ratio. Запит часто читає з диска замість кешу."
|
|
317
431
|
high_seq_scan: "Багато sequential scan. Можливо не вистачає індексу."
|
|
318
432
|
unused_index: "Індекс не використовується. Кандидат на видалення."
|
|
433
|
+
inefficient_index: "Індекс читає значно більше записів, ніж витягує. Ймовірно, порядок колонок складеного індексу не відповідає предикатам запиту."
|
|
434
|
+
fk_without_index: "FK-колонка без підтримуючого індексу. DELETE/UPDATE батьківської таблиці спричинить sequential scan."
|
|
435
|
+
low_correlation: "Низька фізична кореляція між індексом та порядком рядків. Range scan спричинить надлишковий random I/O."
|
|
436
|
+
temp_file_heavy: "Запит скидає дані у тимчасові файли на диск. Розгляньте збільшення work_mem або оптимізацію запиту."
|
|
437
|
+
missing_pk: "Таблиця без первинного ключа. Це ламає логічну реплікацію та може спричинити проблеми з ORM."
|
|
438
|
+
wraparound_risk: "Вік Transaction ID наближається до ліміту wraparound. Потрібен VACUUM FREEZE."
|
|
439
|
+
high_checkpoint_req: "Високий відсоток вимушених чекпоінтів. Розгляньте збільшення max_wal_size."
|
|
319
440
|
high_bloat: "Високий bloat. Потрібен REINDEX або VACUUM."
|
|
320
441
|
many_dead_tuples: "Багато мертвих рядків. Потрібен VACUUM."
|
|
321
442
|
long_running: "Довго виконуваний запит. Може блокувати інші операції."
|
|
322
443
|
blocking: "Блокує інші запити. Потребує уваги."
|
|
323
444
|
idle_in_transaction: "Відкрита транзакція без активності. Блокує VACUUM та утримує локи."
|
|
445
|
+
high_pool_usage: "Висока утилізація пулу з'єднань. Збільште max_connections або впровадьте connection pooling."
|
|
446
|
+
long_wait_time: "Запит занадто довго очікує ресурсів. Перевірте конкуренцію блокувань або проблеми I/O."
|
|
447
|
+
pool_saturation: "Пул з'єднань насичений. Ризик вичерпання з'єднань та помилок застосунку."
|
|
448
|
+
high_connection_churn: "Висока оборотність з'єднань. Впровадьте connection pooling для зниження накладних витрат."
|
|
449
|
+
too_many_short_connections: "Занадто багато короткоживучих з'єднань. Застосунок повинен перевикористовувати з'єднання через pooling."
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgReports
|
|
4
|
+
# Checks runtime environment and warns about outdated or unsupported versions.
|
|
5
|
+
# Called once at boot (Ruby/Rails) and lazily on first DB access (PostgreSQL).
|
|
6
|
+
module Compatibility
|
|
7
|
+
# Keep in sync with gemspec constraints
|
|
8
|
+
MINIMUM_RUBY_VERSION = "2.7"
|
|
9
|
+
MINIMUM_RAILS_VERSION = "5.0"
|
|
10
|
+
MINIMUM_PG_VERSION = 12_00_00 # server_version_num format
|
|
11
|
+
MINIMUM_PG_VERSION_LABEL = "12"
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def check_ruby!
|
|
15
|
+
return if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(MINIMUM_RUBY_VERSION)
|
|
16
|
+
|
|
17
|
+
warn "[pg_reports] Ruby #{RUBY_VERSION} is not supported. " \
|
|
18
|
+
"Minimum required version is Ruby #{MINIMUM_RUBY_VERSION}."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def check_rails!
|
|
22
|
+
return unless defined?(Rails::VERSION::STRING)
|
|
23
|
+
return if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new(MINIMUM_RAILS_VERSION)
|
|
24
|
+
|
|
25
|
+
warn "[pg_reports] Rails #{Rails::VERSION::STRING} is not supported. " \
|
|
26
|
+
"Minimum required version is Rails #{MINIMUM_RAILS_VERSION}."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def check_postgresql!
|
|
30
|
+
version_num = pg_version_num
|
|
31
|
+
return if version_num.nil? # no connection yet — skip silently
|
|
32
|
+
return if version_num >= MINIMUM_PG_VERSION
|
|
33
|
+
|
|
34
|
+
label = pg_version_label(version_num)
|
|
35
|
+
warn "[pg_reports] PostgreSQL #{label} is not supported. " \
|
|
36
|
+
"Minimum required version is PostgreSQL #{MINIMUM_PG_VERSION_LABEL}. " \
|
|
37
|
+
"Some reports may return errors or incomplete data."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def check_all!
|
|
41
|
+
check_ruby!
|
|
42
|
+
check_rails!
|
|
43
|
+
check_postgresql!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def pg_version_num
|
|
49
|
+
connection = PgReports.config.connection
|
|
50
|
+
result = connection.exec_query("SELECT current_setting('server_version_num')::int AS v")
|
|
51
|
+
result.first&.fetch("v", 0).to_i
|
|
52
|
+
rescue
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def pg_version_label(version_num)
|
|
57
|
+
major = version_num / 1_00_00
|
|
58
|
+
minor = (version_num % 1_00_00) / 100
|
|
59
|
+
"#{major}.#{minor}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -13,6 +13,7 @@ module PgReports
|
|
|
13
13
|
|
|
14
14
|
# Index analysis thresholds
|
|
15
15
|
attr_accessor :unused_index_threshold_scans # Index with fewer scans is unused
|
|
16
|
+
attr_accessor :inefficient_index_threshold_ratio # Read/fetch ratio above this is inefficient
|
|
16
17
|
|
|
17
18
|
# Table analysis thresholds
|
|
18
19
|
attr_accessor :bloat_threshold_percent # Tables with more bloat are problematic
|
|
@@ -53,6 +54,7 @@ module PgReports
|
|
|
53
54
|
|
|
54
55
|
# Index thresholds
|
|
55
56
|
@unused_index_threshold_scans = 50
|
|
57
|
+
@inefficient_index_threshold_ratio = 10
|
|
56
58
|
|
|
57
59
|
# Table thresholds
|
|
58
60
|
@bloat_threshold_percent = 20
|
|
@@ -32,6 +32,10 @@ module PgReports
|
|
|
32
32
|
thresholds: {},
|
|
33
33
|
problem_fields: []
|
|
34
34
|
},
|
|
35
|
+
temp_file_queries: {
|
|
36
|
+
thresholds: {temp_mb_written: {warning: 100, critical: 1000}},
|
|
37
|
+
problem_fields: ["temp_mb_written"]
|
|
38
|
+
},
|
|
35
39
|
|
|
36
40
|
# === INDEXES ===
|
|
37
41
|
unused_indexes: {
|
|
@@ -50,6 +54,10 @@ module PgReports
|
|
|
50
54
|
thresholds: {seq_scan_ratio: {warning: 0.5, critical: 0.9}},
|
|
51
55
|
problem_fields: ["seq_scan", "seq_tup_read"]
|
|
52
56
|
},
|
|
57
|
+
inefficient_indexes: {
|
|
58
|
+
thresholds: {read_to_fetch_ratio: {warning: 10, critical: 100}},
|
|
59
|
+
problem_fields: ["read_to_fetch_ratio"]
|
|
60
|
+
},
|
|
53
61
|
index_usage: {
|
|
54
62
|
thresholds: {},
|
|
55
63
|
problem_fields: []
|
|
@@ -62,6 +70,14 @@ module PgReports
|
|
|
62
70
|
thresholds: {size_bytes: {warning: 1073741824, critical: 10737418240}},
|
|
63
71
|
problem_fields: ["size_bytes"]
|
|
64
72
|
},
|
|
73
|
+
fk_without_indexes: {
|
|
74
|
+
thresholds: {},
|
|
75
|
+
problem_fields: ["child_table_size_mb"]
|
|
76
|
+
},
|
|
77
|
+
index_correlation: {
|
|
78
|
+
thresholds: {correlation: {warning: 0.5, critical: 0.2, inverted: true}},
|
|
79
|
+
problem_fields: ["correlation"]
|
|
80
|
+
},
|
|
65
81
|
|
|
66
82
|
# === TABLES ===
|
|
67
83
|
table_sizes: {
|
|
@@ -92,6 +108,10 @@ module PgReports
|
|
|
92
108
|
thresholds: {},
|
|
93
109
|
problem_fields: []
|
|
94
110
|
},
|
|
111
|
+
tables_without_pk: {
|
|
112
|
+
thresholds: {},
|
|
113
|
+
problem_fields: []
|
|
114
|
+
},
|
|
95
115
|
|
|
96
116
|
# === CONNECTIONS ===
|
|
97
117
|
active_connections: {
|
|
@@ -159,6 +179,14 @@ module PgReports
|
|
|
159
179
|
thresholds: {cache_hit_ratio: {warning: 0.95, critical: 0.90, inverted: true}},
|
|
160
180
|
problem_fields: ["cache_hit_ratio"]
|
|
161
181
|
},
|
|
182
|
+
wraparound_risk: {
|
|
183
|
+
thresholds: {pct_towards_wraparound: {warning: 50, critical: 75}},
|
|
184
|
+
problem_fields: ["pct_towards_wraparound"]
|
|
185
|
+
},
|
|
186
|
+
checkpoint_stats: {
|
|
187
|
+
thresholds: {requested_pct: {warning: 50, critical: 75}},
|
|
188
|
+
problem_fields: ["requested_pct"]
|
|
189
|
+
},
|
|
162
190
|
|
|
163
191
|
# === SCHEMA ANALYSIS ===
|
|
164
192
|
missing_validations: {
|
|
@@ -178,6 +206,7 @@ module PgReports
|
|
|
178
206
|
expensive_queries: {name: "Expensive Queries", description: "Queries consuming most total time"},
|
|
179
207
|
missing_index_queries: {name: "Missing Index Queries", description: "Queries potentially missing indexes"},
|
|
180
208
|
low_cache_hit_queries: {name: "Low Cache Hit", description: "Queries with poor cache utilization"},
|
|
209
|
+
temp_file_queries: {name: "Temp File Queries", description: "Queries spilling to disk", new: true},
|
|
181
210
|
all_queries: {name: "All Queries", description: "All query statistics"}
|
|
182
211
|
}
|
|
183
212
|
},
|
|
@@ -190,8 +219,11 @@ module PgReports
|
|
|
190
219
|
duplicate_indexes: {name: "Duplicate Indexes", description: "Redundant indexes"},
|
|
191
220
|
invalid_indexes: {name: "Invalid Indexes", description: "Indexes that failed to build"},
|
|
192
221
|
missing_indexes: {name: "Missing Indexes", description: "Tables potentially missing indexes"},
|
|
222
|
+
inefficient_indexes: {name: "Inefficient Indexes", description: "Indexes with high read-to-fetch ratio", new: true},
|
|
193
223
|
index_usage: {name: "Index Usage", description: "Index scan statistics"},
|
|
194
224
|
bloated_indexes: {name: "Bloated Indexes", description: "Indexes with high bloat"},
|
|
225
|
+
fk_without_indexes: {name: "FK Without Indexes", description: "Foreign keys missing indexes", new: true},
|
|
226
|
+
index_correlation: {name: "Index Correlation", description: "Low physical correlation indexes", new: true},
|
|
195
227
|
index_sizes: {name: "Index Sizes", description: "Index disk usage"}
|
|
196
228
|
}
|
|
197
229
|
},
|
|
@@ -206,6 +238,7 @@ module PgReports
|
|
|
206
238
|
row_counts: {name: "Row Counts", description: "Table row counts"},
|
|
207
239
|
cache_hit_ratios: {name: "Cache Hit Ratios", description: "Table cache statistics"},
|
|
208
240
|
seq_scans: {name: "Sequential Scans", description: "Tables with high sequential scans"},
|
|
241
|
+
tables_without_pk: {name: "No Primary Key", description: "Tables missing primary keys", new: true},
|
|
209
242
|
recently_modified: {name: "Recently Modified", description: "Tables with recent activity"}
|
|
210
243
|
}
|
|
211
244
|
},
|
|
@@ -235,6 +268,8 @@ module PgReports
|
|
|
235
268
|
settings: {name: "Settings", description: "PostgreSQL configuration"},
|
|
236
269
|
extensions: {name: "Extensions", description: "Installed extensions"},
|
|
237
270
|
activity_overview: {name: "Activity Overview", description: "Current activity summary"},
|
|
271
|
+
wraparound_risk: {name: "Wraparound Risk", description: "Transaction ID wraparound proximity", new: true},
|
|
272
|
+
checkpoint_stats: {name: "Checkpoint Stats", description: "Checkpoint and bgwriter statistics", new: true},
|
|
238
273
|
cache_stats: {name: "Cache Stats", description: "Database cache statistics"}
|
|
239
274
|
}
|
|
240
275
|
},
|
|
@@ -272,6 +307,7 @@ module PgReports
|
|
|
272
307
|
what: I18n.t("#{i18n_key}.what", default: ""),
|
|
273
308
|
how: I18n.t("#{i18n_key}.how", default: ""),
|
|
274
309
|
nuances: I18n.t("#{i18n_key}.nuances", default: []),
|
|
310
|
+
ai_prompt: I18n.t("#{i18n_key}.ai_prompt", default: nil),
|
|
275
311
|
thresholds: config[:thresholds],
|
|
276
312
|
problem_fields: config[:problem_fields]
|
|
277
313
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Foreign keys without indexes on the child table
|
|
2
|
+
# Missing indexes cause sequential scans on parent DELETE/UPDATE
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: fk_without_indexes
|
|
6
|
+
module: indexes
|
|
7
|
+
description: "Foreign keys missing indexes on the referencing table"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: indexes
|
|
11
|
+
file: fk_without_indexes
|
|
12
|
+
|
|
13
|
+
title: "Foreign Keys Without Indexes"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- constraint_name
|
|
17
|
+
- child_table
|
|
18
|
+
- child_column
|
|
19
|
+
- parent_table
|
|
20
|
+
- parent_column
|
|
21
|
+
- child_table_size_mb
|
|
22
|
+
|
|
23
|
+
parameters:
|
|
24
|
+
limit:
|
|
25
|
+
type: integer
|
|
26
|
+
default: 50
|
|
27
|
+
description: "Maximum number of results"
|
|
28
|
+
|
|
29
|
+
problem_explanations:
|
|
30
|
+
child_table_size_mb: fk_without_index
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Index correlation analysis
|
|
2
|
+
# Low correlation means physical row order doesn't match index order, causing random I/O
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: index_correlation
|
|
6
|
+
module: indexes
|
|
7
|
+
description: "Indexes with low physical correlation causing excessive random I/O"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: indexes
|
|
11
|
+
file: index_correlation
|
|
12
|
+
|
|
13
|
+
title: "Index Correlation (tables > 10MB, scans > 100)"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- schema
|
|
17
|
+
- table_name
|
|
18
|
+
- column_name
|
|
19
|
+
- index_name
|
|
20
|
+
- correlation
|
|
21
|
+
- table_size_mb
|
|
22
|
+
- idx_scan
|
|
23
|
+
|
|
24
|
+
parameters:
|
|
25
|
+
limit:
|
|
26
|
+
type: integer
|
|
27
|
+
default: 50
|
|
28
|
+
description: "Maximum number of results"
|
|
29
|
+
|
|
30
|
+
problem_explanations:
|
|
31
|
+
correlation: low_correlation
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Inefficient indexes - indexes that are used but scan far more entries than they fetch
|
|
2
|
+
# High read-to-fetch ratio indicates misaligned composite index column order
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: inefficient_indexes
|
|
6
|
+
module: indexes
|
|
7
|
+
description: "Indexes with high read-to-fetch ratio indicating inefficient scans"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: indexes
|
|
11
|
+
file: inefficient_indexes
|
|
12
|
+
|
|
13
|
+
title: "Inefficient Indexes (read/fetch ratio > ${threshold})"
|
|
14
|
+
title_vars:
|
|
15
|
+
threshold:
|
|
16
|
+
source: config
|
|
17
|
+
key: inefficient_index_threshold_ratio
|
|
18
|
+
|
|
19
|
+
columns:
|
|
20
|
+
- schema
|
|
21
|
+
- table_name
|
|
22
|
+
- index_name
|
|
23
|
+
- idx_scan
|
|
24
|
+
- idx_tup_read
|
|
25
|
+
- idx_tup_fetch
|
|
26
|
+
- read_to_fetch_ratio
|
|
27
|
+
- index_size_mb
|
|
28
|
+
- index_definition
|
|
29
|
+
|
|
30
|
+
parameters:
|
|
31
|
+
limit:
|
|
32
|
+
type: integer
|
|
33
|
+
default: 50
|
|
34
|
+
description: "Maximum number of results"
|
|
35
|
+
|
|
36
|
+
filters:
|
|
37
|
+
- field: read_to_fetch_ratio
|
|
38
|
+
operator: gte
|
|
39
|
+
value:
|
|
40
|
+
source: config
|
|
41
|
+
key: inefficient_index_threshold_ratio
|
|
42
|
+
cast: float
|
|
43
|
+
|
|
44
|
+
problem_explanations:
|
|
45
|
+
read_to_fetch_ratio: inefficient_index
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Temp file heavy queries
|
|
2
|
+
# Queries that spill to disk due to insufficient work_mem
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: temp_file_queries
|
|
6
|
+
module: queries
|
|
7
|
+
description: "Queries spilling data to temporary files on disk"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: queries
|
|
11
|
+
file: temp_file_queries
|
|
12
|
+
params:
|
|
13
|
+
max_query_length:
|
|
14
|
+
source: config
|
|
15
|
+
key: max_query_length
|
|
16
|
+
|
|
17
|
+
title: "Temp File Queries"
|
|
18
|
+
|
|
19
|
+
columns:
|
|
20
|
+
- query
|
|
21
|
+
- calls
|
|
22
|
+
- temp_mb_written
|
|
23
|
+
- temp_mb_read
|
|
24
|
+
- total_time_sec
|
|
25
|
+
- mean_time_ms
|
|
26
|
+
- rows
|
|
27
|
+
|
|
28
|
+
parameters:
|
|
29
|
+
limit:
|
|
30
|
+
type: integer
|
|
31
|
+
default: 20
|
|
32
|
+
description: "Maximum number of results"
|
|
33
|
+
|
|
34
|
+
enrichment:
|
|
35
|
+
module: queries
|
|
36
|
+
hook: enrich_with_annotations
|
|
37
|
+
|
|
38
|
+
problem_explanations:
|
|
39
|
+
temp_mb_written: temp_file_heavy
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Transaction ID wraparound risk
|
|
2
|
+
# Monitors proximity to the 2-billion XID limit that triggers emergency shutdown
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: wraparound_risk
|
|
6
|
+
module: system
|
|
7
|
+
description: "Transaction ID wraparound risk for all databases"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: system
|
|
11
|
+
file: wraparound_risk
|
|
12
|
+
|
|
13
|
+
title: "Transaction ID Wraparound Risk"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- database_name
|
|
17
|
+
- xid_age
|
|
18
|
+
- pct_towards_wraparound
|
|
19
|
+
- remaining_xids
|
|
20
|
+
- freeze_max_age
|
|
21
|
+
- status
|
|
22
|
+
- database_size
|
|
23
|
+
|
|
24
|
+
parameters:
|
|
25
|
+
limit:
|
|
26
|
+
type: integer
|
|
27
|
+
default: 50
|
|
28
|
+
description: "Maximum number of results"
|
|
29
|
+
|
|
30
|
+
problem_explanations:
|
|
31
|
+
pct_towards_wraparound: wraparound_risk
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Tables without primary keys
|
|
2
|
+
# Missing PKs break logical replication and make row identification unreliable
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: tables_without_pk
|
|
6
|
+
module: tables
|
|
7
|
+
description: "Tables missing a primary key"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: tables
|
|
11
|
+
file: tables_without_pk
|
|
12
|
+
|
|
13
|
+
title: "Tables Without Primary Keys"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- schema
|
|
17
|
+
- table_name
|
|
18
|
+
- estimated_rows
|
|
19
|
+
- table_size_mb
|
|
20
|
+
|
|
21
|
+
parameters:
|
|
22
|
+
limit:
|
|
23
|
+
type: integer
|
|
24
|
+
default: 50
|
|
25
|
+
description: "Maximum number of results"
|
|
26
|
+
|
|
27
|
+
problem_explanations:
|
|
28
|
+
table_name: missing_pk
|
data/lib/pg_reports/engine.rb
CHANGED
|
@@ -13,6 +13,12 @@ module PgReports
|
|
|
13
13
|
config.i18n.load_path += Dir[root.join("config", "locales", "*.yml")]
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
initializer "pg_reports.compatibility_check" do
|
|
17
|
+
ActiveSupport.on_load(:active_record) do
|
|
18
|
+
PgReports::Compatibility.check_postgresql!
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
initializer "pg_reports.assets" do |_app|
|
|
17
23
|
# Assets are inline in views, no precompilation needed
|
|
18
24
|
end
|
|
@@ -12,6 +12,9 @@ module PgReports
|
|
|
12
12
|
# - duplicate_indexes
|
|
13
13
|
# - invalid_indexes
|
|
14
14
|
# - missing_indexes(limit: 20)
|
|
15
|
+
# - inefficient_indexes(limit: 50)
|
|
16
|
+
# - fk_without_indexes(limit: 50)
|
|
17
|
+
# - index_correlation(limit: 50)
|
|
15
18
|
# - index_usage(limit: 50)
|
|
16
19
|
# - bloated_indexes(limit: 20)
|
|
17
20
|
# - index_sizes(limit: 50)
|
|
@@ -12,7 +12,11 @@ module PgReports
|
|
|
12
12
|
# - settings
|
|
13
13
|
# - extensions
|
|
14
14
|
# - activity_overview
|
|
15
|
+
# - wraparound_risk(limit: 50)
|
|
15
16
|
# - cache_stats
|
|
17
|
+
#
|
|
18
|
+
# Manually implemented (version-dependent SQL):
|
|
19
|
+
# - checkpoint_stats(limit: 10)
|
|
16
20
|
|
|
17
21
|
# pg_stat_statements availability check
|
|
18
22
|
# @return [Boolean] Whether pg_stat_statements is available
|
|
@@ -132,6 +136,22 @@ module PgReports
|
|
|
132
136
|
end
|
|
133
137
|
end
|
|
134
138
|
|
|
139
|
+
# Checkpoint stats — uses version-specific SQL because PostgreSQL 17+
|
|
140
|
+
# moved checkpoint columns from pg_stat_bgwriter to pg_stat_checkpointer
|
|
141
|
+
def checkpoint_stats(limit: 10)
|
|
142
|
+
sql_file = pg_version >= 170_000 ? :checkpoint_stats : :checkpoint_stats_legacy
|
|
143
|
+
data = executor.execute_from_file(:system, sql_file)
|
|
144
|
+
data = data.first(limit) if limit
|
|
145
|
+
|
|
146
|
+
Report.new(
|
|
147
|
+
title: "Checkpoint Statistics",
|
|
148
|
+
data: data,
|
|
149
|
+
columns: %w[checkpoints_timed checkpoints_requested checkpoint_write_time_sec
|
|
150
|
+
checkpoint_sync_time_sec buffers_checkpoint buffers_clean
|
|
151
|
+
bgwriter_stops buffers_alloc requested_pct stats_reset]
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
135
155
|
# Get list of all databases
|
|
136
156
|
# @return [Array<Hash>] List of databases with sizes
|
|
137
157
|
def databases_list
|
|
@@ -152,6 +172,13 @@ module PgReports
|
|
|
152
172
|
|
|
153
173
|
private
|
|
154
174
|
|
|
175
|
+
def pg_version
|
|
176
|
+
@pg_version ||= begin
|
|
177
|
+
result = executor.execute("SELECT current_setting('server_version_num')::int AS v")
|
|
178
|
+
result.first&.fetch("v", 0).to_i
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
155
182
|
def executor
|
|
156
183
|
@executor ||= Executor.new
|
|
157
184
|
end
|