pghero_fork 2.7.3

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 (75) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +391 -0
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +3 -0
  6. data/app/assets/images/pghero/favicon.png +0 -0
  7. data/app/assets/javascripts/pghero/Chart.bundle.js +20755 -0
  8. data/app/assets/javascripts/pghero/application.js +158 -0
  9. data/app/assets/javascripts/pghero/chartkick.js +2436 -0
  10. data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
  11. data/app/assets/javascripts/pghero/jquery.js +10872 -0
  12. data/app/assets/javascripts/pghero/nouislider.js +2672 -0
  13. data/app/assets/stylesheets/pghero/application.css +514 -0
  14. data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
  15. data/app/assets/stylesheets/pghero/nouislider.css +310 -0
  16. data/app/controllers/pg_hero/home_controller.rb +449 -0
  17. data/app/helpers/pg_hero/home_helper.rb +30 -0
  18. data/app/views/layouts/pg_hero/application.html.erb +68 -0
  19. data/app/views/pg_hero/home/_connections_table.html.erb +16 -0
  20. data/app/views/pg_hero/home/_live_queries_table.html.erb +51 -0
  21. data/app/views/pg_hero/home/_queries_table.html.erb +72 -0
  22. data/app/views/pg_hero/home/_query_stats_slider.html.erb +16 -0
  23. data/app/views/pg_hero/home/_suggested_index.html.erb +18 -0
  24. data/app/views/pg_hero/home/connections.html.erb +32 -0
  25. data/app/views/pg_hero/home/explain.html.erb +27 -0
  26. data/app/views/pg_hero/home/index.html.erb +518 -0
  27. data/app/views/pg_hero/home/index_bloat.html.erb +72 -0
  28. data/app/views/pg_hero/home/live_queries.html.erb +11 -0
  29. data/app/views/pg_hero/home/maintenance.html.erb +55 -0
  30. data/app/views/pg_hero/home/queries.html.erb +33 -0
  31. data/app/views/pg_hero/home/relation_space.html.erb +14 -0
  32. data/app/views/pg_hero/home/show_query.html.erb +106 -0
  33. data/app/views/pg_hero/home/space.html.erb +83 -0
  34. data/app/views/pg_hero/home/system.html.erb +34 -0
  35. data/app/views/pg_hero/home/tune.html.erb +53 -0
  36. data/config/routes.rb +32 -0
  37. data/lib/generators/pghero/config_generator.rb +13 -0
  38. data/lib/generators/pghero/query_stats_generator.rb +18 -0
  39. data/lib/generators/pghero/space_stats_generator.rb +18 -0
  40. data/lib/generators/pghero/templates/config.yml.tt +46 -0
  41. data/lib/generators/pghero/templates/query_stats.rb.tt +15 -0
  42. data/lib/generators/pghero/templates/space_stats.rb.tt +13 -0
  43. data/lib/pghero.rb +246 -0
  44. data/lib/pghero/connection.rb +5 -0
  45. data/lib/pghero/database.rb +175 -0
  46. data/lib/pghero/engine.rb +16 -0
  47. data/lib/pghero/methods/basic.rb +160 -0
  48. data/lib/pghero/methods/connections.rb +77 -0
  49. data/lib/pghero/methods/constraints.rb +30 -0
  50. data/lib/pghero/methods/explain.rb +29 -0
  51. data/lib/pghero/methods/indexes.rb +332 -0
  52. data/lib/pghero/methods/kill.rb +28 -0
  53. data/lib/pghero/methods/maintenance.rb +93 -0
  54. data/lib/pghero/methods/queries.rb +75 -0
  55. data/lib/pghero/methods/query_stats.rb +349 -0
  56. data/lib/pghero/methods/replication.rb +74 -0
  57. data/lib/pghero/methods/sequences.rb +124 -0
  58. data/lib/pghero/methods/settings.rb +37 -0
  59. data/lib/pghero/methods/space.rb +141 -0
  60. data/lib/pghero/methods/suggested_indexes.rb +329 -0
  61. data/lib/pghero/methods/system.rb +287 -0
  62. data/lib/pghero/methods/tables.rb +68 -0
  63. data/lib/pghero/methods/users.rb +87 -0
  64. data/lib/pghero/query_stats.rb +5 -0
  65. data/lib/pghero/space_stats.rb +5 -0
  66. data/lib/pghero/stats.rb +6 -0
  67. data/lib/pghero/version.rb +3 -0
  68. data/lib/tasks/pghero.rake +27 -0
  69. data/licenses/LICENSE-chart.js.txt +9 -0
  70. data/licenses/LICENSE-chartkick.js.txt +22 -0
  71. data/licenses/LICENSE-highlight.js.txt +29 -0
  72. data/licenses/LICENSE-jquery.txt +20 -0
  73. data/licenses/LICENSE-moment.txt +22 -0
  74. data/licenses/LICENSE-nouislider.txt +21 -0
  75. metadata +130 -0
@@ -0,0 +1,16 @@
1
+ module PgHero
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace PgHero
4
+
5
+ initializer "pghero", group: :all do |config|
6
+ PgHero.time_zone = PgHero.config["time_zone"] if PgHero.config["time_zone"]
7
+ # View files working with flash
8
+ config.middleware.use ActionDispatch::Flash
9
+
10
+ # We can add all of the public assets from our engine and make them
11
+ # available to use. This allows us to use javascripts, images, stylesheets
12
+ # etc.
13
+ config.middleware.insert_before(::ActionDispatch::Flash, ::ActionDispatch::Static, "#{root}/public")
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,160 @@
1
+ module PgHero
2
+ module Methods
3
+ module Basic
4
+ def ssl_used?
5
+ ssl_used = nil
6
+ with_transaction(rollback: true) do
7
+ begin
8
+ execute("CREATE EXTENSION IF NOT EXISTS sslinfo")
9
+ rescue ActiveRecord::StatementInvalid
10
+ # not superuser
11
+ end
12
+ ssl_used = select_one("SELECT ssl_is_used()")
13
+ end
14
+ ssl_used
15
+ end
16
+
17
+ def database_name
18
+ select_one("SELECT current_database()")
19
+ end
20
+
21
+ def current_user
22
+ select_one("SELECT current_user")
23
+ end
24
+
25
+ def server_version
26
+ @server_version ||= select_one("SHOW server_version")
27
+ end
28
+
29
+ def server_version_num
30
+ @server_version_num ||= select_one("SHOW server_version_num").to_i
31
+ end
32
+
33
+ def quote_ident(value)
34
+ connection.quote_column_name(value)
35
+ end
36
+
37
+ private
38
+
39
+ def select_all(sql, conn: nil, query_columns: [])
40
+ conn ||= connection
41
+ # squish for logs
42
+ retries = 0
43
+ begin
44
+ result = conn.select_all(add_source(squish(sql)))
45
+ if ActiveRecord::VERSION::STRING.to_f >= 6.1
46
+ result = result.map(&:symbolize_keys)
47
+ else
48
+ result = result.map { |row| Hash[row.map { |col, val| [col.to_sym, result.column_types[col].send(:cast_value, val)] }] }
49
+ end
50
+ if filter_data
51
+ query_columns.each do |column|
52
+ result.each do |row|
53
+ begin
54
+ row[column] = PgQuery.normalize(row[column])
55
+ rescue PgQuery::ParseError
56
+ # try replacing "interval $1" with "$1::interval"
57
+ # see https://github.com/lfittl/pg_query/issues/169 for more info
58
+ # this is not ideal since it changes the query slightly
59
+ # we could skip normalization
60
+ # but this has a very small chance of data leakage
61
+ begin
62
+ row[column] = PgQuery.normalize(row[column].gsub(/\binterval\s+(\$\d+)\b/i, "\\1::interval"))
63
+ rescue PgQuery::ParseError
64
+ row[column] = "<unable to filter data>"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ result
71
+ rescue ActiveRecord::StatementInvalid => e
72
+ # fix for random internal errors
73
+ if e.message.include?("PG::InternalError") && retries < 2
74
+ retries += 1
75
+ sleep(0.1)
76
+ retry
77
+ else
78
+ raise e
79
+ end
80
+ end
81
+ end
82
+
83
+ def select_all_stats(sql, **options)
84
+ select_all(sql, **options, conn: stats_connection)
85
+ end
86
+
87
+ def select_all_size(sql)
88
+ result = select_all(sql)
89
+ result.each do |row|
90
+ row[:size] = PgHero.pretty_size(row[:size_bytes])
91
+ end
92
+ result
93
+ end
94
+
95
+ def select_one(sql, conn: nil)
96
+ select_all(sql, conn: conn).first.values.first
97
+ end
98
+
99
+ def select_one_stats(sql)
100
+ select_one(sql, conn: stats_connection)
101
+ end
102
+
103
+ def execute(sql)
104
+ connection.execute(add_source(sql))
105
+ end
106
+
107
+ def connection
108
+ connection_model.connection
109
+ end
110
+
111
+ def stats_connection
112
+ ::PgHero::Stats.connection
113
+ end
114
+
115
+ def insert_stats(table, columns, values)
116
+ values = values.map { |v| "(#{v.map { |v2| quote(v2) }.join(",")})" }.join(",")
117
+ columns = columns.map { |v| quote_table_name(v) }.join(",")
118
+ stats_connection.execute("INSERT INTO #{quote_table_name(table)} (#{columns}) VALUES #{values}")
119
+ end
120
+
121
+ # from ActiveSupport
122
+ def squish(str)
123
+ str.to_s.gsub(/\A[[:space:]]+/, "").gsub(/[[:space:]]+\z/, "").gsub(/[[:space:]]+/, " ")
124
+ end
125
+
126
+ def add_source(sql)
127
+ "#{sql} /*pghero*/"
128
+ end
129
+
130
+ def quote(value)
131
+ connection.quote(value)
132
+ end
133
+
134
+ def quote_table_name(value)
135
+ connection.quote_table_name(value)
136
+ end
137
+
138
+ def unquote(part)
139
+ if part && part.start_with?('"')
140
+ part[1..-2]
141
+ else
142
+ part
143
+ end
144
+ end
145
+
146
+ def with_transaction(lock_timeout: nil, statement_timeout: nil, rollback: false)
147
+ connection_model.transaction do
148
+ select_all "SET LOCAL statement_timeout = #{statement_timeout.to_i}" if statement_timeout
149
+ select_all "SET LOCAL lock_timeout = #{lock_timeout.to_i}" if lock_timeout
150
+ yield
151
+ raise ActiveRecord::Rollback if rollback
152
+ end
153
+ end
154
+
155
+ def table_exists?(table)
156
+ stats_connection.table_exists?(table)
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,77 @@
1
+ module PgHero
2
+ module Methods
3
+ module Connections
4
+ def connections
5
+ if server_version_num >= 90500
6
+ select_all <<-SQL
7
+ SELECT
8
+ pg_stat_activity.pid,
9
+ datname AS database,
10
+ usename AS user,
11
+ application_name AS source,
12
+ client_addr AS ip,
13
+ state,
14
+ ssl
15
+ FROM
16
+ pg_stat_activity
17
+ LEFT JOIN
18
+ pg_stat_ssl ON pg_stat_activity.pid = pg_stat_ssl.pid
19
+ ORDER BY
20
+ pg_stat_activity.pid
21
+ SQL
22
+ else
23
+ select_all <<-SQL
24
+ SELECT
25
+ pid,
26
+ datname AS database,
27
+ usename AS user,
28
+ application_name AS source,
29
+ client_addr AS ip,
30
+ state
31
+ FROM
32
+ pg_stat_activity
33
+ ORDER BY
34
+ pid
35
+ SQL
36
+ end
37
+ end
38
+
39
+ def total_connections
40
+ select_one("SELECT COUNT(*) FROM pg_stat_activity")
41
+ end
42
+
43
+ def connection_states
44
+ states = select_all <<-SQL
45
+ SELECT
46
+ state,
47
+ COUNT(*) AS connections
48
+ FROM
49
+ pg_stat_activity
50
+ GROUP BY
51
+ 1
52
+ ORDER BY
53
+ 2 DESC, 1
54
+ SQL
55
+
56
+ Hash[states.map { |s| [s[:state], s[:connections]] }]
57
+ end
58
+
59
+ def connection_sources
60
+ select_all <<-SQL
61
+ SELECT
62
+ datname AS database,
63
+ usename AS user,
64
+ application_name AS source,
65
+ client_addr AS ip,
66
+ COUNT(*) AS total_connections
67
+ FROM
68
+ pg_stat_activity
69
+ GROUP BY
70
+ 1, 2, 3, 4
71
+ ORDER BY
72
+ 5 DESC, 1, 2, 3, 4
73
+ SQL
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,30 @@
1
+ module PgHero
2
+ module Methods
3
+ module Constraints
4
+ # referenced fields can be nil
5
+ # as not all constraints are foreign keys
6
+ def invalid_constraints
7
+ select_all <<-SQL
8
+ SELECT
9
+ nsp.nspname AS schema,
10
+ rel.relname AS table,
11
+ con.conname AS name,
12
+ fnsp.nspname AS referenced_schema,
13
+ frel.relname AS referenced_table
14
+ FROM
15
+ pg_catalog.pg_constraint con
16
+ INNER JOIN
17
+ pg_catalog.pg_class rel ON rel.oid = con.conrelid
18
+ LEFT JOIN
19
+ pg_catalog.pg_class frel ON frel.oid = con.confrelid
20
+ LEFT JOIN
21
+ pg_catalog.pg_namespace nsp ON nsp.oid = con.connamespace
22
+ LEFT JOIN
23
+ pg_catalog.pg_namespace fnsp ON fnsp.oid = frel.relnamespace
24
+ WHERE
25
+ con.convalidated = 'f'
26
+ SQL
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ module PgHero
2
+ module Methods
3
+ module Explain
4
+ def explain(sql)
5
+ sql = squish(sql)
6
+ explanation = nil
7
+
8
+ # use transaction for safety
9
+ with_transaction(statement_timeout: (explain_timeout_sec * 1000).round, rollback: true) do
10
+ if (sql.sub(/;\z/, "").include?(";") || sql.upcase.include?("COMMIT")) && !explain_safe?
11
+ raise ActiveRecord::StatementInvalid, "Unsafe statement"
12
+ end
13
+ explanation = select_all("EXPLAIN #{sql}").map { |v| v[:"QUERY PLAN"] }.join("\n")
14
+ end
15
+
16
+ explanation
17
+ end
18
+
19
+ private
20
+
21
+ def explain_safe?
22
+ select_all("SELECT 1; SELECT 1")
23
+ false
24
+ rescue ActiveRecord::StatementInvalid
25
+ true
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,332 @@
1
+ module PgHero
2
+ module Methods
3
+ module Indexes
4
+ def index_hit_rate
5
+ select_one <<-SQL
6
+ SELECT
7
+ (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) AS rate
8
+ FROM
9
+ pg_statio_user_indexes
10
+ SQL
11
+ end
12
+
13
+ def index_caching
14
+ select_all <<-SQL
15
+ SELECT
16
+ schemaname AS schema,
17
+ relname AS table,
18
+ indexrelname AS index,
19
+ CASE WHEN idx_blks_hit + idx_blks_read = 0 THEN
20
+ 0
21
+ ELSE
22
+ ROUND(1.0 * idx_blks_hit / (idx_blks_hit + idx_blks_read), 2)
23
+ END AS hit_rate
24
+ FROM
25
+ pg_statio_user_indexes
26
+ ORDER BY
27
+ 3 DESC, 1
28
+ SQL
29
+ end
30
+
31
+ def index_usage
32
+ select_all <<-SQL
33
+ SELECT
34
+ schemaname AS schema,
35
+ relname AS table,
36
+ CASE idx_scan
37
+ WHEN 0 THEN 'Insufficient data'
38
+ ELSE (100 * idx_scan / (seq_scan + idx_scan))::text
39
+ END percent_of_times_index_used,
40
+ n_live_tup AS estimated_rows
41
+ FROM
42
+ pg_stat_user_tables
43
+ ORDER BY
44
+ n_live_tup DESC,
45
+ relname ASC
46
+ SQL
47
+ end
48
+
49
+ def missing_indexes
50
+ select_all <<-SQL
51
+ SELECT
52
+ schemaname AS schema,
53
+ relname AS table,
54
+ CASE idx_scan
55
+ WHEN 0 THEN 'Insufficient data'
56
+ ELSE (100 * idx_scan / (seq_scan + idx_scan))::text
57
+ END percent_of_times_index_used,
58
+ n_live_tup AS estimated_rows
59
+ FROM
60
+ pg_stat_user_tables
61
+ WHERE
62
+ idx_scan > 0
63
+ AND (100 * idx_scan / (seq_scan + idx_scan)) < 95
64
+ AND n_live_tup >= 10000
65
+ ORDER BY
66
+ n_live_tup DESC,
67
+ relname ASC
68
+ SQL
69
+ end
70
+
71
+ def unused_indexes(max_scans: 50, across: [])
72
+ result = select_all_size <<-SQL
73
+ SELECT
74
+ schemaname AS schema,
75
+ relname AS table,
76
+ indexrelname AS index,
77
+ pg_relation_size(i.indexrelid) AS size_bytes,
78
+ idx_scan as index_scans
79
+ FROM
80
+ pg_stat_user_indexes ui
81
+ INNER JOIN
82
+ pg_index i ON ui.indexrelid = i.indexrelid
83
+ WHERE
84
+ NOT indisunique
85
+ AND idx_scan <= #{max_scans.to_i}
86
+ ORDER BY
87
+ pg_relation_size(i.indexrelid) DESC,
88
+ relname ASC
89
+ SQL
90
+
91
+ across.each do |database_id|
92
+ database = PgHero.databases.values.find { |d| d.id == database_id }
93
+ raise PgHero::Error, "Database not found: #{database_id}" unless database
94
+ across_result = Set.new(database.unused_indexes(max_scans: max_scans).map { |v| [v[:schema], v[:index]] })
95
+ result.select! { |v| across_result.include?([v[:schema], v[:index]]) }
96
+ end
97
+
98
+ result
99
+ end
100
+
101
+ def reset_stats
102
+ execute("SELECT pg_stat_reset()")
103
+ true
104
+ end
105
+
106
+ def last_stats_reset_time
107
+ select_one <<-SQL
108
+ SELECT
109
+ pg_stat_get_db_stat_reset_time(oid) AS reset_time
110
+ FROM
111
+ pg_database
112
+ WHERE
113
+ datname = current_database()
114
+ SQL
115
+ end
116
+
117
+ def invalid_indexes(indexes: nil)
118
+ indexes = (indexes || self.indexes).select { |i| !i[:valid] && !i[:creating] }
119
+ indexes.each do |index|
120
+ # map name -> index for backward compatibility
121
+ index[:index] = index[:name]
122
+ end
123
+ indexes
124
+ end
125
+
126
+ # TODO parse array properly
127
+ # https://stackoverflow.com/questions/2204058/list-columns-with-indexes-in-postgresql
128
+ def indexes
129
+ indexes = select_all(<<-SQL
130
+ SELECT
131
+ schemaname AS schema,
132
+ t.relname AS table,
133
+ ix.relname AS name,
134
+ regexp_replace(pg_get_indexdef(i.indexrelid), '^[^\\(]*\\((.*)\\)$', '\\1') AS columns,
135
+ regexp_replace(pg_get_indexdef(i.indexrelid), '.* USING ([^ ]*) \\(.*', '\\1') AS using,
136
+ indisunique AS unique,
137
+ indisprimary AS primary,
138
+ indisvalid AS valid,
139
+ indexprs::text,
140
+ indpred::text,
141
+ pg_get_indexdef(i.indexrelid) AS definition
142
+ FROM
143
+ pg_index i
144
+ INNER JOIN
145
+ pg_class t ON t.oid = i.indrelid
146
+ INNER JOIN
147
+ pg_class ix ON ix.oid = i.indexrelid
148
+ LEFT JOIN
149
+ pg_stat_user_indexes ui ON ui.indexrelid = i.indexrelid
150
+ WHERE
151
+ schemaname IS NOT NULL
152
+ ORDER BY
153
+ 1, 2
154
+ SQL
155
+ ).map { |v| v[:columns] = v[:columns].sub(") WHERE (", " WHERE ").split(", ").map { |c| unquote(c) }; v }
156
+
157
+ # determine if any invalid indexes being created
158
+ # hacky, but works for simple cases
159
+ # can be a race condition, but that's fine
160
+ invalid_indexes = indexes.select { |i| !i[:valid] }
161
+ if invalid_indexes.any?
162
+ create_index_queries = running_queries.select { |q| /\s*CREATE\s+INDEX\s+CONCURRENTLY\s+/i.match(q[:query]) }
163
+ invalid_indexes.each do |index|
164
+ index[:creating] = create_index_queries.any? { |q| q[:query].include?(index[:table]) && index[:columns].all? { |c| q[:query].include?(c) } }
165
+ end
166
+ end
167
+
168
+ indexes
169
+ end
170
+
171
+ def duplicate_indexes(indexes: nil)
172
+ dup_indexes = []
173
+
174
+ indexes_by_table = (indexes || self.indexes).group_by { |i| [i[:schema], i[:table]] }
175
+ indexes_by_table.values.flatten.select { |i| i[:valid] && !i[:primary] && !i[:unique] }.each do |index|
176
+ covering_index = indexes_by_table[[index[:schema], index[:table]]].find { |i| i[:valid] && i[:name] != index[:name] && index_covers?(i[:columns], index[:columns]) && i[:using] == index[:using] && i[:indexprs] == index[:indexprs] && i[:indpred] == index[:indpred] }
177
+ if covering_index && (covering_index[:columns] != index[:columns] || index[:name] > covering_index[:name] || covering_index[:primary] || covering_index[:unique])
178
+ dup_indexes << {unneeded_index: index, covering_index: covering_index}
179
+ end
180
+ end
181
+
182
+ dup_indexes.sort_by { |i| ui = i[:unneeded_index]; [ui[:table], ui[:columns]] }
183
+ end
184
+
185
+ # https://gist.github.com/mbanck/9976015/71888a24e464e2f772182a7eb54f15a125edf398
186
+ # thanks @jberkus and @mbanck
187
+ def index_bloat(min_size: nil)
188
+ min_size ||= index_bloat_bytes
189
+ select_all <<-SQL
190
+ WITH btree_index_atts AS (
191
+ SELECT
192
+ nspname, relname, reltuples, relpages, indrelid, relam,
193
+ regexp_split_to_table(indkey::text, ' ')::smallint AS attnum,
194
+ indexrelid as index_oid
195
+ FROM
196
+ pg_index
197
+ JOIN
198
+ pg_class ON pg_class.oid = pg_index.indexrelid
199
+ JOIN
200
+ pg_namespace ON pg_namespace.oid = pg_class.relnamespace
201
+ JOIN
202
+ pg_am ON pg_class.relam = pg_am.oid
203
+ WHERE
204
+ pg_am.amname = 'btree'
205
+ ),
206
+ index_item_sizes AS (
207
+ SELECT
208
+ i.nspname,
209
+ i.relname,
210
+ i.reltuples,
211
+ i.relpages,
212
+ i.relam,
213
+ (quote_ident(s.schemaname) || '.' || quote_ident(s.tablename))::regclass AS starelid,
214
+ a.attrelid AS table_oid, index_oid,
215
+ current_setting('block_size')::numeric AS bs,
216
+ /* MAXALIGN: 4 on 32bits, 8 on 64bits (and mingw32 ?) */
217
+ CASE
218
+ WHEN version() ~ 'mingw32' OR version() ~ '64-bit' THEN 8
219
+ ELSE 4
220
+ END AS maxalign,
221
+ 24 AS pagehdr,
222
+ /* per tuple header: add index_attribute_bm if some cols are null-able */
223
+ CASE WHEN max(coalesce(s.null_frac,0)) = 0
224
+ THEN 2
225
+ ELSE 6
226
+ END AS index_tuple_hdr,
227
+ /* data len: we remove null values save space using it fractionnal part from stats */
228
+ sum( (1-coalesce(s.null_frac, 0)) * coalesce(s.avg_width, 2048) ) AS nulldatawidth
229
+ FROM
230
+ pg_attribute AS a
231
+ JOIN
232
+ pg_stats AS s ON (quote_ident(s.schemaname) || '.' || quote_ident(s.tablename))::regclass=a.attrelid AND s.attname = a.attname
233
+ JOIN
234
+ btree_index_atts AS i ON i.indrelid = a.attrelid AND a.attnum = i.attnum
235
+ WHERE
236
+ a.attnum > 0
237
+ GROUP BY
238
+ 1, 2, 3, 4, 5, 6, 7, 8, 9
239
+ ),
240
+ index_aligned AS (
241
+ SELECT
242
+ maxalign,
243
+ bs,
244
+ nspname,
245
+ relname AS index_name,
246
+ reltuples,
247
+ relpages,
248
+ relam,
249
+ table_oid,
250
+ index_oid,
251
+ ( 2 +
252
+ maxalign - CASE /* Add padding to the index tuple header to align on MAXALIGN */
253
+ WHEN index_tuple_hdr%maxalign = 0 THEN maxalign
254
+ ELSE index_tuple_hdr%maxalign
255
+ END
256
+ + nulldatawidth + maxalign - CASE /* Add padding to the data to align on MAXALIGN */
257
+ WHEN nulldatawidth::integer%maxalign = 0 THEN maxalign
258
+ ELSE nulldatawidth::integer%maxalign
259
+ END
260
+ )::numeric AS nulldatahdrwidth, pagehdr
261
+ FROM
262
+ index_item_sizes AS s1
263
+ ),
264
+ otta_calc AS (
265
+ SELECT
266
+ bs,
267
+ nspname,
268
+ table_oid,
269
+ index_oid,
270
+ index_name,
271
+ relpages,
272
+ coalesce(
273
+ ceil((reltuples*(4+nulldatahdrwidth))/(bs-pagehdr::float)) +
274
+ CASE WHEN am.amname IN ('hash','btree') THEN 1 ELSE 0 END , 0 /* btree and hash have a metadata reserved block */
275
+ ) AS otta
276
+ FROM
277
+ index_aligned AS s2
278
+ LEFT JOIN
279
+ pg_am am ON s2.relam = am.oid
280
+ ),
281
+ raw_bloat AS (
282
+ SELECT
283
+ nspname,
284
+ c.relname AS table_name,
285
+ index_name,
286
+ bs*(sub.relpages)::bigint AS totalbytes,
287
+ CASE
288
+ WHEN sub.relpages <= otta THEN 0
289
+ ELSE bs*(sub.relpages-otta)::bigint END
290
+ AS wastedbytes,
291
+ CASE
292
+ WHEN sub.relpages <= otta
293
+ THEN 0 ELSE bs*(sub.relpages-otta)::bigint * 100 / (bs*(sub.relpages)::bigint) END
294
+ AS realbloat,
295
+ pg_relation_size(sub.table_oid) as table_bytes,
296
+ stat.idx_scan as index_scans,
297
+ stat.indexrelid
298
+ FROM
299
+ otta_calc AS sub
300
+ JOIN
301
+ pg_class AS c ON c.oid=sub.table_oid
302
+ JOIN
303
+ pg_stat_user_indexes AS stat ON sub.index_oid = stat.indexrelid
304
+ )
305
+ SELECT
306
+ nspname AS schema,
307
+ table_name AS table,
308
+ index_name AS index,
309
+ wastedbytes AS bloat_bytes,
310
+ totalbytes AS index_bytes,
311
+ pg_get_indexdef(rb.indexrelid) AS definition,
312
+ indisprimary AS primary
313
+ FROM
314
+ raw_bloat rb
315
+ INNER JOIN
316
+ pg_index i ON i.indexrelid = rb.indexrelid
317
+ WHERE
318
+ wastedbytes >= #{min_size.to_i}
319
+ ORDER BY
320
+ wastedbytes DESC,
321
+ index_name
322
+ SQL
323
+ end
324
+
325
+ protected
326
+
327
+ def index_covers?(indexed_columns, columns)
328
+ indexed_columns.first(columns.size) == columns
329
+ end
330
+ end
331
+ end
332
+ end