Remove N+1 due to column name resolution in PG adapter methods#53930
Conversation
|
I had a similar PR for indexes #45381, but wasn't able to understand the review comments and why it was not merged 😆 |
|
Hum, actually, I think I misunderstood, this one does a bit more than #45381 right? If so you'll have to rebase it. |
Similar to rails#45381 - remove a couple more N+1 queries when retrieving column names for multi-column `foreign_keys` and `unique_constraints`.
c1934dd to
249c367
Compare
Thanks - rebased! And yes, it does do a bit more - it applies a similar approach to #45381 to |
| def foreign_keys(table_name) | ||
| scope = quoted_scope(table_name) | ||
| fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false) | ||
| 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 |
There was a problem hiding this comment.
This deletes column and primary_key columns, as they can be recovered from conkey_names and confkey_names for a single column key (the first element of each array), and conkey/confkey as we no longer need those as we've resolved the column ids to names
| scope = quoted_scope(table_name) | ||
|
|
||
| unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false) | ||
| SELECT c.conname, c.conrelid, c.conkey, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef |
There was a problem hiding this comment.
Removes conkey, which we've now resolved to its name(s) in conkey_names
| def array_decoder | ||
| @array_decoder ||= PG::TextDecoder::Array.new |
There was a problem hiding this comment.
Since there's a schema cache, we only really need this early, I don't think this memoization is worth it.
Might as well keep it as a local var in the method that need it like before even if it means one extra allocation.
|
cc @matthewd |
|
|
||
| unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false) | ||
| SELECT c.conname, c.conrelid, c.conkey, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef | ||
| SELECT c.conname, c.conrelid, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef, |
There was a problem hiding this comment.
Unique constraints is a very rarely used feature (compared to indexes and foreign keys) and the table usually have at most one. This query is now much more complicated and I think not worth it, so I would avoid doing it.
There was a problem hiding this comment.
I agree that unique constraints are used less-frequently than indexes/FKs. As one data point, at work we do at least make use of them in ~10 cases out of ~150 tables, and of those cases, it turns out most are composite constraints. In terms of additional complication, I personally think it's ok, but it's not my call 😄 A few points:
- It's the same sub-query pattern as in
foreign_keys, so there's a single additional "concept" to understand. (It would have been the same pattern inindexestoo, before your PR showed uspg_get_indexdeffor oneindkeyposition - it's a shame there isn't something similar for FKs/constraints!) - It allows us to remove
column_names_from_column_numbers(and avoid the associated trip to/from the DB), which prevents it being used when inappapropriate in the future - In the future, if/when the minimum supported version of PG is increased, we can reduce some of the incidental complexity:
(
SELECT array_agg(a.attname ORDER BY idx)
FROM (
SELECT idx, c.conkey[idx] AS conkey_elem
FROM generate_subscripts(c.conkey, 1) AS idx
) indexed_conkeys
JOIN pg_attribute a ON a.attrelid = t.oid
AND a.attnum = indexed_conkeys.conkey_elem
) AS conkey_namescan become
(
SELECT array_agg(a.attname ORDER BY idx)
FROM unnest(c.conkey) WITH ORDINALITY i(conkey_elem, idx)
JOIN pg_attribute a ON a.attrelid = t.oid
AND a.attnum = i.conkey_elem
) AS conkey_namesThe main point of my original PR was indexes, which your PR improved. Therefore I feel less strongly about the remainder in this PR, but I thought it was good to make them consistently avoid separate queries. If you or others feel strongly, I'm fine with removing this change for unique_constraints.
| column = Utils.unquote_identifier(row["column"]) | ||
| primary_key = row["primary_key"] | ||
| end | ||
| first_if_only_one = ->(arr) { arr.size == 1 ? arr.first : arr } |
There was a problem hiding this comment.
This lambda is like avoiding creating a private method. I think, better to do an inline if in the code (preferred) or extract a private method.
There was a problem hiding this comment.
Yes, agreed an inline if is better 👍
|
Thanks for the review @fatkodima 👍 I've pushed another commit and replied to your comments - let me know what you think |
|
Thanks, lgtm. 👍 |
Motivation / Background
I spotted in a test (that uses issued-DB-query tracking) that calling
indicesmakes N+1 queries based on the number of indexes on a table. After investigation, it seems that there are a couple of other methods that can be simplified to avoid multiple trips to the DB when resolving column ids to names.Detail
For a table with N indices, rather than issuing one query to retrieve the indices, and then N queries to resolve the indexed column ids to their names, we can instead issue one query that retrieves the indices along with their column names.
Similarly, we can avoid an N+1 when retrieving column names for multi-column
foreign_keysandunique_constraints.N.b. the sub-queries are quite verbose due to maintaining PG 9.3 compatability. If using >= 9.4 then
unnest(...) WITH ORDINALITYwould remove the inner sub-queries that generate the array indices used for ordering, saving ~12 lines. 9.4 was first released nearly 10 years ago, and has been unsupported for nearly 5, so perhaps it is time to raise the minimum version of PG required by Rails?Additional information
With the following test in a file in this repo, we can see the change in behaviour before and after this PR.
If the above is saved in a file called
test.rbthen execute it withDATABASE_URL=postgresql://... ruby test.rb.Before this PR the logged queries are:
and after this PR (after swapping the
gem "rails"when in the Rails repo with this PR's branch checked out):we can see only 3 top-level SELECTs instead of 1+5 + 1+2 + 1+2 = 12
Checklist
Before submitting the PR make sure the following are checked:
[Fix #issue-number]