1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activerecord/lib/active_record/explain.rb
Ryuta Kamizono 157f6a6efe Should not substitute binds when prepared_statements: true
Before IN clause optimization 70ddb8a, Active Record had generated an
SQL with binds when `prepared_statements: true`:

```ruby
# prepared_statements: true
#
#   SELECT `authors`.* FROM `authors` WHERE `authors`.`id` IN (?, ?, ?)
#
# prepared_statements: false
#
#   SELECT `authors`.* FROM `authors` WHERE `authors`.`id` IN (1, 2, 3)
#
Author.where(id: [1, 2, 3]).to_a
```

But now, binds in IN clause is substituted regardless of whether
`prepared_statements: true` or not:

```ruby
# prepared_statements: true
#
#   SELECT `authors`.* FROM `authors` WHERE `authors`.`id`IN (1,2,3)
#
# prepared_statements: false
#
#   SELECT `authors`.* FROM `authors` WHERE `authors`.`id`IN (1,2,3)
#
Author.where(id: [1, 2, 3]).to_a
```

I suppose that is considered as a regression for the context:

> While I would prefer that we fix/avoid the too-many-parameters
problem, but I don't like the idea of globally ditching bind params for
this edge case... we're getting to the point where I'd almost consider
anything that doesn't use a bind to be a bug.

https://github.com/rails/rails/pull/33844#issuecomment-421000003

This makes binds consider whether `prepared_statements: true` or not
(i.e. restore the original behavior as before), but still gain that
optimization when need the substitute binds (`prepared_statements: false`,
`relation.to_sql`). Even when `prepared_statements: true`, it still
much faster than before by optimized (bind node less) binds generation.

```ruby
class Post < ActiveRecord::Base
end

ids = (1..1000).each.map do |n|
  Post.create!.id
end

puts "prepared_statements: #{Post.connection.prepared_statements.inspect}"

Benchmark.ips do |x|
  x.report("where with ids") do
    Post.where(id: ids).to_a
  end
end
```

* Before (200058b011)

`prepared_statements: true`:

```
Warming up --------------------------------------
      where with ids     6.000  i/100ms
Calculating -------------------------------------
      where with ids     63.806  (± 7.8%) i/s -    318.000  in   5.015903s
```

`prepared_statements: false`:

```
Warming up --------------------------------------
      where with ids     7.000  i/100ms
Calculating -------------------------------------
      where with ids     73.550  (± 8.2%) i/s -    371.000  in   5.085672s
```

* Now with this change

`prepared_statements: true`:

```
Warming up --------------------------------------
      where with ids     9.000  i/100ms
Calculating -------------------------------------
      where with ids     91.992  (± 7.6%) i/s -    459.000  in   5.020817s
```

`prepared_statements: false`:

```
Warming up --------------------------------------
      where with ids    10.000  i/100ms
Calculating -------------------------------------
      where with ids    104.335  (± 8.6%) i/s -    520.000  in   5.026425s
```
2020-05-10 21:59:27 +09:00

54 lines
1.4 KiB
Ruby

# frozen_string_literal: true
require "active_record/explain_registry"
module ActiveRecord
module Explain
# Executes the block with the collect flag enabled. Queries are collected
# asynchronously by the subscriber and returned.
def collecting_queries_for_explain # :nodoc:
ExplainRegistry.collect = true
yield
ExplainRegistry.queries
ensure
ExplainRegistry.reset
end
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
# Returns a formatted string ready to be logged.
def exec_explain(queries) # :nodoc:
str = queries.map do |sql, binds|
msg = +"EXPLAIN for: #{sql}"
unless binds.empty?
msg << " "
msg << binds.map { |attr| render_bind(attr) }.inspect
end
msg << "\n"
msg << connection.explain(sql, binds)
end.join("\n")
# Overriding inspect to be more human readable, especially in the console.
def str.inspect
self
end
str
end
private
def render_bind(attr)
if ActiveModel::Attribute === attr
value = if attr.type.binary? && attr.value
"<#{attr.value_for_database.to_s.bytesize} bytes of binary data>"
else
connection.type_cast(attr.value_for_database)
end
else
value = connection.type_cast(attr)
attr = nil
end
[attr&.name, value]
end
end
end