pg_reports 0.5.4 → 0.6.1
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 +69 -0
- data/README.md +123 -370
- data/app/controllers/pg_reports/dashboard_controller.rb +21 -21
- data/app/views/layouts/pg_reports/application.html.erb +135 -69
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +22 -22
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +105 -55
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +49 -11
- data/app/views/pg_reports/dashboard/index.html.erb +123 -114
- data/app/views/pg_reports/dashboard/show.html.erb +30 -26
- data/config/locales/en.yml +597 -0
- data/config/locales/ru.yml +562 -0
- data/config/locales/uk.yml +607 -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 +112 -5
- 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/schema_analysis/always_null_columns.yml +31 -0
- data/lib/pg_reports/definitions/schema_analysis/unused_columns.yml +32 -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/definitions/tables/unused_tables.yml +30 -0
- data/lib/pg_reports/definitions/tables/update_hotspots.yml +32 -0
- data/lib/pg_reports/engine.rb +6 -0
- data/lib/pg_reports/module_generator.rb +2 -1
- data/lib/pg_reports/modules/indexes.rb +3 -0
- data/lib/pg_reports/modules/queries.rb +1 -0
- data/lib/pg_reports/modules/schema_analysis.rb +261 -2
- 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 +64 -36
- data/lib/pg_reports/report_definition.rb +20 -24
- 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/schema_analysis/always_null_columns.sql +25 -0
- data/lib/pg_reports/sql/schema_analysis/unused_columns.sql +36 -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/sql/tables/unused_tables.sql +19 -0
- data/lib/pg_reports/sql/tables/update_hotspots.sql +26 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +5 -0
- metadata +24 -1
|
@@ -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,11 +179,56 @@ 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: {
|
|
165
193
|
thresholds: {},
|
|
166
194
|
problem_fields: []
|
|
195
|
+
},
|
|
196
|
+
unused_columns: {
|
|
197
|
+
thresholds: {},
|
|
198
|
+
problem_fields: ["column_name"]
|
|
199
|
+
},
|
|
200
|
+
always_null_columns: {
|
|
201
|
+
thresholds: {null_pct: {warning: 99, critical: 100}},
|
|
202
|
+
problem_fields: ["column_name", "null_pct"]
|
|
203
|
+
},
|
|
204
|
+
polymorphic_without_index: {
|
|
205
|
+
thresholds: {},
|
|
206
|
+
problem_fields: ["coverage"]
|
|
207
|
+
},
|
|
208
|
+
counter_cache_issues: {
|
|
209
|
+
thresholds: {},
|
|
210
|
+
problem_fields: ["issue", "expected_column"]
|
|
211
|
+
},
|
|
212
|
+
soft_delete_without_scope: {
|
|
213
|
+
thresholds: {},
|
|
214
|
+
problem_fields: ["status", "soft_delete_column"]
|
|
215
|
+
},
|
|
216
|
+
orphan_tables: {
|
|
217
|
+
thresholds: {},
|
|
218
|
+
problem_fields: ["classification"]
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
# === TABLES (extra) ===
|
|
222
|
+
update_hotspots: {
|
|
223
|
+
thresholds: {
|
|
224
|
+
updates_per_row: {warning: 10, critical: 100},
|
|
225
|
+
hot_update_pct: {warning: 50, critical: 20, inverted: true}
|
|
226
|
+
},
|
|
227
|
+
problem_fields: ["updates_per_row", "hot_update_pct"]
|
|
228
|
+
},
|
|
229
|
+
unused_tables: {
|
|
230
|
+
thresholds: {total_size_mb: {warning: 10, critical: 100}},
|
|
231
|
+
problem_fields: ["table_name"]
|
|
167
232
|
}
|
|
168
233
|
}.freeze
|
|
169
234
|
|
|
@@ -178,6 +243,7 @@ module PgReports
|
|
|
178
243
|
expensive_queries: {name: "Expensive Queries", description: "Queries consuming most total time"},
|
|
179
244
|
missing_index_queries: {name: "Missing Index Queries", description: "Queries potentially missing indexes"},
|
|
180
245
|
low_cache_hit_queries: {name: "Low Cache Hit", description: "Queries with poor cache utilization"},
|
|
246
|
+
temp_file_queries: {name: "Temp File Queries", description: "Queries spilling to disk"},
|
|
181
247
|
all_queries: {name: "All Queries", description: "All query statistics"}
|
|
182
248
|
}
|
|
183
249
|
},
|
|
@@ -190,8 +256,11 @@ module PgReports
|
|
|
190
256
|
duplicate_indexes: {name: "Duplicate Indexes", description: "Redundant indexes"},
|
|
191
257
|
invalid_indexes: {name: "Invalid Indexes", description: "Indexes that failed to build"},
|
|
192
258
|
missing_indexes: {name: "Missing Indexes", description: "Tables potentially missing indexes"},
|
|
259
|
+
inefficient_indexes: {name: "Inefficient Indexes", description: "Indexes with high read-to-fetch ratio"},
|
|
193
260
|
index_usage: {name: "Index Usage", description: "Index scan statistics"},
|
|
194
261
|
bloated_indexes: {name: "Bloated Indexes", description: "Indexes with high bloat"},
|
|
262
|
+
fk_without_indexes: {name: "FK Without Indexes", description: "Foreign keys missing indexes"},
|
|
263
|
+
index_correlation: {name: "Index Correlation", description: "Low physical correlation indexes"},
|
|
195
264
|
index_sizes: {name: "Index Sizes", description: "Index disk usage"}
|
|
196
265
|
}
|
|
197
266
|
},
|
|
@@ -206,7 +275,10 @@ module PgReports
|
|
|
206
275
|
row_counts: {name: "Row Counts", description: "Table row counts"},
|
|
207
276
|
cache_hit_ratios: {name: "Cache Hit Ratios", description: "Table cache statistics"},
|
|
208
277
|
seq_scans: {name: "Sequential Scans", description: "Tables with high sequential scans"},
|
|
209
|
-
|
|
278
|
+
tables_without_pk: {name: "No Primary Key", description: "Tables missing primary keys"},
|
|
279
|
+
recently_modified: {name: "Recently Modified", description: "Tables with recent activity"},
|
|
280
|
+
update_hotspots: {name: "Update Hotspots", description: "Same rows or indexed columns updated repeatedly", new: true},
|
|
281
|
+
unused_tables: {name: "Unused Tables", description: "Tables never queried since the last stats reset", new: true}
|
|
210
282
|
}
|
|
211
283
|
},
|
|
212
284
|
connections: {
|
|
@@ -235,6 +307,8 @@ module PgReports
|
|
|
235
307
|
settings: {name: "Settings", description: "PostgreSQL configuration"},
|
|
236
308
|
extensions: {name: "Extensions", description: "Installed extensions"},
|
|
237
309
|
activity_overview: {name: "Activity Overview", description: "Current activity summary"},
|
|
310
|
+
wraparound_risk: {name: "Wraparound Risk", description: "Transaction ID wraparound proximity"},
|
|
311
|
+
checkpoint_stats: {name: "Checkpoint Stats", description: "Checkpoint and bgwriter statistics"},
|
|
238
312
|
cache_stats: {name: "Cache Stats", description: "Database cache statistics"}
|
|
239
313
|
}
|
|
240
314
|
},
|
|
@@ -243,21 +317,53 @@ module PgReports
|
|
|
243
317
|
icon: "🔍",
|
|
244
318
|
color: "#06b6d4",
|
|
245
319
|
reports: {
|
|
246
|
-
missing_validations: {name: "Missing Validations", description: "Unique indexes without model validations"}
|
|
320
|
+
missing_validations: {name: "Missing Validations", description: "Unique indexes without model validations"},
|
|
321
|
+
unused_columns: {name: "Unused Columns", description: "Columns that have only ever held a single value", new: true},
|
|
322
|
+
always_null_columns: {name: "Always-NULL Columns", description: "Nullable columns that contain only NULL", new: true},
|
|
323
|
+
polymorphic_without_index: {name: "Polymorphic Without Index", description: "Polymorphic associations missing composite index", new: true},
|
|
324
|
+
counter_cache_issues: {name: "Counter Cache Issues", description: "counter_cache declarations whose target column is missing", new: true},
|
|
325
|
+
soft_delete_without_scope: {name: "Soft Delete Without Scope", description: "Soft-delete columns with no model scope filtering them", new: true},
|
|
326
|
+
orphan_tables: {name: "Orphan Tables", description: "DB tables without a corresponding Rails model", new: true}
|
|
247
327
|
}
|
|
248
328
|
}
|
|
249
329
|
}.freeze
|
|
250
330
|
|
|
251
331
|
def self.all
|
|
252
|
-
REPORTS
|
|
332
|
+
REPORTS.each_with_object({}) do |(cat_key, cat), result|
|
|
333
|
+
result[cat_key] = localized_category(cat_key, cat)
|
|
334
|
+
end
|
|
253
335
|
end
|
|
254
336
|
|
|
255
337
|
def self.find(category, report)
|
|
256
|
-
REPORTS.dig(category.to_sym, :reports, report.to_sym)
|
|
338
|
+
rep = REPORTS.dig(category.to_sym, :reports, report.to_sym)
|
|
339
|
+
return nil unless rep
|
|
340
|
+
|
|
341
|
+
localized_report(report.to_sym, rep)
|
|
257
342
|
end
|
|
258
343
|
|
|
259
344
|
def self.category(category)
|
|
260
|
-
REPORTS[category.to_sym]
|
|
345
|
+
cat = REPORTS[category.to_sym]
|
|
346
|
+
return nil unless cat
|
|
347
|
+
|
|
348
|
+
localized_category(category.to_sym, cat)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Build category hash with localized name and report names
|
|
352
|
+
def self.localized_category(cat_key, cat)
|
|
353
|
+
cat.merge(
|
|
354
|
+
name: I18n.t("pg_reports.categories.#{cat_key}", default: cat[:name]),
|
|
355
|
+
reports: cat[:reports].each_with_object({}) do |(rep_key, rep), reports|
|
|
356
|
+
reports[rep_key] = localized_report(rep_key, rep)
|
|
357
|
+
end
|
|
358
|
+
)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Build report hash with localized name and description
|
|
362
|
+
def self.localized_report(rep_key, rep)
|
|
363
|
+
rep.merge(
|
|
364
|
+
name: I18n.t("pg_reports.reports.#{rep_key}.name", default: rep[:name]),
|
|
365
|
+
description: I18n.t("pg_reports.reports.#{rep_key}.description", default: rep[:description])
|
|
366
|
+
)
|
|
261
367
|
end
|
|
262
368
|
|
|
263
369
|
# Returns full documentation for a report including I18n translations
|
|
@@ -272,6 +378,7 @@ module PgReports
|
|
|
272
378
|
what: I18n.t("#{i18n_key}.what", default: ""),
|
|
273
379
|
how: I18n.t("#{i18n_key}.how", default: ""),
|
|
274
380
|
nuances: I18n.t("#{i18n_key}.nuances", default: []),
|
|
381
|
+
ai_prompt: I18n.t("#{i18n_key}.ai_prompt", default: nil),
|
|
275
382
|
thresholds: config[:thresholds],
|
|
276
383
|
problem_fields: config[:problem_fields]
|
|
277
384
|
}
|
|
@@ -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
|
+
# Columns where 100% of rows are NULL
|
|
2
|
+
# Strong indicator the application no longer populates these fields
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: always_null_columns
|
|
6
|
+
module: schema_analysis
|
|
7
|
+
description: "Nullable columns that contain only NULL values"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: schema_analysis
|
|
11
|
+
file: always_null_columns
|
|
12
|
+
|
|
13
|
+
title: "Always-NULL Columns"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- schema
|
|
17
|
+
- table_name
|
|
18
|
+
- column_name
|
|
19
|
+
- data_type
|
|
20
|
+
- null_pct
|
|
21
|
+
- column_default
|
|
22
|
+
- estimated_rows
|
|
23
|
+
|
|
24
|
+
parameters:
|
|
25
|
+
limit:
|
|
26
|
+
type: integer
|
|
27
|
+
default: 100
|
|
28
|
+
description: "Maximum number of results"
|
|
29
|
+
|
|
30
|
+
problem_explanations:
|
|
31
|
+
column_name: always_null_column
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Columns whose value has never changed since table creation
|
|
2
|
+
# Detects dead fields that the application code no longer touches
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: unused_columns
|
|
6
|
+
module: schema_analysis
|
|
7
|
+
description: "Columns that appear to have never been updated (still hold a single value)"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: schema_analysis
|
|
11
|
+
file: unused_columns
|
|
12
|
+
|
|
13
|
+
title: "Unused Columns"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- schema
|
|
17
|
+
- table_name
|
|
18
|
+
- column_name
|
|
19
|
+
- data_type
|
|
20
|
+
- sole_value
|
|
21
|
+
- column_default
|
|
22
|
+
- null_pct
|
|
23
|
+
- estimated_rows
|
|
24
|
+
|
|
25
|
+
parameters:
|
|
26
|
+
limit:
|
|
27
|
+
type: integer
|
|
28
|
+
default: 100
|
|
29
|
+
description: "Maximum number of results"
|
|
30
|
+
|
|
31
|
+
problem_explanations:
|
|
32
|
+
column_name: unused_column
|
|
@@ -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
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Tables never read since the last stats reset
|
|
2
|
+
# Zero seq_scan and zero idx_scan -- code does not query them
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: unused_tables
|
|
6
|
+
module: tables
|
|
7
|
+
description: "Tables with zero read activity since the last statistics reset"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: tables
|
|
11
|
+
file: unused_tables
|
|
12
|
+
|
|
13
|
+
title: "Unused Tables"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- schema
|
|
17
|
+
- table_name
|
|
18
|
+
- live_rows
|
|
19
|
+
- total_size_mb
|
|
20
|
+
- last_analyzed
|
|
21
|
+
- db_stats_since
|
|
22
|
+
|
|
23
|
+
parameters:
|
|
24
|
+
limit:
|
|
25
|
+
type: integer
|
|
26
|
+
default: 50
|
|
27
|
+
description: "Maximum number of results"
|
|
28
|
+
|
|
29
|
+
problem_explanations:
|
|
30
|
+
table_name: unused_table
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Tables with disproportionately heavy update activity
|
|
2
|
+
# High updates_per_row signals same rows updated repeatedly; low hot_update_pct signals indexed columns are being updated
|
|
3
|
+
|
|
4
|
+
report:
|
|
5
|
+
name: update_hotspots
|
|
6
|
+
module: tables
|
|
7
|
+
description: "Tables with high update-to-row ratio or poor HOT update efficiency"
|
|
8
|
+
|
|
9
|
+
sql:
|
|
10
|
+
category: tables
|
|
11
|
+
file: update_hotspots
|
|
12
|
+
|
|
13
|
+
title: "Update Hotspots"
|
|
14
|
+
|
|
15
|
+
columns:
|
|
16
|
+
- schema
|
|
17
|
+
- table_name
|
|
18
|
+
- live_rows
|
|
19
|
+
- updates
|
|
20
|
+
- updates_per_row
|
|
21
|
+
- hot_update_pct
|
|
22
|
+
- dead_rows
|
|
23
|
+
|
|
24
|
+
parameters:
|
|
25
|
+
limit:
|
|
26
|
+
type: integer
|
|
27
|
+
default: 30
|
|
28
|
+
description: "Maximum number of results"
|
|
29
|
+
|
|
30
|
+
problem_explanations:
|
|
31
|
+
updates_per_row: hot_rows
|
|
32
|
+
hot_update_pct: low_hot_update
|
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
|
|
@@ -17,7 +17,8 @@ module PgReports
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def self.get_module(module_name)
|
|
20
|
-
|
|
20
|
+
const_name = module_name.to_s.split("_").map(&:capitalize).join
|
|
21
|
+
PgReports::Modules.const_get(const_name)
|
|
21
22
|
rescue NameError
|
|
22
23
|
# Module doesn't exist, skip it
|
|
23
24
|
# We don't auto-create modules to avoid conflicts
|
|
@@ -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)
|