未验证 提交 1db05065 编写于 作者: R Ryuta Kamizono 提交者: GitHub

Merge pull request #35615 from kamipo/optimizer_hints

Support Optimizer Hints
* Support Optimizer Hints.
In most databases, there is a way to control the optimizer is by using optimizer hints,
which can be specified within individual statements.
Example (for MySQL):
Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)")
# SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`
Example (for PostgreSQL with pg_hint_plan):
Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
# SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"
See also:
* https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
* https://pghintplan.osdn.jp/pg_hint_plan.html
* https://docs.oracle.com/en/database/oracle/oracle-database/12.2/tgsql/influencing-the-optimizer.html
* https://docs.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-query?view=sql-server-2017
* https://www.ibm.com/support/knowledgecenter/en/SSEPGG_11.1.0/com.ibm.db2.luw.admin.perf.doc/doc/c0070117.html
*Ryuta Kamizono*
* Fix query attribute method on user-defined attribute to be aware of typecasted value.
For example, the following code no longer return false as casted non-empty string:
......
......@@ -384,6 +384,11 @@ def supports_foreign_tables?
false
end
# Does this adapter support optimizer hints?
def supports_optimizer_hints?
false
end
def supports_lazy_transactions?
false
end
......
......@@ -103,6 +103,11 @@ def supports_virtual_columns?
mariadb? || version >= "5.7.5"
end
# See https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html for more details.
def supports_optimizer_hints?
!mariadb? && version >= "5.7.7"
end
def supports_advisory_locks?
true
end
......
......@@ -351,6 +351,13 @@ def supports_pgcrypto_uuid?
postgresql_version >= 90400
end
def supports_optimizer_hints?
unless defined?(@has_pg_hint_plan)
@has_pg_hint_plan = extension_available?("pg_hint_plan")
end
@has_pg_hint_plan
end
def supports_lazy_transactions?
true
end
......@@ -381,9 +388,12 @@ def disable_extension(name)
}
end
def extension_available?(name)
query_value("SELECT true FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
end
def extension_enabled?(name)
res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA")
res.cast_values.first
query_value("SELECT installed_version IS NOT NULL FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
end
def extensions
......
......@@ -14,7 +14,7 @@ module Querying
:find_each, :find_in_batches, :in_batches,
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
:where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
:having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only,
:having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
:count, :average, :minimum, :maximum, :sum, :calculate,
:pluck, :pick, :ids
].freeze # :nodoc:
......
......@@ -5,7 +5,7 @@ module ActiveRecord
class Relation
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :left_outer_joins, :references,
:extending, :unscope]
:extending, :unscope, :optimizer_hints]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache]
......
......@@ -901,6 +901,29 @@ def extending!(*modules, &block) # :nodoc:
self
end
# Specify optimizer hints to be used in the SELECT statement.
#
# Example (for MySQL):
#
# Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)")
# # SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`
#
# Example (for PostgreSQL with pg_hint_plan):
#
# Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
# # SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"
def optimizer_hints(*args)
check_if_method_has_arguments!(:optimizer_hints, args)
spawn.optimizer_hints!(*args)
end
def optimizer_hints!(*args) # :nodoc:
args.flatten!
self.optimizer_hints_values += args
self
end
# Reverse the existing order clause on the relation.
#
# User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC'
......@@ -977,6 +1000,7 @@ def build_arel(aliases)
build_select(arel)
arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
arel.distinct(distinct_value)
arel.from(build_from) unless from_clause.empty?
arel.lock(lock_value) if lock_value
......
......@@ -4,7 +4,7 @@ module Arel # :nodoc: all
module Nodes
class SelectCore < Arel::Nodes::Node
attr_accessor :projections, :wheres, :groups, :windows
attr_accessor :havings, :source, :set_quantifier
attr_accessor :havings, :source, :set_quantifier, :optimizer_hints
def initialize
super()
......@@ -42,7 +42,7 @@ def initialize_copy(other)
def hash
[
@source, @set_quantifier, @projections,
@source, @set_quantifier, @projections, @optimizer_hints,
@wheres, @groups, @havings, @windows
].hash
end
......@@ -51,6 +51,7 @@ def eql?(other)
self.class == other.class &&
self.source == other.source &&
self.set_quantifier == other.set_quantifier &&
self.optimizer_hints == other.optimizer_hints &&
self.projections == other.projections &&
self.wheres == other.wheres &&
self.groups == other.groups &&
......
......@@ -35,6 +35,7 @@ def eql?(other)
Not
Offset
On
OptimizerHints
Ordering
RollUp
}.each do |name|
......
......@@ -146,6 +146,13 @@ def projections=(projections)
@ctx.projections = projections
end
def optimizer_hints(*hints)
unless hints.empty?
@ctx.optimizer_hints = Arel::Nodes::OptimizerHints.new(hints)
end
self
end
def distinct(value = true)
if value
@ctx.set_quantifier = Arel::Nodes::Distinct.new
......
......@@ -35,6 +35,7 @@ def unary(o)
alias :visit_Arel_Nodes_Ascending :unary
alias :visit_Arel_Nodes_Descending :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
alias :visit_Arel_Nodes_OptimizerHints :unary
def function(o)
visit o.expressions
......
......@@ -82,6 +82,7 @@ def unary(o)
alias :visit_Arel_Nodes_Offset :unary
alias :visit_Arel_Nodes_On :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
alias :visit_Arel_Nodes_OptimizerHints :unary
alias :visit_Arel_Nodes_Preceding :unary
alias :visit_Arel_Nodes_Following :unary
alias :visit_Arel_Nodes_Rows :unary
......
......@@ -4,6 +4,14 @@ module Arel # :nodoc: all
module Visitors
class IBM_DB < Arel::Visitors::ToSql
private
def visit_Arel_Nodes_SelectCore(o, collector)
collector = super
maybe_visit o.optimizer_hints, collector
end
def visit_Arel_Nodes_OptimizerHints(o, collector)
collector << "/* <OPTGUIDELINES>#{sanitize_as_sql_comment(o).join}</OPTGUIDELINES> */"
end
def visit_Arel_Nodes_Limit(o, collector)
collector << "FETCH FIRST "
......@@ -16,6 +24,10 @@ def is_distinct_from(o, collector)
collector = visit [o.left, o.right, 0, 1], collector
collector << ")"
end
def collect_optimizer_hints(o, collector)
collector
end
end
end
end
......@@ -42,10 +42,15 @@ def visit_Arel_Nodes_SelectCore(o, collector)
collector
end
def visit_Arel_Nodes_OptimizerHints(o, collector)
collector << "/*+ #{sanitize_as_sql_comment(o).join(", ")} */"
end
def visit_Arel_Nodes_Offset(o, collector)
collector << "SKIP "
visit o.expr, collector
end
def visit_Arel_Nodes_Limit(o, collector)
collector << "FIRST "
visit o.expr, collector
......
......@@ -76,6 +76,15 @@ def visit_Arel_Nodes_SelectStatement(o, collector)
end
end
def visit_Arel_Nodes_SelectCore(o, collector)
collector = super
maybe_visit o.optimizer_hints, collector
end
def visit_Arel_Nodes_OptimizerHints(o, collector)
collector << "OPTION (#{sanitize_as_sql_comment(o).join(", ")})"
end
def get_offset_limit_clause(o)
first_row = o.offset ? o.offset.expr.to_i + 1 : 1
last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil
......@@ -103,6 +112,10 @@ def visit_Arel_Nodes_DeleteStatement(o, collector)
end
end
def collect_optimizer_hints(o, collector)
collector
end
def determine_order_by(orders, x)
if orders.any?
orders
......
......@@ -219,6 +219,7 @@ def visit_Arel_Nodes_SelectOptions(o, collector)
def visit_Arel_Nodes_SelectCore(o, collector)
collector << "SELECT"
collector = collect_optimizer_hints(o, collector)
collector = maybe_visit o.set_quantifier, collector
collect_nodes_for o.projections, collector, SPACE
......@@ -236,6 +237,10 @@ def visit_Arel_Nodes_SelectCore(o, collector)
collector
end
def visit_Arel_Nodes_OptimizerHints(o, collector)
collector << "/*+ #{sanitize_as_sql_comment(o).join(" ")} */"
end
def collect_nodes_for(nodes, collector, spacer, connector = COMMA)
unless nodes.empty?
collector << spacer
......@@ -799,6 +804,14 @@ def quote_column_name(name)
@connection.quote_column_name(name)
end
def sanitize_as_sql_comment(o)
o.expr.map { |v| v.gsub(%r{ /\*\+?\s* | \s*\*/ }x, "") }
end
def collect_optimizer_hints(o, collector)
maybe_visit o.optimizer_hints, collector
end
def maybe_visit(thing, collector)
return collector unless thing
collector << " "
......
# frozen_string_literal: true
require "cases/helper"
require "models/post"
if supports_optimizer_hints?
class Mysql2OptimzerHintsTest < ActiveRecord::Mysql2TestCase
fixtures :posts
def test_optimizer_hints
assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
posts = posts.select(:id).where(author_id: [0, 1])
assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
end
assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
posts = posts.select(:id).where(author_id: [0, 1])
assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/post"
if supports_optimizer_hints?
class PostgresqlOptimzerHintsTest < ActiveRecord::PostgreSQLTestCase
fixtures :posts
def setup
enable_extension!("pg_hint_plan", ActiveRecord::Base.connection)
end
def test_optimizer_hints
assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
posts = Post.optimizer_hints("SeqScan(posts)")
posts = posts.select(:id).where(author_id: [0, 1])
assert_includes posts.explain, "Seq Scan on posts"
end
assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
posts = posts.select(:id).where(author_id: [0, 1])
assert_includes posts.explain, "Seq Scan on posts"
end
end
end
end
......@@ -64,6 +64,7 @@ def supports_default_expression?
supports_insert_on_duplicate_skip?
supports_insert_on_duplicate_update?
supports_insert_conflict_target?
supports_optimizer_hints?
].each do |method_name|
define_method method_name do
ActiveRecord::Base.connection.public_send(method_name)
......
......@@ -308,6 +308,8 @@ def test_migrate_enable_and_disable_extension
migration2 = DisableExtension1.new
migration3 = DisableExtension2.new
assert_equal true, Horse.connection.extension_available?("hstore")
migration1.migrate(:up)
migration2.migrate(:up)
assert_equal true, Horse.connection.extension_enabled?("hstore")
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册