mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-02-24 23:44:09 -05:00
PERF: Active Record #pluck is twice faster
PERF: SqlBuilder bypasses AR converters and uses PG directly
This commit is contained in:
parent
e81f122a3f
commit
51d82fc25d
4 changed files with 143 additions and 34 deletions
108
lib/freedom_patches/fast_pluck.rb
Normal file
108
lib/freedom_patches/fast_pluck.rb
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
# Speeds up #pluck so its about 2.2x faster, importantly makes pluck avoid creation of a slew
|
||||||
|
# of AR objects
|
||||||
|
|
||||||
|
require_dependency 'sql_builder'
|
||||||
|
|
||||||
|
class ActiveRecord::Relation
|
||||||
|
|
||||||
|
# class RailsDateTimeDecoder < PG::SimpleDecoder
|
||||||
|
# def decode(string, tuple=nil, field=nil)
|
||||||
|
# if Rails.version >= "4.2.0"
|
||||||
|
# @caster ||= ActiveRecord::Type::DateTime.new
|
||||||
|
# @caster.type_cast_from_database(string)
|
||||||
|
# else
|
||||||
|
# ActiveRecord::ConnectionAdapters::Column.string_to_time string
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# class ActiveRecordTypeMap < PG::BasicTypeMapForResults
|
||||||
|
# def initialize(connection)
|
||||||
|
# super(connection)
|
||||||
|
# rm_coder 0, 1114
|
||||||
|
# add_coder RailsDateTimeDecoder.new(name: "timestamp", oid: 1114, format: 0)
|
||||||
|
# # we don't need deprecations
|
||||||
|
# self.default_type_map = PG::TypeMapInRuby.new
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# def self.pg_type_map
|
||||||
|
# conn = ActiveRecord::Base.connection.raw_connection
|
||||||
|
# @typemap ||= ActiveRecordTypeMap.new(conn)
|
||||||
|
# end
|
||||||
|
|
||||||
|
class ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
||||||
|
if Rails.version >= "4.2.0"
|
||||||
|
def select_raw(arel, name = nil, binds = [], &block)
|
||||||
|
arel, binds = binds_from_relation arel, binds
|
||||||
|
sql = to_sql(arel, binds)
|
||||||
|
execute_and_clear(sql, name, binds, &block)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
|
||||||
|
def select_raw(arel, name = nil, binds = [], &block)
|
||||||
|
arel, binds = binds_from_relation arel, binds
|
||||||
|
sql = to_sql(arel, binds)
|
||||||
|
|
||||||
|
result = without_prepared_statement?(binds) ? exec_no_cache(sql, 'SQL', binds) :
|
||||||
|
exec_cache(sql, 'SQL', binds)
|
||||||
|
yield result, nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pluck(*cols)
|
||||||
|
|
||||||
|
conn = ActiveRecord::Base.connection
|
||||||
|
relation = self
|
||||||
|
|
||||||
|
cols.map! do |column_name|
|
||||||
|
if column_name.is_a?(Symbol) && attribute_alias?(column_name)
|
||||||
|
attribute_alias(column_name)
|
||||||
|
else
|
||||||
|
column_name.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
if has_include?(cols.first)
|
||||||
|
construct_relation_for_association_calculations.pluck(*cols)
|
||||||
|
else
|
||||||
|
relation = spawn
|
||||||
|
|
||||||
|
relation.select_values = cols.map { |cn|
|
||||||
|
columns_hash.key?(cn) ? arel_table[cn] : cn
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.select_raw(relation) do |result,_|
|
||||||
|
result.type_map = SqlBuilder.pg_type_map
|
||||||
|
result.nfields == 1 ? result.column_values(0) : result.values
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# require 'benchmark/ips'
|
||||||
|
#
|
||||||
|
# ENV['RAILS_ENV'] = 'production'
|
||||||
|
# require File.expand_path("../../config/environment", __FILE__)
|
||||||
|
#
|
||||||
|
# Benchmark.ips do |x|
|
||||||
|
# x.report("fast_pluck") do
|
||||||
|
# Post.where(topic_id: 48464).fast_pluck(:id)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# x.report("pluck") do
|
||||||
|
# Post.where(topic_id: 48464).pluck(:id)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# % ruby tmp/fast_pluck.rb
|
||||||
|
# Calculating -------------------------------------
|
||||||
|
# fast_pluck 165.000 i/100ms
|
||||||
|
# pluck 80.000 i/100ms
|
||||||
|
# -------------------------------------------------
|
||||||
|
# fast_pluck 1.720k (± 8.8%) i/s - 8.580k
|
||||||
|
# pluck 807.913 (± 4.0%) i/s - 4.080k
|
||||||
|
#
|
|
@ -36,9 +36,9 @@ class SqlBuilder
|
||||||
when :where, :where2
|
when :where, :where2
|
||||||
joined = "WHERE " << v.map{|c| "(" << c << ")" }.join(" AND ")
|
joined = "WHERE " << v.map{|c| "(" << c << ")" }.join(" AND ")
|
||||||
when :join
|
when :join
|
||||||
joined = v.map{|v| "JOIN " << v }.join("\n")
|
joined = v.map{|item| "JOIN " << item }.join("\n")
|
||||||
when :left_join
|
when :left_join
|
||||||
joined = v.map{|v| "LEFT JOIN " << v }.join("\n")
|
joined = v.map{|item| "LEFT JOIN " << item }.join("\n")
|
||||||
when :limit
|
when :limit
|
||||||
joined = "LIMIT " << v.last.to_s
|
joined = "LIMIT " << v.last.to_s
|
||||||
when :offset
|
when :offset
|
||||||
|
@ -69,46 +69,50 @@ class SqlBuilder
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
#AS reloads this on tests
|
|
||||||
remove_const :FTYPE_MAP if defined? FTYPE_MAP
|
|
||||||
|
|
||||||
if Rails.version >= "4.2.0"
|
|
||||||
FTYPE_MAP = {
|
|
||||||
23 => ActiveRecord::Type::Integer.new,
|
|
||||||
1114 => ActiveRecord::Type::DateTime.new,
|
|
||||||
16 => ActiveRecord::Type::Boolean.new
|
|
||||||
}
|
|
||||||
else
|
|
||||||
FTYPE_MAP = {
|
|
||||||
23 => :value_to_integer,
|
|
||||||
1114 => :string_to_time,
|
|
||||||
16 => :value_to_boolean
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.map_exec(klass, sql, args = {})
|
def self.map_exec(klass, sql, args = {})
|
||||||
self.new(sql).map_exec(klass, args)
|
self.new(sql).map_exec(klass, args)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class RailsDateTimeDecoder < PG::SimpleDecoder
|
||||||
|
def decode(string, tuple=nil, field=nil)
|
||||||
|
if Rails.version >= "4.2.0"
|
||||||
|
@caster ||= ActiveRecord::Type::DateTime.new
|
||||||
|
@caster.type_cast_from_database(string)
|
||||||
|
else
|
||||||
|
ActiveRecord::ConnectionAdapters::Column.string_to_time string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
class ActiveRecordTypeMap < PG::BasicTypeMapForResults
|
||||||
|
def initialize(connection)
|
||||||
|
super(connection)
|
||||||
|
rm_coder 0, 1114
|
||||||
|
add_coder RailsDateTimeDecoder.new(name: "timestamp", oid: 1114, format: 0)
|
||||||
|
# we don't need deprecations
|
||||||
|
self.default_type_map = PG::TypeMapInRuby.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.pg_type_map
|
||||||
|
conn = ActiveRecord::Base.connection.raw_connection
|
||||||
|
@typemap ||= ActiveRecordTypeMap.new(conn)
|
||||||
|
end
|
||||||
|
|
||||||
def map_exec(klass = OpenStruct, args = {})
|
def map_exec(klass = OpenStruct, args = {})
|
||||||
results = exec(args)
|
results = exec(args)
|
||||||
|
results.type_map = SqlBuilder.pg_type_map
|
||||||
|
|
||||||
setters = results.fields.each_with_index.map do |f, index|
|
setters = results.fields.each_with_index.map do |f, index|
|
||||||
[(f.dup << "=").to_sym, FTYPE_MAP[results.ftype(index)]]
|
f.dup << "="
|
||||||
end
|
end
|
||||||
|
|
||||||
values = results.values
|
values = results.values
|
||||||
values.map! do |row|
|
values.map! do |row|
|
||||||
mapped = klass.new
|
mapped = klass.new
|
||||||
setters.each_with_index do |mapper, index|
|
setters.each_with_index do |name, index|
|
||||||
translated = row[index]
|
mapped.send name, row[index]
|
||||||
if mapper[1] && !translated.nil?
|
|
||||||
if Rails.version >= "4.2.0"
|
|
||||||
translated = mapper[1].type_cast_from_database(translated)
|
|
||||||
else
|
|
||||||
translated = ActiveRecord::ConnectionAdapters::Column.send mapper[1], translated
|
|
||||||
end
|
|
||||||
end
|
|
||||||
mapped.send mapper[0], translated
|
|
||||||
end
|
end
|
||||||
mapped
|
mapped
|
||||||
end
|
end
|
||||||
|
|
|
@ -137,7 +137,6 @@ class TopicQuery
|
||||||
pinned_ids = query.where('pinned_at IS NOT NULL AND category_id = ?', category.id)
|
pinned_ids = query.where('pinned_at IS NOT NULL AND category_id = ?', category.id)
|
||||||
.order('pinned_at DESC').pluck(:id)
|
.order('pinned_at DESC').pluck(:id)
|
||||||
non_pinned_ids = query.where('pinned_at IS NULL OR category_id <> ?', category.id).pluck(:id)
|
non_pinned_ids = query.where('pinned_at IS NULL OR category_id <> ?', category.id).pluck(:id)
|
||||||
|
|
||||||
(pinned_ids + non_pinned_ids)[0...@options[:per_page]]
|
(pinned_ids + non_pinned_ids)[0...@options[:per_page]]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,6 @@ require 'spec_helper'
|
||||||
require_dependency 'post_destroyer'
|
require_dependency 'post_destroyer'
|
||||||
|
|
||||||
describe PostAction do
|
describe PostAction do
|
||||||
it { is_expected.to belong_to :user }
|
|
||||||
it { is_expected.to belong_to :post }
|
|
||||||
it { is_expected.to belong_to :post_action_type }
|
|
||||||
it { is_expected.to rate_limit }
|
it { is_expected.to rate_limit }
|
||||||
|
|
||||||
let(:moderator) { Fabricate(:moderator) }
|
let(:moderator) { Fabricate(:moderator) }
|
||||||
|
@ -458,6 +455,7 @@ describe PostAction do
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(topic.reload.closed).to eq(true)
|
expect(topic.reload.closed).to eq(true)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue