seanmuth opened a new issue, #72393:
URL: https://github.com/apache/airflow/issues/72393

   ## Summary
   
   `DagModelOperation.find_orm_dags` 
(`airflow-core/src/airflow/dag_processing/collection.py`) eagerly loads five 
separate one-to-many collections via `joinedload()` in a single query:
   
   ```python
   def find_orm_dags(self, *, session: Session) -> dict[str, DagModel]:
       stmt: Select[Unpack[tuple[DagModel]]] = with_row_locks(
           (
               select(DagModel)
               .options(joinedload(DagModel.tags, innerjoin=False))
               .where(DagModel.dag_id.in_(self.dags))
               .options(joinedload(DagModel.schedule_asset_references))
               .options(joinedload(DagModel.schedule_asset_alias_references))
               .options(joinedload(DagModel.task_outlet_asset_references))
               .options(joinedload(DagModel.dag_owner_links))
           ),
           of=DagModel,
           session=session,
       )
       return {dm.dag_id: dm for dm in session.scalars(stmt).unique()}
   ```
   
   Joining more than one one-to-many collection in the same query via 
`joinedload` is a well-known SQLAlchemy anti-pattern: it produces a 
cartesian-product row explosion. A DAG with, say, 3 tags × 2 asset references × 
2 owner links returns 12 duplicate rows, each carrying the full wide `dag.*` 
column set repeated. Called across however many `dag_id`s are in a given 
`update_dag_parsing_results_in_db` call (which scales with DAG count per 
file/sweep), this multiplies fast.
   
   ## Evidence (production)
   
   Caught live via `pg_stat_activity` on a customer deployment with a large 
dynamically-generated DAG set (1,400+ DAGs across several files):
   
   ```
   runtime: 00:00:10.241513, state: active, wait_event: ClientWrite, 
wait_event_type: Client
   application_name: astro-agent 1.13.1 [task:...]
   query: SELECT dag.dag_id, dag.is_paused, ... dag_tag_1.name, 
dag_tag_1.dag_id AS dag_id_1,
          dag_owner_attributes_1.dag_id AS dag_id_2, dag_owner_attributes_1.o...
   ```
   
   `wait_event: ClientWrite` is the key detail — Postgres has already computed 
the result and is blocked *sending* it, because the client (asyncpg connection) 
isn't draining the socket fast enough. That points at result-set size, not 
query planning/execution cost, as the bottleneck. This customer reports these 
commonly exceeding 10 seconds. Response latency downstream (this call sits in 
the request path of the DAG-processor's parse-result heartbeat) has been 
observed causing client-side request timeouts/disconnects (HTTP 499s) on the 
receiving end in the wild.
   
   ## Proposed fix
   
   Replace the multiple `joinedload()` calls on one-to-many collections with 
`selectinload()`. `selectinload` issues one follow-up `WHERE dag_id IN (...)` 
query per collection instead of one giant multi-way join, giving the same 
eager-loading outcome without the multiplicative row blowup. Since there are 
five separate collections here, this trades one huge query for five small ones 
— plausibly still a large net win, but worth benchmarking against a DAG set 
with many tags/owners/asset-refs per DAG to confirm before merging.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to