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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +12 -4
  4. data/app/controllers/pg_reports/dashboard_controller.rb +6 -2
  5. data/app/views/layouts/pg_reports/application.html.erb +70 -61
  6. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +53 -1
  7. data/app/views/pg_reports/dashboard/_show_styles.html.erb +31 -11
  8. data/app/views/pg_reports/dashboard/index.html.erb +80 -9
  9. data/app/views/pg_reports/dashboard/show.html.erb +6 -2
  10. data/config/locales/en.yml +109 -0
  11. data/config/locales/ru.yml +81 -0
  12. data/config/locales/uk.yml +126 -0
  13. data/lib/pg_reports/compatibility.rb +63 -0
  14. data/lib/pg_reports/configuration.rb +2 -0
  15. data/lib/pg_reports/dashboard/reports_registry.rb +36 -0
  16. data/lib/pg_reports/definitions/indexes/fk_without_indexes.yml +30 -0
  17. data/lib/pg_reports/definitions/indexes/index_correlation.yml +31 -0
  18. data/lib/pg_reports/definitions/indexes/inefficient_indexes.yml +45 -0
  19. data/lib/pg_reports/definitions/queries/temp_file_queries.yml +39 -0
  20. data/lib/pg_reports/definitions/system/wraparound_risk.yml +31 -0
  21. data/lib/pg_reports/definitions/tables/tables_without_pk.yml +28 -0
  22. data/lib/pg_reports/engine.rb +6 -0
  23. data/lib/pg_reports/modules/indexes.rb +3 -0
  24. data/lib/pg_reports/modules/queries.rb +1 -0
  25. data/lib/pg_reports/modules/system.rb +27 -0
  26. data/lib/pg_reports/modules/tables.rb +1 -0
  27. data/lib/pg_reports/query_monitor.rb +139 -42
  28. data/lib/pg_reports/sql/indexes/fk_without_indexes.sql +23 -0
  29. data/lib/pg_reports/sql/indexes/index_correlation.sql +27 -0
  30. data/lib/pg_reports/sql/indexes/inefficient_indexes.sql +22 -0
  31. data/lib/pg_reports/sql/queries/temp_file_queries.sql +16 -0
  32. data/lib/pg_reports/sql/system/checkpoint_stats.sql +20 -0
  33. data/lib/pg_reports/sql/system/checkpoint_stats_legacy.sql +19 -0
  34. data/lib/pg_reports/sql/system/wraparound_risk.sql +21 -0
  35. data/lib/pg_reports/sql/tables/tables_without_pk.sql +20 -0
  36. data/lib/pg_reports/version.rb +1 -1
  37. data/lib/pg_reports.rb +5 -0
  38. metadata +16 -1
@@ -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
@@ -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)
@@ -13,6 +13,7 @@ module PgReports
13
13
  # - expensive_queries(limit: 20)
14
14
  # - missing_index_queries(limit: 20)
15
15
  # - low_cache_hit_queries(limit: 20, min_calls: 100)
16
+ # - temp_file_queries(limit: 20)
16
17
  # - all_queries(limit: 50)
17
18
 
18
19
  # Reset pg_stat_statements statistics
@@ -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