activerecord-analyze 0.3.0 → 0.7.0

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