lib/dexter/indexer.rb in pgdexter-0.5.1 vs lib/dexter/indexer.rb in pgdexter-0.5.2
- old
+ new
@@ -183,11 +183,11 @@
def create_hypothetical_indexes(queries)
candidates = {}
# get initial costs for queries
calculate_plan(queries)
- explainable_queries = queries.select { |q| q.explainable? && q.high_cost? }
+ explainable_queries = queries.select { |q| q.plans.any? && q.high_cost? }
# filter tables for performance
tables = Set.new(explainable_queries.flat_map(&:tables))
tables_from_views = Set.new(explainable_queries.flat_map(&:tables_from_views))
@@ -519,33 +519,39 @@
end
rescue PG::ConnectionBad => e
raise Dexter::Abort, e.message
end
- def execute(query, pretty: true, params: [])
- # use exec_params instead of exec for security
+ def execute(query, pretty: true, params: [], use_exec: false)
+ # use exec_params instead of exec when possible for security
#
# Unlike PQexec, PQexecParams allows at most one SQL command in the given string.
# (There can be semicolons in it, but not more than one nonempty command.)
# This is a limitation of the underlying protocol, but has some usefulness
# as an extra defense against SQL-injection attacks.
# https://www.postgresql.org/docs/current/static/libpq-exec.html
query = squish(query) if pretty
log colorize("[sql] #{query}#{params.any? ? " /*#{params.to_json}*/" : ""}", :cyan) if @log_sql
@mutex.synchronize do
- conn.exec_params("#{query} /*dexter*/", params).to_a
+ if use_exec
+ conn.exec("#{query} /*dexter*/").to_a
+ else
+ conn.exec_params("#{query} /*dexter*/", params).to_a
+ end
end
end
def plan(query)
prepared = false
transaction = false
# try to EXPLAIN normalized queries
# https://dev.to/yugabyte/explain-from-pgstatstatements-normalized-queries-how-to-always-get-the-generic-plan-in--5cfi
- explain_normalized = query.include?("$1")
+ normalized = query.include?("$1")
+ generic_plan = normalized && server_version_num >= 160000
+ explain_normalized = normalized && !generic_plan
if explain_normalized
prepared_name = "dexter_prepared"
execute("PREPARE #{prepared_name} AS #{safe_statement(query)}", pretty: false)
prepared = true
params = execute("SELECT array_length(parameter_types, 1) AS params FROM pg_prepared_statements WHERE name = $1", params: [prepared_name]).first["params"].to_i
@@ -564,15 +570,17 @@
execute("ROLLBACK")
execute("BEGIN")
end
end
+ explain_prefix = generic_plan ? "GENERIC_PLAN, " : ""
+
# strip semi-colons as another measure of defense
- plan = JSON.parse(execute("EXPLAIN (FORMAT JSON) #{safe_statement(query)}", pretty: false).first["QUERY PLAN"], max_nesting: 1000).first["Plan"]
+ plan = JSON.parse(execute("EXPLAIN (#{explain_prefix}FORMAT JSON) #{safe_statement(query)}", pretty: false, use_exec: generic_plan).first["QUERY PLAN"], max_nesting: 1000).first["Plan"]
if @log_explain
# Pass format to prevent ANALYZE
- puts execute("EXPLAIN (FORMAT TEXT) #{safe_statement(query)}", pretty: false).map { |r| r["QUERY PLAN"] }.join("\n")
+ puts execute("EXPLAIN (#{explain_prefix}FORMAT TEXT) #{safe_statement(query)}", pretty: false, use_exec: generic_plan).map { |r| r["QUERY PLAN"] }.join("\n")
end
plan
ensure
if explain_normalized