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 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: