sql_optimizer 0.1.0 → 0.1.1

This diff has not been reviewed by any users.
Log in in order to be able to vote.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab7497b7fd4fbc615fc5586afe11df80da8b6032812374009a12c0b18d095957
4
- data.tar.gz: 9b5b25829db414590d19bfa701337d789108a8e240148f0754394890fd3de2c1
3
+ metadata.gz: '096f95c4b92b1cf43b3406da1dca462622ad9c0883528bec6b9b004c8574fb21'
4
+ data.tar.gz: ee6ae051ca2eb5db5aa5751f33bfba62089b9a2347fbfb9864b3d461717aa3ed
5
5
  SHA512:
6
- metadata.gz: a6cb066577dd57672c671fb54f8eb36aaf52c84ae313d4adeacd72f67fcdf953555ad2ecd4202f5bb00feef1d774679de56c3d4cd7e8d138c986ded03366e1f3
7
- data.tar.gz: 664819cb4acf6c94c8c622c25e259187e80cbc325750154007bfe432fd8eaecc130947e15d89c40b26280e0bf9e464b57b8124300a8251afc05401184b6f7a83
6
+ metadata.gz: 570b079b36dc8cfc9a218c117dd6e13d21438589b57c99080f925067ac505c42c1f7b26a788214787c229db1f8caa68b2092a630af321aa8ccab4f661cef0472
7
+ data.tar.gz: b844e1b144de765d464c64506c93e9e0a68989e10c4fb1cec4c5e2a4f5459a0cd95f73698be7c71dae57ce0d73236df2a769936279f4294b4c49a25a35c123c4
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # SqlOptimizer
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/sql_optimizer`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Write usage instructions here
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
@@ -1,5 +1,5 @@
1
1
  module SqlOptimizer
2
2
 
3
- VERSION = '0.1.0'.freeze
3
+ VERSION = '0.1.1'.freeze
4
4
 
5
5
  end
@@ -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
- # # 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'] = "TODO: Set to 'http://mygemserver.com'"
20
- #
21
- # spec.metadata['homepage_uri'] = spec.homepage
22
- # spec.metadata['source_code_uri'] = "TODO: Put your gem's public repo URL here."
23
- # spec.metadata['changelog_uri'] = "TODO: Put your gem's CHANGELOG.md URL here."
24
- # else
25
- # raise 'RubyGems 2.0 or newer is required to protect against ' \
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.0
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-03-05 00:00:00.000000000 Z
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: