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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/README.md +123 -370
  4. data/app/controllers/pg_reports/dashboard_controller.rb +21 -21
  5. data/app/views/layouts/pg_reports/application.html.erb +135 -69
  6. data/app/views/pg_reports/dashboard/_show_modals.html.erb +22 -22
  7. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +105 -55
  8. data/app/views/pg_reports/dashboard/_show_styles.html.erb +49 -11
  9. data/app/views/pg_reports/dashboard/index.html.erb +123 -114
  10. data/app/views/pg_reports/dashboard/show.html.erb +30 -26
  11. data/config/locales/en.yml +597 -0
  12. data/config/locales/ru.yml +562 -0
  13. data/config/locales/uk.yml +607 -0
  14. data/lib/pg_reports/compatibility.rb +63 -0
  15. data/lib/pg_reports/configuration.rb +2 -0
  16. data/lib/pg_reports/dashboard/reports_registry.rb +112 -5
  17. data/lib/pg_reports/definitions/indexes/fk_without_indexes.yml +30 -0
  18. data/lib/pg_reports/definitions/indexes/index_correlation.yml +31 -0
  19. data/lib/pg_reports/definitions/indexes/inefficient_indexes.yml +45 -0
  20. data/lib/pg_reports/definitions/queries/temp_file_queries.yml +39 -0
  21. data/lib/pg_reports/definitions/schema_analysis/always_null_columns.yml +31 -0
  22. data/lib/pg_reports/definitions/schema_analysis/unused_columns.yml +32 -0
  23. data/lib/pg_reports/definitions/system/wraparound_risk.yml +31 -0
  24. data/lib/pg_reports/definitions/tables/tables_without_pk.yml +28 -0
  25. data/lib/pg_reports/definitions/tables/unused_tables.yml +30 -0
  26. data/lib/pg_reports/definitions/tables/update_hotspots.yml +32 -0
  27. data/lib/pg_reports/engine.rb +6 -0
  28. data/lib/pg_reports/module_generator.rb +2 -1
  29. data/lib/pg_reports/modules/indexes.rb +3 -0
  30. data/lib/pg_reports/modules/queries.rb +1 -0
  31. data/lib/pg_reports/modules/schema_analysis.rb +261 -2
  32. data/lib/pg_reports/modules/system.rb +27 -0
  33. data/lib/pg_reports/modules/tables.rb +1 -0
  34. data/lib/pg_reports/query_monitor.rb +64 -36
  35. data/lib/pg_reports/report_definition.rb +20 -24
  36. data/lib/pg_reports/sql/indexes/fk_without_indexes.sql +23 -0
  37. data/lib/pg_reports/sql/indexes/index_correlation.sql +27 -0
  38. data/lib/pg_reports/sql/indexes/inefficient_indexes.sql +22 -0
  39. data/lib/pg_reports/sql/queries/temp_file_queries.sql +16 -0
  40. data/lib/pg_reports/sql/schema_analysis/always_null_columns.sql +25 -0
  41. data/lib/pg_reports/sql/schema_analysis/unused_columns.sql +36 -0
  42. data/lib/pg_reports/sql/system/checkpoint_stats.sql +20 -0
  43. data/lib/pg_reports/sql/system/checkpoint_stats_legacy.sql +19 -0
  44. data/lib/pg_reports/sql/system/wraparound_risk.sql +21 -0
  45. data/lib/pg_reports/sql/tables/tables_without_pk.sql +20 -0
  46. data/lib/pg_reports/sql/tables/unused_tables.sql +19 -0
  47. data/lib/pg_reports/sql/tables/update_hotspots.sql +26 -0
  48. data/lib/pg_reports/version.rb +1 -1
  49. data/lib/pg_reports.rb +5 -0
  50. metadata +24 -1
@@ -51,6 +51,16 @@ ru:
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 @@ ru:
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 @@ ru:
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 @@ ru:
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."
@@ -192,6 +244,29 @@ ru:
192
244
  - "Высокая активность UPDATE создаёт dead tuples — следите за vacuum."
193
245
  - "HOT updates (Heap Only Tuple) эффективнее обычных — индексы не обновляются."
194
246
 
247
+ update_hotspots:
248
+ title: "Горячие точки UPDATE"
249
+ what: "Таблицы, где одни и те же строки обновляются многократно, либо где индексированные колонки получают hot-update."
250
+ how: "Считает два соотношения из pg_stat_user_tables: updates_per_row = n_tup_upd / n_live_tup (амплификация записи на строку) и hot_update_pct = n_tup_hot_upd / n_tup_upd (низкое значение = индексированная колонка попадает в SET, что ломает HOT)."
251
+ nuances:
252
+ - "updates_per_row >> 1 означает, что отдельные строки переписываются многократно — типично для счётчиков, статусов, last_seen_at, позиций."
253
+ - "Паттерны рефакторинга для горячих строк: вынести изменчивую колонку в соседнюю 1:1 'state'-таблицу; перейти на event/log-таблицу с периодическими свёртками; буферизировать записи в фоне; debounce на стороне приложения."
254
+ - "Низкий hot_update_pct (например, <50%) = обновляется индексированная колонка. Либо удалите индекс, либо вынесите колонку в менее индексированную таблицу."
255
+ - "PostgreSQL не считает UPDATE по колонкам. Чтобы найти виновную колонку, парсите pg_stat_statements: имена колонок в SET сохраняются (нормализуются только литералы)."
256
+ - "Горячие строки + fillfactor по умолчанию (100) убивают HOT. ALTER TABLE ... SET (fillfactor=80) оставляет место для in-place UPDATE."
257
+ - "Высокий n_dead_tup коррелирует с горячими строками — autovacuum должен успевать, иначе bloat будет расти быстро."
258
+
259
+ unused_tables:
260
+ title: "Неиспользуемые таблицы"
261
+ what: "Таблицы с нулём seq_scan и нулём idx_scan с момента последнего сброса статистики — код их не читает."
262
+ how: "Фильтрует pg_stat_user_tables по seq_scan = 0 AND idx_scan = 0. Показывает db_stats_since (pg_stat_database.stats_reset), чтобы было понятно, на каком окне сделан вывод."
263
+ nuances:
264
+ - "Убедитесь, что db_stats_since покрывает репрезентативный период — минимум неделя, лучше полный отчётный/биллинговый цикл. Недавний stats_reset, рестарт или обновление PG обнуляют выводы."
265
+ - "Таблицы, читаемые только репликами, НЕ считаются на праймари. Проверьте реплики отдельно перед удалением."
266
+ - "Таблицы, к которым обращаются только COPY, pg_dump или logical replication, могут выглядеть unused — эти операции не инкрементят счётчики сканов."
267
+ - "FK на эту таблицу могут держать её 'в работе' через каскады без прямых запросов."
268
+ - "Безопасная последовательность: переименовать → подождать полный цикл → удалить. Или переместить в отдельную archive-схему."
269
+
195
270
  # === CONNECTIONS ===
196
271
  active_connections:
197
272
  title: "Active Connections"
@@ -326,6 +401,28 @@ ru:
326
401
  - "Полезно для быстрой оценки состояния базы."
327
402
  - "Сравнивайте с baseline для выявления аномалий."
328
403
 
404
+ wraparound_risk:
405
+ title: "Wraparound Risk"
406
+ what: "Близость возраста Transaction ID к лимиту wraparound в 2 миллиарда."
407
+ how: "Читает age(datfrozenxid) из pg_database. Когда значение приближается к 2^31 (~2.1 млрд), PostgreSQL остановится для предотвращения повреждения данных."
408
+ nuances:
409
+ - "autovacuum_freeze_max_age (по умолчанию 200M) автоматически запускает агрессивный anti-wraparound VACUUM."
410
+ - "Если age превышает freeze_max_age, anti-wraparound VACUUM уже должен работать — если нет, расследуйте причину."
411
+ - "Долгоживущие транзакции не дают VACUUM продвинуть frozen XID — мониторьте idle-in-transaction."
412
+ - "В экстренных случаях запустите VACUUM FREEZE на самых больших/старых таблицах первыми."
413
+ - "Pct > 50% — предупреждение; > 75% — критично, требует немедленных действий."
414
+
415
+ checkpoint_stats:
416
+ title: "Checkpoint Stats"
417
+ what: "Частота чекпоинтов и метрики производительности background writer."
418
+ how: "Данные из pg_stat_bgwriter показывают количество чекпоинтов, тайминги и распределение записи буферов."
419
+ nuances:
420
+ - "checkpoints_timed = запланированные чекпоинты (нормально); checkpoints_req = вынужденные чекпоинты (под нагрузкой)."
421
+ - "Высокий requested_pct означает, что WAL заполняется до checkpoint_timeout — увеличьте max_wal_size."
422
+ - "buffers_backend > 0 означает, что бэкенды сами пишут грязные буферы — увеличьте shared_buffers или активность bgwriter."
423
+ - "bgwriter_stops (maxwritten_clean) > 0 означает, что bgwriter достиг лимита за раунд — увеличьте bgwriter_lru_maxpages."
424
+ - "Статистика накапливается с момента stats_reset — сравнивайте за периоды для осмысленного анализа."
425
+
329
426
  cache_stats:
330
427
  title: "Cache Stats"
331
428
  what: "Статистика кэширования базы данных."
@@ -336,6 +433,74 @@ ru:
336
433
  - "Низкий cache hit: увеличьте shared_buffers (до 25% RAM), или проблема в запросах."
337
434
 
338
435
  # === SCHEMA ANALYSIS ===
436
+ unused_columns:
437
+ title: "Неиспользуемые колонки"
438
+ what: "Колонки, которые когда-либо хранили только одно уникальное значение — сильный признак, что приложение их больше не обновляет."
439
+ how: "Читает pg_stats.n_distinct = 1 (одно значение по выборке) и джойнит pg_attrdef для отображения default. Исключает первичные ключи и колонки уникальных индексов, чтобы убрать шум."
440
+ nuances:
441
+ - "n_distinct — оценка по выборке. Сделайте ANALYZE перед тем, как доверять результату. Устаревшая статистика даёт ложные срабатывания."
442
+ - "Таблицы меньше ~1000 строк исключены — на маленькой выборке одно значение статистически бессмысленно."
443
+ - "Ложные срабатывания: feature flags, enum-колонки с одним released-значением, колонки с дефолтом и одним валидным вариантом."
444
+ - "Истинные находки: колонки, у которых удалили Ruby/ORM-аксессор, но миграцию на drop column не написали; статусы, заменённые другим механизмом, но оставленные в схеме."
445
+ - "PostgreSQL не отслеживает запись по колонкам, это эвристика. Перед удалением grep'ните кодовую базу по имени колонки."
446
+ - "Смотрите также `always_null_columns` — тот же диагноз с другого ракурса."
447
+
448
+ always_null_columns:
449
+ title: "Колонки, всегда NULL"
450
+ what: "Nullable-колонки, где 100% строк = NULL — приложение перестало (или никогда не начинало) их писать."
451
+ how: "Читает pg_stats.null_frac >= 0.999 (фактически все NULL) и исключает колонки с NOT NULL. Джойнит pg_attrdef, чтобы показать оставшийся default."
452
+ nuances:
453
+ - "null_frac — оценка по выборке. Сделайте ANALYZE, если сомневаетесь. Перепроверьте после репрезентативного окна нагрузки."
454
+ - "Колонка может быть NULL, потому что путь записи редко срабатывает (premium-only поля, opt-in фичи). Проверяйте перед удалением."
455
+ - "Если null_pct = 100 И есть не-NULL default — default не используется, удаляйте вместе с колонкой."
456
+ - "Некоторые ORM сериализуют пустые строки как '' вместо NULL; этот отчёт их не поймает. Если нужен такой сигнал — отдельный отчёт."
457
+ - "Сравните с `unused_columns` — он ловит колонки с одним значением, этот — все-NULL. Вместе покрывают основные паттерны мёртвых колонок."
458
+
459
+ polymorphic_without_index:
460
+ title: "Polymorphic без индекса"
461
+ what: "Полиморфные `belongs_to`-ассоциации, у которых пара `(*_type, *_id)` не покрыта составным индексом."
462
+ how: "Обходит `ActiveRecord::Base.descendants`, собирает `belongs_to`-рефлексии с `polymorphic: true` и проверяет `pg_index` на наличие индекса, покрывающего обе колонки."
463
+ nuances:
464
+ - "PostgreSQL не сможет использовать два одиночных индекса так же эффективно, как один составной для `WHERE x_type = ? AND x_id = ?` — это базовый паттерн загрузки полиморфной ассоциации."
465
+ - "Подсказка `coverage` уточняет пробел: \"neither indexed\" / \"only id indexed\" / \"only type indexed\" / \"type and id indexed separately\". Последние два — частичные решения, не полные."
466
+ - "На маленьких таблицах безвредно; на больших (Comment, Note, Activity, AuditLog) каждый запрос к ассоциации превращается в seq scan."
467
+ - "Предлагаемая миграция использует порядок `(type, id)` — у type обычно ниже cardinality, индекс сужается быстрее."
468
+ - "Перед обходом моделей выполняется `eager_load!`, чтобы в dev результат был полным."
469
+
470
+ counter_cache_issues:
471
+ title: "Проблемы counter_cache"
472
+ what: "`belongs_to ..., counter_cache: ...` декларации, у которых целевая колонка отсутствует в родительской таблице."
473
+ how: "Обходит все модели, находит `belongs_to` с опцией `counter_cache`, разрешает имя ожидаемой колонки (`<child_table>_count` для `counter_cache: true` или явный символ/строка) и проверяет колонки родителя."
474
+ nuances:
475
+ - "Отсутствующая колонка = counter молча сломан: записи никуда не идут, `parent.<assoc>_count` возвращает nil, любой код, читающий cached-значение, получает stale или ноль."
476
+ - "Обратное направление (колонка `*_count` без декларации `counter_cache`) намеренно не флагается — слишком много false positives для вручную поддерживаемых счётчиков."
477
+ - "Родительский класс резолвится через `assoc.klass`; если класс отсутствует или не загружается — строка пропускается."
478
+ - "Имя по умолчанию для `counter_cache: true`: на родителя добавляется колонка с именем по таблице **дочерней** модели в множественном числе + `_count`. Пример: `Comment belongs_to :user, counter_cache: true` → `users.comments_count`."
479
+ - "Перед обходом моделей выполняется `eager_load!`, чтобы в dev результат был полным."
480
+
481
+ soft_delete_without_scope:
482
+ title: "Soft delete без scope"
483
+ what: "Таблицы с колонкой `deleted_at` / `discarded_at` / `archived_at`, у модели которых нет scope, фильтрующего soft-deleted строки."
484
+ how: "Для каждой таблицы проверяется наличие канонических имён soft-delete колонок. Если есть — находится модель и проверяется `acts_as_paranoid` (paranoia), `discard_column` (discard) или `default_scope`, чей сгенерированный SQL ссылается на колонку."
485
+ nuances:
486
+ - "Без default scope любой обычный `Model.where(...)` возвращает soft-deleted строки. Это тихо протекает в отчёты, индексы, поиск, экспорты."
487
+ - "Если команда сознательно отказалась от default scope (предпочитая явные `kept`/`with_deleted`), отчёт даст по строке на каждую такую модель — это intentional, можно игнорировать."
488
+ - "Метод детекции default_scope: строит `model.all.to_sql` и ищет имя колонки. Ловит `default_scope { where(deleted_at: nil) }` и эквиваленты, но пропускает scope из concerns, которые ещё не загружены."
489
+ - "Таблицы без модели рапортуются отдельно со статусом `no_model` — обычно значит, что таблица используется только raw SQL и требует ручного ревью."
490
+ - "Перед обходом моделей выполняется `eager_load!`, чтобы в dev результат был полным."
491
+
492
+ orphan_tables:
493
+ title: "Orphan-таблицы"
494
+ what: "Таблицы БД без соответствующего Rails-класса модели."
495
+ how: "Перечисляет все таблицы из `pg_class` (за исключением `schema_migrations` и `ar_internal_metadata`) и пытается найти модель по обычным naming conventions. Таблицы, не находящие модель, попадают в отчёт."
496
+ nuances:
497
+ - "Три классификации: `join_table_candidate` (ровно две `*_id` колонки и больше ничего — вероятно, легитимная HABTM-таблица), `join_model_without_class` (несколько FK + дополнительные поля — вероятно, должна была быть моделью join'а), `legacy` (всё остальное)."
498
+ - "`join_table_candidate` обычно intentional — Rails не требует модель для HABTM. Просмотреть и игнорировать."
499
+ - "`legacy` — самые интересные строки: таблицы, созданные до того, как добавили модель; переименованные-но-не-удалённые; созданные мимо Rails другим сервисом; принадлежащие удалённой фиче."
500
+ - "Сверьте с `unused_tables` (категория Таблицы) — таблица одновременно orphan И с нулём чтений = сильный кандидат на удаление."
501
+ - "False positives: namespaced-модели (`Admin::User` → `admin_users`), STI-подклассы на одной базовой таблице, модели в engines, которые не eager-loadятся."
502
+ - "Row count приблизительный (n_live_tup из pg_stat_user_tables) — недавно записанные таблицы могут показывать 0 до следующего ANALYZE."
503
+
339
504
  missing_validations:
340
505
  title: "Отсутствующие валидации"
341
506
  what: "Уникальные индексы в базе данных без соответствующих валидаций uniqueness в Rails-моделях."
@@ -356,6 +521,13 @@ ru:
356
521
  low_cache_hit: "Низкий cache hit ratio. Запрос часто читает с диска вместо кэша."
357
522
  high_seq_scan: "Много sequential scan. Возможно не хватает индекса."
358
523
  unused_index: "Индекс не используется. Кандидат на удаление."
524
+ inefficient_index: "Индекс читает гораздо больше записей, чем извлекает. Вероятно, порядок колонок составного индекса не соответствует предикатам запроса."
525
+ fk_without_index: "FK-колонка без поддерживающего индекса. DELETE/UPDATE родительской таблицы вызовет sequential scan."
526
+ low_correlation: "Низкая физическая корреляция между индексом и порядком строк. Range scan вызовет избыточный random I/O."
527
+ temp_file_heavy: "Запрос сбрасывает данные во временные файлы на диск. Рассмотрите увеличение work_mem или оптимизацию запроса."
528
+ missing_pk: "Таблица без первичного ключа. Это ломает логическую репликацию и может вызвать проблемы с ORM."
529
+ wraparound_risk: "Возраст Transaction ID приближается к лимиту wraparound. Требуется VACUUM FREEZE."
530
+ high_checkpoint_req: "Высокий процент вынужденных чекпоинтов. Рассмотрите увеличение max_wal_size."
359
531
  high_bloat: "Высокий bloat. Требуется REINDEX или VACUUM."
360
532
  many_dead_tuples: "Много мёртвых строк. Требуется VACUUM."
361
533
  long_running: "Долго выполняющийся запрос. Может блокировать другие операции."
@@ -366,3 +538,393 @@ ru:
366
538
  pool_saturation: "Пул соединений насыщен. Риск исчерпания соединений и ошибок приложения."
367
539
  high_connection_churn: "Высокая оборачиваемость соединений. Внедрите connection pooling для снижения накладных расходов."
368
540
  too_many_short_connections: "Слишком много короткоживущих соединений. Приложение должно переиспользовать соединения через pooling."
541
+ unused_column: "Колонка имеет одно значение во всех строках. Скорее всего, не обновлялась с момента создания — кандидат на удаление."
542
+ always_null_column: "Колонка на 100% NULL. Приложение больше не пишет в это поле — кандидат на удаление."
543
+ hot_rows: "Одни и те же строки обновляются многократно. Рассмотрите разделение горячих/холодных колонок, батчинг записи или event-log таблицу."
544
+ low_hot_update: "Низкий процент HOT updates — обновляются индексированные колонки. Удалите индекс или вынесите колонку, чтобы снизить write amplification."
545
+ unused_table: "Таблица не читалась с момента последнего сброса статистики. Кандидат на архивацию или удаление — сначала проверьте окно статистики."
546
+ polymorphic_no_index: "Полиморфная ассоциация без составного индекса (type, id). С ростом таблицы запросы будут seq-scan'ить."
547
+ counter_cache_missing_column: "Колонка counter_cache отсутствует в родительской таблице. Counter молча сломан — записи никуда не идут."
548
+ soft_delete_unprotected: "Soft-delete колонка без scope, фильтрующего её. Обычные запросы возвращают удалённые строки."
549
+ orphan_table_legacy: "Таблица без Rails-модели. Скорее всего, legacy — проверьте перед удалением."
550
+
551
+ # UI strings shown in the dashboard chrome (buttons, modals, toasts, etc.)
552
+ ui:
553
+ branding:
554
+ title: "PgReports"
555
+ subtitle: "Дашборд анализа PostgreSQL"
556
+ page_title: "PgReports — Дашборд"
557
+ navigation:
558
+ dashboard: "Дашборд"
559
+ back: "← Назад"
560
+ actions:
561
+ cancel: "Отмена"
562
+ retry: "Повторить"
563
+ copy: "📋 Копировать"
564
+ copy_query: "📋 Копировать запрос"
565
+ copy_code: "📋 Копировать код"
566
+ copy_to_clipboard_title: "Скопировать в буфер обмена"
567
+ copied_feedback: "✓ Скопировано!"
568
+ clear_all: "Очистить всё"
569
+ run_report: "▶ Запустить отчёт"
570
+ export: "⬇ Экспорт"
571
+ download_text: "📄 Текст (.txt)"
572
+ download_csv: "📊 CSV (.csv)"
573
+ download_json: "📋 JSON (.json)"
574
+ download: "📥 Скачать"
575
+ copy_ai_prompt: "Копировать промпт"
576
+ send_telegram: "📨 Telegram"
577
+ sending: "Отправка..."
578
+ reset_statistics: "🗑️ Сбросить статистику"
579
+ resetting: "Сброс..."
580
+ confirm_reset: "Да, сбросить"
581
+ create_extension: "⚡ Создать расширение"
582
+ creating: "Создание..."
583
+ ide_settings_button_title: "Настройки IDE"
584
+ explain_analyze: "📊 EXPLAIN ANALYZE"
585
+ execute_query: "▶ Выполнить запрос"
586
+ create_migration_file: "📁 Создать файл и открыть в IDE"
587
+ start_monitoring: "▶ Запустить мониторинг"
588
+ stop_monitoring: "⏹ Остановить мониторинг"
589
+ starting: "Запуск..."
590
+ stopping: "Остановка..."
591
+ load_history: "📜 Загрузить историю (50)"
592
+ loading: "Загрузка..."
593
+ running: "Выполнение..."
594
+ save_for_comparison: "📌 Сохранить для сравнения"
595
+ saved_marker: "📌 Сохранено"
596
+ status:
597
+ pg_stat_ready: "pg_stat_statements готов"
598
+ extension_installed: "Расширение установлено, не предзагружено"
599
+ preloaded: "Предзагружено, расширение не создано"
600
+ not_configured: "Не настроено"
601
+ monitoring_unavailable: "Live-мониторинг недоступен"
602
+ modals:
603
+ enable_pg_stat_title: "Включение pg_stat_statements"
604
+ enable_pg_stat_intro: "Чтобы включить pg_stat_statements, выполните следующие шаги:"
605
+ edit_postgresql_conf: "Отредактируйте postgresql.conf:"
606
+ restart_postgresql: "Перезапустите PostgreSQL:"
607
+ create_extension_step: "Создайте расширение:"
608
+ enable_button_note: "Или нажмите кнопку «Создать расширение» после перезапуска."
609
+ reset_stats_title: "⚠️ Сброс статистики"
610
+ reset_stats_confirm: "Вы уверены, что хотите сбросить статистику pg_stat_statements?"
611
+ reset_stats_warning: "Это действие очистит всю собранную статистику запросов и не может быть отменено."
612
+ ide_settings_title: "⚙️ Настройки IDE"
613
+ problem_detected_title: "⚠️ Обнаружена проблема"
614
+ query_analyzer_title: "📊 Анализатор запроса"
615
+ query_label: "Запрос:"
616
+ parameters_label: "Параметры:"
617
+ migration_title: "🗑️ Миграция удаления индекса"
618
+ migration_subtitle: "Сгенерированная миграция для удаления индекса:"
619
+ migration_warning: "Создание миграции сгенерирует файл миграции в вашем проекте. Запуск этой миграции удалит индекс из БД, что может значительно повлиять на производительность приложения."
620
+ migration_warning_dev_only: "Эту операцию следует выполнять только в локальном dev-окружении."
621
+ query_execution_disabled_title: "⚠️ Выполнение запросов отключено"
622
+ query_execution_disabled_intro: "Чтобы включить эту функцию, добавьте в конфигурацию:"
623
+ settings:
624
+ default_ide_label: "IDE по умолчанию для ссылок на исходники:"
625
+ ide_show_menu: "Показывать меню (по умолчанию)"
626
+ ide_vscode_wsl: "VS Code (WSL)"
627
+ ide_vscode: "VS Code"
628
+ ide_rubymine: "RubyMine"
629
+ ide_intellij: "IntelliJ IDEA"
630
+ ide_cursor_wsl: "Cursor (WSL)"
631
+ ide_cursor: "Cursor"
632
+ monitoring:
633
+ live_title: "Live-мониторинг"
634
+ update_interval: "Обновление каждые 5с"
635
+ toggle_title: "Переключить live-мониторинг"
636
+ query_monitor_title: "Монитор SQL-запросов"
637
+ session_label: "Сессия:"
638
+ queries_label: "Запросов:"
639
+ feed_empty: "Нажмите «Запустить мониторинг», чтобы начать захват SQL-запросов"
640
+ feed_no_queries: "Запросы пока не захвачены..."
641
+ unknown_source: "Источник неизвестен"
642
+ expand_collapse_title: "Развернуть / свернуть"
643
+ metrics:
644
+ connections_label: "Соединения"
645
+ tps_label: "TPS"
646
+ tps_unit: "тр/с"
647
+ commit_label: "commit:"
648
+ rollback_label: "rollback:"
649
+ cache_hit_label: "Cache hit"
650
+ cache_hit_detail: "блоки heap из кэша"
651
+ long_queries_label: "Долгие запросы"
652
+ queries_unit: "запросов"
653
+ long_running_threshold: "> 60с выполнения"
654
+ blocked_label: "Заблокировано"
655
+ processes_unit: "процессов"
656
+ waiting_for_locks: "ждут блокировок"
657
+ percent_used_suffix: "% использовано"
658
+ categories:
659
+ requires_pg_stat: "🔒 Требуется pg_stat_statements"
660
+ reports_count_suffix: "отчётов"
661
+ documentation:
662
+ toggle_title: "📖 Что показывает этот отчёт?"
663
+ what_section: "📋 Что"
664
+ why_section: "❓ Почему это важно"
665
+ nuances_section: "⚠️ Нюансы"
666
+ thresholds_section: "📊 Пороги"
667
+ threshold_warning_label: "⚠️ Warning:"
668
+ threshold_critical_label: "🔴 Critical:"
669
+ threshold_inverted_note: "(меньше — хуже)"
670
+ filters:
671
+ title: "🔍 Параметры фильтрации"
672
+ current_value: "сейчас"
673
+ saved:
674
+ title: "📌 Сохранено для сравнения"
675
+ saved_at_prefix: "▸ Сохранено:"
676
+ click_to_expand: "Нажмите, чтобы развернуть"
677
+ confirm_clear_all: "Удалить все сохранённые записи для этого отчёта?"
678
+ remove_title: "Удалить"
679
+ results:
680
+ title: "Результаты"
681
+ click_run_hint: "Нажмите «Запустить отчёт», чтобы получить данные"
682
+ empty_message: "Проблем не найдено. Всё хорошо!"
683
+ showing_first_of_total: "Показаны первые %{count} из %{total} строк"
684
+ no_rows_returned: "Нет строк"
685
+ rows_label: "Строк:"
686
+ execution_time_label: "Время выполнения:"
687
+ null_placeholder: "<null>"
688
+ sections:
689
+ recommendation: "💡 Рекомендация"
690
+ detected_issues: "⚠️ Обнаруженные проблемы"
691
+ execution_plan: "📊 План выполнения"
692
+ line_label: "Строка"
693
+ current_label: "Текущее:"
694
+ threshold_label: "Порог:"
695
+ threshold_inverted_long: "(инверсия: меньшие значения — хуже)"
696
+ warning_eq: "warning"
697
+ critical_eq: "critical"
698
+ levels:
699
+ critical: "🔴 Критично"
700
+ warning: "⚠️ Внимание"
701
+ errors:
702
+ error_prefix: "Ошибка:"
703
+ unable_fetch_metrics: "Не удалось получить статистику БД."
704
+ possible_causes: "Возможные причины:"
705
+ cause_permissions: "Недостаточно прав для доступа к БД"
706
+ cause_views: "Системные представления статистики недоступны"
707
+ cause_connection: "Проблемы с соединением"
708
+ fetch_metrics_failed: "Не удалось получить live-метрики"
709
+ fetch_metrics_check_perms: "Не удалось получить статистику БД. Проверьте права доступа."
710
+ insufficient_database_perms: "Недостаточно прав для доступа к системным представлениям статистики"
711
+ network_error_prefix: "Сетевая ошибка:"
712
+ copy_failed: "Не удалось скопировать"
713
+ run_report_first: "Сначала запустите отчёт"
714
+ run_report_failed: "Не удалось запустить отчёт"
715
+ report_not_found: "Отчёт не найден"
716
+ send_telegram_failed: "Не удалось отправить в Telegram"
717
+ reset_stats_failed: "Не удалось сбросить статистику"
718
+ start_monitoring_failed: "Не удалось запустить мониторинг"
719
+ stop_monitoring_failed: "Не удалось остановить мониторинг"
720
+ load_history_failed: "Не удалось загрузить историю:"
721
+ no_query_history: "История запросов не найдена"
722
+ decode_query_failed: "Не удалось декодировать запрос"
723
+ explain_analyze_failed: "Не удалось выполнить EXPLAIN ANALYZE"
724
+ execute_query_failed: "Не удалось выполнить запрос"
725
+ create_migration_failed: "Не удалось создать миграцию"
726
+ explain_disabled_toast: "⚠️ EXPLAIN ANALYZE отключен. Включите в конфигурации: config.allow_raw_query_execution = true"
727
+ execute_disabled_toast: "⚠️ Выполнение запросов отключено. Включите в конфигурации: config.allow_raw_query_execution = true"
728
+ query_monitoring_error: "Ошибка мониторинга запросов"
729
+ query_hash_required: "Требуется хэш запроса"
730
+ query_execution_disabled: "Выполнение запросов из дашборда отключено. Включите в конфигурации: 'config.allow_raw_query_execution = true'"
731
+ query_not_found_expired: "Запрос не найден или истёк срок действия. Обновите страницу."
732
+ security_violation_prefix: "Нарушение безопасности:"
733
+ trigger_variables_not_allowed: "Нельзя выполнить EXPLAIN ANALYZE для запросов с триггерными переменными (NEW, OLD). Они доступны только в контексте триггерных функций."
734
+ missing_parameter_values: "Укажите значения для всех плейсхолдеров параметров ($1, $2 и т.д.)"
735
+ migration_dev_only: "Создание миграций разрешено только в development-окружении"
736
+ filename_code_required: "Имя файла и код обязательны"
737
+ invalid_filename_format: "Неверный формат имени файла миграции"
738
+ migrations_dir_not_found: "Каталог миграций не найден"
739
+ success:
740
+ statistics_reset: "Статистика успешно сброшена"
741
+ report_generated: "Отчёт успешно сгенерирован"
742
+ ai_prompt_copied: "AI-промпт скопирован в буфер обмена"
743
+ explain_copied: "Вывод EXPLAIN скопирован в буфер обмена"
744
+ migration_copied: "Код миграции скопирован в буфер обмена"
745
+ migration_created: "Миграция успешно создана"
746
+ telegram_sent: "Отчёт отправлен в Telegram"
747
+ queries_loaded: "Загружено %{count} запросов из истории"
748
+ record_saved: "Запись сохранена для сравнения"
749
+ record_removed: "Запись удалена"
750
+ record_removed_saved: "Запись удалена из сохранённых"
751
+ all_saved_cleared: "Все сохранённые записи очищены"
752
+
753
+ # Category names (shown on the dashboard grid)
754
+ categories:
755
+ queries: "Запросы"
756
+ indexes: "Индексы"
757
+ tables: "Таблицы"
758
+ connections: "Соединения"
759
+ system: "Система"
760
+ schema_analysis: "Анализ схемы"
761
+
762
+ # Report names and short descriptions (shown on the dashboard listing)
763
+ reports:
764
+ slow_queries:
765
+ name: "Медленные запросы"
766
+ description: "Запросы с высоким средним временем выполнения"
767
+ heavy_queries:
768
+ name: "Частые запросы"
769
+ description: "Самые часто вызываемые запросы"
770
+ expensive_queries:
771
+ name: "Дорогие запросы"
772
+ description: "Запросы с наибольшим суммарным временем"
773
+ missing_index_queries:
774
+ name: "Без индексов"
775
+ description: "Запросы, которым возможно нужны индексы"
776
+ low_cache_hit_queries:
777
+ name: "Низкий cache hit"
778
+ description: "Запросы с плохой утилизацией кэша"
779
+ temp_file_queries:
780
+ name: "Сброс на диск"
781
+ description: "Запросы, сбрасывающие данные на диск"
782
+ all_queries:
783
+ name: "Все запросы"
784
+ description: "Полная статистика запросов"
785
+ unused_indexes:
786
+ name: "Неиспользуемые индексы"
787
+ description: "Индексы, редко или никогда не сканируемые"
788
+ duplicate_indexes:
789
+ name: "Дубликаты индексов"
790
+ description: "Избыточные индексы"
791
+ invalid_indexes:
792
+ name: "Невалидные индексы"
793
+ description: "Индексы, которые не удалось построить"
794
+ missing_indexes:
795
+ name: "Отсутствующие индексы"
796
+ description: "Таблицы, которым возможно нужны индексы"
797
+ inefficient_indexes:
798
+ name: "Неэффективные индексы"
799
+ description: "Индексы с высоким read-to-fetch ratio"
800
+ index_usage:
801
+ name: "Использование индексов"
802
+ description: "Статистика сканирования индексов"
803
+ bloated_indexes:
804
+ name: "Раздутые индексы"
805
+ description: "Индексы с высоким bloat"
806
+ fk_without_indexes:
807
+ name: "FK без индексов"
808
+ description: "Внешние ключи без поддерживающего индекса"
809
+ index_correlation:
810
+ name: "Корреляция индексов"
811
+ description: "Индексы с низкой физической корреляцией"
812
+ index_sizes:
813
+ name: "Размеры индексов"
814
+ description: "Использование диска индексами"
815
+ table_sizes:
816
+ name: "Размеры таблиц"
817
+ description: "Использование диска таблицами"
818
+ bloated_tables:
819
+ name: "Раздутые таблицы"
820
+ description: "Таблицы с высокой долей dead tuples"
821
+ vacuum_needed:
822
+ name: "Нужен VACUUM"
823
+ description: "Таблицы, которым требуется VACUUM"
824
+ row_counts:
825
+ name: "Количество строк"
826
+ description: "Число строк в таблицах"
827
+ cache_hit_ratios:
828
+ name: "Cache hit таблиц"
829
+ description: "Статистика кэша по таблицам"
830
+ seq_scans:
831
+ name: "Sequential scans"
832
+ description: "Таблицы с большим числом sequential scan"
833
+ tables_without_pk:
834
+ name: "Без первичного ключа"
835
+ description: "Таблицы без первичного ключа"
836
+ recently_modified:
837
+ name: "Недавно изменённые"
838
+ description: "Таблицы с недавней активностью"
839
+ update_hotspots:
840
+ name: "Горячие точки UPDATE"
841
+ description: "Одни и те же строки или индексированные колонки часто обновляются"
842
+ unused_tables:
843
+ name: "Неиспользуемые таблицы"
844
+ description: "Таблицы, не запрашиваемые с момента последнего сброса статистики"
845
+ active_connections:
846
+ name: "Активные соединения"
847
+ description: "Текущие подключения к БД"
848
+ connection_stats:
849
+ name: "Статистика соединений"
850
+ description: "Соединения по состоянию"
851
+ long_running_queries:
852
+ name: "Долгие запросы"
853
+ description: "Запросы, выполняющиеся долго"
854
+ blocking_queries:
855
+ name: "Блокирующие запросы"
856
+ description: "Запросы, блокирующие другие"
857
+ locks:
858
+ name: "Блокировки"
859
+ description: "Текущие блокировки в БД"
860
+ idle_connections:
861
+ name: "Idle соединения"
862
+ description: "Простаивающие соединения"
863
+ pool_usage:
864
+ name: "Использование пула"
865
+ description: "Утилизация пула соединений"
866
+ pool_wait_times:
867
+ name: "Время ожидания"
868
+ description: "Анализ ожидания ресурсов"
869
+ pool_saturation:
870
+ name: "Насыщение пула"
871
+ description: "Предупреждения о здоровье пула"
872
+ connection_churn:
873
+ name: "Churn соединений"
874
+ description: "Анализ жизненного цикла соединений"
875
+ database_sizes:
876
+ name: "Размеры баз данных"
877
+ description: "Размер всех баз"
878
+ settings:
879
+ name: "Настройки"
880
+ description: "Конфигурация PostgreSQL"
881
+ extensions:
882
+ name: "Расширения"
883
+ description: "Установленные расширения"
884
+ activity_overview:
885
+ name: "Обзор активности"
886
+ description: "Сводка текущей активности"
887
+ wraparound_risk:
888
+ name: "Риск wraparound"
889
+ description: "Близость к лимиту Transaction ID"
890
+ checkpoint_stats:
891
+ name: "Статистика checkpoint"
892
+ description: "Метрики чекпоинтов и bgwriter"
893
+ cache_stats:
894
+ name: "Статистика кэша"
895
+ description: "Статистика кэширования БД"
896
+ missing_validations:
897
+ name: "Отсутствующие валидации"
898
+ description: "Уникальные индексы без валидаций модели"
899
+ unused_columns:
900
+ name: "Неиспользуемые колонки"
901
+ description: "Колонки, имеющие лишь одно значение"
902
+ always_null_columns:
903
+ name: "Всегда NULL"
904
+ description: "Nullable-колонки, содержащие только NULL"
905
+ polymorphic_without_index:
906
+ name: "Polymorphic без индекса"
907
+ description: "Полиморфные ассоциации без составного индекса"
908
+ counter_cache_issues:
909
+ name: "Проблемы counter_cache"
910
+ description: "counter_cache декларации без целевой колонки"
911
+ soft_delete_without_scope:
912
+ name: "Soft delete без scope"
913
+ description: "Soft-delete колонки без scope, фильтрующего их"
914
+ orphan_tables:
915
+ name: "Orphan-таблицы"
916
+ description: "Таблицы БД без соответствующей Rails-модели"
917
+
918
+ # Filter parameter labels and descriptions
919
+ parameters:
920
+ limit:
921
+ label: "Лимит"
922
+ description: "Максимальное число результатов"
923
+ min_calls:
924
+ label: "Мин. вызовов"
925
+ description: "Минимальное число вызовов запроса"
926
+ min_duration_seconds:
927
+ label: "Мин. длительность (сек)"
928
+ description: "Минимальная длительность запроса в секундах"
929
+ threshold_label: "%{field} — порог"
930
+ threshold_description: "Переопределить порог для %{field}"