sql_optimizer 0.1.0 → 0.1.1
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 +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:
|