sql_optimizer 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +20 -4
- data/app/controllers/sql_optimizer_controller.rb +35 -0
- data/app/views/sql_optimizer/index.slim +118 -0
- data/lib/generators/sql_optimizer_generator.rb +57 -0
- data/lib/sql_optimizer/analyze.rb +54 -0
- data/lib/sql_optimizer/version.rb +1 -1
- data/sql_optimizer.gemspec +10 -12
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '096f95c4b92b1cf43b3406da1dca462622ad9c0883528bec6b9b004c8574fb21'
|
4
|
+
data.tar.gz: ee6ae051ca2eb5db5aa5751f33bfba62089b9a2347fbfb9864b3d461717aa3ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 570b079b36dc8cfc9a218c117dd6e13d21438589b57c99080f925067ac505c42c1f7b26a788214787c229db1f8caa68b2092a630af321aa8ccab4f661cef0472
|
7
|
+
data.tar.gz: b844e1b144de765d464c64506c93e9e0a68989e10c4fb1cec4c5e2a4f5459a0cd95f73698be7c71dae57ce0d73236df2a769936279f4294b4c49a25a35c123c4
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# SqlOptimizer
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
SqlOptimizer this is a gem for query optimization in your app. You can use two our method to check you query: `anayle` and `check_n_plus_one`. This is not so much, but we'll add more in the future. Also you can visit [localhost:3000/sql_optimizer](http://localhost:3000/sql_optimizer) to see information about queries in your app.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -19,10 +17,28 @@ And then execute:
|
|
19
17
|
Or install it yourself as:
|
20
18
|
|
21
19
|
$ gem install sql_optimizer
|
20
|
+
|
21
|
+
Run `rails g sql_optimizer` to load all files and migrations
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
25
|
-
|
25
|
+
Visit [localhost:3000/sql_optimizer](http://localhost:3000/sql_optimizer) to see information about queries
|
26
|
+
|
27
|
+
### Analyze
|
28
|
+
|
29
|
+
Add to your query `analyze` method to see full information about queries
|
30
|
+
For example:
|
31
|
+
```
|
32
|
+
MyModel.scope.analyze
|
33
|
+
```
|
34
|
+
|
35
|
+
### Check n+1
|
36
|
+
|
37
|
+
Add to your query `check_n_plus_one` method to see if query has n+1 and if has, you'll get recommendation how to omit this.
|
38
|
+
For example:
|
39
|
+
```
|
40
|
+
MyModel.scope.check_n_plus_one
|
41
|
+
```
|
26
42
|
|
27
43
|
## Development
|
28
44
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class SqlOptimizerController < ApplicationController
|
2
|
+
|
3
|
+
layout false
|
4
|
+
|
5
|
+
def index
|
6
|
+
@query_logs = QueryLog.where.not(source: nil)
|
7
|
+
@popular_queries = @query_logs.group_by(&:query).sort_by { |_, val| val.size }.last(3)
|
8
|
+
@max_query = @query_logs.order(duration: :desc).first
|
9
|
+
@min_query = @query_logs.order(:duration).first
|
10
|
+
end
|
11
|
+
|
12
|
+
def graph
|
13
|
+
@query_logs = QueryLog.where.not(source: nil)
|
14
|
+
render json: collect_graph.to_json
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def collect_graph
|
20
|
+
@query_logs.group_by(&:query).first(10).map.with_index do |query, i|
|
21
|
+
durations = query[1].map(&:duration)
|
22
|
+
avg_time = durations.reduce(:+) / durations.size.to_f
|
23
|
+
{
|
24
|
+
index: i,
|
25
|
+
avg_time: avg_time.round(2),
|
26
|
+
data: {
|
27
|
+
query: query[1].first.query,
|
28
|
+
name: query[1].first.source,
|
29
|
+
size: query[1].size
|
30
|
+
}
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
doctype html
|
2
|
+
html
|
3
|
+
head
|
4
|
+
title SQL Logs Dashboard
|
5
|
+
meta http-equiv="Content-Type" content="text/html; charset=UTF-8"
|
6
|
+
script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"
|
7
|
+
link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
8
|
+
script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"
|
9
|
+
script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"
|
10
|
+
body
|
11
|
+
.header
|
12
|
+
a.logo SQL Logs Dashboard
|
13
|
+
- if @query_logs.any?
|
14
|
+
.main
|
15
|
+
.custom-column
|
16
|
+
#chart style="height: 250px; width: 700px;"
|
17
|
+
.custom-column
|
18
|
+
.wrap
|
19
|
+
h1 Три найчастiшi запити
|
20
|
+
- @popular_queries.each do |pq|
|
21
|
+
p = pq.first
|
22
|
+
.main
|
23
|
+
.custom-column
|
24
|
+
.wrap
|
25
|
+
h1 Запит з найдовшим запитом
|
26
|
+
p Час: #{@max_query.duration}
|
27
|
+
p Запит: #{@max_query.query}
|
28
|
+
.custom-column
|
29
|
+
.wrap
|
30
|
+
h1 Запит з найменшим запитом
|
31
|
+
p Час: #{@min_query.duration}
|
32
|
+
p Запит: #{@min_query.query}
|
33
|
+
|
34
|
+
javascript:
|
35
|
+
$(document).ready(function () {
|
36
|
+
$.ajax({
|
37
|
+
type: 'GET',
|
38
|
+
datatype: "json",
|
39
|
+
url: '/graph',
|
40
|
+
success: function (rendered_data) {
|
41
|
+
initGraph(rendered_data);
|
42
|
+
}
|
43
|
+
});
|
44
|
+
|
45
|
+
var initGraph = function (data) {
|
46
|
+
new Morris.Line({
|
47
|
+
element: 'chart',
|
48
|
+
data: data,
|
49
|
+
hoverCallback: function (index, options, content, _row) {
|
50
|
+
// debugger
|
51
|
+
content = `<div class='morris-hover-row-label'>Name: ${options['data'][index]["data"]["name"]}</div>
|
52
|
+
<div class='morris-hover-point' style='color: #0b62a4'>
|
53
|
+
Query: ${options['data'][index]["data"]["query"]}</br>
|
54
|
+
Avg time: ${options['data'][index]["avg_time"]}</br>
|
55
|
+
Number of calls: ${options['data'][index]["data"]["size"]}</br>
|
56
|
+
</div>`
|
57
|
+
return(content);
|
58
|
+
// $(".morris-hover").html(content);
|
59
|
+
},
|
60
|
+
xkey: 'index',
|
61
|
+
ykeys: ['avg_time'],
|
62
|
+
labels: ['avg_time']
|
63
|
+
});
|
64
|
+
}
|
65
|
+
});
|
66
|
+
|
67
|
+
scss:
|
68
|
+
.header {
|
69
|
+
overflow: hidden;
|
70
|
+
background-color: #f1f1f1;
|
71
|
+
padding: 20px 10px;
|
72
|
+
cursor: pointer;
|
73
|
+
}
|
74
|
+
|
75
|
+
.header a {
|
76
|
+
float: left;
|
77
|
+
color: black;
|
78
|
+
text-align: center;
|
79
|
+
padding: 12px;
|
80
|
+
text-decoration: none;
|
81
|
+
font-size: 18px;
|
82
|
+
line-height: 25px;
|
83
|
+
border-radius: 4px;
|
84
|
+
}
|
85
|
+
|
86
|
+
.header a.logo {
|
87
|
+
font-size: 25px;
|
88
|
+
font-weight: bold;
|
89
|
+
}
|
90
|
+
|
91
|
+
.main {
|
92
|
+
display: flex;
|
93
|
+
flex-direction: row;
|
94
|
+
width: 95%;
|
95
|
+
padding: 20px;
|
96
|
+
|
97
|
+
.custom-column {
|
98
|
+
width: 50%;
|
99
|
+
margin: 20px;
|
100
|
+
|
101
|
+
.wrap {
|
102
|
+
display: flex;
|
103
|
+
flex-direction: column;
|
104
|
+
border: 1px solid silver;
|
105
|
+
width: 100%;
|
106
|
+
height: 250px;
|
107
|
+
padding: 15px;
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class SqlOptimizerGenerator < Rails::Generators::Base
|
2
|
+
|
3
|
+
desc 'This generator creates an migration file, model file, sql_optimizer file and append to routes lines'
|
4
|
+
def migrate_query_logs
|
5
|
+
migration_number = Dir.glob("#{Rails.root}/db/migrate/*").max_by { |name| name[/\d+/].to_i }[/\d+/].to_i + 1
|
6
|
+
create_file "db/migrate/#{migration_number}_create_query_logs.rb", migration_content
|
7
|
+
create_file 'app/models/query_log.rb', model_content
|
8
|
+
application(nil, env: :development) { initializer_content }
|
9
|
+
route route_content
|
10
|
+
rake 'db:migrate'
|
11
|
+
puts 'Done'
|
12
|
+
end
|
13
|
+
|
14
|
+
def migration_content
|
15
|
+
<<~RUBY
|
16
|
+
class CreateQueryLogs < ActiveRecord::Migration[6.0]
|
17
|
+
def change
|
18
|
+
create_table :query_logs do |t|
|
19
|
+
t.string :query
|
20
|
+
t.float :duration
|
21
|
+
t.string :source
|
22
|
+
t.bigint :follow_id
|
23
|
+
t.integer :n_plus_one_size, default: 1
|
24
|
+
|
25
|
+
t.timestamps
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
RUBY
|
30
|
+
end
|
31
|
+
|
32
|
+
def model_content
|
33
|
+
<<~RUBY
|
34
|
+
class QueryLog < ApplicationRecord
|
35
|
+
end
|
36
|
+
RUBY
|
37
|
+
end
|
38
|
+
|
39
|
+
def route_content
|
40
|
+
<<~RUBY
|
41
|
+
if Rails.env.development?
|
42
|
+
get 'sql_optimizer', to: 'sql_optimizer#index'
|
43
|
+
get 'graph', to: 'sql_optimizer#graph'
|
44
|
+
end
|
45
|
+
|
46
|
+
RUBY
|
47
|
+
end
|
48
|
+
|
49
|
+
def initializer_content
|
50
|
+
<<~RUBY
|
51
|
+
# This is for tracking sql queries
|
52
|
+
QueryTrace.enable!
|
53
|
+
|
54
|
+
RUBY
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -16,6 +16,17 @@ module ActiveRecord
|
|
16
16
|
def analyze
|
17
17
|
exec_analyze(collecting_queries_for_explain { exec_queries })
|
18
18
|
end
|
19
|
+
|
20
|
+
def check_n_plus_one
|
21
|
+
query_logs = QueryLog.all
|
22
|
+
query_log = query_logs.find_by(follow_id: query_logs.ids)
|
23
|
+
if query_log.present?
|
24
|
+
to_include = query_log.query[/"(.*?)"/].delete('\"')
|
25
|
+
logger.debug "Add includes(:#{to_include}) to omit n+1 query"
|
26
|
+
else
|
27
|
+
logger.debug "n+1 query does'n find"
|
28
|
+
end
|
29
|
+
end
|
19
30
|
end
|
20
31
|
end
|
21
32
|
|
@@ -30,6 +41,7 @@ module ActiveRecord
|
|
30
41
|
end
|
31
42
|
msg << "\n"
|
32
43
|
msg << connection.analyze(sql, binds)
|
44
|
+
msg
|
33
45
|
end.join("\n")
|
34
46
|
|
35
47
|
def str.inspect
|
@@ -40,3 +52,45 @@ module ActiveRecord
|
|
40
52
|
end
|
41
53
|
end
|
42
54
|
end
|
55
|
+
|
56
|
+
module QueryTrace
|
57
|
+
def self.enable!
|
58
|
+
::ActiveRecord::LogSubscriber.send(:include, self)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.append_features(klass)
|
62
|
+
super
|
63
|
+
klass.class_eval do
|
64
|
+
unless method_defined?(:log_info_without_trace)
|
65
|
+
alias_method :log_info_without_trace, :sql
|
66
|
+
alias_method :sql, :log_info_with_trace
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def log_info_with_trace(event)
|
72
|
+
log_info_without_trace(event)
|
73
|
+
|
74
|
+
return if event.payload[:name].nil? ||
|
75
|
+
event.payload[:name] == 'SCHEMA' ||
|
76
|
+
event.payload[:name].include?('SchemaMigration') ||
|
77
|
+
%w[BEGIN COMMIT ROLLBACK].include?(event.payload[:sql].to_s) ||
|
78
|
+
!ActiveRecord::Base.connection.table_exists?(QueryLog.table_name) ||
|
79
|
+
event.payload[:sql].include?('query_logs')
|
80
|
+
|
81
|
+
logger = ActiveRecord::Base.logger
|
82
|
+
ActiveRecord::Base.logger = nil
|
83
|
+
query_logs = QueryLog.last(2)
|
84
|
+
if query_logs.last.present? && query_logs.last.query == event.payload[:sql]
|
85
|
+
query_logs.last.update(follow_id: query_logs.first.id,
|
86
|
+
n_plus_one_size: query_logs.last.n_plus_one_size + 1)
|
87
|
+
else
|
88
|
+
QueryLog.create(
|
89
|
+
query: event.payload[:sql],
|
90
|
+
source: event.payload[:name],
|
91
|
+
duration: event.duration.round(3)
|
92
|
+
)
|
93
|
+
end
|
94
|
+
ActiveRecord::Base.logger = logger
|
95
|
+
end
|
96
|
+
end
|
data/sql_optimizer.gemspec
CHANGED
@@ -13,18 +13,16 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.homepage = 'https://github.com/NikitaKorchma/sql_optimizer'
|
14
14
|
spec.license = 'MIT'
|
15
15
|
|
16
|
-
#
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
# 'public gem pushes.'
|
27
|
-
# end
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
20
|
+
# spec.metadata['homepage_uri'] = spec.homepage
|
21
|
+
# spec.metadata['source_code_uri'] = "TODO: Put your gem's public repo URL here."
|
22
|
+
# spec.metadata['changelog_uri'] = "TODO: Put your gem's CHANGELOG.md URL here."
|
23
|
+
else
|
24
|
+
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
|
25
|
+
end
|
28
26
|
|
29
27
|
# Specify which files should be added to the gem when it is released.
|
30
28
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sql_optimizer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nikita Korchma
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-05-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -68,8 +68,11 @@ files:
|
|
68
68
|
- LICENSE.txt
|
69
69
|
- README.md
|
70
70
|
- Rakefile
|
71
|
+
- app/controllers/sql_optimizer_controller.rb
|
72
|
+
- app/views/sql_optimizer/index.slim
|
71
73
|
- bin/console
|
72
74
|
- bin/setup
|
75
|
+
- lib/generators/sql_optimizer_generator.rb
|
73
76
|
- lib/sql_optimizer.rb
|
74
77
|
- lib/sql_optimizer/analyze.rb
|
75
78
|
- lib/sql_optimizer/version.rb
|
@@ -77,7 +80,8 @@ files:
|
|
77
80
|
homepage: https://github.com/NikitaKorchma/sql_optimizer
|
78
81
|
licenses:
|
79
82
|
- MIT
|
80
|
-
metadata:
|
83
|
+
metadata:
|
84
|
+
allowed_push_host: https://rubygems.org
|
81
85
|
post_install_message:
|
82
86
|
rdoc_options: []
|
83
87
|
require_paths:
|