pghero_fork 2.7.3

Sign up to get free protection for your applications and to get access to all the features.
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