explain.rb 2.7 KB
Newer Older
1
module ActiveRecord
2
  module Explain # :nodoc:
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
    # logging_query_plan calls could appear nested in the call stack. In
    # particular this happens when a relation fetches its records, since
    # that results in find_by_sql calls downwards.
    #
    # This flag allows nested calls to detect this situation and bypass
    # it, thus preventing repeated EXPLAINs.
    LOGGING_QUERY_PLAN = :logging_query_plan

    # If auto explain is enabled, this method triggers EXPLAIN logging for the
    # queries triggered by the block if it takes more than the threshold as a
    # whole. That is, the threshold is not checked against each individual
    # query, but against the duration of the entire block. This approach is
    # convenient for relations.
    def logging_query_plan(&block)
      if (t = auto_explain_threshold_in_seconds) && !Thread.current[LOGGING_QUERY_PLAN]
        begin
          Thread.current[LOGGING_QUERY_PLAN] = true
          start = Time.now
          result, sqls, binds = collecting_sqls_for_explain(&block)
          logger.warn(exec_explain(sqls, binds)) if Time.now - start > t
          result
        ensure
          Thread.current[LOGGING_QUERY_PLAN] = false
        end
      else
        block.call
      end
    end

    # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on
    # our own EXPLAINs now matter how loopingly beautiful that would be.
34
    SKIP_EXPLAIN_FOR = %w(SCHEMA EXPLAIN)
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
    def ignore_explain_notification?(payload)
      payload[:exception] || SKIP_EXPLAIN_FOR.include?(payload[:name])
    end

    # Collects all queries executed while the passed block runs. Returns an
    # array with three elements, the result of the block, the strings with the
    # queries, and their respective bindings.
    def collecting_sqls_for_explain(&block)
      sqls  = []
      binds = []
      callback = lambda do |*args|
        payload = args.last
        unless ignore_explain_notification?(payload)
          sqls  << payload[:sql]
          binds << payload[:binds]
        end
      end

      result = nil
      ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
        result = block.call
      end

      [result, sqls, binds]
    end

    # Makes the adapter execute EXPLAIN for the given queries and bindings.
    # Returns a formatted string ready to be logged.
    def exec_explain(sqls, binds)
      sqls.zip(binds).map do |sql, bind|
        [].tap do |msg|
          msg << "EXPLAIN for: #{sql}"
          unless bind.empty?
            bind_msg = bind.map {|col, val| [col.name, val]}.inspect
            msg.last << " #{bind_msg}"
          end
          msg << connection.explain(sql, bind)
        end.join("\n")
      end.join("\n")
    end
  end
end