1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Improve performance of #one? and #many?

#one? and #many? generate a query like `SELECT COUNT(*) FROM posts` under the hood, and then compare if the result is equal (or greater) to 1. That count operation can be really slow for large tables or complex conditions, but there's no need to count all the records in these cases. It's much faster just by adding a limit, like `SELECT COUNT(*) FROM posts LIMIT 2`
This commit is contained in:
Gonzalo Riestra 2021-05-04 16:34:42 +02:00
parent e2b09c52ca
commit b3caf0c99a
3 changed files with 30 additions and 2 deletions

View file

@ -1,3 +1,7 @@
* Improve performance of `one?` and `many?` by limiting the generated count query to 2 results.
*Gonzalo Riestra*
* Don't check type when using `if_not_exists` on `add_column`. * Don't check type when using `if_not_exists` on `add_column`.
Previously, if a migration called `add_column` with the `if_not_exists` option set to true Previously, if a migration called `add_column` with the `if_not_exists` option set to true

View file

@ -33,6 +33,7 @@ module ActiveRecord
@delegate_to_klass = false @delegate_to_klass = false
@future_result = nil @future_result = nil
@records = nil @records = nil
@limited_count = nil
end end
def initialize_copy(other) def initialize_copy(other)
@ -294,13 +295,15 @@ module ActiveRecord
# Returns true if there is exactly one record. # Returns true if there is exactly one record.
def one? def one?
return super if block_given? return super if block_given?
limit_value ? records.one? : size == 1 return records.one? if limit_value || loaded?
limited_count == 1
end end
# Returns true if there is more than one record. # Returns true if there is more than one record.
def many? def many?
return super if block_given? return super if block_given?
limit_value ? records.many? : size > 1 return records.many? if limit_value || loaded?
limited_count > 1
end end
# Returns a stable cache key that can be used to identify this query. # Returns a stable cache key that can be used to identify this query.
@ -690,6 +693,7 @@ module ActiveRecord
@offsets = @take = nil @offsets = @take = nil
@cache_keys = nil @cache_keys = nil
@records = nil @records = nil
@limited_count = nil
self self
end end
@ -964,5 +968,9 @@ module ActiveRecord
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
string.scan(/[a-zA-Z_][.\w]+(?=.?\.)/).map!(&:downcase) - ["raw_sql_"] string.scan(/[a-zA-Z_][.\w]+(?=.?\.)/).map!(&:downcase) - ["raw_sql_"]
end end
def limited_count
@limited_count ||= limit(2).count
end
end end
end end

View file

@ -2278,6 +2278,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_no_queries { assert firm.clients.many? } assert_no_queries { assert firm.clients.many? }
end end
def test_subsequent_calls_to_many_should_not_use_query
firm = companies(:first_firm)
assert_queries(1) do
firm.clients.many?
firm.clients.many?
end
end
def test_calling_many_should_defer_to_collection_if_using_a_block def test_calling_many_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm) firm = companies(:first_firm)
assert_queries(1) do assert_queries(1) do
@ -2354,6 +2362,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_no_queries { assert_not firm.clients.one? } assert_no_queries { assert_not firm.clients.one? }
end end
def test_subsequent_calls_to_one_should_not_use_query
firm = companies(:first_firm)
assert_queries(1) do
firm.clients.one?
firm.clients.one?
end
end
def test_calling_one_should_defer_to_collection_if_using_a_block def test_calling_one_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm) firm = companies(:first_firm)
assert_queries(1) do assert_queries(1) do