pghero 2.1.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pghero might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/CONTRIBUTING.md +9 -7
- data/README.md +6 -4
- data/app/controllers/pg_hero/home_controller.rb +10 -2
- data/app/views/pg_hero/home/explain.html.erb +1 -1
- data/app/views/pg_hero/home/index.html.erb +16 -6
- data/lib/generators/pghero/templates/{config.yml → config.yml.tt} +3 -3
- data/lib/generators/pghero/templates/{query_stats.rb → query_stats.rb.tt} +0 -0
- data/lib/generators/pghero/templates/{space_stats.rb → space_stats.rb.tt} +0 -0
- data/lib/pghero.rb +2 -2
- data/lib/pghero/methods/connections.rb +16 -0
- data/lib/pghero/methods/indexes.rb +32 -26
- data/lib/pghero/methods/maintenance.rb +1 -1
- data/lib/pghero/methods/query_stats.rb +2 -2
- data/lib/pghero/methods/replication.rb +1 -1
- data/lib/pghero/methods/sequences.rb +6 -2
- data/lib/pghero/methods/suggested_indexes.rb +1 -5
- data/lib/pghero/version.rb +1 -1
- metadata +9 -45
- data/.gitattributes +0 -1
- data/.github/ISSUE_TEMPLATE.md +0 -7
- data/.gitignore +0 -22
- data/.travis.yml +0 -16
- data/Gemfile +0 -6
- data/Rakefile +0 -8
- data/guides/Contributing.md +0 -16
- data/guides/Docker.md +0 -89
- data/guides/Heroku.md +0 -102
- data/guides/Linux.md +0 -296
- data/guides/Permissions.md +0 -57
- data/guides/Query-Stats.md +0 -60
- data/guides/Rails.md +0 -339
- data/guides/Suggested-Indexes.md +0 -19
- data/pghero.gemspec +0 -35
- data/test/basic_test.rb +0 -38
- data/test/best_index_test.rb +0 -180
- data/test/gemfiles/activerecord41.gemfile +0 -6
- data/test/gemfiles/activerecord42.gemfile +0 -6
- data/test/suggested_indexes_test.rb +0 -18
- data/test/test_helper.rb +0 -66
data/guides/Suggested-Indexes.md
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
# How PgHero Suggests Indexes
|
2
|
-
|
3
|
-
1. Get the most time-consuming queries from [pg_stat_statements](https://www.postgresql.org/docs/current/static/pgstatstatements.html).
|
4
|
-
|
5
|
-
2. Parse queries with [pg_query](https://github.com/lfittl/pg_query). Look for a single table with a `WHERE` clause that consists of only `=`, `IN`, `IS NULL` or `IS NOT NULL` and/or an `ORDER BY` clause.
|
6
|
-
|
7
|
-
3. Use the [pg_stats](https://www.postgresql.org/docs/current/static/view-pg-stats.html) view to get estimates about distinct rows and percent of `NULL` values for each column.
|
8
|
-
|
9
|
-
4. For each column in the `WHERE` clause, sort by the highest [cardinality](https://en.wikipedia.org/wiki/Cardinality_(SQL_statements)) (most unique values). This allows the database to narrow its search the fastest. Perform [row estimation](https://www.postgresql.org/docs/current/static/row-estimation-examples.html) to get the expected number of rows as we add columns to the index.
|
10
|
-
|
11
|
-
5. Continue this process with columns in the `ORDER BY` clause.
|
12
|
-
|
13
|
-
6. To make sure we don’t add useless columns, stop once we narrow it down to 50 rows in steps 5 or 6. Also, recheck the last columns to make sure they add value.
|
14
|
-
|
15
|
-
7. Profit :moneybag:
|
16
|
-
|
17
|
-
## TODO
|
18
|
-
|
19
|
-
- examples
|
data/pghero.gemspec
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
lib = File.expand_path("../lib", __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require "pghero/version"
|
5
|
-
|
6
|
-
Gem::Specification.new do |spec|
|
7
|
-
spec.name = "pghero"
|
8
|
-
spec.version = PgHero::VERSION
|
9
|
-
spec.authors = ["Andrew Kane"]
|
10
|
-
spec.email = ["andrew@chartkick.com"]
|
11
|
-
spec.summary = "A performance dashboard for Postgres"
|
12
|
-
spec.homepage = "https://github.com/ankane/pghero"
|
13
|
-
spec.license = "MIT"
|
14
|
-
|
15
|
-
spec.files = `git ls-files -z`.split("\x0")
|
16
|
-
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features|guides)/})
|
18
|
-
spec.require_paths = ["lib"]
|
19
|
-
|
20
|
-
spec.required_ruby_version = ">= 2.2.0"
|
21
|
-
|
22
|
-
spec.add_dependency "activerecord"
|
23
|
-
|
24
|
-
spec.add_development_dependency "activerecord-import"
|
25
|
-
spec.add_development_dependency "bundler"
|
26
|
-
spec.add_development_dependency "minitest"
|
27
|
-
spec.add_development_dependency "rake"
|
28
|
-
|
29
|
-
if RUBY_PLATFORM == "java"
|
30
|
-
spec.add_development_dependency "activerecord-jdbcpostgresql-adapter"
|
31
|
-
else
|
32
|
-
spec.add_development_dependency "pg", "< 1.0.0"
|
33
|
-
spec.add_development_dependency "pg_query"
|
34
|
-
end
|
35
|
-
end
|
data/test/basic_test.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
require_relative "test_helper"
|
2
|
-
|
3
|
-
class BasicTest < Minitest::Test
|
4
|
-
def setup
|
5
|
-
City.delete_all
|
6
|
-
end
|
7
|
-
|
8
|
-
def test_explain
|
9
|
-
City.create!
|
10
|
-
PgHero.explain("ANALYZE DELETE FROM cities")
|
11
|
-
assert_equal 1, City.count
|
12
|
-
end
|
13
|
-
|
14
|
-
def test_explain_multiple_statements
|
15
|
-
City.create!
|
16
|
-
assert_raises(ActiveRecord::StatementInvalid) { PgHero.explain("ANALYZE DELETE FROM cities; DELETE FROM cities; COMMIT") }
|
17
|
-
end
|
18
|
-
|
19
|
-
def test_analyze_tables
|
20
|
-
assert PgHero.analyze_tables
|
21
|
-
end
|
22
|
-
|
23
|
-
def test_relation_sizes
|
24
|
-
assert PgHero.relation_sizes
|
25
|
-
end
|
26
|
-
|
27
|
-
def test_transaction_id_danger
|
28
|
-
assert PgHero.transaction_id_danger(threshold: 10000000000)
|
29
|
-
end
|
30
|
-
|
31
|
-
def test_autovacuum_danger
|
32
|
-
assert PgHero.autovacuum_danger
|
33
|
-
end
|
34
|
-
|
35
|
-
def test_databases
|
36
|
-
assert PgHero.databases[:primary].running_queries
|
37
|
-
end
|
38
|
-
end
|
data/test/best_index_test.rb
DELETED
@@ -1,180 +0,0 @@
|
|
1
|
-
require_relative "test_helper"
|
2
|
-
|
3
|
-
class BestIndexTest < Minitest::Test
|
4
|
-
def test_where
|
5
|
-
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id = 1"
|
6
|
-
end
|
7
|
-
|
8
|
-
def test_all_values
|
9
|
-
index = PgHero.best_index("SELECT * FROM users WHERE login_attempts = 1 ORDER BY created_at")
|
10
|
-
expected = {
|
11
|
-
found: true,
|
12
|
-
structure: {table: "users", where: [{column: "login_attempts", op: "="}], sort: [{column: "created_at", direction: "asc"}]},
|
13
|
-
index: {table: "users", columns: ["login_attempts", "created_at"]},
|
14
|
-
rows: 5000,
|
15
|
-
row_estimates: {"login_attempts (=)" => 167, "created_at (sort)" => 1},
|
16
|
-
row_progression: [5000, 167, 0]
|
17
|
-
}
|
18
|
-
assert_equal expected, index
|
19
|
-
end
|
20
|
-
|
21
|
-
def test_where_multiple_columns
|
22
|
-
assert_best_index ({table: "users", columns: ["city_id", "login_attempts"]}), "SELECT * FROM users WHERE city_id = 1 and login_attempts = 2"
|
23
|
-
end
|
24
|
-
|
25
|
-
def test_where_unique
|
26
|
-
assert_best_index ({table: "users", columns: ["email"]}), "SELECT * FROM users WHERE city_id = 1 AND email = 'person2@example.org'"
|
27
|
-
end
|
28
|
-
|
29
|
-
def test_order
|
30
|
-
assert_best_index ({table: "users", columns: ["created_at"]}), "SELECT * FROM users ORDER BY created_at"
|
31
|
-
end
|
32
|
-
|
33
|
-
def test_order_multiple
|
34
|
-
assert_best_index ({table: "users", columns: ["login_attempts", "created_at"]}), "SELECT * FROM users ORDER BY login_attempts, created_at"
|
35
|
-
end
|
36
|
-
|
37
|
-
def test_order_multiple_direction
|
38
|
-
assert_best_index ({table: "users", columns: ["login_attempts"]}), "SELECT * FROM users ORDER BY login_attempts DESC, created_at"
|
39
|
-
end
|
40
|
-
|
41
|
-
def test_order_multiple_unique
|
42
|
-
assert_best_index ({table: "users", columns: ["id"]}), "SELECT * FROM users ORDER BY id, created_at"
|
43
|
-
end
|
44
|
-
|
45
|
-
def test_where_unique_order
|
46
|
-
assert_best_index ({table: "users", columns: ["email"]}), "SELECT * FROM users WHERE email = 'person2@example.org' ORDER BY created_at"
|
47
|
-
end
|
48
|
-
|
49
|
-
def test_where_order
|
50
|
-
assert_best_index ({table: "users", columns: ["login_attempts", "created_at"]}), "SELECT * FROM users WHERE login_attempts = 1 ORDER BY created_at"
|
51
|
-
end
|
52
|
-
|
53
|
-
def test_where_order_unknown
|
54
|
-
assert_best_index ({table: "users", columns: ["login_attempts"]}), "SELECT * FROM users WHERE login_attempts = 1 ORDER BY NOW()"
|
55
|
-
end
|
56
|
-
|
57
|
-
def test_where_in
|
58
|
-
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id IN (1, 2)"
|
59
|
-
end
|
60
|
-
|
61
|
-
def test_like
|
62
|
-
assert_best_index ({table: "users", columns: ["email gist_trgm_ops"], using: "gist"}), "SELECT * FROM users WHERE email LIKE ?"
|
63
|
-
end
|
64
|
-
|
65
|
-
def test_like_where
|
66
|
-
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id = ? AND email LIKE ?"
|
67
|
-
end
|
68
|
-
|
69
|
-
def test_like_where2
|
70
|
-
assert_best_index ({table: "users", columns: ["email gist_trgm_ops"], using: "gist"}), "SELECT * FROM users WHERE email LIKE ? AND active = ?"
|
71
|
-
end
|
72
|
-
|
73
|
-
def test_ilike
|
74
|
-
assert_best_index ({table: "users", columns: ["email gist_trgm_ops"], using: "gist"}), "SELECT * FROM users WHERE email ILIKE ?"
|
75
|
-
end
|
76
|
-
|
77
|
-
def test_not_equals
|
78
|
-
assert_best_index ({table: "users", columns: ["login_attempts"]}), "SELECT * FROM users WHERE city_id != ? and login_attempts = 2"
|
79
|
-
end
|
80
|
-
|
81
|
-
def test_not_in
|
82
|
-
assert_best_index ({table: "users", columns: ["login_attempts"]}), "SELECT * FROM users WHERE city_id NOT IN (?) and login_attempts = 2"
|
83
|
-
end
|
84
|
-
|
85
|
-
def test_between
|
86
|
-
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id BETWEEN 1 AND 2"
|
87
|
-
end
|
88
|
-
|
89
|
-
def test_multiple_range
|
90
|
-
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id > ? and login_attempts > ?"
|
91
|
-
end
|
92
|
-
|
93
|
-
def test_where_prepared
|
94
|
-
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id = $1"
|
95
|
-
end
|
96
|
-
|
97
|
-
def test_where_normalized
|
98
|
-
assert_best_index ({table: "users", columns: ["city_id"]}), "SELECT * FROM users WHERE city_id = ?"
|
99
|
-
end
|
100
|
-
|
101
|
-
def test_is_null
|
102
|
-
assert_best_index ({table: "users", columns: ["zip_code"]}), "SELECT * FROM users WHERE zip_code IS NULL"
|
103
|
-
end
|
104
|
-
|
105
|
-
def test_is_null_equal
|
106
|
-
assert_best_index ({table: "users", columns: ["zip_code", "login_attempts"]}), "SELECT * FROM users WHERE zip_code IS NULL AND login_attempts = ?"
|
107
|
-
end
|
108
|
-
|
109
|
-
def test_is_not_null
|
110
|
-
assert_best_index ({table: "users", columns: ["login_attempts"]}), "SELECT * FROM users WHERE zip_code IS NOT NULL AND login_attempts = ?"
|
111
|
-
end
|
112
|
-
|
113
|
-
def test_update
|
114
|
-
assert_best_index ({table: "users", columns: ["city_id"]}), "UPDATE users SET email = 'test' WHERE city_id = 1"
|
115
|
-
end
|
116
|
-
|
117
|
-
def test_delete
|
118
|
-
assert_best_index ({table: "users", columns: ["city_id"]}), "DELETE FROM users WHERE city_id = 1"
|
119
|
-
end
|
120
|
-
|
121
|
-
def test_parse_error
|
122
|
-
assert_no_index "Parse error", "SELECT *123'"
|
123
|
-
end
|
124
|
-
|
125
|
-
def test_stats_not_found
|
126
|
-
assert_no_index "Stats not found", "SELECT * FROM non_existent_table WHERE id = 1"
|
127
|
-
end
|
128
|
-
|
129
|
-
def test_unknown_structure
|
130
|
-
assert_no_index "Unknown structure", "SELECT NOW()"
|
131
|
-
end
|
132
|
-
|
133
|
-
def test_where_or
|
134
|
-
assert_no_index "Unknown structure", "SELECT FROM users WHERE login_attempts = 0 OR login_attempts = 1"
|
135
|
-
end
|
136
|
-
|
137
|
-
def test_where_nested_or
|
138
|
-
assert_no_index "Unknown structure", "SELECT FROM users WHERE city_id = 1 AND (login_attempts = 0 OR login_attempts = 1)"
|
139
|
-
end
|
140
|
-
|
141
|
-
|
142
|
-
def test_multiple_tables
|
143
|
-
assert_no_index "JOIN not supported yet", "SELECT * FROM users INNER JOIN cities ON cities.id = users.city_id"
|
144
|
-
end
|
145
|
-
|
146
|
-
def test_no_columns
|
147
|
-
assert_no_index "No columns to index", "SELECT * FROM users"
|
148
|
-
end
|
149
|
-
|
150
|
-
def test_small_table
|
151
|
-
assert_no_index "No index needed if less than 500 rows", "SELECT * FROM states WHERE name = 'State 1'"
|
152
|
-
end
|
153
|
-
|
154
|
-
def test_system_table
|
155
|
-
assert_no_index "System table", "SELECT COUNT(*) AS count FROM pg_extension WHERE extname = ?"
|
156
|
-
end
|
157
|
-
|
158
|
-
def test_insert
|
159
|
-
assert_no_index "INSERT statement", "INSERT INTO users (login_attempts) VALUES (1)"
|
160
|
-
end
|
161
|
-
|
162
|
-
def test_set
|
163
|
-
assert_no_index "SET statement", "set client_encoding to 'UTF8'"
|
164
|
-
end
|
165
|
-
|
166
|
-
protected
|
167
|
-
|
168
|
-
def assert_best_index(expected, statement)
|
169
|
-
index = PgHero.best_index(statement)
|
170
|
-
assert_nil index[:explanation]
|
171
|
-
assert index[:found]
|
172
|
-
assert_equal expected, index[:index]
|
173
|
-
end
|
174
|
-
|
175
|
-
def assert_no_index(explanation, statement)
|
176
|
-
index = PgHero.best_index(statement)
|
177
|
-
assert !index[:found]
|
178
|
-
assert_equal explanation, index[:explanation]
|
179
|
-
end
|
180
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
require_relative "test_helper"
|
2
|
-
|
3
|
-
class SuggestedIndexesTest < Minitest::Test
|
4
|
-
def setup
|
5
|
-
skip if ENV["TRAVIS_CI"]
|
6
|
-
PgHero.reset_query_stats
|
7
|
-
end
|
8
|
-
|
9
|
-
def test_basic
|
10
|
-
User.where(email: "person1@example.org").first
|
11
|
-
assert_equal [{table: "users", columns: ["email"]}], PgHero.suggested_indexes.map { |q| q.except(:queries, :details) }
|
12
|
-
end
|
13
|
-
|
14
|
-
def test_existing_index
|
15
|
-
User.where("updated_at > ?", Time.now).to_a
|
16
|
-
assert_equal [], PgHero.suggested_indexes.map { |q| q.except(:queries, :details) }
|
17
|
-
end
|
18
|
-
end
|
data/test/test_helper.rb
DELETED
@@ -1,66 +0,0 @@
|
|
1
|
-
require "bundler/setup"
|
2
|
-
Bundler.require(:default)
|
3
|
-
require "minitest/autorun"
|
4
|
-
require "minitest/pride"
|
5
|
-
require "pg_query"
|
6
|
-
require "activerecord-import"
|
7
|
-
|
8
|
-
# for Minitest < 5
|
9
|
-
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
|
10
|
-
|
11
|
-
ActiveRecord::Base.establish_connection adapter: "postgresql", database: "pghero_test"
|
12
|
-
|
13
|
-
ActiveRecord::Migration.enable_extension "pg_stat_statements"
|
14
|
-
|
15
|
-
ActiveRecord::Migration.create_table :cities, force: true do |t|
|
16
|
-
t.string :name
|
17
|
-
end
|
18
|
-
|
19
|
-
ActiveRecord::Migration.create_table :states, force: true do |t|
|
20
|
-
t.string :name
|
21
|
-
end
|
22
|
-
|
23
|
-
ActiveRecord::Migration.create_table :users, force: true do |t|
|
24
|
-
t.integer :city_id
|
25
|
-
t.integer :login_attempts
|
26
|
-
t.string :email
|
27
|
-
t.string :zip_code
|
28
|
-
t.boolean :active
|
29
|
-
t.timestamp :created_at
|
30
|
-
t.timestamp :updated_at
|
31
|
-
end
|
32
|
-
ActiveRecord::Migration.add_index :users, :updated_at
|
33
|
-
|
34
|
-
class City < ActiveRecord::Base
|
35
|
-
end
|
36
|
-
|
37
|
-
class State < ActiveRecord::Base
|
38
|
-
end
|
39
|
-
|
40
|
-
class User < ActiveRecord::Base
|
41
|
-
end
|
42
|
-
|
43
|
-
states =
|
44
|
-
50.times.map do |i|
|
45
|
-
{
|
46
|
-
name: "State #{i}"
|
47
|
-
}
|
48
|
-
end
|
49
|
-
State.import states, validate: false
|
50
|
-
ActiveRecord::Base.connection.execute("ANALYZE states")
|
51
|
-
|
52
|
-
users =
|
53
|
-
5000.times.map do |i|
|
54
|
-
city_id = i % 100
|
55
|
-
{
|
56
|
-
city_id: city_id,
|
57
|
-
email: "person#{i}@example.org",
|
58
|
-
login_attempts: rand(30),
|
59
|
-
zip_code: i % 40 == 0 ? nil : "12345",
|
60
|
-
active: true,
|
61
|
-
created_at: Time.now - rand(50).days,
|
62
|
-
updated_at: Time.now - rand(50).days
|
63
|
-
}
|
64
|
-
end
|
65
|
-
User.import users, validate: false
|
66
|
-
ActiveRecord::Base.connection.execute("ANALYZE users")
|