activerecord-analyze 0.3.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d52550c794a58f382c5e5e1d1b78463bc0766969e5e3d7818b494bbf7027924e
4
- data.tar.gz: 2a884a86a595021e5afc2c43083bb51287db83a5ac56da1ff4a9fe985c35870b
3
+ metadata.gz: bc476a2e8768df2625298f3b8d3ea7ed574fcfa86c85daf7bdc8793c7773a13c
4
+ data.tar.gz: acec1d6f2dd3fb3862cc7def00e18f3696f7de11060c6ce1ef91d6da67175375
5
5
  SHA512:
6
- metadata.gz: 7e10b5364ed42150d2c41f49c3378f0f4e44e886e9f6100ccc073500ed5185e2d97c39f74c3922544ad36ec1f52c7d947ec1385e573bcf994e0c96af461913ad
7
- data.tar.gz: 7d531a91c688a6c0127cc55b16f253ccbc9ada4a67b382c7dbfa25694b20ce5e0d33e18ed8ba7d81d5745e6a626c8e2b5769f6a4af52231b28187e461cd07bff
6
+ metadata.gz: 01ec2a670e10f7a44ae251bfd90f5f15f81ad8e1c280d99439a955e7553f30c67309cd3aeecd5e8c6a1bc844831e66f37bd5edbbc8f92aa5f284035eff943c30
7
+ data.tar.gz: 42f5fd9278bfb98caaf549d064a2d664c7c1d1cb1629fde8568144dbfd79d729ae6f3506915e26634d17fbf57ab1089d59c2f927697f218d0fa091c8976009f0
@@ -0,0 +1,30 @@
1
+ version: 2
2
+ jobs:
3
+ test:
4
+ docker:
5
+ - image: circleci/ruby:2.6.5
6
+ environment:
7
+ DATABASE_URL: postgresql://postgres:secret@localhost:5432/activerecord-analyze-test
8
+ - image: circleci/postgres:11.5
9
+ environment:
10
+ POSTGRES_USER: postgres
11
+ POSTGRES_DB: activerecord-analyze-test
12
+ POSTGRES_PASSWORD: secret
13
+ parallelism: 1
14
+ steps:
15
+ - checkout
16
+ - run: gem update --system
17
+ - run: gem install bundler
18
+ - run: bundle install --path vendor/bundle
19
+ - run: sudo apt-get update
20
+ - run: sudo apt install postgresql-client-11
21
+ - run: dockerize -wait tcp://localhost:5432 -timeout 1m
22
+ - run:
23
+ name: Run specs
24
+ command: |
25
+ bundle exec rspec spec/
26
+ workflows:
27
+ version: 2
28
+ test:
29
+ jobs:
30
+ - test
data/README.md CHANGED
@@ -1,7 +1,13 @@
1
- # ActiveRecord Analyze
1
+ # ActiveRecord Analyze [![Gem Version](https://badge.fury.io/rb/activerecord-analyze.svg)](https://badge.fury.io/rb/ruby-pg-extras) [![CircleCI](https://circleci.com/gh/pawurb/activerecord-analyze.svg?style=svg)](https://circleci.com/gh/pawurb/activerecord-analyze)
2
2
 
3
3
  This gem adds an `analyze` method to Active Record query objects. It executes `EXPLAIN ANALYZE` on a query SQL.
4
4
 
5
+ You can check out this blog post for more info on how to [debug and fix slow queries in Rails apps](https://pawelurbanek.com/slow-rails-queries).
6
+
7
+ The following `format` options are supported `:json, :hash, :yaml, :text, :xml`. Especially the `:json` format is useful because it let's you visualize a query plan using [a visualizer tool](https://tatiyants.com/pev/#/plans/new).
8
+
9
+ ![PG Query visualizer plan](https://raw.githubusercontent.com/pawurb/activerecord-analyze/master/query-plan.png)
10
+
5
11
  ## Installation
6
12
 
7
13
  In your Gemfile:
@@ -12,6 +18,97 @@ gem 'activerecord-analyze'
12
18
 
13
19
  ```
14
20
 
21
+ ## Options
22
+
23
+ The `analyze` method supports the following EXPLAIN query options ([PostgreSQL docs reference](https://www.postgresql.org/docs/12/sql-explain.html)):
24
+
25
+ ```
26
+ BUFFERS [ boolean ] (default: true)
27
+ VERBOSE [ boolean ] (default: true)
28
+ COSTS [ boolean ] (default: false)
29
+ SETTINGS [ boolean ] (default: false)
30
+ TIMING [ boolean ] (default: false)
31
+ SUMMARY [ boolean ] (default: false)
32
+ FORMAT { TEXT | XML | JSON | YAML }
33
+ ```
34
+
35
+ You can execute it like that:
36
+
37
+ ```ruby
38
+
39
+ User.all.analyze(
40
+ format: :json,
41
+ verbose: true,
42
+ costs: true,
43
+ settings: true,
44
+ buffers: true,
45
+ timing: true,
46
+ summary: true
47
+ )
48
+
49
+ # EXPLAIN (FORMAT JSON, ANALYZE, VERBOSE, COSTS, SETTINGS, BUFFERS, TIMING, SUMMARY)
50
+ # SELECT "users".* FROM "users"
51
+ # [
52
+ # {
53
+ # "Plan": {
54
+ # "Node Type": "Seq Scan",
55
+ # "Parallel Aware": false,
56
+ # "Relation Name": "users",
57
+ # "Schema": "public",
58
+ # "Alias": "users",
59
+ # "Startup Cost": 0.00,
60
+ # "Total Cost": 11.56,
61
+ # "Plan Rows": 520,
62
+ # "Plan Width": 127,
63
+ # "Actual Startup Time": 0.006,
64
+ # "Actual Total Time": 0.007,
65
+ # "Actual Rows": 2,
66
+ # "Actual Loops": 1,
67
+ # "Output": ["id", "team_id", "email"],
68
+ # "Shared Hit Blocks": 1,
69
+ # "Shared Read Blocks": 0,
70
+ # "Shared Dirtied Blocks": 0,
71
+ # "Shared Written Blocks": 0,
72
+ # "Local Hit Blocks": 0,
73
+ # "Local Read Blocks": 0,
74
+ # "Local Dirtied Blocks": 0,
75
+ # "Local Written Blocks": 0,
76
+ # "Temp Read Blocks": 0,
77
+ # "Temp Written Blocks": 0,
78
+ # "I/O Read Time": 0.000,
79
+ # "I/O Write Time": 0.000
80
+ # },
81
+ # "Settings": {
82
+ # "cpu_index_tuple_cost": "0.001",
83
+ # "cpu_operator_cost": "0.0005",
84
+ # "cpu_tuple_cost": "0.003",
85
+ # "effective_cache_size": "10800000kB",
86
+ # "max_parallel_workers_per_gather": "1",
87
+ # "random_page_cost": "2",
88
+ # "work_mem": "100MB"
89
+ # },
90
+ # "Planning Time": 0.033,
91
+ # "Triggers": [
92
+ # ],
93
+ # "Execution Time": 0.018
94
+ # }
95
+ # ]
96
+
97
+ ```
98
+
99
+ Optionally you can disable running the `ANALYZE` query and only generate the plan:
100
+
101
+ ```ruby
102
+
103
+ User.all.analyze(analyze: false)
104
+
105
+ # EXPLAIN ANALYZE for: SELECT "users".* FROM "users"
106
+ # QUERY PLAN
107
+ # ----------------------------------------------------------
108
+ # Seq Scan on users (cost=0.00..15.20 rows=520 width=127)
109
+
110
+ ```
111
+
15
112
  ### Disclaimer
16
113
 
17
- It is a bit experimental and can break with new Rails release. As soon as [this pull request](https://github.com/rails/rails/pull/31374/) is merged it will be obsolete.
114
+ It is a bit experimental and can break with new Rails release.
@@ -16,4 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib"]
17
17
  gem.license = "MIT"
18
18
  gem.add_dependency "rails", ">= 5.1.0"
19
+ gem.add_development_dependency "rake"
20
+ gem.add_development_dependency "pg"
21
+ gem.add_development_dependency "rspec"
19
22
  end
@@ -0,0 +1,11 @@
1
+ version: '3'
2
+
3
+ services:
4
+ postgres:
5
+ image: postgres:11.5-alpine
6
+ environment:
7
+ POSTGRES_USER: postgres
8
+ POSTGRES_DB: ruby-pg-extras-test
9
+ POSTGRES_PASSWORD: secret
10
+ ports:
11
+ - '5432:5432'
@@ -2,9 +2,67 @@ module ActiveRecord
2
2
  module ConnectionAdapters
3
3
  module PostgreSQL
4
4
  module DatabaseStatements
5
- def analyze(arel, binds = [])
6
- sql = "EXPLAIN ANALYZE #{to_sql(arel, binds)}"
7
- PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN ANALYZE", binds))
5
+ def analyze(arel, binds = [], opts = {})
6
+ default_opts = {
7
+ buffers: true,
8
+ verbose: true
9
+ }
10
+
11
+ opts = default_opts.merge(opts)
12
+
13
+ format_sql = if fmt = opts[:format].presence
14
+ case fmt
15
+ when :json
16
+ "FORMAT JSON,"
17
+ when :hash
18
+ "FORMAT JSON,"
19
+ when :yaml
20
+ "FORMAT YAML,"
21
+ when :text
22
+ "FORMAT TEXT,"
23
+ when :xml
24
+ "FORMAT XML,"
25
+ end
26
+ end
27
+
28
+ verbose_sql = if opts[:verbose] == true
29
+ ", VERBOSE"
30
+ end
31
+
32
+ costs_sql = if opts[:costs] == true
33
+ ", COSTS"
34
+ end
35
+
36
+ settings_sql = if opts[:settings] == true
37
+ ", SETTINGS"
38
+ end
39
+
40
+ buffers_sql = if opts[:buffers] == true
41
+ ", BUFFERS"
42
+ end
43
+
44
+ timing_sql = if opts[:timing] == true
45
+ ", TIMING"
46
+ end
47
+
48
+ summary_sql = if opts[:summary] == true
49
+ ", SUMMARY"
50
+ end
51
+
52
+ analyze_sql = if opts[:analyze] == false
53
+ ""
54
+ else
55
+ "ANALYZE"
56
+ end
57
+
58
+ opts_sql = "(#{format_sql} #{analyze_sql}#{verbose_sql}#{costs_sql}#{settings_sql}#{buffers_sql}#{timing_sql}#{summary_sql})"
59
+ .strip.gsub(/\s+/, " ")
60
+ .gsub(/\(\s?\s?\s?,/, "(")
61
+ .gsub(/\s,\s/, " ")
62
+ .gsub(/\(\s?\)/, "")
63
+
64
+ sql = "EXPLAIN #{opts_sql} #{to_sql(arel, binds)}"
65
+ PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN #{opts_sql}".strip, binds))
8
66
  end
9
67
  end
10
68
  end
@@ -13,23 +71,42 @@ end
13
71
 
14
72
  module ActiveRecord
15
73
  class Relation
16
- def analyze
17
- exec_analyze(collecting_queries_for_explain { exec_queries })
74
+ def analyze(opts = {})
75
+ res = exec_analyze(collecting_queries_for_explain { exec_queries }, opts)
76
+ if [:json, :hash].include?(opts[:format])
77
+ start = res.index("[\n")
78
+ finish = res.rindex("]")
79
+ raw_json = res.slice(start, finish - start + 1)
80
+
81
+ if opts[:format] == :json
82
+ JSON.parse(raw_json).to_json
83
+ elsif opts[:format] == :hash
84
+ JSON.parse(raw_json)
85
+ end
86
+ else
87
+ res
88
+ end
18
89
  end
19
90
  end
20
91
  end
21
92
 
22
93
  module ActiveRecord
23
94
  module Explain
24
- def exec_analyze(queries) # :nodoc:
95
+ def exec_analyze(queries, opts = {}) # :nodoc:
25
96
  str = queries.map do |sql, binds|
26
- msg = "EXPLAIN ANALYZE for: #{sql}".dup
97
+ analyze_msg = if opts[:analyze] == false
98
+ ""
99
+ else
100
+ " ANALYZE"
101
+ end
102
+
103
+ msg = "EXPLAIN#{analyze_msg} for: #{sql}".dup
27
104
  unless binds.empty?
28
105
  msg << " "
29
106
  msg << binds.map { |attr| render_bind(attr) }.inspect
30
107
  end
31
108
  msg << "\n"
32
- msg << connection.analyze(sql, binds)
109
+ msg << connection.analyze(sql, binds, opts)
33
110
  end.join("\n")
34
111
 
35
112
  # Overriding inspect to be more human readable, especially in the console.
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordAnalyze
2
- VERSION = "0.3.0"
2
+ VERSION = "0.7.0"
3
3
  end
data/query-plan.png ADDED
Binary file
data/spec/main_spec.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require "migrations/create_users_migration.rb"
5
+
6
+ class User < ActiveRecord::Base; end
7
+
8
+ describe "ActiveRecord analyze" do
9
+ before(:all) do
10
+ ActiveRecord::Base.establish_connection(
11
+ ENV.fetch("DATABASE_URL")
12
+ )
13
+
14
+ @schema_migration = ActiveRecord::Base.connection.schema_migration
15
+ ActiveRecord::Migrator.new(:up, [CreateUsers.new], @schema_migration).migrate
16
+ end
17
+
18
+ describe "no opts" do
19
+ it "works" do
20
+ expect do
21
+ User.all.analyze
22
+ end.not_to raise_error
23
+ end
24
+ end
25
+
26
+ describe "format json" do
27
+ it "works" do
28
+ result = User.all.analyze(format: :json)
29
+ expect(JSON.parse(result)[0].keys.sort).to eq [
30
+ "Execution Time", "Plan", "Planning Time", "Triggers"
31
+ ]
32
+ end
33
+ end
34
+
35
+ describe "format hash" do
36
+ it "works" do
37
+ result = User.all.analyze(format: :hash)
38
+ expect(result[0].keys.sort).to eq [
39
+ "Execution Time", "Plan", "Planning Time", "Triggers"
40
+ ]
41
+ end
42
+ end
43
+
44
+ describe "supports options" do
45
+ it "works" do
46
+ result = User.all.limit(10).where.not(email: nil).analyze(
47
+ format: :hash,
48
+ costs: true,
49
+ timing: true,
50
+ summary: true
51
+ )
52
+ expect(result[0].keys.sort).to eq [
53
+ "Execution Time", "Plan", "Planning Time", "Triggers"
54
+ ]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,10 @@
1
+ class CreateUsers < ActiveRecord::Migration::Current
2
+ def change
3
+ create_table :users do |t|
4
+ t.string :name
5
+ t.string :email
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require "active_record/railtie"
5
+ require 'bundler/setup'
6
+ require_relative '../lib/activerecord-analyze'
7
+
8
+ ENV["DATABASE_URL"] ||= "postgresql://postgres:secret@localhost:5432/activerecord-analyze-test"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-analyze
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - pawurb
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-19 00:00:00.000000000 Z
11
+ date: 2021-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -24,6 +24,48 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 5.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pg
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
27
69
  description: ' Gem adds an "analyze" method to all Active Record query objects. Compatible
28
70
  with PostgreSQL database. '
29
71
  email:
@@ -32,20 +74,26 @@ executables: []
32
74
  extensions: []
33
75
  extra_rdoc_files: []
34
76
  files:
77
+ - ".circleci/config.yml"
35
78
  - ".gitignore"
36
79
  - Gemfile
37
80
  - LICENSE.txt
38
81
  - README.md
39
82
  - Rakefile
40
83
  - activerecord-analyze.gemspec
84
+ - docker-compose.yml.sample
41
85
  - lib/activerecord-analyze.rb
42
86
  - lib/activerecord-analyze/main.rb
43
87
  - lib/activerecord-analyze/version.rb
88
+ - query-plan.png
89
+ - spec/main_spec.rb
90
+ - spec/migrations/create_users_migration.rb
91
+ - spec/spec_helper.rb
44
92
  homepage: http://github.com/pawurb/activerecord-analyze
45
93
  licenses:
46
94
  - MIT
47
95
  metadata: {}
48
- post_install_message:
96
+ post_install_message:
49
97
  rdoc_options: []
50
98
  require_paths:
51
99
  - lib
@@ -60,8 +108,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
60
108
  - !ruby/object:Gem::Version
61
109
  version: '0'
62
110
  requirements: []
63
- rubygems_version: 3.0.6
64
- signing_key:
111
+ rubygems_version: 3.1.4
112
+ signing_key:
65
113
  specification_version: 4
66
114
  summary: Add EXPLAIN ANALYZE to Active Record query objects
67
- test_files: []
115
+ test_files:
116
+ - spec/main_spec.rb
117
+ - spec/migrations/create_users_migration.rb
118
+ - spec/spec_helper.rb