Skip to content

is there any way asyncpg could be configured to not do a giant query for PG custom types when you prepare #1078

Description

@zzzeek

hey there -

as you know, SQLAlchemy relies upon knowing what the names of columns will be in result sets, and for asycnpg that forces us to use PreparedStatements for statements that we want to fetch rows from.

When the preparedstatement has a custom type like an ENUM inside of it, asyncpg does a giant query up front to cache information about the datatype, here it is from my test case below inside my PG SQL log:

2023-09-1710:04:41.545 EDT [1728595] LOG: execute __asyncpg_stmt_3__: WITH RECURSIVE typeinfo_tree(
oid, ns, name, kind, basetype, elemtype, elemdelim,
range_subtype, attrtypoids, attrnames, depth)
AS (
SELECTti.oid, ti.ns, ti.name, ti.kind, ti.basetype,
ti.elemtype, ti.elemdelim, ti.range_subtype,
ti.attrtypoids, ti.attrnames, 0FROM
(
SELECTt.oidASoid,
ns.nspnameAS ns,
t.typnameAS name,
t.typtypeAS kind,
(CASE WHEN t.typtype='d' THEN
(WITH RECURSIVE typebases(oid, depth) AS (
SELECTt2.typbasetypeASoid,
0AS depth
FROM
pg_type t2
WHEREt2.oid=t.oidUNION ALLSELECTt2.typbasetypeASoid,
tb.depth+1AS depth
FROM
pg_type t2,
typebases tb
WHEREtb.oid=t2.oidANDt2.typbasetype!=0
) SELECToidFROM typebases ORDER BY depth DESCLIMIT1)
ELSE NULL
END) AS basetype,
t.typelemAS elemtype,
elem_t.typdelimAS elemdelim,
COALESCE(
range_t.rngsubtype,
multirange_t.rngsubtype) AS range_subtype,
(CASE WHEN t.typtype='c' THEN
(SELECT
array_agg(ia.atttypidORDER BYia.attnum)
FROM
pg_attribute ia
INNER JOIN pg_class c
ON (ia.attrelid=c.oid)
WHEREia.attnum>0AND NOT ia.attisdroppedANDc.reltype=t.oid)
ELSE NULL
END) AS attrtypoids,
(CASE WHEN t.typtype='c' THEN
(SELECT
array_agg(ia.attname::textORDER BYia.attnum)
FROM
pg_attribute ia
INNER JOIN pg_class c
ON (ia.attrelid=c.oid)
WHEREia.attnum>0AND NOT ia.attisdroppedANDc.reltype=t.oid)
ELSE NULL
END) AS attrnames
FROMpg_catalog.pg_typeAS t
INNER JOINpg_catalog.pg_namespace ns ON (
ns.oid=t.typnamespace)
LEFT JOIN pg_type elem_t ON (
t.typlen=-1ANDt.typelem!=0ANDt.typelem=elem_t.oid
)
LEFT JOIN pg_range range_t ON (
t.oid=range_t.rngtypid
)
LEFT JOIN pg_range multirange_t ON (
t.oid=multirange_t.rngmultitypid
)
)
AS ti
WHEREti.oid= any($1::oid[])
UNION ALLSELECTti.oid, ti.ns, ti.name, ti.kind, ti.basetype,
ti.elemtype, ti.elemdelim, ti.range_subtype,
ti.attrtypoids, ti.attrnames, tt.depth+1FROM
(
SELECTt.oidASoid,
ns.nspnameAS ns,
t.typnameAS name,
t.typtypeAS kind,
(CASE WHEN t.typtype='d' THEN
(WITH RECURSIVE typebases(oid, depth) AS (
SELECTt2.typbasetypeASoid,
0AS depth
FROM
pg_type t2
WHEREt2.oid=t.oidUNION ALLSELECTt2.typbasetypeASoid,
tb.depth+1AS depth
FROM
pg_type t2,
typebases tb
WHEREtb.oid=t2.oidANDt2.typbasetype!=0
) SELECToidFROM typebases ORDER BY depth DESCLIMIT1)
ELSE NULL
END) AS basetype,
t.typelemAS elemtype,
elem_t.typdelimAS elemdelim,
COALESCE(
range_t.rngsubtype,
multirange_t.rngsubtype) AS range_subtype,
(CASE WHEN t.typtype='c' THEN
(SELECT
array_agg(ia.atttypidORDER BYia.attnum)
FROM
pg_attribute ia
INNER JOIN pg_class c
ON (ia.attrelid=c.oid)
WHEREia.attnum>0AND NOT ia.attisdroppedANDc.reltype=t.oid)
ELSE NULL
END) AS attrtypoids,
(CASE WHEN t.typtype='c' THEN
(SELECT
array_agg(ia.attname::textORDER BYia.attnum)
FROM
pg_attribute ia
INNER JOIN pg_class c
ON (ia.attrelid=c.oid)
WHEREia.attnum>0AND NOT ia.attisdroppedANDc.reltype=t.oid)
ELSE NULL
END) AS attrnames
FROMpg_catalog.pg_typeAS t
INNER JOINpg_catalog.pg_namespace ns ON (
ns.oid=t.typnamespace)
LEFT JOIN pg_type elem_t ON (
t.typlen=-1ANDt.typelem!=0ANDt.typelem=elem_t.oid
)
LEFT JOIN pg_range range_t ON (
t.oid=range_t.rngtypid
)
LEFT JOIN pg_range multirange_t ON (
t.oid=multirange_t.rngmultitypid
)
)
ti,
typeinfo_tree tt
WHERE
(tt.elemtypeIS NOT NULLANDti.oid=tt.elemtype)
OR (tt.attrtypoidsIS NOT NULLANDti.oid= any(tt.attrtypoids))
OR (tt.range_subtypeIS NOT NULLANDti.oid=tt.range_subtype)
OR (tt.basetypeIS NOT NULLANDti.oid=tt.basetype)
)
SELECT DISTINCT*,
basetype::regtype::textAS basetype_name,
elemtype::regtype::textAS elemtype_name,
range_subtype::regtype::textAS range_subtype_name
FROM
typeinfo_tree
ORDER BY
depth DESC2023-09-1710:04:41.545 EDT [1728595] DETAIL: parameters: $1='{2732440}'

This is I assume once per connection/type, but we still are getting performance concerns about it as we see in #10356. SQLAlchemy uses a connection pool by default however this is still an upfront cost and some folks don't use the pool.

I would assume the purpose of this query has to do with get_attributes() having all the information about the oid for the type.

We don't actually need the "type" part of get_attributes(), just the names (we already know the types on our end). Is there a possibility asyncpg could have some kind of connection parameter, or prepared statement parameter, that is something to the effect use_varchar_for_custom_type or omit_custom_type or something such that this giant query on prepare can be skipped?

Demo that produces the query in question:

importasyncioimportasyncpgasyncdefmain():
conn=awaitasyncpg.connect(
user="scott", password="tiger", database="test"
)
awaitconn.execute("DROP TABLE IF EXISTS mood_test")
awaitconn.execute(
"DROP TYPE IF EXISTS mood"
)
awaitconn.execute(
"CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')"
)
awaitconn.execute(
"CREATE TABLE mood_test (id SERIAL primary key, current_mood mood)"
)
awaitconn.fetch("SELECT 'right before the prepare'")
pp=awaitconn.prepare("SELECT id, current_mood FROM mood_test")
awaitconn.fetch("SELECT 'right after the prepare'")
cols=pp.get_attributes()
awaitconn.fetch("SELECT 'right after the get_attributes'")
print(f"attributes: {cols}")
awaitconn.close()
asyncio.run(main())

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions