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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +391 -0
- data/CONTRIBUTING.md +42 -0
- data/LICENSE.txt +22 -0
- data/README.md +3 -0
- data/app/assets/images/pghero/favicon.png +0 -0
- data/app/assets/javascripts/pghero/Chart.bundle.js +20755 -0
- data/app/assets/javascripts/pghero/application.js +158 -0
- data/app/assets/javascripts/pghero/chartkick.js +2436 -0
- data/app/assets/javascripts/pghero/highlight.pack.js +2 -0
- data/app/assets/javascripts/pghero/jquery.js +10872 -0
- data/app/assets/javascripts/pghero/nouislider.js +2672 -0
- data/app/assets/stylesheets/pghero/application.css +514 -0
- data/app/assets/stylesheets/pghero/arduino-light.css +86 -0
- data/app/assets/stylesheets/pghero/nouislider.css +310 -0
- data/app/controllers/pg_hero/home_controller.rb +449 -0
- data/app/helpers/pg_hero/home_helper.rb +30 -0
- data/app/views/layouts/pg_hero/application.html.erb +68 -0
- data/app/views/pg_hero/home/_connections_table.html.erb +16 -0
- data/app/views/pg_hero/home/_live_queries_table.html.erb +51 -0
- data/app/views/pg_hero/home/_queries_table.html.erb +72 -0
- data/app/views/pg_hero/home/_query_stats_slider.html.erb +16 -0
- data/app/views/pg_hero/home/_suggested_index.html.erb +18 -0
- data/app/views/pg_hero/home/connections.html.erb +32 -0
- data/app/views/pg_hero/home/explain.html.erb +27 -0
- data/app/views/pg_hero/home/index.html.erb +518 -0
- data/app/views/pg_hero/home/index_bloat.html.erb +72 -0
- data/app/views/pg_hero/home/live_queries.html.erb +11 -0
- data/app/views/pg_hero/home/maintenance.html.erb +55 -0
- data/app/views/pg_hero/home/queries.html.erb +33 -0
- data/app/views/pg_hero/home/relation_space.html.erb +14 -0
- data/app/views/pg_hero/home/show_query.html.erb +106 -0
- data/app/views/pg_hero/home/space.html.erb +83 -0
- data/app/views/pg_hero/home/system.html.erb +34 -0
- data/app/views/pg_hero/home/tune.html.erb +53 -0
- data/config/routes.rb +32 -0
- data/lib/generators/pghero/config_generator.rb +13 -0
- data/lib/generators/pghero/query_stats_generator.rb +18 -0
- data/lib/generators/pghero/space_stats_generator.rb +18 -0
- data/lib/generators/pghero/templates/config.yml.tt +46 -0
- data/lib/generators/pghero/templates/query_stats.rb.tt +15 -0
- data/lib/generators/pghero/templates/space_stats.rb.tt +13 -0
- data/lib/pghero.rb +246 -0
- data/lib/pghero/connection.rb +5 -0
- data/lib/pghero/database.rb +175 -0
- data/lib/pghero/engine.rb +16 -0
- data/lib/pghero/methods/basic.rb +160 -0
- data/lib/pghero/methods/connections.rb +77 -0
- data/lib/pghero/methods/constraints.rb +30 -0
- data/lib/pghero/methods/explain.rb +29 -0
- data/lib/pghero/methods/indexes.rb +332 -0
- data/lib/pghero/methods/kill.rb +28 -0
- data/lib/pghero/methods/maintenance.rb +93 -0
- data/lib/pghero/methods/queries.rb +75 -0
- data/lib/pghero/methods/query_stats.rb +349 -0
- data/lib/pghero/methods/replication.rb +74 -0
- data/lib/pghero/methods/sequences.rb +124 -0
- data/lib/pghero/methods/settings.rb +37 -0
- data/lib/pghero/methods/space.rb +141 -0
- data/lib/pghero/methods/suggested_indexes.rb +329 -0
- data/lib/pghero/methods/system.rb +287 -0
- data/lib/pghero/methods/tables.rb +68 -0
- data/lib/pghero/methods/users.rb +87 -0
- data/lib/pghero/query_stats.rb +5 -0
- data/lib/pghero/space_stats.rb +5 -0
- data/lib/pghero/stats.rb +6 -0
- data/lib/pghero/version.rb +3 -0
- data/lib/tasks/pghero.rake +27 -0
- data/licenses/LICENSE-chart.js.txt +9 -0
- data/licenses/LICENSE-chartkick.js.txt +22 -0
- data/licenses/LICENSE-highlight.js.txt +29 -0
- data/licenses/LICENSE-jquery.txt +20 -0
- data/licenses/LICENSE-moment.txt +22 -0
- data/licenses/LICENSE-nouislider.txt +21 -0
- 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
|