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())
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:
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_typeoromit_custom_typeor something such that this giant query on prepare can be skipped?Demo that produces the query in question: