Skip to content

Commit a26bc53

Browse files
authored
Merge pull request rails#53930 from owst/avoid_postgres_column_name_lookup_n_plus_one
Remove N+1 due to column name resolution in PG adapter methods
2 parents 1b6babf + d04eaf3 commit a26bc53

File tree

1 file changed

+39
-30
lines changed

1 file changed

+39
-30
lines changed

activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def indexes(table_name) # :nodoc:
112112
inddef = row[3]
113113
comment = row[4]
114114
valid = row[5]
115-
columns = row[6]
115+
columns = decode_string_array(row[6])
116116

117117
using, expressions, include, nulls_not_distinct, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: INCLUDE \((.+?)\))?( NULLS NOT DISTINCT)?(?: WHERE (.+))?\z/m).flatten
118118

@@ -123,9 +123,6 @@ def indexes(table_name) # :nodoc:
123123
if indkey.include?(0)
124124
columns = expressions
125125
else
126-
decoder = PG::TextDecoder::Array.new
127-
columns = decoder.decode(columns)
128-
129126
# prevent INCLUDE columns from being matched
130127
columns.reject! { |c| include_columns.include?(c) }
131128

@@ -591,36 +588,45 @@ def add_foreign_key(from_table, to_table, **options)
591588
def foreign_keys(table_name)
592589
scope = quoted_scope(table_name)
593590
fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
594-
SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conkey, c.confkey, c.conrelid, c.confrelid
591+
SELECT t2.oid::regclass::text AS to_table, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conrelid, c.confrelid,
592+
(
593+
SELECT array_agg(a.attname ORDER BY idx)
594+
FROM (
595+
SELECT idx, c.conkey[idx] AS conkey_elem
596+
FROM generate_subscripts(c.conkey, 1) AS idx
597+
) indexed_conkeys
598+
JOIN pg_attribute a ON a.attrelid = t1.oid
599+
AND a.attnum = indexed_conkeys.conkey_elem
600+
) AS conkey_names,
601+
(
602+
SELECT array_agg(a.attname ORDER BY idx)
603+
FROM (
604+
SELECT idx, c.confkey[idx] AS confkey_elem
605+
FROM generate_subscripts(c.confkey, 1) AS idx
606+
) indexed_confkeys
607+
JOIN pg_attribute a ON a.attrelid = t2.oid
608+
AND a.attnum = indexed_confkeys.confkey_elem
609+
) AS confkey_names
595610
FROM pg_constraint c
596611
JOIN pg_class t1 ON c.conrelid = t1.oid
597612
JOIN pg_class t2 ON c.confrelid = t2.oid
598-
JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
599-
JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
600-
JOIN pg_namespace t3 ON c.connamespace = t3.oid
613+
JOIN pg_namespace n ON c.connamespace = n.oid
601614
WHERE c.contype = 'f'
602615
AND t1.relname = #{scope[:name]}
603-
AND t3.nspname = #{scope[:schema]}
616+
AND n.nspname = #{scope[:schema]}
604617
ORDER BY c.conname
605618
SQL
606619

607620
fk_info.map do |row|
608621
to_table = Utils.unquote_identifier(row["to_table"])
609-
conkey = row["conkey"].scan(/\d+/).map(&:to_i)
610-
confkey = row["confkey"].scan(/\d+/).map(&:to_i)
611622

612-
if conkey.size > 1
613-
column = column_names_from_column_numbers(row["conrelid"], conkey)
614-
primary_key = column_names_from_column_numbers(row["confrelid"], confkey)
615-
else
616-
column = Utils.unquote_identifier(row["column"])
617-
primary_key = row["primary_key"]
618-
end
623+
column = decode_string_array(row["conkey_names"])
624+
primary_key = decode_string_array(row["confkey_names"])
619625

620626
options = {
621-
column: column,
627+
column: column.size == 1 ? column.first : column,
622628
name: row["name"],
623-
primary_key: primary_key
629+
primary_key: primary_key.size == 1 ? primary_key.first : primary_key
624630
}
625631

626632
options[:on_delete] = extract_foreign_key_action(row["on_delete"])
@@ -705,7 +711,16 @@ def unique_constraints(table_name)
705711
scope = quoted_scope(table_name)
706712

707713
unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
708-
SELECT c.conname, c.conrelid, c.conkey, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef
714+
SELECT c.conname, c.conrelid, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef,
715+
(
716+
SELECT array_agg(a.attname ORDER BY idx)
717+
FROM (
718+
SELECT idx, c.conkey[idx] AS conkey_elem
719+
FROM generate_subscripts(c.conkey, 1) AS idx
720+
) indexed_conkeys
721+
JOIN pg_attribute a ON a.attrelid = t.oid
722+
AND a.attnum = indexed_conkeys.conkey_elem
723+
) AS conkey_names
709724
FROM pg_constraint c
710725
JOIN pg_class t ON c.conrelid = t.oid
711726
JOIN pg_namespace n ON n.oid = c.connamespace
@@ -715,8 +730,7 @@ def unique_constraints(table_name)
715730
SQL
716731

717732
unique_info.map do |row|
718-
conkey = row["conkey"].delete("{}").split(",").map(&:to_i)
719-
columns = column_names_from_column_numbers(row["conrelid"], conkey)
733+
columns = decode_string_array(row["conkey_names"])
720734

721735
nulls_not_distinct = row["constraintdef"].start_with?("UNIQUE NULLS NOT DISTINCT")
722736
deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
@@ -1156,13 +1170,8 @@ def extract_schema_qualified_name(string)
11561170
[name.schema, name.identifier]
11571171
end
11581172

1159-
def column_names_from_column_numbers(table_oid, column_numbers)
1160-
Hash[query(<<~SQL, "SCHEMA")].values_at(*column_numbers).compact
1161-
SELECT a.attnum, a.attname
1162-
FROM pg_attribute a
1163-
WHERE a.attrelid = #{table_oid}
1164-
AND a.attnum IN (#{column_numbers.join(", ")})
1165-
SQL
1173+
def decode_string_array(value)
1174+
PG::TextDecoder::Array.new.decode(value)
11661175
end
11671176
end
11681177
end

0 commit comments

Comments
 (0)