[{"data":1,"prerenderedAt":3275},["ShallowReactive",2],{"page-\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per\u002Fusing-yield-per-to-stream-millions-of-rows-in-async\u002F":3},{"id":4,"title":5,"body":6,"description":3267,"extension":3268,"meta":3269,"navigation":131,"path":3271,"seo":3272,"stem":3273,"__hash__":3274},"content\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per\u002Fusing-yield-per-to-stream-millions-of-rows-in-async\u002Findex.md","Using yield_per to Stream Millions of Rows in Async SQLAlchemy",{"type":7,"value":8,"toc":3248},"minimark",[9,13,36,41,44,326,333,691,695,700,703,747,758,1079,1083,1102,1110,1134,1138,1156,1159,1168,1172,1179,1186,1189,1220,1526,1530,1669,1673,2118,2122,2126,2132,2152,2158,2162,2172,2182,2690,2697,2701,2706,2728,2737,2741,2746,2771,2776,2796,2801,2824,2829,2842,3216,3220,3244],[10,11,5],"h1",{"id":12},"using-yield_per-to-stream-millions-of-rows-in-async-sqlalchemy",[14,15,16,17,21,22,25,26,29,30,35],"p",{},"Call ",[18,19,20],"code",{},"AsyncSession.stream()"," with ",[18,23,24],{},".execution_options(yield_per=N)"," (or the shorthand ",[18,27,28],{},"stream_scalars()",") to iterate millions of rows without loading them all into memory at once — this technique is covered in full on the ",[31,32,34],"a",{"href":33},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per\u002F","Streaming Large Result Sets with yield_per"," cluster.",[37,38,40],"h2",{"id":39},"quick-answer","Quick Answer",[14,42,43],{},"Before — the legacy buffered approach loads every matching row into Python memory in a single round-trip:",[45,46,51],"pre",{"className":47,"code":48,"language":49,"meta":50,"style":50},"language-python shiki shiki-themes github-light github-dark","# BEFORE: buffered fetch — dangerous for large tables\nimport asyncio\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nimport sqlalchemy as sa\n\nengine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:password@localhost\u002Fdb\")\n\nclass Base(DeclarativeBase):\n    pass\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    user_id: Mapped[int]\n    total_cents: Mapped[int]\n    status: Mapped[str]\n\nasync def export_all_orders_buffered(session: AsyncSession) -> list[Order]:\n    # Loads every row into memory — will OOM on millions of rows\n    result = await session.execute(select(Order))\n    return result.scalars().all()\n","python","",[18,52,53,62,73,87,100,113,126,133,152,157,176,182,187,202,213,245,256,266,277,282,297,303,317],{"__ignoreMap":50},[54,55,58],"span",{"class":56,"line":57},"line",1,[54,59,61],{"class":60},"sJ8bj","# BEFORE: buffered fetch — dangerous for large tables\n",[54,63,65,69],{"class":56,"line":64},2,[54,66,68],{"class":67},"szBVR","import",[54,70,72],{"class":71},"sVt8B"," asyncio\n",[54,74,76,79,82,84],{"class":56,"line":75},3,[54,77,78],{"class":67},"from",[54,80,81],{"class":71}," sqlalchemy ",[54,83,68],{"class":67},[54,85,86],{"class":71}," select\n",[54,88,90,92,95,97],{"class":56,"line":89},4,[54,91,78],{"class":67},[54,93,94],{"class":71}," sqlalchemy.ext.asyncio ",[54,96,68],{"class":67},[54,98,99],{"class":71}," AsyncSession, create_async_engine\n",[54,101,103,105,108,110],{"class":56,"line":102},5,[54,104,78],{"class":67},[54,106,107],{"class":71}," sqlalchemy.orm ",[54,109,68],{"class":67},[54,111,112],{"class":71}," DeclarativeBase, Mapped, mapped_column\n",[54,114,116,118,120,123],{"class":56,"line":115},6,[54,117,68],{"class":67},[54,119,81],{"class":71},[54,121,122],{"class":67},"as",[54,124,125],{"class":71}," sa\n",[54,127,129],{"class":56,"line":128},7,[54,130,132],{"emptyLinePlaceholder":131},true,"\n",[54,134,136,139,142,145,149],{"class":56,"line":135},8,[54,137,138],{"class":71},"engine ",[54,140,141],{"class":67},"=",[54,143,144],{"class":71}," create_async_engine(",[54,146,148],{"class":147},"sZZnC","\"postgresql+asyncpg:\u002F\u002Fuser:password@localhost\u002Fdb\"",[54,150,151],{"class":71},")\n",[54,153,155],{"class":56,"line":154},9,[54,156,132],{"emptyLinePlaceholder":131},[54,158,160,163,167,170,173],{"class":56,"line":159},10,[54,161,162],{"class":67},"class",[54,164,166],{"class":165},"sScJk"," Base",[54,168,169],{"class":71},"(",[54,171,172],{"class":165},"DeclarativeBase",[54,174,175],{"class":71},"):\n",[54,177,179],{"class":56,"line":178},11,[54,180,181],{"class":67},"    pass\n",[54,183,185],{"class":56,"line":184},12,[54,186,132],{"emptyLinePlaceholder":131},[54,188,190,192,195,197,200],{"class":56,"line":189},13,[54,191,162],{"class":67},[54,193,194],{"class":165}," Order",[54,196,169],{"class":71},[54,198,199],{"class":165},"Base",[54,201,175],{"class":71},[54,203,205,208,210],{"class":56,"line":204},14,[54,206,207],{"class":71},"    __tablename__ ",[54,209,141],{"class":67},[54,211,212],{"class":147}," \"orders\"\n",[54,214,216,220,223,226,229,231,234,238,240,243],{"class":56,"line":215},15,[54,217,219],{"class":218},"sj4cs","    id",[54,221,222],{"class":71},": Mapped[",[54,224,225],{"class":218},"int",[54,227,228],{"class":71},"] ",[54,230,141],{"class":67},[54,232,233],{"class":71}," mapped_column(",[54,235,237],{"class":236},"s4XuR","primary_key",[54,239,141],{"class":67},[54,241,242],{"class":218},"True",[54,244,151],{"class":71},[54,246,248,251,253],{"class":56,"line":247},16,[54,249,250],{"class":71},"    user_id: Mapped[",[54,252,225],{"class":218},[54,254,255],{"class":71},"]\n",[54,257,259,262,264],{"class":56,"line":258},17,[54,260,261],{"class":71},"    total_cents: Mapped[",[54,263,225],{"class":218},[54,265,255],{"class":71},[54,267,269,272,275],{"class":56,"line":268},18,[54,270,271],{"class":71},"    status: Mapped[",[54,273,274],{"class":218},"str",[54,276,255],{"class":71},[54,278,280],{"class":56,"line":279},19,[54,281,132],{"emptyLinePlaceholder":131},[54,283,285,288,291,294],{"class":56,"line":284},20,[54,286,287],{"class":67},"async",[54,289,290],{"class":67}," def",[54,292,293],{"class":165}," export_all_orders_buffered",[54,295,296],{"class":71},"(session: AsyncSession) -> list[Order]:\n",[54,298,300],{"class":56,"line":299},21,[54,301,302],{"class":60},"    # Loads every row into memory — will OOM on millions of rows\n",[54,304,306,309,311,314],{"class":56,"line":305},22,[54,307,308],{"class":71},"    result ",[54,310,141],{"class":67},[54,312,313],{"class":67}," await",[54,315,316],{"class":71}," session.execute(select(Order))\n",[54,318,320,323],{"class":56,"line":319},23,[54,321,322],{"class":67},"    return",[54,324,325],{"class":71}," result.scalars().all()\n",[14,327,328,329,332],{},"After — streaming with ",[18,330,331],{},"yield_per"," keeps memory flat regardless of result set size:",[45,334,336],{"className":47,"code":335,"language":49,"meta":50,"style":50},"# AFTER: async streaming with yield_per\nimport asyncio\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nimport sqlalchemy as sa\n\nengine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:password@localhost\u002Fdb\")\nAsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\n\nclass Base(DeclarativeBase):\n    pass\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    user_id: Mapped[int]\n    total_cents: Mapped[int]\n    status: Mapped[str]\n\nasync def export_all_orders_streamed(session: AsyncSession) -> None:\n    stmt = select(Order).where(Order.status == \"completed\").order_by(Order.id)\n\n    async with await session.stream_scalars(stmt.execution_options(yield_per=1000)) as stream:\n        async for order in stream:\n            # process one Order ORM object at a time; peak memory stays near 1 000 rows\n            await process_order(order)\n\nasync def process_order(order: Order) -> None:\n    print(f\"Processing order {order.id}, total: {order.total_cents \u002F 100:.2f}\")\n",[18,337,338,343,349,359,370,380,390,394,406,426,430,442,446,450,462,470,492,500,508,516,520,538,557,561,590,607,613,622,627,644],{"__ignoreMap":50},[54,339,340],{"class":56,"line":57},[54,341,342],{"class":60},"# AFTER: async streaming with yield_per\n",[54,344,345,347],{"class":56,"line":64},[54,346,68],{"class":67},[54,348,72],{"class":71},[54,350,351,353,355,357],{"class":56,"line":75},[54,352,78],{"class":67},[54,354,81],{"class":71},[54,356,68],{"class":67},[54,358,86],{"class":71},[54,360,361,363,365,367],{"class":56,"line":89},[54,362,78],{"class":67},[54,364,94],{"class":71},[54,366,68],{"class":67},[54,368,369],{"class":71}," AsyncSession, create_async_engine, async_sessionmaker\n",[54,371,372,374,376,378],{"class":56,"line":102},[54,373,78],{"class":67},[54,375,107],{"class":71},[54,377,68],{"class":67},[54,379,112],{"class":71},[54,381,382,384,386,388],{"class":56,"line":115},[54,383,68],{"class":67},[54,385,81],{"class":71},[54,387,122],{"class":67},[54,389,125],{"class":71},[54,391,392],{"class":56,"line":128},[54,393,132],{"emptyLinePlaceholder":131},[54,395,396,398,400,402,404],{"class":56,"line":135},[54,397,138],{"class":71},[54,399,141],{"class":67},[54,401,144],{"class":71},[54,403,148],{"class":147},[54,405,151],{"class":71},[54,407,408,411,413,416,419,421,424],{"class":56,"line":154},[54,409,410],{"class":71},"AsyncSessionLocal ",[54,412,141],{"class":67},[54,414,415],{"class":71}," async_sessionmaker(engine, ",[54,417,418],{"class":236},"expire_on_commit",[54,420,141],{"class":67},[54,422,423],{"class":218},"False",[54,425,151],{"class":71},[54,427,428],{"class":56,"line":159},[54,429,132],{"emptyLinePlaceholder":131},[54,431,432,434,436,438,440],{"class":56,"line":178},[54,433,162],{"class":67},[54,435,166],{"class":165},[54,437,169],{"class":71},[54,439,172],{"class":165},[54,441,175],{"class":71},[54,443,444],{"class":56,"line":184},[54,445,181],{"class":67},[54,447,448],{"class":56,"line":189},[54,449,132],{"emptyLinePlaceholder":131},[54,451,452,454,456,458,460],{"class":56,"line":204},[54,453,162],{"class":67},[54,455,194],{"class":165},[54,457,169],{"class":71},[54,459,199],{"class":165},[54,461,175],{"class":71},[54,463,464,466,468],{"class":56,"line":215},[54,465,207],{"class":71},[54,467,141],{"class":67},[54,469,212],{"class":147},[54,471,472,474,476,478,480,482,484,486,488,490],{"class":56,"line":247},[54,473,219],{"class":218},[54,475,222],{"class":71},[54,477,225],{"class":218},[54,479,228],{"class":71},[54,481,141],{"class":67},[54,483,233],{"class":71},[54,485,237],{"class":236},[54,487,141],{"class":67},[54,489,242],{"class":218},[54,491,151],{"class":71},[54,493,494,496,498],{"class":56,"line":258},[54,495,250],{"class":71},[54,497,225],{"class":218},[54,499,255],{"class":71},[54,501,502,504,506],{"class":56,"line":268},[54,503,261],{"class":71},[54,505,225],{"class":218},[54,507,255],{"class":71},[54,509,510,512,514],{"class":56,"line":279},[54,511,271],{"class":71},[54,513,274],{"class":218},[54,515,255],{"class":71},[54,517,518],{"class":56,"line":284},[54,519,132],{"emptyLinePlaceholder":131},[54,521,522,524,526,529,532,535],{"class":56,"line":299},[54,523,287],{"class":67},[54,525,290],{"class":67},[54,527,528],{"class":165}," export_all_orders_streamed",[54,530,531],{"class":71},"(session: AsyncSession) -> ",[54,533,534],{"class":218},"None",[54,536,537],{"class":71},":\n",[54,539,540,543,545,548,551,554],{"class":56,"line":305},[54,541,542],{"class":71},"    stmt ",[54,544,141],{"class":67},[54,546,547],{"class":71}," select(Order).where(Order.status ",[54,549,550],{"class":67},"==",[54,552,553],{"class":147}," \"completed\"",[54,555,556],{"class":71},").order_by(Order.id)\n",[54,558,559],{"class":56,"line":319},[54,560,132],{"emptyLinePlaceholder":131},[54,562,564,567,570,572,575,577,579,582,585,587],{"class":56,"line":563},24,[54,565,566],{"class":67},"    async",[54,568,569],{"class":67}," with",[54,571,313],{"class":67},[54,573,574],{"class":71}," session.stream_scalars(stmt.execution_options(",[54,576,331],{"class":236},[54,578,141],{"class":67},[54,580,581],{"class":218},"1000",[54,583,584],{"class":71},")) ",[54,586,122],{"class":67},[54,588,589],{"class":71}," stream:\n",[54,591,593,596,599,602,605],{"class":56,"line":592},25,[54,594,595],{"class":67},"        async",[54,597,598],{"class":67}," for",[54,600,601],{"class":71}," order ",[54,603,604],{"class":67},"in",[54,606,589],{"class":71},[54,608,610],{"class":56,"line":609},26,[54,611,612],{"class":60},"            # process one Order ORM object at a time; peak memory stays near 1 000 rows\n",[54,614,616,619],{"class":56,"line":615},27,[54,617,618],{"class":67},"            await",[54,620,621],{"class":71}," process_order(order)\n",[54,623,625],{"class":56,"line":624},28,[54,626,132],{"emptyLinePlaceholder":131},[54,628,630,632,634,637,640,642],{"class":56,"line":629},29,[54,631,287],{"class":67},[54,633,290],{"class":67},[54,635,636],{"class":165}," process_order",[54,638,639],{"class":71},"(order: Order) -> ",[54,641,534],{"class":218},[54,643,537],{"class":71},[54,645,647,650,652,655,658,661,664,667,670,672,675,678,681,684,686,689],{"class":56,"line":646},30,[54,648,649],{"class":218},"    print",[54,651,169],{"class":71},[54,653,654],{"class":67},"f",[54,656,657],{"class":147},"\"Processing order ",[54,659,660],{"class":218},"{",[54,662,663],{"class":71},"order.id",[54,665,666],{"class":218},"}",[54,668,669],{"class":147},", total: ",[54,671,660],{"class":218},[54,673,674],{"class":71},"order.total_cents ",[54,676,677],{"class":67},"\u002F",[54,679,680],{"class":218}," 100",[54,682,683],{"class":67},":.2f",[54,685,666],{"class":218},[54,687,688],{"class":147},"\"",[54,690,151],{"class":71},[37,692,694],{"id":693},"execution-context-async-workflow-integration","Execution Context & Async Workflow Integration",[696,697,699],"h3",{"id":698},"asyncsessionstream-vs-stream_scalars","AsyncSession.stream() vs stream_scalars()",[14,701,702],{},"SQLAlchemy exposes two coroutines for async streaming:",[704,705,706,729],"ul",{},[707,708,709,712,713,716,717,720,721,724,725,728],"li",{},[18,710,711],{},"AsyncSession.stream(stmt)"," returns an ",[18,714,715],{},"AsyncMappingResult"," (or ",[18,718,719],{},"AsyncResult",") — each iteration yields a ",[18,722,723],{},"Row"," object whose columns are accessed by name or index. Use this when you have multi-column projections (",[18,726,727],{},"select(Order.id, Order.total_cents)",") or when you need access to the raw row tuple.",[707,730,731,734,735,738,739,742,743,746],{},[18,732,733],{},"AsyncSession.stream_scalars(stmt)"," is a convenience wrapper that calls ",[18,736,737],{},"stream()"," and then applies ",[18,740,741],{},".scalars()"," — each iteration yields fully-hydrated ORM model instances. Use this for single-entity queries where you want ",[18,744,745],{},"Order"," objects directly.",[14,748,749,750,753,754,757],{},"Both accept ",[18,751,752],{},"execution_options(yield_per=N)"," either on the statement before passing it in, or applied directly on the connection via ",[18,755,756],{},"session.execute(stmt, execution_options={\"yield_per\": N})",".",[45,759,761],{"className":47,"code":760,"language":49,"meta":50,"style":50},"import asyncio\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nimport sqlalchemy as sa\n\nengine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:password@localhost\u002Fdb\")\nAsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\n\nclass Base(DeclarativeBase):\n    pass\n\nclass Invoice(Base):\n    __tablename__ = \"invoices\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    user_id: Mapped[int]\n    amount_cents: Mapped[int]\n    issued_at: Mapped[sa.DateTime] = mapped_column(sa.DateTime(timezone=True))\n\nasync def stream_invoice_summaries(session: AsyncSession) -> None:\n    # stream() for multi-column projections\n    stmt = (\n        select(Invoice.id, Invoice.user_id, Invoice.amount_cents)\n        .order_by(Invoice.id)\n        .execution_options(yield_per=500)\n    )\n    async with await session.stream(stmt) as stream:\n        async for row in stream:\n            print(f\"invoice={row.id} user={row.user_id} amount={row.amount_cents}\")\n",[18,762,763,769,779,789,799,809,813,825,841,845,857,861,865,878,887,909,917,926,946,950,965,970,979,984,989,1003,1008,1023,1036],{"__ignoreMap":50},[54,764,765,767],{"class":56,"line":57},[54,766,68],{"class":67},[54,768,72],{"class":71},[54,770,771,773,775,777],{"class":56,"line":64},[54,772,78],{"class":67},[54,774,81],{"class":71},[54,776,68],{"class":67},[54,778,86],{"class":71},[54,780,781,783,785,787],{"class":56,"line":75},[54,782,78],{"class":67},[54,784,94],{"class":71},[54,786,68],{"class":67},[54,788,369],{"class":71},[54,790,791,793,795,797],{"class":56,"line":89},[54,792,78],{"class":67},[54,794,107],{"class":71},[54,796,68],{"class":67},[54,798,112],{"class":71},[54,800,801,803,805,807],{"class":56,"line":102},[54,802,68],{"class":67},[54,804,81],{"class":71},[54,806,122],{"class":67},[54,808,125],{"class":71},[54,810,811],{"class":56,"line":115},[54,812,132],{"emptyLinePlaceholder":131},[54,814,815,817,819,821,823],{"class":56,"line":128},[54,816,138],{"class":71},[54,818,141],{"class":67},[54,820,144],{"class":71},[54,822,148],{"class":147},[54,824,151],{"class":71},[54,826,827,829,831,833,835,837,839],{"class":56,"line":135},[54,828,410],{"class":71},[54,830,141],{"class":67},[54,832,415],{"class":71},[54,834,418],{"class":236},[54,836,141],{"class":67},[54,838,423],{"class":218},[54,840,151],{"class":71},[54,842,843],{"class":56,"line":154},[54,844,132],{"emptyLinePlaceholder":131},[54,846,847,849,851,853,855],{"class":56,"line":159},[54,848,162],{"class":67},[54,850,166],{"class":165},[54,852,169],{"class":71},[54,854,172],{"class":165},[54,856,175],{"class":71},[54,858,859],{"class":56,"line":178},[54,860,181],{"class":67},[54,862,863],{"class":56,"line":184},[54,864,132],{"emptyLinePlaceholder":131},[54,866,867,869,872,874,876],{"class":56,"line":189},[54,868,162],{"class":67},[54,870,871],{"class":165}," Invoice",[54,873,169],{"class":71},[54,875,199],{"class":165},[54,877,175],{"class":71},[54,879,880,882,884],{"class":56,"line":204},[54,881,207],{"class":71},[54,883,141],{"class":67},[54,885,886],{"class":147}," \"invoices\"\n",[54,888,889,891,893,895,897,899,901,903,905,907],{"class":56,"line":215},[54,890,219],{"class":218},[54,892,222],{"class":71},[54,894,225],{"class":218},[54,896,228],{"class":71},[54,898,141],{"class":67},[54,900,233],{"class":71},[54,902,237],{"class":236},[54,904,141],{"class":67},[54,906,242],{"class":218},[54,908,151],{"class":71},[54,910,911,913,915],{"class":56,"line":247},[54,912,250],{"class":71},[54,914,225],{"class":218},[54,916,255],{"class":71},[54,918,919,922,924],{"class":56,"line":258},[54,920,921],{"class":71},"    amount_cents: Mapped[",[54,923,225],{"class":218},[54,925,255],{"class":71},[54,927,928,931,933,936,939,941,943],{"class":56,"line":268},[54,929,930],{"class":71},"    issued_at: Mapped[sa.DateTime] ",[54,932,141],{"class":67},[54,934,935],{"class":71}," mapped_column(sa.DateTime(",[54,937,938],{"class":236},"timezone",[54,940,141],{"class":67},[54,942,242],{"class":218},[54,944,945],{"class":71},"))\n",[54,947,948],{"class":56,"line":279},[54,949,132],{"emptyLinePlaceholder":131},[54,951,952,954,956,959,961,963],{"class":56,"line":284},[54,953,287],{"class":67},[54,955,290],{"class":67},[54,957,958],{"class":165}," stream_invoice_summaries",[54,960,531],{"class":71},[54,962,534],{"class":218},[54,964,537],{"class":71},[54,966,967],{"class":56,"line":299},[54,968,969],{"class":60},"    # stream() for multi-column projections\n",[54,971,972,974,976],{"class":56,"line":305},[54,973,542],{"class":71},[54,975,141],{"class":67},[54,977,978],{"class":71}," (\n",[54,980,981],{"class":56,"line":319},[54,982,983],{"class":71},"        select(Invoice.id, Invoice.user_id, Invoice.amount_cents)\n",[54,985,986],{"class":56,"line":563},[54,987,988],{"class":71},"        .order_by(Invoice.id)\n",[54,990,991,994,996,998,1001],{"class":56,"line":592},[54,992,993],{"class":71},"        .execution_options(",[54,995,331],{"class":236},[54,997,141],{"class":67},[54,999,1000],{"class":218},"500",[54,1002,151],{"class":71},[54,1004,1005],{"class":56,"line":609},[54,1006,1007],{"class":71},"    )\n",[54,1009,1010,1012,1014,1016,1019,1021],{"class":56,"line":615},[54,1011,566],{"class":67},[54,1013,569],{"class":67},[54,1015,313],{"class":67},[54,1017,1018],{"class":71}," session.stream(stmt) ",[54,1020,122],{"class":67},[54,1022,589],{"class":71},[54,1024,1025,1027,1029,1032,1034],{"class":56,"line":624},[54,1026,595],{"class":67},[54,1028,598],{"class":67},[54,1030,1031],{"class":71}," row ",[54,1033,604],{"class":67},[54,1035,589],{"class":71},[54,1037,1038,1041,1043,1045,1048,1050,1053,1055,1058,1060,1063,1065,1068,1070,1073,1075,1077],{"class":56,"line":629},[54,1039,1040],{"class":218},"            print",[54,1042,169],{"class":71},[54,1044,654],{"class":67},[54,1046,1047],{"class":147},"\"invoice=",[54,1049,660],{"class":218},[54,1051,1052],{"class":71},"row.id",[54,1054,666],{"class":218},[54,1056,1057],{"class":147}," user=",[54,1059,660],{"class":218},[54,1061,1062],{"class":71},"row.user_id",[54,1064,666],{"class":218},[54,1066,1067],{"class":147}," amount=",[54,1069,660],{"class":218},[54,1071,1072],{"class":71},"row.amount_cents",[54,1074,666],{"class":218},[54,1076,688],{"class":147},[54,1078,151],{"class":71},[696,1080,1082],{"id":1081},"event-loop-safety-and-connection-held-open","Event-Loop Safety and Connection Held Open",[14,1084,1085,1086,1088,1089,1093,1094,1097,1098,1101],{},"The most important operational fact about ",[18,1087,331],{}," streaming is that ",[1090,1091,1092],"strong",{},"the database connection is held checked out for the entire duration of the async-for loop",". Unlike a buffered ",[18,1095,1096],{},"await session.execute()"," call that fetches all rows and releases the connection immediately, a streaming cursor keeps the connection occupied until the ",[18,1099,1100],{},"async with"," context manager exits.",[14,1103,1104,1105,1109],{},"This has a direct impact on connection pool sizing. If your application streams 10 concurrent exports, you need at least 10 connections in the pool. A pool sized for normal OLTP request\u002Fresponse patterns will exhaust quickly under heavy streaming load — see the guidance in ",[31,1106,1108],{"href":1107},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002F","Handling Connection Leaks and Pool Exhaustion"," for tuning strategies.",[14,1111,1112,1113,1116,1117,1119,1120,1122,1123,1126,1127,1129,1130,1133],{},"The result hydration happens in batches of exactly ",[18,1114,1115],{},"N"," rows (the ",[18,1118,331],{}," value). asyncpg fetches those ",[18,1121,1115],{}," rows from the server-side cursor, hands them to SQLAlchemy's result processor, and the ",[18,1124,1125],{},"async for"," loop delivers them one ORM object at a time. After consuming all ",[18,1128,1115],{}," objects, the next iteration triggers another ",[18,1131,1132],{},"fetchmany(N)"," call over the same persistent cursor.",[696,1135,1137],{"id":1136},"asyncpg-server-side-cursor-behavior","asyncpg Server-Side Cursor Behavior",[14,1139,1140,1141,1144,1145,1148,1149,1152,1153,1155],{},"asyncpg implements PostgreSQL's ",[18,1142,1143],{},"DECLARE CURSOR … FOR SELECT …"," protocol when a query is executed in a streaming context. These are ",[1090,1146,1147],{},"unnamed cursors"," by default — created for the duration of the transaction and destroyed when the transaction ends. Named cursors (created with ",[18,1150,1151],{},"DECLARE name CURSOR FOR …",") are also supported in asyncpg but SQLAlchemy's ",[18,1154,331],{}," mechanism uses the unnamed form.",[14,1157,1158],{},"The cursor lives inside a transaction. SQLAlchemy automatically wraps the streaming session in an implicit transaction if one is not already open. This is important for two reasons:",[1160,1161,1162,1165],"ol",{},[707,1163,1164],{},"The cursor is only valid within its originating transaction — you cannot pass it across connection boundaries.",[707,1166,1167],{},"The connection cannot be returned to the pool until both the cursor is closed and the transaction is committed or rolled back.",[696,1169,1171],{"id":1170},"pgbouncer-in-transaction-pooling-mode","PgBouncer in Transaction Pooling Mode",[14,1173,1174,1175,1178],{},"If your infrastructure routes connections through PgBouncer in ",[1090,1176,1177],{},"transaction pooling mode",", server-side cursors will break. Transaction pooling returns the underlying PostgreSQL connection to the pool at the end of each transaction, which means the cursor — which must outlive individual statements but lives inside a transaction — is destroyed between asyncpg fetch calls.",[14,1180,1181,1182,1185],{},"The symptom is typically ",[18,1183,1184],{},"asyncpg.exceptions.InterfaceError"," or a silent empty result after the first batch.",[14,1187,1188],{},"Your options:",[704,1190,1191,1198,1209],{},[707,1192,1193,1194,1197],{},"Switch PgBouncer to ",[1090,1195,1196],{},"session pooling mode"," for the connections used by streaming queries. This keeps the same backend connection for the life of the application connection, allowing cursors to persist.",[707,1199,1200,1201,1204,1205,1208],{},"Use ",[18,1202,1203],{},"NullPool"," (",[18,1206,1207],{},"create_async_engine(..., poolclass=NullPool)",") for a dedicated streaming engine that connects directly to PostgreSQL without going through PgBouncer. This is the most reliable approach when you cannot reconfigure PgBouncer.",[707,1210,1211,1212,1214,1215,1219],{},"Avoid ",[18,1213,331],{}," entirely for this pool and instead use ",[31,1216,1218],{"href":1217},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per\u002Fpaginating-large-result-sets-with-keyset-pagination\u002F","keyset pagination"," to simulate streaming through sequential ranged queries that each complete in a single round-trip.",[45,1221,1223],{"className":47,"code":1222,"language":49,"meta":50,"style":50},"import asyncio\nfrom sqlalchemy.pool import NullPool\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nimport sqlalchemy as sa\n\n# Dedicated streaming engine that bypasses PgBouncer\nstreaming_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:password@postgres-direct:5432\u002Fdb\",\n    poolclass=NullPool,\n)\nStreamingSession = async_sessionmaker(streaming_engine, expire_on_commit=False)\n\nclass Base(DeclarativeBase):\n    pass\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    user_id: Mapped[int]\n    total_cents: Mapped[int]\n    status: Mapped[str]\n\nasync def safe_stream_for_pgbouncer(session: AsyncSession) -> None:\n    stmt = select(Order).order_by(Order.id).execution_options(yield_per=2000)\n    async with await session.stream_scalars(stmt) as stream:\n        async for order in stream:\n            await process_order(order)\n\nasync def process_order(order: Order) -> None:\n    pass\n",[18,1224,1225,1231,1243,1253,1263,1273,1283,1287,1292,1302,1310,1320,1324,1342,1346,1358,1362,1366,1378,1386,1408,1416,1424,1432,1436,1451,1469,1484,1496,1502,1506,1521],{"__ignoreMap":50},[54,1226,1227,1229],{"class":56,"line":57},[54,1228,68],{"class":67},[54,1230,72],{"class":71},[54,1232,1233,1235,1238,1240],{"class":56,"line":64},[54,1234,78],{"class":67},[54,1236,1237],{"class":71}," sqlalchemy.pool ",[54,1239,68],{"class":67},[54,1241,1242],{"class":71}," NullPool\n",[54,1244,1245,1247,1249,1251],{"class":56,"line":75},[54,1246,78],{"class":67},[54,1248,81],{"class":71},[54,1250,68],{"class":67},[54,1252,86],{"class":71},[54,1254,1255,1257,1259,1261],{"class":56,"line":89},[54,1256,78],{"class":67},[54,1258,94],{"class":71},[54,1260,68],{"class":67},[54,1262,369],{"class":71},[54,1264,1265,1267,1269,1271],{"class":56,"line":102},[54,1266,78],{"class":67},[54,1268,107],{"class":71},[54,1270,68],{"class":67},[54,1272,112],{"class":71},[54,1274,1275,1277,1279,1281],{"class":56,"line":115},[54,1276,68],{"class":67},[54,1278,81],{"class":71},[54,1280,122],{"class":67},[54,1282,125],{"class":71},[54,1284,1285],{"class":56,"line":128},[54,1286,132],{"emptyLinePlaceholder":131},[54,1288,1289],{"class":56,"line":135},[54,1290,1291],{"class":60},"# Dedicated streaming engine that bypasses PgBouncer\n",[54,1293,1294,1297,1299],{"class":56,"line":154},[54,1295,1296],{"class":71},"streaming_engine ",[54,1298,141],{"class":67},[54,1300,1301],{"class":71}," create_async_engine(\n",[54,1303,1304,1307],{"class":56,"line":159},[54,1305,1306],{"class":147},"    \"postgresql+asyncpg:\u002F\u002Fuser:password@postgres-direct:5432\u002Fdb\"",[54,1308,1309],{"class":71},",\n",[54,1311,1312,1315,1317],{"class":56,"line":178},[54,1313,1314],{"class":236},"    poolclass",[54,1316,141],{"class":67},[54,1318,1319],{"class":71},"NullPool,\n",[54,1321,1322],{"class":56,"line":184},[54,1323,151],{"class":71},[54,1325,1326,1329,1331,1334,1336,1338,1340],{"class":56,"line":189},[54,1327,1328],{"class":71},"StreamingSession ",[54,1330,141],{"class":67},[54,1332,1333],{"class":71}," async_sessionmaker(streaming_engine, ",[54,1335,418],{"class":236},[54,1337,141],{"class":67},[54,1339,423],{"class":218},[54,1341,151],{"class":71},[54,1343,1344],{"class":56,"line":204},[54,1345,132],{"emptyLinePlaceholder":131},[54,1347,1348,1350,1352,1354,1356],{"class":56,"line":215},[54,1349,162],{"class":67},[54,1351,166],{"class":165},[54,1353,169],{"class":71},[54,1355,172],{"class":165},[54,1357,175],{"class":71},[54,1359,1360],{"class":56,"line":247},[54,1361,181],{"class":67},[54,1363,1364],{"class":56,"line":258},[54,1365,132],{"emptyLinePlaceholder":131},[54,1367,1368,1370,1372,1374,1376],{"class":56,"line":268},[54,1369,162],{"class":67},[54,1371,194],{"class":165},[54,1373,169],{"class":71},[54,1375,199],{"class":165},[54,1377,175],{"class":71},[54,1379,1380,1382,1384],{"class":56,"line":279},[54,1381,207],{"class":71},[54,1383,141],{"class":67},[54,1385,212],{"class":147},[54,1387,1388,1390,1392,1394,1396,1398,1400,1402,1404,1406],{"class":56,"line":284},[54,1389,219],{"class":218},[54,1391,222],{"class":71},[54,1393,225],{"class":218},[54,1395,228],{"class":71},[54,1397,141],{"class":67},[54,1399,233],{"class":71},[54,1401,237],{"class":236},[54,1403,141],{"class":67},[54,1405,242],{"class":218},[54,1407,151],{"class":71},[54,1409,1410,1412,1414],{"class":56,"line":299},[54,1411,250],{"class":71},[54,1413,225],{"class":218},[54,1415,255],{"class":71},[54,1417,1418,1420,1422],{"class":56,"line":305},[54,1419,261],{"class":71},[54,1421,225],{"class":218},[54,1423,255],{"class":71},[54,1425,1426,1428,1430],{"class":56,"line":319},[54,1427,271],{"class":71},[54,1429,274],{"class":218},[54,1431,255],{"class":71},[54,1433,1434],{"class":56,"line":563},[54,1435,132],{"emptyLinePlaceholder":131},[54,1437,1438,1440,1442,1445,1447,1449],{"class":56,"line":592},[54,1439,287],{"class":67},[54,1441,290],{"class":67},[54,1443,1444],{"class":165}," safe_stream_for_pgbouncer",[54,1446,531],{"class":71},[54,1448,534],{"class":218},[54,1450,537],{"class":71},[54,1452,1453,1455,1457,1460,1462,1464,1467],{"class":56,"line":609},[54,1454,542],{"class":71},[54,1456,141],{"class":67},[54,1458,1459],{"class":71}," select(Order).order_by(Order.id).execution_options(",[54,1461,331],{"class":236},[54,1463,141],{"class":67},[54,1465,1466],{"class":218},"2000",[54,1468,151],{"class":71},[54,1470,1471,1473,1475,1477,1480,1482],{"class":56,"line":615},[54,1472,566],{"class":67},[54,1474,569],{"class":67},[54,1476,313],{"class":67},[54,1478,1479],{"class":71}," session.stream_scalars(stmt) ",[54,1481,122],{"class":67},[54,1483,589],{"class":71},[54,1485,1486,1488,1490,1492,1494],{"class":56,"line":624},[54,1487,595],{"class":67},[54,1489,598],{"class":67},[54,1491,601],{"class":71},[54,1493,604],{"class":67},[54,1495,589],{"class":71},[54,1497,1498,1500],{"class":56,"line":629},[54,1499,618],{"class":67},[54,1501,621],{"class":71},[54,1503,1504],{"class":56,"line":646},[54,1505,132],{"emptyLinePlaceholder":131},[54,1507,1509,1511,1513,1515,1517,1519],{"class":56,"line":1508},31,[54,1510,287],{"class":67},[54,1512,290],{"class":67},[54,1514,636],{"class":165},[54,1516,639],{"class":71},[54,1518,534],{"class":218},[54,1520,537],{"class":71},[54,1522,1524],{"class":56,"line":1523},32,[54,1525,181],{"class":67},[37,1527,1529],{"id":1528},"resolving-warnings-errors-common-mistakes","Resolving Warnings, Errors & Common Mistakes",[1531,1532,1533,1549],"table",{},[1534,1535,1536],"thead",{},[1537,1538,1539,1543,1546],"tr",{},[1540,1541,1542],"th",{},"Warning \u002F Error",[1540,1544,1545],{},"Root Cause",[1540,1547,1548],{},"Production Fix",[1550,1551,1552,1590,1618,1648],"tbody",{},[1537,1553,1554,1560,1569],{},[1555,1556,1557],"td",{},[18,1558,1559],{},"sqlalchemy.exc.InvalidRequestError: 'yield_per' cannot be used with 'joined' eager loading",[1555,1561,1562,1565,1566,1568],{},[18,1563,1564],{},"joinedload()"," generates a JOIN that produces duplicate parent rows — SQLAlchemy's de-duplication logic requires all rows to be buffered before it can reassemble relationships. ",[18,1567,331],{}," bypasses this buffering.",[1555,1570,1571,1572,21,1574,1577,1578,1581,1582,1584,1585,1589],{},"Replace ",[18,1573,1564],{},[18,1575,1576],{},"selectinload()"," or ",[18,1579,1580],{},"subqueryload()",". These strategies fire a separate query for related objects and are fully compatible with ",[18,1583,331],{},". See ",[31,1586,1588],{"href":1587},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fcomplex-joins-and-relationship-loading-strategies\u002Fusing-selectinload-vs-joinedload-for-n1-prevention\u002F","selectinload vs joinedload"," for the trade-offs.",[1537,1591,1592,1597,1600],{},[1555,1593,1594],{},[18,1595,1596],{},"asyncpg.exceptions.InterfaceError: cannot use a connection that has been closed",[1555,1598,1599],{},"The connection was returned to the pool (or closed) before the async-for loop finished consuming the cursor. Common causes: PgBouncer transaction pooling reclaiming the connection, or a timeout closing the underlying socket mid-stream.",[1555,1601,1602,1603,1605,1606,1609,1610,1613,1614,1617],{},"For PgBouncer: use session pooling or ",[18,1604,1203],{},". For timeouts: increase ",[18,1607,1608],{},"command_timeout"," on ",[18,1611,1612],{},"create_async_engine"," and ensure your PostgreSQL ",[18,1615,1616],{},"statement_timeout"," is set generously for the streaming user\u002Frole.",[1537,1619,1620,1625,1637],{},[1555,1621,1622],{},[18,1623,1624],{},"sqlalchemy.exc.ResourceClosedError: This result object does not return rows.",[1555,1626,1627,1628,1630,1631,1633,1634,1636],{},"The streaming context manager was exited (or the ",[18,1629,1100],{}," block ended) before iteration was complete, or ",[18,1632,28],{}," was awaited outside of an ",[18,1635,1100],{}," block.",[1555,1638,1639,1640,1643,1644,1647],{},"Always use ",[18,1641,1642],{},"async with await session.stream_scalars(stmt) as stream:"," and complete iteration inside the block. Do not ",[18,1645,1646],{},"await session.stream_scalars()"," and store the result to iterate later outside the context.",[1537,1649,1650,1655,1658],{},[1555,1651,1652],{},[18,1653,1654],{},"asyncpg.exceptions.InvalidSQLStatementNameError: prepared statement \"__asyncpg_X\" does not exist",[1555,1656,1657],{},"PgBouncer in transaction pooling mode does not allow prepared statements to persist across transactions. asyncpg caches prepared statements by default.",[1555,1659,1660,1661,1664,1665,1668],{},"Set ",[18,1662,1663],{},"prepared_statement_cache_size=0"," in the asyncpg connect arguments: ",[18,1666,1667],{},"create_async_engine(\"postgresql+asyncpg:\u002F\u002F...\", connect_args={\"prepared_statement_cache_size\": 0})",". Also consider switching PgBouncer to session pooling for the streaming pool.",[696,1670,1672],{"id":1671},"fixing-the-joinedload-incompatibility","Fixing the joinedload Incompatibility",[45,1674,1676],{"className":47,"code":1675,"language":49,"meta":50,"style":50},"import asyncio\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, selectinload\nimport sqlalchemy as sa\n\nengine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:password@localhost\u002Fdb\")\nAsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\n\nclass Base(DeclarativeBase):\n    pass\n\nclass User(Base):\n    __tablename__ = \"users\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    email: Mapped[str]\n    orders: Mapped[list[\"Order\"]] = relationship(back_populates=\"user\")\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    user_id: Mapped[int] = mapped_column(sa.ForeignKey(\"users.id\"))\n    total_cents: Mapped[int]\n    status: Mapped[str]\n    user: Mapped[\"User\"] = relationship(back_populates=\"orders\")\n\nasync def stream_orders_with_users(session: AsyncSession) -> None:\n    # WRONG: joinedload + yield_per raises InvalidRequestError\n    # stmt = select(Order).options(joinedload(Order.user)).execution_options(yield_per=1000)\n\n    # CORRECT: selectinload fires a second query per batch, compatible with yield_per\n    stmt = (\n        select(Order)\n        .options(selectinload(Order.user))\n        .where(Order.status == \"pending\")\n        .order_by(Order.id)\n        .execution_options(yield_per=1000)\n    )\n    async with await session.stream_scalars(stmt) as stream:\n        async for order in stream:\n            print(f\"Order {order.id} placed by {order.user.email}\")\n",[18,1677,1678,1684,1694,1704,1715,1725,1729,1741,1757,1761,1773,1777,1781,1794,1803,1825,1834,1860,1864,1876,1884,1906,1924,1932,1940,1963,1967,1982,1987,1992,1996,2001,2009,2015,2021,2034,2040,2053,2058,2073,2086],{"__ignoreMap":50},[54,1679,1680,1682],{"class":56,"line":57},[54,1681,68],{"class":67},[54,1683,72],{"class":71},[54,1685,1686,1688,1690,1692],{"class":56,"line":64},[54,1687,78],{"class":67},[54,1689,81],{"class":71},[54,1691,68],{"class":67},[54,1693,86],{"class":71},[54,1695,1696,1698,1700,1702],{"class":56,"line":75},[54,1697,78],{"class":67},[54,1699,94],{"class":71},[54,1701,68],{"class":67},[54,1703,369],{"class":71},[54,1705,1706,1708,1710,1712],{"class":56,"line":89},[54,1707,78],{"class":67},[54,1709,107],{"class":71},[54,1711,68],{"class":67},[54,1713,1714],{"class":71}," DeclarativeBase, Mapped, mapped_column, relationship, selectinload\n",[54,1716,1717,1719,1721,1723],{"class":56,"line":102},[54,1718,68],{"class":67},[54,1720,81],{"class":71},[54,1722,122],{"class":67},[54,1724,125],{"class":71},[54,1726,1727],{"class":56,"line":115},[54,1728,132],{"emptyLinePlaceholder":131},[54,1730,1731,1733,1735,1737,1739],{"class":56,"line":128},[54,1732,138],{"class":71},[54,1734,141],{"class":67},[54,1736,144],{"class":71},[54,1738,148],{"class":147},[54,1740,151],{"class":71},[54,1742,1743,1745,1747,1749,1751,1753,1755],{"class":56,"line":135},[54,1744,410],{"class":71},[54,1746,141],{"class":67},[54,1748,415],{"class":71},[54,1750,418],{"class":236},[54,1752,141],{"class":67},[54,1754,423],{"class":218},[54,1756,151],{"class":71},[54,1758,1759],{"class":56,"line":154},[54,1760,132],{"emptyLinePlaceholder":131},[54,1762,1763,1765,1767,1769,1771],{"class":56,"line":159},[54,1764,162],{"class":67},[54,1766,166],{"class":165},[54,1768,169],{"class":71},[54,1770,172],{"class":165},[54,1772,175],{"class":71},[54,1774,1775],{"class":56,"line":178},[54,1776,181],{"class":67},[54,1778,1779],{"class":56,"line":184},[54,1780,132],{"emptyLinePlaceholder":131},[54,1782,1783,1785,1788,1790,1792],{"class":56,"line":189},[54,1784,162],{"class":67},[54,1786,1787],{"class":165}," User",[54,1789,169],{"class":71},[54,1791,199],{"class":165},[54,1793,175],{"class":71},[54,1795,1796,1798,1800],{"class":56,"line":204},[54,1797,207],{"class":71},[54,1799,141],{"class":67},[54,1801,1802],{"class":147}," \"users\"\n",[54,1804,1805,1807,1809,1811,1813,1815,1817,1819,1821,1823],{"class":56,"line":215},[54,1806,219],{"class":218},[54,1808,222],{"class":71},[54,1810,225],{"class":218},[54,1812,228],{"class":71},[54,1814,141],{"class":67},[54,1816,233],{"class":71},[54,1818,237],{"class":236},[54,1820,141],{"class":67},[54,1822,242],{"class":218},[54,1824,151],{"class":71},[54,1826,1827,1830,1832],{"class":56,"line":247},[54,1828,1829],{"class":71},"    email: Mapped[",[54,1831,274],{"class":218},[54,1833,255],{"class":71},[54,1835,1836,1839,1842,1845,1847,1850,1853,1855,1858],{"class":56,"line":258},[54,1837,1838],{"class":71},"    orders: Mapped[list[",[54,1840,1841],{"class":147},"\"Order\"",[54,1843,1844],{"class":71},"]] ",[54,1846,141],{"class":67},[54,1848,1849],{"class":71}," relationship(",[54,1851,1852],{"class":236},"back_populates",[54,1854,141],{"class":67},[54,1856,1857],{"class":147},"\"user\"",[54,1859,151],{"class":71},[54,1861,1862],{"class":56,"line":268},[54,1863,132],{"emptyLinePlaceholder":131},[54,1865,1866,1868,1870,1872,1874],{"class":56,"line":279},[54,1867,162],{"class":67},[54,1869,194],{"class":165},[54,1871,169],{"class":71},[54,1873,199],{"class":165},[54,1875,175],{"class":71},[54,1877,1878,1880,1882],{"class":56,"line":284},[54,1879,207],{"class":71},[54,1881,141],{"class":67},[54,1883,212],{"class":147},[54,1885,1886,1888,1890,1892,1894,1896,1898,1900,1902,1904],{"class":56,"line":299},[54,1887,219],{"class":218},[54,1889,222],{"class":71},[54,1891,225],{"class":218},[54,1893,228],{"class":71},[54,1895,141],{"class":67},[54,1897,233],{"class":71},[54,1899,237],{"class":236},[54,1901,141],{"class":67},[54,1903,242],{"class":218},[54,1905,151],{"class":71},[54,1907,1908,1910,1912,1914,1916,1919,1922],{"class":56,"line":305},[54,1909,250],{"class":71},[54,1911,225],{"class":218},[54,1913,228],{"class":71},[54,1915,141],{"class":67},[54,1917,1918],{"class":71}," mapped_column(sa.ForeignKey(",[54,1920,1921],{"class":147},"\"users.id\"",[54,1923,945],{"class":71},[54,1925,1926,1928,1930],{"class":56,"line":319},[54,1927,261],{"class":71},[54,1929,225],{"class":218},[54,1931,255],{"class":71},[54,1933,1934,1936,1938],{"class":56,"line":563},[54,1935,271],{"class":71},[54,1937,274],{"class":218},[54,1939,255],{"class":71},[54,1941,1942,1945,1948,1950,1952,1954,1956,1958,1961],{"class":56,"line":592},[54,1943,1944],{"class":71},"    user: Mapped[",[54,1946,1947],{"class":147},"\"User\"",[54,1949,228],{"class":71},[54,1951,141],{"class":67},[54,1953,1849],{"class":71},[54,1955,1852],{"class":236},[54,1957,141],{"class":67},[54,1959,1960],{"class":147},"\"orders\"",[54,1962,151],{"class":71},[54,1964,1965],{"class":56,"line":609},[54,1966,132],{"emptyLinePlaceholder":131},[54,1968,1969,1971,1973,1976,1978,1980],{"class":56,"line":615},[54,1970,287],{"class":67},[54,1972,290],{"class":67},[54,1974,1975],{"class":165}," stream_orders_with_users",[54,1977,531],{"class":71},[54,1979,534],{"class":218},[54,1981,537],{"class":71},[54,1983,1984],{"class":56,"line":624},[54,1985,1986],{"class":60},"    # WRONG: joinedload + yield_per raises InvalidRequestError\n",[54,1988,1989],{"class":56,"line":629},[54,1990,1991],{"class":60},"    # stmt = select(Order).options(joinedload(Order.user)).execution_options(yield_per=1000)\n",[54,1993,1994],{"class":56,"line":646},[54,1995,132],{"emptyLinePlaceholder":131},[54,1997,1998],{"class":56,"line":1508},[54,1999,2000],{"class":60},"    # CORRECT: selectinload fires a second query per batch, compatible with yield_per\n",[54,2002,2003,2005,2007],{"class":56,"line":1523},[54,2004,542],{"class":71},[54,2006,141],{"class":67},[54,2008,978],{"class":71},[54,2010,2012],{"class":56,"line":2011},33,[54,2013,2014],{"class":71},"        select(Order)\n",[54,2016,2018],{"class":56,"line":2017},34,[54,2019,2020],{"class":71},"        .options(selectinload(Order.user))\n",[54,2022,2024,2027,2029,2032],{"class":56,"line":2023},35,[54,2025,2026],{"class":71},"        .where(Order.status ",[54,2028,550],{"class":67},[54,2030,2031],{"class":147}," \"pending\"",[54,2033,151],{"class":71},[54,2035,2037],{"class":56,"line":2036},36,[54,2038,2039],{"class":71},"        .order_by(Order.id)\n",[54,2041,2043,2045,2047,2049,2051],{"class":56,"line":2042},37,[54,2044,993],{"class":71},[54,2046,331],{"class":236},[54,2048,141],{"class":67},[54,2050,581],{"class":218},[54,2052,151],{"class":71},[54,2054,2056],{"class":56,"line":2055},38,[54,2057,1007],{"class":71},[54,2059,2061,2063,2065,2067,2069,2071],{"class":56,"line":2060},39,[54,2062,566],{"class":67},[54,2064,569],{"class":67},[54,2066,313],{"class":67},[54,2068,1479],{"class":71},[54,2070,122],{"class":67},[54,2072,589],{"class":71},[54,2074,2076,2078,2080,2082,2084],{"class":56,"line":2075},40,[54,2077,595],{"class":67},[54,2079,598],{"class":67},[54,2081,601],{"class":71},[54,2083,604],{"class":67},[54,2085,589],{"class":71},[54,2087,2089,2091,2093,2095,2098,2100,2102,2104,2107,2109,2112,2114,2116],{"class":56,"line":2088},41,[54,2090,1040],{"class":218},[54,2092,169],{"class":71},[54,2094,654],{"class":67},[54,2096,2097],{"class":147},"\"Order ",[54,2099,660],{"class":218},[54,2101,663],{"class":71},[54,2103,666],{"class":218},[54,2105,2106],{"class":147}," placed by ",[54,2108,660],{"class":218},[54,2110,2111],{"class":71},"order.user.email",[54,2113,666],{"class":218},[54,2115,688],{"class":147},[54,2117,151],{"class":71},[37,2119,2121],{"id":2120},"advanced-async-streaming-optimization","Advanced Async Streaming Optimization",[696,2123,2125],{"id":2124},"choosing-the-yield_per-batch-size","Choosing the yield_per Batch Size",[14,2127,2128,2129,2131],{},"The right ",[18,2130,1115],{}," depends on row width. A practical rule of thumb:",[704,2133,2134,2140,2146],{},[707,2135,2136,2139],{},[1090,2137,2138],{},"Narrow rows"," (5–10 integer\u002Fstring columns, ~200 bytes each): N = 2 000–5 000",[707,2141,2142,2145],{},[1090,2143,2144],{},"Medium rows"," (~1 KB each, with text fields): N = 500–1 000",[707,2147,2148,2151],{},[1090,2149,2150],{},"Wide rows"," (JSONB blobs, long text, ~10 KB each): N = 50–200",[14,2153,2154,2155,2157],{},"The goal is to keep each batch between 1 MB and 5 MB of raw data. Too small and you pay excessive round-trip overhead; too large and you spike memory on each batch and delay the first ",[18,2156,1125],{}," iteration.",[696,2159,2161],{"id":2160},"parallel-post-processing-with-partitions-and-asynciogather","Parallel Post-Processing with partitions() and asyncio.gather",[14,2163,2164,2165,2168,2169,2171],{},"SQLAlchemy's async stream objects expose a ",[18,2166,2167],{},"partitions(size)"," method that yields lists of ",[18,2170,1115],{}," objects at once instead of one at a time. This is the key to parallelising CPU-bound or I\u002FO-bound post-processing while still streaming from the database.",[14,2173,2174,2175,2177,2178,2181],{},"The pattern: collect a partition (a list of ",[18,2176,1115],{}," ORM objects), then dispatch the batch to an async worker. Multiple partitions can be processed concurrently using ",[18,2179,2180],{},"asyncio.gather"," while the next partition is being fetched.",[45,2183,2185],{"className":47,"code":2184,"language":49,"meta":50,"style":50},"import asyncio\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nimport sqlalchemy as sa\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:password@localhost\u002Fdb\",\n    pool_size=20,\n    max_overflow=10,\n)\nAsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\n\nclass Base(DeclarativeBase):\n    pass\n\nclass Invoice(Base):\n    __tablename__ = \"invoices\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    user_id: Mapped[int]\n    amount_cents: Mapped[int]\n    status: Mapped[str]\n\nasync def send_invoice_batch(invoices: list[Invoice]) -> None:\n    \"\"\"Simulate async I\u002FO work — e.g. calling an external billing API.\"\"\"\n    await asyncio.sleep(0)  # replace with real async I\u002FO\n    print(f\"Dispatched {len(invoices)} invoices\")\n\nasync def stream_and_process_invoices_parallel(session: AsyncSession) -> None:\n    stmt = (\n        select(Invoice)\n        .where(Invoice.status == \"ready_to_send\")\n        .order_by(Invoice.id)\n        .execution_options(yield_per=500)\n    )\n\n    in_flight: list[asyncio.Task] = []\n    max_concurrent = 4  # tune based on external API rate limits\n\n    async with await session.stream_scalars(stmt) as stream:\n        async for partition in stream.partitions(500):\n            # Launch processing task without awaiting it immediately\n            task = asyncio.create_task(send_invoice_batch(list(partition)))\n            in_flight.append(task)\n\n            # Keep at most max_concurrent tasks running at once\n            if len(in_flight) >= max_concurrent:\n                await asyncio.gather(*in_flight)\n                in_flight.clear()\n\n    # Drain any remaining tasks after the stream is exhausted\n    if in_flight:\n        await asyncio.gather(*in_flight)\n",[18,2186,2187,2193,2203,2213,2223,2233,2237,2245,2252,2264,2276,2280,2296,2300,2312,2316,2320,2332,2340,2362,2370,2378,2386,2390,2406,2411,2428,2452,2456,2471,2479,2484,2496,2500,2512,2516,2520,2530,2543,2547,2561,2579,2585,2602,2608,2613,2619,2637,2652,2658,2663,2669,2678],{"__ignoreMap":50},[54,2188,2189,2191],{"class":56,"line":57},[54,2190,68],{"class":67},[54,2192,72],{"class":71},[54,2194,2195,2197,2199,2201],{"class":56,"line":64},[54,2196,78],{"class":67},[54,2198,81],{"class":71},[54,2200,68],{"class":67},[54,2202,86],{"class":71},[54,2204,2205,2207,2209,2211],{"class":56,"line":75},[54,2206,78],{"class":67},[54,2208,94],{"class":71},[54,2210,68],{"class":67},[54,2212,369],{"class":71},[54,2214,2215,2217,2219,2221],{"class":56,"line":89},[54,2216,78],{"class":67},[54,2218,107],{"class":71},[54,2220,68],{"class":67},[54,2222,112],{"class":71},[54,2224,2225,2227,2229,2231],{"class":56,"line":102},[54,2226,68],{"class":67},[54,2228,81],{"class":71},[54,2230,122],{"class":67},[54,2232,125],{"class":71},[54,2234,2235],{"class":56,"line":115},[54,2236,132],{"emptyLinePlaceholder":131},[54,2238,2239,2241,2243],{"class":56,"line":128},[54,2240,138],{"class":71},[54,2242,141],{"class":67},[54,2244,1301],{"class":71},[54,2246,2247,2250],{"class":56,"line":135},[54,2248,2249],{"class":147},"    \"postgresql+asyncpg:\u002F\u002Fuser:password@localhost\u002Fdb\"",[54,2251,1309],{"class":71},[54,2253,2254,2257,2259,2262],{"class":56,"line":154},[54,2255,2256],{"class":236},"    pool_size",[54,2258,141],{"class":67},[54,2260,2261],{"class":218},"20",[54,2263,1309],{"class":71},[54,2265,2266,2269,2271,2274],{"class":56,"line":159},[54,2267,2268],{"class":236},"    max_overflow",[54,2270,141],{"class":67},[54,2272,2273],{"class":218},"10",[54,2275,1309],{"class":71},[54,2277,2278],{"class":56,"line":178},[54,2279,151],{"class":71},[54,2281,2282,2284,2286,2288,2290,2292,2294],{"class":56,"line":184},[54,2283,410],{"class":71},[54,2285,141],{"class":67},[54,2287,415],{"class":71},[54,2289,418],{"class":236},[54,2291,141],{"class":67},[54,2293,423],{"class":218},[54,2295,151],{"class":71},[54,2297,2298],{"class":56,"line":189},[54,2299,132],{"emptyLinePlaceholder":131},[54,2301,2302,2304,2306,2308,2310],{"class":56,"line":204},[54,2303,162],{"class":67},[54,2305,166],{"class":165},[54,2307,169],{"class":71},[54,2309,172],{"class":165},[54,2311,175],{"class":71},[54,2313,2314],{"class":56,"line":215},[54,2315,181],{"class":67},[54,2317,2318],{"class":56,"line":247},[54,2319,132],{"emptyLinePlaceholder":131},[54,2321,2322,2324,2326,2328,2330],{"class":56,"line":258},[54,2323,162],{"class":67},[54,2325,871],{"class":165},[54,2327,169],{"class":71},[54,2329,199],{"class":165},[54,2331,175],{"class":71},[54,2333,2334,2336,2338],{"class":56,"line":268},[54,2335,207],{"class":71},[54,2337,141],{"class":67},[54,2339,886],{"class":147},[54,2341,2342,2344,2346,2348,2350,2352,2354,2356,2358,2360],{"class":56,"line":279},[54,2343,219],{"class":218},[54,2345,222],{"class":71},[54,2347,225],{"class":218},[54,2349,228],{"class":71},[54,2351,141],{"class":67},[54,2353,233],{"class":71},[54,2355,237],{"class":236},[54,2357,141],{"class":67},[54,2359,242],{"class":218},[54,2361,151],{"class":71},[54,2363,2364,2366,2368],{"class":56,"line":284},[54,2365,250],{"class":71},[54,2367,225],{"class":218},[54,2369,255],{"class":71},[54,2371,2372,2374,2376],{"class":56,"line":299},[54,2373,921],{"class":71},[54,2375,225],{"class":218},[54,2377,255],{"class":71},[54,2379,2380,2382,2384],{"class":56,"line":305},[54,2381,271],{"class":71},[54,2383,274],{"class":218},[54,2385,255],{"class":71},[54,2387,2388],{"class":56,"line":319},[54,2389,132],{"emptyLinePlaceholder":131},[54,2391,2392,2394,2396,2399,2402,2404],{"class":56,"line":563},[54,2393,287],{"class":67},[54,2395,290],{"class":67},[54,2397,2398],{"class":165}," send_invoice_batch",[54,2400,2401],{"class":71},"(invoices: list[Invoice]) -> ",[54,2403,534],{"class":218},[54,2405,537],{"class":71},[54,2407,2408],{"class":56,"line":592},[54,2409,2410],{"class":147},"    \"\"\"Simulate async I\u002FO work — e.g. calling an external billing API.\"\"\"\n",[54,2412,2413,2416,2419,2422,2425],{"class":56,"line":609},[54,2414,2415],{"class":67},"    await",[54,2417,2418],{"class":71}," asyncio.sleep(",[54,2420,2421],{"class":218},"0",[54,2423,2424],{"class":71},")  ",[54,2426,2427],{"class":60},"# replace with real async I\u002FO\n",[54,2429,2430,2432,2434,2436,2439,2442,2445,2447,2450],{"class":56,"line":615},[54,2431,649],{"class":218},[54,2433,169],{"class":71},[54,2435,654],{"class":67},[54,2437,2438],{"class":147},"\"Dispatched ",[54,2440,2441],{"class":218},"{len",[54,2443,2444],{"class":71},"(invoices)",[54,2446,666],{"class":218},[54,2448,2449],{"class":147}," invoices\"",[54,2451,151],{"class":71},[54,2453,2454],{"class":56,"line":624},[54,2455,132],{"emptyLinePlaceholder":131},[54,2457,2458,2460,2462,2465,2467,2469],{"class":56,"line":629},[54,2459,287],{"class":67},[54,2461,290],{"class":67},[54,2463,2464],{"class":165}," stream_and_process_invoices_parallel",[54,2466,531],{"class":71},[54,2468,534],{"class":218},[54,2470,537],{"class":71},[54,2472,2473,2475,2477],{"class":56,"line":646},[54,2474,542],{"class":71},[54,2476,141],{"class":67},[54,2478,978],{"class":71},[54,2480,2481],{"class":56,"line":1508},[54,2482,2483],{"class":71},"        select(Invoice)\n",[54,2485,2486,2489,2491,2494],{"class":56,"line":1523},[54,2487,2488],{"class":71},"        .where(Invoice.status ",[54,2490,550],{"class":67},[54,2492,2493],{"class":147}," \"ready_to_send\"",[54,2495,151],{"class":71},[54,2497,2498],{"class":56,"line":2011},[54,2499,988],{"class":71},[54,2501,2502,2504,2506,2508,2510],{"class":56,"line":2017},[54,2503,993],{"class":71},[54,2505,331],{"class":236},[54,2507,141],{"class":67},[54,2509,1000],{"class":218},[54,2511,151],{"class":71},[54,2513,2514],{"class":56,"line":2023},[54,2515,1007],{"class":71},[54,2517,2518],{"class":56,"line":2036},[54,2519,132],{"emptyLinePlaceholder":131},[54,2521,2522,2525,2527],{"class":56,"line":2042},[54,2523,2524],{"class":71},"    in_flight: list[asyncio.Task] ",[54,2526,141],{"class":67},[54,2528,2529],{"class":71}," []\n",[54,2531,2532,2535,2537,2540],{"class":56,"line":2055},[54,2533,2534],{"class":71},"    max_concurrent ",[54,2536,141],{"class":67},[54,2538,2539],{"class":218}," 4",[54,2541,2542],{"class":60},"  # tune based on external API rate limits\n",[54,2544,2545],{"class":56,"line":2060},[54,2546,132],{"emptyLinePlaceholder":131},[54,2548,2549,2551,2553,2555,2557,2559],{"class":56,"line":2075},[54,2550,566],{"class":67},[54,2552,569],{"class":67},[54,2554,313],{"class":67},[54,2556,1479],{"class":71},[54,2558,122],{"class":67},[54,2560,589],{"class":71},[54,2562,2563,2565,2567,2570,2572,2575,2577],{"class":56,"line":2088},[54,2564,595],{"class":67},[54,2566,598],{"class":67},[54,2568,2569],{"class":71}," partition ",[54,2571,604],{"class":67},[54,2573,2574],{"class":71}," stream.partitions(",[54,2576,1000],{"class":218},[54,2578,175],{"class":71},[54,2580,2582],{"class":56,"line":2581},42,[54,2583,2584],{"class":60},"            # Launch processing task without awaiting it immediately\n",[54,2586,2588,2591,2593,2596,2599],{"class":56,"line":2587},43,[54,2589,2590],{"class":71},"            task ",[54,2592,141],{"class":67},[54,2594,2595],{"class":71}," asyncio.create_task(send_invoice_batch(",[54,2597,2598],{"class":218},"list",[54,2600,2601],{"class":71},"(partition)))\n",[54,2603,2605],{"class":56,"line":2604},44,[54,2606,2607],{"class":71},"            in_flight.append(task)\n",[54,2609,2611],{"class":56,"line":2610},45,[54,2612,132],{"emptyLinePlaceholder":131},[54,2614,2616],{"class":56,"line":2615},46,[54,2617,2618],{"class":60},"            # Keep at most max_concurrent tasks running at once\n",[54,2620,2622,2625,2628,2631,2634],{"class":56,"line":2621},47,[54,2623,2624],{"class":67},"            if",[54,2626,2627],{"class":218}," len",[54,2629,2630],{"class":71},"(in_flight) ",[54,2632,2633],{"class":67},">=",[54,2635,2636],{"class":71}," max_concurrent:\n",[54,2638,2640,2643,2646,2649],{"class":56,"line":2639},48,[54,2641,2642],{"class":67},"                await",[54,2644,2645],{"class":71}," asyncio.gather(",[54,2647,2648],{"class":67},"*",[54,2650,2651],{"class":71},"in_flight)\n",[54,2653,2655],{"class":56,"line":2654},49,[54,2656,2657],{"class":71},"                in_flight.clear()\n",[54,2659,2661],{"class":56,"line":2660},50,[54,2662,132],{"emptyLinePlaceholder":131},[54,2664,2666],{"class":56,"line":2665},51,[54,2667,2668],{"class":60},"    # Drain any remaining tasks after the stream is exhausted\n",[54,2670,2672,2675],{"class":56,"line":2671},52,[54,2673,2674],{"class":67},"    if",[54,2676,2677],{"class":71}," in_flight:\n",[54,2679,2681,2684,2686,2688],{"class":56,"line":2680},53,[54,2682,2683],{"class":67},"        await",[54,2685,2645],{"class":71},[54,2687,2648],{"class":67},[54,2689,2651],{"class":71},[14,2691,2692,2693,2696],{},"This pattern achieves genuine parallelism: while asyncpg is fetching the next partition from PostgreSQL, the event loop runs the ",[18,2694,2695],{},"send_invoice_batch"," coroutines for already-fetched partitions. The database connection stays open throughout (one connection consumed for the duration), while external I\u002FO for processed batches overlaps with ongoing fetch work.",[696,2698,2700],{"id":2699},"applying-execution_options-on-the-statement-vs-on-the-connection","Applying execution_options on the Statement vs on the Connection",[14,2702,2703,2705],{},[18,2704,331],{}," can be set in two places:",[1160,2707,2708,2718],{},[707,2709,2710,2713,2714,2717],{},[1090,2711,2712],{},"On the statement"," (recommended): ",[18,2715,2716],{},"stmt.execution_options(yield_per=N)"," — scoped to this query only, composable with query builders.",[707,2719,2720,2723,2724,2727],{},[1090,2721,2722],{},"On the connection",": ",[18,2725,2726],{},"await session.execute(stmt, execution_options={\"yield_per\": N})"," — useful when you receive a pre-built statement from a helper and cannot modify it.",[14,2729,2730,2731,2733,2734,757],{},"Avoid setting ",[18,2732,331],{}," at the engine or session level — it would apply to all queries on that session, including writes and DDL, which do not return rows and will raise ",[18,2735,2736],{},"ResourceClosedError",[37,2738,2740],{"id":2739},"frequently-asked-questions","Frequently Asked Questions",[14,2742,2743],{},[1090,2744,2745],{},"Does yield_per work with synchronous SQLAlchemy sessions?",[14,2747,2748,2749,2752,2753,2756,2757,2760,2761,2763,2764,2767,2768,2770],{},"Yes — the synchronous ",[18,2750,2751],{},"Session"," has equivalent ",[18,2754,2755],{},"session.execute(stmt.yield_per(N))"," support (note the ORM-level ",[18,2758,2759],{},".yield_per()"," method on the statement, available since SQLAlchemy 1.4). The async version uses ",[18,2762,752],{}," on the statement or the ",[18,2765,2766],{},"execute()"," call. The behavior is identical: rows are fetched in batches of ",[18,2769,1115],{}," from a server-side cursor, keeping memory flat.",[14,2772,2773],{},[1090,2774,2775],{},"How does yield_per interact with SQLAlchemy's identity map?",[14,2777,2778,2779,2781,2782,2784,2785,2788,2789,21,2792,2795],{},"Each hydrated ORM object is registered in the session's identity map as normal. With ",[18,2780,331],{},", the identity map grows by ",[18,2783,1115],{}," entries per batch and is never cleared between batches. For very long streams (millions of rows of distinct objects), the identity map itself can become a memory concern. If you do not need change-tracking on streamed objects, call ",[18,2786,2787],{},"session.expunge_all()"," periodically inside the loop, or use ",[18,2790,2791],{},"session.execute(stmt)",[18,2793,2794],{},".mappings()"," to get plain dicts instead of ORM instances.",[14,2797,2798],{},[1090,2799,2800],{},"Can I use yield_per inside a Celery task?",[14,2802,2803,2804,2807,2808,21,2810,2812,2813,1577,2816,2819,2820,757],{},"Celery workers are synchronous by default. Using async SQLAlchemy inside a Celery task requires running the coroutine via ",[18,2805,2806],{},"asyncio.run()"," or a persistent event loop per worker. A simpler approach for Celery is to use the synchronous ",[18,2809,2751],{},[18,2811,331],{}," directly. If you are running Celery with ",[18,2814,2815],{},"gevent",[18,2817,2818],{},"eventlet"," concurrency, use the synchronous path — asyncpg is not compatible with those monkey-patching concurrency models. For details on integrating async SQLAlchemy with task workers, see ",[31,2821,2823],{"href":2822},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fchoosing-between-asyncpg-and-psycopg-async-drivers\u002Fusing-sqlalchemy-async-with-celery-task-workers\u002F","Using SQLAlchemy Async with Celery Task Workers",[14,2825,2826],{},[1090,2827,2828],{},"What happens if an exception is raised inside the async-for loop?",[14,2830,2831,2832,2834,2835,2838,2839,2841],{},"The ",[18,2833,1100],{}," context manager on the stream handles cleanup. If an exception propagates out of the loop body, the context manager's ",[18,2836,2837],{},"__aexit__"," closes the cursor and rolls back the implicit transaction. The connection is returned to the pool. No manual cleanup is required — but you should ensure the ",[18,2840,1642],{}," pattern is used rather than manually awaiting and storing the stream object.",[45,2843,2845],{"className":47,"code":2844,"language":49,"meta":50,"style":50},"import asyncio\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nimport sqlalchemy as sa\n\nengine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:password@localhost\u002Fdb\")\nAsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\n\nclass Base(DeclarativeBase):\n    pass\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    user_id: Mapped[int]\n    total_cents: Mapped[int]\n    status: Mapped[str]\n\nasync def stream_with_error_handling() -> None:\n    async with AsyncSessionLocal() as session:\n        stmt = select(Order).order_by(Order.id).execution_options(yield_per=1000)\n        try:\n            async with await session.stream_scalars(stmt) as stream:\n                async for order in stream:\n                    if order.total_cents \u003C 0:\n                        raise ValueError(f\"Negative total on order {order.id}\")\n                    await process_order(order)\n        except ValueError as exc:\n            # stream cursor is already closed by the context manager\n            print(f\"Stream aborted: {exc}\")\n            # session transaction is rolled back automatically on context manager exit\n\nasync def process_order(order: Order) -> None:\n    pass\n",[18,2846,2847,2853,2863,2873,2883,2893,2897,2909,2925,2929,2941,2945,2949,2961,2969,2991,2999,3007,3015,3019,3035,3049,3066,3073,3088,3101,3117,3142,3149,3162,3167,3189,3194,3198,3212],{"__ignoreMap":50},[54,2848,2849,2851],{"class":56,"line":57},[54,2850,68],{"class":67},[54,2852,72],{"class":71},[54,2854,2855,2857,2859,2861],{"class":56,"line":64},[54,2856,78],{"class":67},[54,2858,81],{"class":71},[54,2860,68],{"class":67},[54,2862,86],{"class":71},[54,2864,2865,2867,2869,2871],{"class":56,"line":75},[54,2866,78],{"class":67},[54,2868,94],{"class":71},[54,2870,68],{"class":67},[54,2872,369],{"class":71},[54,2874,2875,2877,2879,2881],{"class":56,"line":89},[54,2876,78],{"class":67},[54,2878,107],{"class":71},[54,2880,68],{"class":67},[54,2882,112],{"class":71},[54,2884,2885,2887,2889,2891],{"class":56,"line":102},[54,2886,68],{"class":67},[54,2888,81],{"class":71},[54,2890,122],{"class":67},[54,2892,125],{"class":71},[54,2894,2895],{"class":56,"line":115},[54,2896,132],{"emptyLinePlaceholder":131},[54,2898,2899,2901,2903,2905,2907],{"class":56,"line":128},[54,2900,138],{"class":71},[54,2902,141],{"class":67},[54,2904,144],{"class":71},[54,2906,148],{"class":147},[54,2908,151],{"class":71},[54,2910,2911,2913,2915,2917,2919,2921,2923],{"class":56,"line":135},[54,2912,410],{"class":71},[54,2914,141],{"class":67},[54,2916,415],{"class":71},[54,2918,418],{"class":236},[54,2920,141],{"class":67},[54,2922,423],{"class":218},[54,2924,151],{"class":71},[54,2926,2927],{"class":56,"line":154},[54,2928,132],{"emptyLinePlaceholder":131},[54,2930,2931,2933,2935,2937,2939],{"class":56,"line":159},[54,2932,162],{"class":67},[54,2934,166],{"class":165},[54,2936,169],{"class":71},[54,2938,172],{"class":165},[54,2940,175],{"class":71},[54,2942,2943],{"class":56,"line":178},[54,2944,181],{"class":67},[54,2946,2947],{"class":56,"line":184},[54,2948,132],{"emptyLinePlaceholder":131},[54,2950,2951,2953,2955,2957,2959],{"class":56,"line":189},[54,2952,162],{"class":67},[54,2954,194],{"class":165},[54,2956,169],{"class":71},[54,2958,199],{"class":165},[54,2960,175],{"class":71},[54,2962,2963,2965,2967],{"class":56,"line":204},[54,2964,207],{"class":71},[54,2966,141],{"class":67},[54,2968,212],{"class":147},[54,2970,2971,2973,2975,2977,2979,2981,2983,2985,2987,2989],{"class":56,"line":215},[54,2972,219],{"class":218},[54,2974,222],{"class":71},[54,2976,225],{"class":218},[54,2978,228],{"class":71},[54,2980,141],{"class":67},[54,2982,233],{"class":71},[54,2984,237],{"class":236},[54,2986,141],{"class":67},[54,2988,242],{"class":218},[54,2990,151],{"class":71},[54,2992,2993,2995,2997],{"class":56,"line":247},[54,2994,250],{"class":71},[54,2996,225],{"class":218},[54,2998,255],{"class":71},[54,3000,3001,3003,3005],{"class":56,"line":258},[54,3002,261],{"class":71},[54,3004,225],{"class":218},[54,3006,255],{"class":71},[54,3008,3009,3011,3013],{"class":56,"line":268},[54,3010,271],{"class":71},[54,3012,274],{"class":218},[54,3014,255],{"class":71},[54,3016,3017],{"class":56,"line":279},[54,3018,132],{"emptyLinePlaceholder":131},[54,3020,3021,3023,3025,3028,3031,3033],{"class":56,"line":284},[54,3022,287],{"class":67},[54,3024,290],{"class":67},[54,3026,3027],{"class":165}," stream_with_error_handling",[54,3029,3030],{"class":71},"() -> ",[54,3032,534],{"class":218},[54,3034,537],{"class":71},[54,3036,3037,3039,3041,3044,3046],{"class":56,"line":299},[54,3038,566],{"class":67},[54,3040,569],{"class":67},[54,3042,3043],{"class":71}," AsyncSessionLocal() ",[54,3045,122],{"class":67},[54,3047,3048],{"class":71}," session:\n",[54,3050,3051,3054,3056,3058,3060,3062,3064],{"class":56,"line":305},[54,3052,3053],{"class":71},"        stmt ",[54,3055,141],{"class":67},[54,3057,1459],{"class":71},[54,3059,331],{"class":236},[54,3061,141],{"class":67},[54,3063,581],{"class":218},[54,3065,151],{"class":71},[54,3067,3068,3071],{"class":56,"line":319},[54,3069,3070],{"class":67},"        try",[54,3072,537],{"class":71},[54,3074,3075,3078,3080,3082,3084,3086],{"class":56,"line":563},[54,3076,3077],{"class":67},"            async",[54,3079,569],{"class":67},[54,3081,313],{"class":67},[54,3083,1479],{"class":71},[54,3085,122],{"class":67},[54,3087,589],{"class":71},[54,3089,3090,3093,3095,3097,3099],{"class":56,"line":592},[54,3091,3092],{"class":67},"                async",[54,3094,598],{"class":67},[54,3096,601],{"class":71},[54,3098,604],{"class":67},[54,3100,589],{"class":71},[54,3102,3103,3106,3109,3112,3115],{"class":56,"line":609},[54,3104,3105],{"class":67},"                    if",[54,3107,3108],{"class":71}," order.total_cents ",[54,3110,3111],{"class":67},"\u003C",[54,3113,3114],{"class":218}," 0",[54,3116,537],{"class":71},[54,3118,3119,3122,3125,3127,3129,3132,3134,3136,3138,3140],{"class":56,"line":615},[54,3120,3121],{"class":67},"                        raise",[54,3123,3124],{"class":218}," ValueError",[54,3126,169],{"class":71},[54,3128,654],{"class":67},[54,3130,3131],{"class":147},"\"Negative total on order ",[54,3133,660],{"class":218},[54,3135,663],{"class":71},[54,3137,666],{"class":218},[54,3139,688],{"class":147},[54,3141,151],{"class":71},[54,3143,3144,3147],{"class":56,"line":624},[54,3145,3146],{"class":67},"                    await",[54,3148,621],{"class":71},[54,3150,3151,3154,3156,3159],{"class":56,"line":629},[54,3152,3153],{"class":67},"        except",[54,3155,3124],{"class":218},[54,3157,3158],{"class":67}," as",[54,3160,3161],{"class":71}," exc:\n",[54,3163,3164],{"class":56,"line":646},[54,3165,3166],{"class":60},"            # stream cursor is already closed by the context manager\n",[54,3168,3169,3171,3173,3175,3178,3180,3183,3185,3187],{"class":56,"line":1508},[54,3170,1040],{"class":218},[54,3172,169],{"class":71},[54,3174,654],{"class":67},[54,3176,3177],{"class":147},"\"Stream aborted: ",[54,3179,660],{"class":218},[54,3181,3182],{"class":71},"exc",[54,3184,666],{"class":218},[54,3186,688],{"class":147},[54,3188,151],{"class":71},[54,3190,3191],{"class":56,"line":1523},[54,3192,3193],{"class":60},"            # session transaction is rolled back automatically on context manager exit\n",[54,3195,3196],{"class":56,"line":2011},[54,3197,132],{"emptyLinePlaceholder":131},[54,3199,3200,3202,3204,3206,3208,3210],{"class":56,"line":2017},[54,3201,287],{"class":67},[54,3203,290],{"class":67},[54,3205,636],{"class":165},[54,3207,639],{"class":71},[54,3209,534],{"class":218},[54,3211,537],{"class":71},[54,3213,3214],{"class":56,"line":2023},[54,3215,181],{"class":67},[37,3217,3219],{"id":3218},"related","Related",[704,3221,3222,3227,3233,3239],{},[707,3223,3224,3226],{},[31,3225,34],{"href":33}," — parent cluster covering all streaming strategies",[707,3228,3229,3232],{},[31,3230,3231],{"href":1217},"Paginating Large Result Sets with Keyset Pagination"," — an alternative to server-side cursors compatible with PgBouncer transaction pooling",[707,3234,3235,3238],{},[31,3236,3237],{"href":1587},"Using selectinload vs joinedload for N+1 Prevention"," — required reading before adding eager loads to streaming queries",[707,3240,3241,3243],{},[31,3242,1108],{"href":1107}," — streaming holds connections open; understand the pool sizing implications",[3245,3246,3247],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":50,"searchDepth":64,"depth":64,"links":3249},[3250,3251,3257,3260,3265,3266],{"id":39,"depth":64,"text":40},{"id":693,"depth":64,"text":694,"children":3252},[3253,3254,3255,3256],{"id":698,"depth":75,"text":699},{"id":1081,"depth":75,"text":1082},{"id":1136,"depth":75,"text":1137},{"id":1170,"depth":75,"text":1171},{"id":1528,"depth":64,"text":1529,"children":3258},[3259],{"id":1671,"depth":75,"text":1672},{"id":2120,"depth":64,"text":2121,"children":3261},[3262,3263,3264],{"id":2124,"depth":75,"text":2125},{"id":2160,"depth":75,"text":2161},{"id":2699,"depth":75,"text":2700},{"id":2739,"depth":64,"text":2740},{"id":3218,"depth":64,"text":3219},"Call AsyncSession.stream() with .execution_options(yield_per=N) (or the shorthand stream_scalars()) to iterate millions of rows without loading them all into memory at once — this technique is covered in full on the Streaming Large Result Sets with yield_per cluster.","md",{"date":3270},"2026-06-18","\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per\u002Fusing-yield-per-to-stream-millions-of-rows-in-async",{"title":5,"description":3267},"advanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per\u002Fusing-yield-per-to-stream-millions-of-rows-in-async\u002Findex","K0OYebNohtBrsJhKOBYxwD-b2Plxz00H7QO4Hf0nI0Q",1781810028981]