pghero 1.1.4 → 1.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 +10 -0
- data/Gemfile +2 -0
- data/app/controllers/pg_hero/home_controller.rb +38 -4
- data/app/views/layouts/pg_hero/application.html.erb +9 -0
- data/app/views/pg_hero/home/_connections_table.html.erb +2 -2
- data/app/views/pg_hero/home/_queries_table.html.erb +10 -0
- data/app/views/pg_hero/home/_query_stats_slider.html.erb +23 -2
- data/app/views/pg_hero/home/_suggested_index.html.erb +1 -0
- data/app/views/pg_hero/home/connections.html.erb +1 -1
- data/app/views/pg_hero/home/explain.html.erb +3 -0
- data/app/views/pg_hero/home/index.html.erb +92 -2
- data/app/views/pg_hero/home/maintenance.html.erb +32 -0
- data/app/views/pg_hero/home/queries.html.erb +4 -0
- data/app/views/pg_hero/home/system.html.erb +3 -0
- data/config/routes.rb +2 -0
- data/guides/Rails.md +17 -0
- data/guides/Suggested-Indexes.md +19 -0
- data/lib/pghero.rb +445 -22
- data/lib/pghero/tasks.rb +5 -0
- data/lib/pghero/version.rb +1 -1
- data/test/best_index_test.rb +131 -0
- data/test/explain_test.rb +18 -0
- data/test/suggested_indexes_test.rb +14 -0
- data/test/test_helper.rb +44 -2
- metadata +12 -4
- data/test/pghero_test.rb +0 -18
@@ -4,6 +4,9 @@
|
|
4
4
|
<h1>CPU</h1>
|
5
5
|
<div style="margin-bottom: 20px;"><%= line_chart cpu_usage_path, max: 100, colors: ["#5bc0de"], library: {pointSize: 0, lineWidth: 5} %></div>
|
6
6
|
|
7
|
+
<h1>Load</h1>
|
8
|
+
<div style="margin-bottom: 20px;"><%= line_chart load_stats_path, colors: ["#5bc0de", "#d9534f"], library: {pointSize: 0, lineWidth: 5} %></div>
|
9
|
+
|
7
10
|
<h1>Connections</h1>
|
8
11
|
<div style="margin-bottom: 20px;"><%= line_chart connection_stats_path, colors: ["#5bc0de"], library: {pointSize: 0, lineWidth: 5} %></div>
|
9
12
|
|
data/config/routes.rb
CHANGED
@@ -8,9 +8,11 @@ PgHero::Engine.routes.draw do
|
|
8
8
|
get "cpu_usage", to: "home#cpu_usage"
|
9
9
|
get "connection_stats", to: "home#connection_stats"
|
10
10
|
get "replication_lag_stats", to: "home#replication_lag_stats"
|
11
|
+
get "load_stats", to: "home#load_stats"
|
11
12
|
get "explain", to: "home#explain"
|
12
13
|
get "tune", to: "home#tune"
|
13
14
|
get "connections", to: "home#connections"
|
15
|
+
get "maintenance", to: "home#maintenance"
|
14
16
|
post "kill", to: "home#kill"
|
15
17
|
post "kill_long_running_queries", to: "home#kill_long_running_queries"
|
16
18
|
post "kill_all", to: "home#kill_all"
|
data/guides/Rails.md
CHANGED
@@ -18,6 +18,16 @@ mount PgHero::Engine, at: "pghero"
|
|
18
18
|
|
19
19
|
Be sure to [secure the dashboard](#security) in production.
|
20
20
|
|
21
|
+
### Suggested Indexes
|
22
|
+
|
23
|
+
PgHero can suggest indexes to add. To enable, add to your Gemfile:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
gem 'pg_query'
|
27
|
+
```
|
28
|
+
|
29
|
+
and make sure [query stats](#query-stats) are enabled. Read about how it works [here](Suggested-Indexes.md).
|
30
|
+
|
21
31
|
## Insights
|
22
32
|
|
23
33
|
```ruby
|
@@ -55,6 +65,13 @@ PgHero.query_stats
|
|
55
65
|
PgHero.slow_queries
|
56
66
|
```
|
57
67
|
|
68
|
+
Suggested indexes
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
PgHero.suggested_indexes
|
72
|
+
PgHero.best_index(query)
|
73
|
+
```
|
74
|
+
|
58
75
|
Security
|
59
76
|
|
60
77
|
```ruby
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# How PgHero Suggests Indexes
|
2
|
+
|
3
|
+
1. Get the most time-consuming queries from [pg_stat_statements](http://www.postgresql.org/docs/9.3/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](http://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](http://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/lib/pghero.rb
CHANGED
@@ -17,15 +17,25 @@ module PgHero
|
|
17
17
|
end
|
18
18
|
|
19
19
|
class << self
|
20
|
-
attr_accessor :long_running_query_sec, :slow_query_ms, :slow_query_calls, :total_connections_threshold, :env
|
20
|
+
attr_accessor :long_running_query_sec, :slow_query_ms, :slow_query_calls, :total_connections_threshold, :cache_hit_rate_threshold, :env, :show_migrations
|
21
21
|
end
|
22
22
|
self.long_running_query_sec = (ENV["PGHERO_LONG_RUNNING_QUERY_SEC"] || 60).to_i
|
23
23
|
self.slow_query_ms = (ENV["PGHERO_SLOW_QUERY_MS"] || 20).to_i
|
24
24
|
self.slow_query_calls = (ENV["PGHERO_SLOW_QUERY_CALLS"] || 100).to_i
|
25
25
|
self.total_connections_threshold = (ENV["PGHERO_TOTAL_CONNECTIONS_THRESHOLD"] || 100).to_i
|
26
|
+
self.cache_hit_rate_threshold = 99
|
26
27
|
self.env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
28
|
+
self.show_migrations = true
|
27
29
|
|
28
30
|
class << self
|
31
|
+
def time_zone=(time_zone)
|
32
|
+
@time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
|
33
|
+
end
|
34
|
+
|
35
|
+
def time_zone
|
36
|
+
@time_zone || Time.zone
|
37
|
+
end
|
38
|
+
|
29
39
|
def config
|
30
40
|
Thread.current[:pghero_config] ||= begin
|
31
41
|
path = "config/pghero.yml"
|
@@ -167,6 +177,39 @@ module PgHero
|
|
167
177
|
).first["rate"].to_f
|
168
178
|
end
|
169
179
|
|
180
|
+
def table_caching
|
181
|
+
select_all <<-SQL
|
182
|
+
SELECT
|
183
|
+
relname AS table,
|
184
|
+
CASE WHEN heap_blks_hit + heap_blks_read = 0 THEN
|
185
|
+
0
|
186
|
+
ELSE
|
187
|
+
ROUND(1.0 * heap_blks_hit / (heap_blks_hit + heap_blks_read), 2)
|
188
|
+
END AS hit_rate
|
189
|
+
FROM
|
190
|
+
pg_statio_user_tables
|
191
|
+
ORDER BY
|
192
|
+
2 DESC, 1
|
193
|
+
SQL
|
194
|
+
end
|
195
|
+
|
196
|
+
def index_caching
|
197
|
+
select_all <<-SQL
|
198
|
+
SELECT
|
199
|
+
indexrelname AS index,
|
200
|
+
relname AS table,
|
201
|
+
CASE WHEN idx_blks_hit + idx_blks_read = 0 THEN
|
202
|
+
0
|
203
|
+
ELSE
|
204
|
+
ROUND(1.0 * idx_blks_hit / (idx_blks_hit + idx_blks_read), 2)
|
205
|
+
END AS hit_rate
|
206
|
+
FROM
|
207
|
+
pg_statio_user_indexes
|
208
|
+
ORDER BY
|
209
|
+
3 DESC, 1
|
210
|
+
SQL
|
211
|
+
end
|
212
|
+
|
170
213
|
def index_usage
|
171
214
|
select_all <<-SQL
|
172
215
|
SELECT
|
@@ -288,24 +331,44 @@ module PgHero
|
|
288
331
|
select_all("SELECT COUNT(*) FROM pg_stat_activity WHERE pid <> pg_backend_pid()").first["count"].to_i
|
289
332
|
end
|
290
333
|
|
291
|
-
def connection_sources
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
334
|
+
def connection_sources(options = {})
|
335
|
+
if options[:by_database]
|
336
|
+
select_all <<-SQL
|
337
|
+
SELECT
|
338
|
+
application_name AS source,
|
339
|
+
client_addr AS ip,
|
340
|
+
datname AS database,
|
341
|
+
COUNT(*) AS total_connections
|
342
|
+
FROM
|
343
|
+
pg_stat_activity
|
344
|
+
WHERE
|
345
|
+
pid <> pg_backend_pid()
|
346
|
+
GROUP BY
|
347
|
+
1, 2, 3
|
348
|
+
ORDER BY
|
349
|
+
COUNT(*) DESC,
|
350
|
+
application_name ASC,
|
351
|
+
client_addr ASC
|
352
|
+
SQL
|
353
|
+
else
|
354
|
+
select_all <<-SQL
|
355
|
+
SELECT
|
356
|
+
application_name AS source,
|
357
|
+
client_addr AS ip,
|
358
|
+
COUNT(*) AS total_connections
|
359
|
+
FROM
|
360
|
+
pg_stat_activity
|
361
|
+
WHERE
|
362
|
+
pid <> pg_backend_pid()
|
363
|
+
GROUP BY
|
364
|
+
application_name,
|
365
|
+
ip
|
366
|
+
ORDER BY
|
367
|
+
COUNT(*) DESC,
|
368
|
+
application_name ASC,
|
369
|
+
client_addr ASC
|
370
|
+
SQL
|
371
|
+
end
|
309
372
|
end
|
310
373
|
|
311
374
|
# http://www.postgresql.org/docs/9.1/static/routine-vacuuming.html#VACUUM-FOR-WRAPAROUND
|
@@ -332,7 +395,7 @@ module PgHero
|
|
332
395
|
def autovacuum_danger
|
333
396
|
select_all <<-SQL
|
334
397
|
SELECT
|
335
|
-
c.oid::regclass as table,
|
398
|
+
c.oid::regclass::text as table,
|
336
399
|
(SELECT setting FROM pg_settings WHERE name = 'autovacuum_freeze_max_age')::int -
|
337
400
|
GREATEST(AGE(c.relfrozenxid), AGE(t.relfrozenxid)) AS transactions_before_autovacuum
|
338
401
|
FROM
|
@@ -347,6 +410,21 @@ module PgHero
|
|
347
410
|
SQL
|
348
411
|
end
|
349
412
|
|
413
|
+
def maintenance_info
|
414
|
+
select_all <<-SQL
|
415
|
+
SELECT
|
416
|
+
relname AS table,
|
417
|
+
last_vacuum,
|
418
|
+
last_autovacuum,
|
419
|
+
last_analyze,
|
420
|
+
last_autoanalyze
|
421
|
+
FROM
|
422
|
+
pg_stat_user_tables
|
423
|
+
ORDER BY
|
424
|
+
relname ASC
|
425
|
+
SQL
|
426
|
+
end
|
427
|
+
|
350
428
|
def kill(pid)
|
351
429
|
execute("SELECT pg_terminate_backend(#{pid.to_i})").first["pg_terminate_backend"] == "t"
|
352
430
|
end
|
@@ -387,11 +465,19 @@ module PgHero
|
|
387
465
|
query_stats << value
|
388
466
|
end
|
389
467
|
sort = options[:sort] || "total_minutes"
|
390
|
-
query_stats.sort_by { |q| -q[sort] }.first(100)
|
468
|
+
query_stats = query_stats.sort_by { |q| -q[sort] }.first(100)
|
469
|
+
if options[:min_average_time]
|
470
|
+
query_stats.reject! { |q| q["average_time"].to_f < options[:min_average_time] }
|
471
|
+
end
|
472
|
+
if options[:min_calls]
|
473
|
+
query_stats.reject! { |q| q["calls"].to_i < options[:min_calls] }
|
474
|
+
end
|
475
|
+
query_stats
|
391
476
|
end
|
392
477
|
|
393
478
|
def slow_queries(options = {})
|
394
|
-
query_stats
|
479
|
+
query_stats = options[:query_stats] || self.query_stats(options.except(:query_stats))
|
480
|
+
query_stats.select { |q| q["calls"].to_i >= slow_query_calls.to_i && q["average_time"].to_i >= slow_query_ms.to_i }
|
395
481
|
end
|
396
482
|
|
397
483
|
def query_stats_available?
|
@@ -495,6 +581,14 @@ module PgHero
|
|
495
581
|
rds_stats("ReplicaLag")
|
496
582
|
end
|
497
583
|
|
584
|
+
def read_iops_stats
|
585
|
+
rds_stats("ReadIOPS")
|
586
|
+
end
|
587
|
+
|
588
|
+
def write_iops_stats
|
589
|
+
rds_stats("WriteIOPS")
|
590
|
+
end
|
591
|
+
|
498
592
|
def rds_stats(metric_name)
|
499
593
|
if system_stats_enabled?
|
500
594
|
client =
|
@@ -655,8 +749,337 @@ module PgHero
|
|
655
749
|
select_all("SELECT EXTRACT(EPOCH FROM NOW() - pg_last_xact_replay_timestamp()) AS replication_lag").first["replication_lag"].to_f
|
656
750
|
end
|
657
751
|
|
752
|
+
# TODO parse array properly
|
753
|
+
# http://stackoverflow.com/questions/2204058/list-columns-with-indexes-in-postgresql
|
754
|
+
def indexes
|
755
|
+
select_all( <<-SQL
|
756
|
+
SELECT
|
757
|
+
t.relname AS table,
|
758
|
+
ix.relname AS name,
|
759
|
+
regexp_replace(pg_get_indexdef(indexrelid), '.*\\((.*)\\)', '\\1') AS columns,
|
760
|
+
regexp_replace(pg_get_indexdef(indexrelid), '.* USING (.*) \\(.*', '\\1') AS using,
|
761
|
+
indisunique AS unique,
|
762
|
+
indisprimary AS primary,
|
763
|
+
indisvalid AS valid,
|
764
|
+
indexprs::text,
|
765
|
+
indpred::text
|
766
|
+
FROM
|
767
|
+
pg_index i
|
768
|
+
INNER JOIN
|
769
|
+
pg_class t ON t.oid = i.indrelid
|
770
|
+
INNER JOIN
|
771
|
+
pg_class ix ON ix.oid = i.indexrelid
|
772
|
+
ORDER BY
|
773
|
+
1, 2
|
774
|
+
SQL
|
775
|
+
).map { |v| v["columns"] = v["columns"].split(", "); v }
|
776
|
+
end
|
777
|
+
|
778
|
+
def duplicate_indexes
|
779
|
+
indexes = []
|
780
|
+
|
781
|
+
indexes_by_table = self.indexes.group_by { |i| i["table"] }
|
782
|
+
indexes_by_table.values.flatten.select { |i| i["primary"] == "f" && i["unique"] == "f" && !i["indexprs"] && !i["indpred"] && i["valid"] == "t" }.each do |index|
|
783
|
+
covering_index = indexes_by_table[index["table"]].find { |i| index_covers?(i["columns"], index["columns"]) && i["using"] == index["using"] && i["name"] != index["name"] && i["valid"] == "t" }
|
784
|
+
if covering_index
|
785
|
+
indexes << {"unneeded_index" => index, "covering_index" => covering_index}
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
indexes.sort_by { |i| ui = i["unneeded_index"]; [ui["table"], ui["columns"]] }
|
790
|
+
end
|
791
|
+
|
792
|
+
def suggested_indexes_enabled?
|
793
|
+
defined?(PgQuery) && query_stats_enabled?
|
794
|
+
end
|
795
|
+
|
796
|
+
# TODO clean this mess
|
797
|
+
def suggested_indexes_by_query(options = {})
|
798
|
+
best_indexes = {}
|
799
|
+
|
800
|
+
if suggested_indexes_enabled?
|
801
|
+
# get most time-consuming queries
|
802
|
+
queries = options[:queries] || (options[:query_stats] || self.query_stats(historical: true, start_at: 24.hours.ago)).map { |qs| qs["query"] }
|
803
|
+
|
804
|
+
# get best indexes for queries
|
805
|
+
best_indexes = best_index_helper(queries)
|
806
|
+
|
807
|
+
if best_indexes.any?
|
808
|
+
existing_columns = Hash.new { |hash, key| hash[key] = [] }
|
809
|
+
self.indexes.each do |i|
|
810
|
+
existing_columns[i["table"]] << i["columns"]
|
811
|
+
end
|
812
|
+
|
813
|
+
best_indexes.each do |query, best_index|
|
814
|
+
if best_index[:found]
|
815
|
+
index = best_index[:index]
|
816
|
+
covering_index = existing_columns[index[:table]].find { |e| index_covers?(e, index[:columns]) }
|
817
|
+
if covering_index
|
818
|
+
best_index[:covering_index] = covering_index
|
819
|
+
best_index[:explanation] = "Covered by index on (#{covering_index.join(", ")})"
|
820
|
+
end
|
821
|
+
end
|
822
|
+
end
|
823
|
+
end
|
824
|
+
end
|
825
|
+
|
826
|
+
best_indexes
|
827
|
+
end
|
828
|
+
|
829
|
+
def suggested_indexes(options = {})
|
830
|
+
indexes = []
|
831
|
+
|
832
|
+
(options[:suggested_indexes_by_query] || suggested_indexes_by_query(options)).select { |s, i| i[:found] && !i[:covering_index] }.group_by { |s, i| i[:index] }.each do |index, group|
|
833
|
+
indexes << index.merge(queries: group.map(&:first))
|
834
|
+
end
|
835
|
+
|
836
|
+
indexes.sort_by { |i| [i[:table], i[:columns]] }
|
837
|
+
end
|
838
|
+
|
839
|
+
def autoindex(options = {})
|
840
|
+
suggested_indexes.each do |index|
|
841
|
+
p index
|
842
|
+
if options[:create]
|
843
|
+
connection.execute("CREATE INDEX CONCURRENTLY ON #{quote_table_name(index[:table])} (#{index[:columns].map { |c| quote_table_name(c) }.join(",")})")
|
844
|
+
end
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
848
|
+
def autoindex_all(options = {})
|
849
|
+
config["databases"].keys.each do |database|
|
850
|
+
with(database) do
|
851
|
+
puts "Autoindexing #{database}..."
|
852
|
+
autoindex(options)
|
853
|
+
end
|
854
|
+
end
|
855
|
+
end
|
856
|
+
|
857
|
+
def best_index(statement, options = {})
|
858
|
+
best_index_helper([statement])[statement]
|
859
|
+
end
|
860
|
+
|
861
|
+
def column_stats(options = {})
|
862
|
+
tables = options[:table] ? Array(options[:table]) : nil
|
863
|
+
select_all <<-SQL
|
864
|
+
SELECT
|
865
|
+
tablename AS table,
|
866
|
+
attname AS column,
|
867
|
+
null_frac,
|
868
|
+
n_distinct,
|
869
|
+
n_live_tup
|
870
|
+
FROM
|
871
|
+
pg_stats
|
872
|
+
INNER JOIN
|
873
|
+
pg_class ON pg_class.relname = pg_stats.tablename
|
874
|
+
INNER JOIN
|
875
|
+
pg_stat_user_tables ON pg_class.relname = pg_stat_user_tables.relname
|
876
|
+
WHERE
|
877
|
+
#{tables ? "pg_class.relname IN (#{tables.map { |t| quote(t) }.join(", ")})" : "1 = 1"}
|
878
|
+
ORDER BY
|
879
|
+
1, 2
|
880
|
+
SQL
|
881
|
+
end
|
882
|
+
|
658
883
|
private
|
659
884
|
|
885
|
+
def best_index_helper(statements)
|
886
|
+
indexes = {}
|
887
|
+
|
888
|
+
# see if this is a query we understand and can use
|
889
|
+
parts = {}
|
890
|
+
statements.each do |statement|
|
891
|
+
parts[statement] = best_index_structure(statement)
|
892
|
+
end
|
893
|
+
|
894
|
+
# get stats about columns for relevant tables
|
895
|
+
tables = parts.values.map { |t| t[:table] }.uniq
|
896
|
+
if tables.any?
|
897
|
+
column_stats = self.column_stats(table: tables).group_by { |i| i["table"] }
|
898
|
+
end
|
899
|
+
|
900
|
+
# find best index based on query structure and column stats
|
901
|
+
parts.each do |statement, structure|
|
902
|
+
index = {found: false}
|
903
|
+
|
904
|
+
if structure[:error]
|
905
|
+
index[:explanation] = structure[:error]
|
906
|
+
elsif structure[:table].start_with?("pg_")
|
907
|
+
index[:explanation] = "System table"
|
908
|
+
else
|
909
|
+
index[:structure] = structure
|
910
|
+
|
911
|
+
table = structure[:table]
|
912
|
+
where = structure[:where]
|
913
|
+
sort = structure[:sort]
|
914
|
+
|
915
|
+
ranks = Hash[column_stats[table].to_a.map { |r| [r["column"], r] }]
|
916
|
+
|
917
|
+
columns = (where + sort).map { |c| c[:column] }.uniq
|
918
|
+
|
919
|
+
if columns.any? && columns.all? { |c| ranks[c] }
|
920
|
+
first_desc = sort.index { |c| c[:direction] == "desc" }
|
921
|
+
if first_desc
|
922
|
+
sort = sort.first(first_desc + 1)
|
923
|
+
end
|
924
|
+
where = where.sort_by { |c| [row_estimates(ranks[c[:column]], nil, c[:op]), c[:column]] } + sort
|
925
|
+
|
926
|
+
index[:row_estimates] = Hash[where.map { |c| [c[:column], row_estimates(ranks[c[:column]], nil, c[:op]).round] }]
|
927
|
+
|
928
|
+
rows_left = ranks[where.first[:column]]["n_live_tup"].to_i
|
929
|
+
index[:rows] = rows_left
|
930
|
+
|
931
|
+
# no index needed if less than 500 rows
|
932
|
+
if rows_left >= 500
|
933
|
+
|
934
|
+
# if most values are unique, no need to index others
|
935
|
+
final_where = []
|
936
|
+
prev_rows_left = [rows_left]
|
937
|
+
where.each do |c|
|
938
|
+
next if final_where.include?(c[:column])
|
939
|
+
final_where << c[:column]
|
940
|
+
rows_left = row_estimates(ranks[c[:column]], rows_left, c[:op])
|
941
|
+
prev_rows_left << rows_left
|
942
|
+
if rows_left < 50
|
943
|
+
break
|
944
|
+
end
|
945
|
+
end
|
946
|
+
|
947
|
+
index[:row_progression] = prev_rows_left.map(&:round)
|
948
|
+
|
949
|
+
# if the last indexes don't give us much, don't include
|
950
|
+
if prev_rows_left.last > 50
|
951
|
+
prev_rows_left.reverse!
|
952
|
+
(prev_rows_left.size - 1).times do |i|
|
953
|
+
if prev_rows_left[i] > prev_rows_left[i + 1] * 0.1
|
954
|
+
final_where.pop
|
955
|
+
else
|
956
|
+
break
|
957
|
+
end
|
958
|
+
end
|
959
|
+
end
|
960
|
+
|
961
|
+
if final_where.any?
|
962
|
+
index[:found] = true
|
963
|
+
index[:index] = {table: table, columns: final_where}
|
964
|
+
end
|
965
|
+
else
|
966
|
+
index[:explanation] = "No index needed if less than 500 rows"
|
967
|
+
end
|
968
|
+
else
|
969
|
+
index[:explanation] = "No columns to index"
|
970
|
+
end
|
971
|
+
end
|
972
|
+
|
973
|
+
indexes[statement] = index
|
974
|
+
end
|
975
|
+
|
976
|
+
indexes
|
977
|
+
end
|
978
|
+
|
979
|
+
def best_index_structure(statement)
|
980
|
+
begin
|
981
|
+
tree = PgQuery.parse(statement).parsetree
|
982
|
+
rescue PgQuery::ParseError
|
983
|
+
return {error: "Parse error"}
|
984
|
+
end
|
985
|
+
return {error: "Unknown structure"} unless tree.size == 1
|
986
|
+
|
987
|
+
tree = tree.first
|
988
|
+
table = parse_table(tree) rescue nil
|
989
|
+
unless table
|
990
|
+
error =
|
991
|
+
case tree.keys.first
|
992
|
+
when "INSERT INTO"
|
993
|
+
"INSERT statement"
|
994
|
+
when "SET"
|
995
|
+
"SET statement"
|
996
|
+
else
|
997
|
+
"Unknown structure"
|
998
|
+
end
|
999
|
+
return {error: error}
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
select = tree["SELECT"] || tree["DELETE FROM"] || tree["UPDATE"]
|
1003
|
+
where = (select["whereClause"] ? parse_where(select["whereClause"]) : []) rescue nil
|
1004
|
+
return {error: "Unknown structure"} unless where
|
1005
|
+
|
1006
|
+
sort = (select["sortClause"] ? parse_sort(select["sortClause"]) : []) rescue nil
|
1007
|
+
return {error: "Unknown structure"} unless sort
|
1008
|
+
|
1009
|
+
{table: table, where: where, sort: sort}
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
def index_covers?(indexed_columns, columns)
|
1013
|
+
indexed_columns.first(columns.size) == columns
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
# TODO better row estimation
|
1017
|
+
# http://www.postgresql.org/docs/current/static/row-estimation-examples.html
|
1018
|
+
def row_estimates(stats, rows_left = nil, op = nil)
|
1019
|
+
rows_left ||= stats["n_live_tup"].to_i
|
1020
|
+
case op
|
1021
|
+
when "null"
|
1022
|
+
rows_left * stats["null_frac"].to_f
|
1023
|
+
when "not_null"
|
1024
|
+
rows_left * (1 - stats["null_frac"].to_f)
|
1025
|
+
else
|
1026
|
+
rows_left *= (1 - stats["null_frac"].to_f)
|
1027
|
+
if stats["n_distinct"].to_f == 0
|
1028
|
+
0
|
1029
|
+
elsif stats["n_distinct"].to_f < 0
|
1030
|
+
if stats["n_live_tup"].to_i > 0
|
1031
|
+
(-1 / stats["n_distinct"].to_f) * (rows_left / stats["n_live_tup"].to_f)
|
1032
|
+
else
|
1033
|
+
0
|
1034
|
+
end
|
1035
|
+
else
|
1036
|
+
rows_left / stats["n_distinct"].to_f
|
1037
|
+
end
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
def parse_table(tree)
|
1042
|
+
case tree.keys.first
|
1043
|
+
when "SELECT"
|
1044
|
+
tree["SELECT"]["fromClause"].first["RANGEVAR"]["relname"]
|
1045
|
+
when "DELETE FROM"
|
1046
|
+
tree["DELETE FROM"]["relation"]["RANGEVAR"]["relname"]
|
1047
|
+
when "UPDATE"
|
1048
|
+
tree["UPDATE"]["relation"]["RANGEVAR"]["relname"]
|
1049
|
+
else
|
1050
|
+
nil
|
1051
|
+
end
|
1052
|
+
end
|
1053
|
+
|
1054
|
+
# TODO capture values
|
1055
|
+
def parse_where(tree)
|
1056
|
+
if tree["AEXPR AND"]
|
1057
|
+
left = parse_where(tree["AEXPR AND"]["lexpr"])
|
1058
|
+
right = parse_where(tree["AEXPR AND"]["rexpr"])
|
1059
|
+
if left && right
|
1060
|
+
left + right
|
1061
|
+
end
|
1062
|
+
elsif tree["AEXPR"] && ["="].include?(tree["AEXPR"]["name"].first)
|
1063
|
+
[{column: tree["AEXPR"]["lexpr"]["COLUMNREF"]["fields"].last, op: tree["AEXPR"]["name"].first}]
|
1064
|
+
elsif tree["AEXPR IN"] && tree["AEXPR IN"]["name"].first == "="
|
1065
|
+
[{column: tree["AEXPR IN"]["lexpr"]["COLUMNREF"]["fields"].last, op: "in"}]
|
1066
|
+
elsif tree["NULLTEST"]
|
1067
|
+
op = tree["NULLTEST"]["nulltesttype"] == 1 ? "not_null" : "null"
|
1068
|
+
[{column: tree["NULLTEST"]["arg"]["COLUMNREF"]["fields"].last, op: op}]
|
1069
|
+
else
|
1070
|
+
nil
|
1071
|
+
end
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
def parse_sort(sort_clause)
|
1075
|
+
sort_clause.map do |v|
|
1076
|
+
{
|
1077
|
+
column: v["SORTBY"]["node"]["COLUMNREF"]["fields"].last,
|
1078
|
+
direction: v["SORTBY"]["sortby_dir"] == 2 ? "desc" : "asc"
|
1079
|
+
}
|
1080
|
+
end
|
1081
|
+
end
|
1082
|
+
|
660
1083
|
def table_grant_commands(privilege, tables, user)
|
661
1084
|
tables.map do |table|
|
662
1085
|
"GRANT #{privilege} ON TABLE #{table} TO #{user}"
|