[{"data":1,"prerenderedAt":3384},["ShallowReactive",2],{"page-\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per\u002F":3},{"id":4,"title":5,"body":6,"description":3376,"extension":3377,"meta":3378,"navigation":124,"path":3380,"seo":3381,"stem":3382,"__hash__":3383},"content\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per\u002Findex.md","Streaming Large Result Sets with yield_per in SQLAlchemy 2.0",{"type":7,"value":8,"toc":3356},"minimark",[9,13,28,33,36,45,75,78,390,396,532,539,543,550,634,656,659,1095,1102,1243,1249,1253,1256,1278,1419,1441,1447,1451,1456,1476,1657,1664,1668,1684,1832,1836,1847,1855,1863,1880,2319,2334,2338,2347,2362,2365,2371,2378,2386,2390,2396,2566,2572,2576,2584,2588,2592,2595,2893,2905,2909,2923,2975,2988,3023,3027,3164,3168,3173,3191,3196,3213,3218,3228,3233,3270,3275,3319,3323,3352],[10,11,5],"h1",{"id":12},"streaming-large-result-sets-with-yield_per-in-sqlalchemy-20",[14,15,16,17,21,22,27],"p",{},"SQLAlchemy's ",[18,19,20],"code",{},"yield_per"," keeps memory flat when processing millions of rows — part of the broader ",[23,24,26],"a",{"href":25},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002F","Advanced Query Patterns and Bulk Data Operations"," toolkit.",[29,30,32],"h2",{"id":31},"concept-execution-model","Concept & Execution Model",[14,34,35],{},"Every database query produces a result set. The simplest approach is to fetch the entire result into client memory before your application code touches a single row. For queries returning thousands of records that approach is invisible — rows arrive quickly, Python allocates a flat list, your loop iterates, and the list is garbage-collected. Scale that to ten million rows and the picture changes: the database streams hundreds of megabytes across the wire, the DBAPI buffers them, SQLAlchemy hydrates ORM instances for each row, and the Python heap swells proportionally. On a typical 4 GB container that sequence ends in an OOM kill long before any business logic runs.",[14,37,38,40,41,44],{},[18,39,20],{}," solves the problem by activating server-side cursors, which tell the database to hold the result set open and deliver rows in fixed-size batches on demand. Rather than one massive transfer, the client issues a series of ",[18,42,43],{},"FETCH N"," commands, processes the batch, discards it, then requests the next. Peak memory becomes a function of batch size, not total row count — a budget you control explicitly.",[14,46,47,48,51,52,55,56,59,60,63,64,67,68,71,72,74],{},"SQLAlchemy 2.0 integrates this behaviour at two levels. The ",[18,49,50],{},"execution_options(yield_per=N)"," call on a ",[18,53,54],{},"Select"," statement works identically on the synchronous ",[18,57,58],{},"Session"," and the asynchronous ",[18,61,62],{},"AsyncSession",". Underneath, the ORM delegates to the DBAPI cursor's ",[18,65,66],{},"fetchmany()"," method (or the asyncpg-native equivalent), so the streaming guarantee holds regardless of whether your application uses ",[18,69,70],{},"await"," or plain function calls. The identity map, relationship loading, and type coercion layers all remain active — rows are fully hydrated ORM instances, not raw tuples — which distinguishes ",[18,73,20],{}," from bypassing the ORM entirely.",[14,76,77],{},"The minimal synchronous form:",[79,80,85],"pre",{"className":81,"code":82,"language":83,"meta":84,"style":84},"language-python shiki shiki-themes github-light github-dark","from sqlalchemy import select\nfrom sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_column\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass Invoice(Base):\n    __tablename__ = \"invoices\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    tenant_id: Mapped[int] = mapped_column(index=True)\n    total_cents: Mapped[int] = mapped_column()\n    status: Mapped[str] = mapped_column()\n\n\ndef stream_invoices_sync(session: Session, tenant_id: int) -> None:\n    stmt = (\n        select(Invoice)\n        .where(Invoice.tenant_id == tenant_id)\n        .order_by(Invoice.id)\n        .execution_options(yield_per=2000)\n    )\n    for invoice in session.scalars(stmt):\n        process(invoice)\n","python","",[18,86,87,106,119,126,131,150,156,161,166,181,194,227,250,265,280,285,290,313,324,330,342,348,363,369,384],{"__ignoreMap":84},[88,89,92,96,100,103],"span",{"class":90,"line":91},"line",1,[88,93,95],{"class":94},"szBVR","from",[88,97,99],{"class":98},"sVt8B"," sqlalchemy ",[88,101,102],{"class":94},"import",[88,104,105],{"class":98}," select\n",[88,107,109,111,114,116],{"class":90,"line":108},2,[88,110,95],{"class":94},[88,112,113],{"class":98}," sqlalchemy.orm ",[88,115,102],{"class":94},[88,117,118],{"class":98}," Session, DeclarativeBase, Mapped, mapped_column\n",[88,120,122],{"class":90,"line":121},3,[88,123,125],{"emptyLinePlaceholder":124},true,"\n",[88,127,129],{"class":90,"line":128},4,[88,130,125],{"emptyLinePlaceholder":124},[88,132,134,137,141,144,147],{"class":90,"line":133},5,[88,135,136],{"class":94},"class",[88,138,140],{"class":139},"sScJk"," Base",[88,142,143],{"class":98},"(",[88,145,146],{"class":139},"DeclarativeBase",[88,148,149],{"class":98},"):\n",[88,151,153],{"class":90,"line":152},6,[88,154,155],{"class":94},"    pass\n",[88,157,159],{"class":90,"line":158},7,[88,160,125],{"emptyLinePlaceholder":124},[88,162,164],{"class":90,"line":163},8,[88,165,125],{"emptyLinePlaceholder":124},[88,167,169,171,174,176,179],{"class":90,"line":168},9,[88,170,136],{"class":94},[88,172,173],{"class":139}," Invoice",[88,175,143],{"class":98},[88,177,178],{"class":139},"Base",[88,180,149],{"class":98},[88,182,184,187,190],{"class":90,"line":183},10,[88,185,186],{"class":98},"    __tablename__ ",[88,188,189],{"class":94},"=",[88,191,193],{"class":192},"sZZnC"," \"invoices\"\n",[88,195,197,201,204,207,210,212,215,219,221,224],{"class":90,"line":196},11,[88,198,200],{"class":199},"sj4cs","    id",[88,202,203],{"class":98},": Mapped[",[88,205,206],{"class":199},"int",[88,208,209],{"class":98},"] ",[88,211,189],{"class":94},[88,213,214],{"class":98}," mapped_column(",[88,216,218],{"class":217},"s4XuR","primary_key",[88,220,189],{"class":94},[88,222,223],{"class":199},"True",[88,225,226],{"class":98},")\n",[88,228,230,233,235,237,239,241,244,246,248],{"class":90,"line":229},12,[88,231,232],{"class":98},"    tenant_id: Mapped[",[88,234,206],{"class":199},[88,236,209],{"class":98},[88,238,189],{"class":94},[88,240,214],{"class":98},[88,242,243],{"class":217},"index",[88,245,189],{"class":94},[88,247,223],{"class":199},[88,249,226],{"class":98},[88,251,253,256,258,260,262],{"class":90,"line":252},13,[88,254,255],{"class":98},"    total_cents: Mapped[",[88,257,206],{"class":199},[88,259,209],{"class":98},[88,261,189],{"class":94},[88,263,264],{"class":98}," mapped_column()\n",[88,266,268,271,274,276,278],{"class":90,"line":267},14,[88,269,270],{"class":98},"    status: Mapped[",[88,272,273],{"class":199},"str",[88,275,209],{"class":98},[88,277,189],{"class":94},[88,279,264],{"class":98},[88,281,283],{"class":90,"line":282},15,[88,284,125],{"emptyLinePlaceholder":124},[88,286,288],{"class":90,"line":287},16,[88,289,125],{"emptyLinePlaceholder":124},[88,291,293,296,299,302,304,307,310],{"class":90,"line":292},17,[88,294,295],{"class":94},"def",[88,297,298],{"class":139}," stream_invoices_sync",[88,300,301],{"class":98},"(session: Session, tenant_id: ",[88,303,206],{"class":199},[88,305,306],{"class":98},") -> ",[88,308,309],{"class":199},"None",[88,311,312],{"class":98},":\n",[88,314,316,319,321],{"class":90,"line":315},18,[88,317,318],{"class":98},"    stmt ",[88,320,189],{"class":94},[88,322,323],{"class":98}," (\n",[88,325,327],{"class":90,"line":326},19,[88,328,329],{"class":98},"        select(Invoice)\n",[88,331,333,336,339],{"class":90,"line":332},20,[88,334,335],{"class":98},"        .where(Invoice.tenant_id ",[88,337,338],{"class":94},"==",[88,340,341],{"class":98}," tenant_id)\n",[88,343,345],{"class":90,"line":344},21,[88,346,347],{"class":98},"        .order_by(Invoice.id)\n",[88,349,351,354,356,358,361],{"class":90,"line":350},22,[88,352,353],{"class":98},"        .execution_options(",[88,355,20],{"class":217},[88,357,189],{"class":94},[88,359,360],{"class":199},"2000",[88,362,226],{"class":98},[88,364,366],{"class":90,"line":365},23,[88,367,368],{"class":98},"    )\n",[88,370,372,375,378,381],{"class":90,"line":371},24,[88,373,374],{"class":94},"    for",[88,376,377],{"class":98}," invoice ",[88,379,380],{"class":94},"in",[88,382,383],{"class":98}," session.scalars(stmt):\n",[88,385,387],{"class":90,"line":386},25,[88,388,389],{"class":98},"        process(invoice)\n",[14,391,392,393,395],{},"The identical operation on an ",[18,394,62],{},":",[79,397,399],{"className":81,"code":398,"language":83,"meta":84,"style":84},"from sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\n\nasync def stream_invoices_async(session: AsyncSession, tenant_id: int) -> None:\n    stmt = (\n        select(Invoice)\n        .where(Invoice.tenant_id == tenant_id)\n        .order_by(Invoice.id)\n        .execution_options(yield_per=2000)\n    )\n    async with session.stream_scalars(stmt) as result:\n        async for invoice in result:\n            await process(invoice)\n",[18,400,401,411,423,427,431,453,461,465,473,477,489,493,510,524],{"__ignoreMap":84},[88,402,403,405,407,409],{"class":90,"line":91},[88,404,95],{"class":94},[88,406,99],{"class":98},[88,408,102],{"class":94},[88,410,105],{"class":98},[88,412,413,415,418,420],{"class":90,"line":108},[88,414,95],{"class":94},[88,416,417],{"class":98}," sqlalchemy.ext.asyncio ",[88,419,102],{"class":94},[88,421,422],{"class":98}," AsyncSession\n",[88,424,425],{"class":90,"line":121},[88,426,125],{"emptyLinePlaceholder":124},[88,428,429],{"class":90,"line":128},[88,430,125],{"emptyLinePlaceholder":124},[88,432,433,436,439,442,445,447,449,451],{"class":90,"line":133},[88,434,435],{"class":94},"async",[88,437,438],{"class":94}," def",[88,440,441],{"class":139}," stream_invoices_async",[88,443,444],{"class":98},"(session: AsyncSession, tenant_id: ",[88,446,206],{"class":199},[88,448,306],{"class":98},[88,450,309],{"class":199},[88,452,312],{"class":98},[88,454,455,457,459],{"class":90,"line":152},[88,456,318],{"class":98},[88,458,189],{"class":94},[88,460,323],{"class":98},[88,462,463],{"class":90,"line":158},[88,464,329],{"class":98},[88,466,467,469,471],{"class":90,"line":163},[88,468,335],{"class":98},[88,470,338],{"class":94},[88,472,341],{"class":98},[88,474,475],{"class":90,"line":168},[88,476,347],{"class":98},[88,478,479,481,483,485,487],{"class":90,"line":183},[88,480,353],{"class":98},[88,482,20],{"class":217},[88,484,189],{"class":94},[88,486,360],{"class":199},[88,488,226],{"class":98},[88,490,491],{"class":90,"line":196},[88,492,368],{"class":98},[88,494,495,498,501,504,507],{"class":90,"line":229},[88,496,497],{"class":94},"    async",[88,499,500],{"class":94}," with",[88,502,503],{"class":98}," session.stream_scalars(stmt) ",[88,505,506],{"class":94},"as",[88,508,509],{"class":98}," result:\n",[88,511,512,515,518,520,522],{"class":90,"line":252},[88,513,514],{"class":94},"        async",[88,516,517],{"class":94}," for",[88,519,377],{"class":98},[88,521,380],{"class":94},[88,523,509],{"class":98},[88,525,526,529],{"class":90,"line":267},[88,527,528],{"class":94},"            await",[88,530,531],{"class":98}," process(invoice)\n",[14,533,534,535,538],{},"Both forms activate server-side cursors. The synchronous version exhausts the generator inside the same thread; the async version uses an ",[18,536,537],{},"async for"," loop without blocking the event loop between fetches.",[29,540,542],{"id":541},"query-construction-async-execution-patterns","Query Construction & Async Execution Patterns",[14,544,545,546,549],{},"SQLAlchemy 2.0's unified ",[18,547,548],{},"select()"," construct is the entry point for all streaming queries, whether you reach for the ORM or Core. The four execution surfaces most relevant to large result sets are:",[551,552,553,569],"table",{},[554,555,556],"thead",{},[557,558,559,563,566],"tr",{},[560,561,562],"th",{},"Surface",[560,564,565],{},"Returns",[560,567,568],{},"When to use",[570,571,572,588,603,619],"tbody",{},[557,573,574,580,585],{},[575,576,577],"td",{},[18,578,579],{},"session.execute(stmt)",[575,581,582],{},[18,583,584],{},"Result[Row]",[575,586,587],{},"Row tuples, Core queries",[557,589,590,595,600],{},[575,591,592],{},[18,593,594],{},"session.scalars(stmt)",[575,596,597],{},[18,598,599],{},"ScalarResult[T]",[575,601,602],{},"ORM instances, single entity",[557,604,605,610,616],{},[575,606,607],{},[18,608,609],{},"await session.stream(stmt)",[575,611,612,615],{},[18,613,614],{},"AsyncResult[Row]"," (context manager)",[575,617,618],{},"Async row tuples",[557,620,621,626,631],{},[575,622,623],{},[18,624,625],{},"await session.stream_scalars(stmt)",[575,627,628,615],{},[18,629,630],{},"AsyncScalarResult[T]",[575,632,633],{},"Async ORM instances",[14,635,636,637,640,641,644,645,647,648,651,652,655],{},"The ",[18,638,639],{},"stream()"," and ",[18,642,643],{},"stream_scalars()"," methods on ",[18,646,62],{}," exist specifically for streaming; they return asynchronous context managers that hold the database cursor open for the duration of the ",[18,649,650],{},"async with"," block. Attempting to iterate the result after exiting the block raises ",[18,653,654],{},"InvalidRequestError"," because the underlying connection has been returned to the pool.",[14,657,658],{},"A complete async pattern that feeds an external queue:",[79,660,662],{"className":81,"code":661,"language":83,"meta":84,"style":84},"import asyncio\nfrom typing import AsyncGenerator\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    tenant_id: Mapped[int] = mapped_column(index=True)\n    amount_cents: Mapped[int] = mapped_column()\n    state: Mapped[str] = mapped_column()\n\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:secret@localhost\u002Fbilling\",\n    pool_size=10,\n    max_overflow=5,\n    pool_pre_ping=True,\n)\nAsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\n\n\nasync def enqueue_pending_orders(\n    queue: asyncio.Queue,\n    tenant_id: int,\n    batch_size: int = 1000,\n) -> None:\n    stmt = (\n        select(Order)\n        .where(Order.tenant_id == tenant_id, Order.state == \"pending\")\n        .order_by(Order.id)\n        .execution_options(yield_per=batch_size)\n    )\n    async with AsyncSessionLocal() as session:\n        async with session.stream_scalars(stmt) as result:\n            async for order in result:\n                await queue.put(order)\n",[18,663,664,671,683,693,704,715,719,723,735,739,743,747,760,769,791,811,824,837,841,845,855,863,875,887,898,902,923,928,933,946,952,962,978,987,996,1002,1020,1026,1038,1043,1058,1071,1086],{"__ignoreMap":84},[88,665,666,668],{"class":90,"line":91},[88,667,102],{"class":94},[88,669,670],{"class":98}," asyncio\n",[88,672,673,675,678,680],{"class":90,"line":108},[88,674,95],{"class":94},[88,676,677],{"class":98}," typing ",[88,679,102],{"class":94},[88,681,682],{"class":98}," AsyncGenerator\n",[88,684,685,687,689,691],{"class":90,"line":121},[88,686,95],{"class":94},[88,688,99],{"class":98},[88,690,102],{"class":94},[88,692,105],{"class":98},[88,694,695,697,699,701],{"class":90,"line":128},[88,696,95],{"class":94},[88,698,417],{"class":98},[88,700,102],{"class":94},[88,702,703],{"class":98}," AsyncSession, create_async_engine, async_sessionmaker\n",[88,705,706,708,710,712],{"class":90,"line":133},[88,707,95],{"class":94},[88,709,113],{"class":98},[88,711,102],{"class":94},[88,713,714],{"class":98}," DeclarativeBase, Mapped, mapped_column\n",[88,716,717],{"class":90,"line":152},[88,718,125],{"emptyLinePlaceholder":124},[88,720,721],{"class":90,"line":158},[88,722,125],{"emptyLinePlaceholder":124},[88,724,725,727,729,731,733],{"class":90,"line":163},[88,726,136],{"class":94},[88,728,140],{"class":139},[88,730,143],{"class":98},[88,732,146],{"class":139},[88,734,149],{"class":98},[88,736,737],{"class":90,"line":168},[88,738,155],{"class":94},[88,740,741],{"class":90,"line":183},[88,742,125],{"emptyLinePlaceholder":124},[88,744,745],{"class":90,"line":196},[88,746,125],{"emptyLinePlaceholder":124},[88,748,749,751,754,756,758],{"class":90,"line":229},[88,750,136],{"class":94},[88,752,753],{"class":139}," Order",[88,755,143],{"class":98},[88,757,178],{"class":139},[88,759,149],{"class":98},[88,761,762,764,766],{"class":90,"line":252},[88,763,186],{"class":98},[88,765,189],{"class":94},[88,767,768],{"class":192}," \"orders\"\n",[88,770,771,773,775,777,779,781,783,785,787,789],{"class":90,"line":267},[88,772,200],{"class":199},[88,774,203],{"class":98},[88,776,206],{"class":199},[88,778,209],{"class":98},[88,780,189],{"class":94},[88,782,214],{"class":98},[88,784,218],{"class":217},[88,786,189],{"class":94},[88,788,223],{"class":199},[88,790,226],{"class":98},[88,792,793,795,797,799,801,803,805,807,809],{"class":90,"line":282},[88,794,232],{"class":98},[88,796,206],{"class":199},[88,798,209],{"class":98},[88,800,189],{"class":94},[88,802,214],{"class":98},[88,804,243],{"class":217},[88,806,189],{"class":94},[88,808,223],{"class":199},[88,810,226],{"class":98},[88,812,813,816,818,820,822],{"class":90,"line":287},[88,814,815],{"class":98},"    amount_cents: Mapped[",[88,817,206],{"class":199},[88,819,209],{"class":98},[88,821,189],{"class":94},[88,823,264],{"class":98},[88,825,826,829,831,833,835],{"class":90,"line":292},[88,827,828],{"class":98},"    state: Mapped[",[88,830,273],{"class":199},[88,832,209],{"class":98},[88,834,189],{"class":94},[88,836,264],{"class":98},[88,838,839],{"class":90,"line":315},[88,840,125],{"emptyLinePlaceholder":124},[88,842,843],{"class":90,"line":326},[88,844,125],{"emptyLinePlaceholder":124},[88,846,847,850,852],{"class":90,"line":332},[88,848,849],{"class":98},"engine ",[88,851,189],{"class":94},[88,853,854],{"class":98}," create_async_engine(\n",[88,856,857,860],{"class":90,"line":344},[88,858,859],{"class":192},"    \"postgresql+asyncpg:\u002F\u002Fuser:secret@localhost\u002Fbilling\"",[88,861,862],{"class":98},",\n",[88,864,865,868,870,873],{"class":90,"line":350},[88,866,867],{"class":217},"    pool_size",[88,869,189],{"class":94},[88,871,872],{"class":199},"10",[88,874,862],{"class":98},[88,876,877,880,882,885],{"class":90,"line":365},[88,878,879],{"class":217},"    max_overflow",[88,881,189],{"class":94},[88,883,884],{"class":199},"5",[88,886,862],{"class":98},[88,888,889,892,894,896],{"class":90,"line":371},[88,890,891],{"class":217},"    pool_pre_ping",[88,893,189],{"class":94},[88,895,223],{"class":199},[88,897,862],{"class":98},[88,899,900],{"class":90,"line":386},[88,901,226],{"class":98},[88,903,905,908,910,913,916,918,921],{"class":90,"line":904},26,[88,906,907],{"class":98},"AsyncSessionLocal ",[88,909,189],{"class":94},[88,911,912],{"class":98}," async_sessionmaker(engine, ",[88,914,915],{"class":217},"expire_on_commit",[88,917,189],{"class":94},[88,919,920],{"class":199},"False",[88,922,226],{"class":98},[88,924,926],{"class":90,"line":925},27,[88,927,125],{"emptyLinePlaceholder":124},[88,929,931],{"class":90,"line":930},28,[88,932,125],{"emptyLinePlaceholder":124},[88,934,936,938,940,943],{"class":90,"line":935},29,[88,937,435],{"class":94},[88,939,438],{"class":94},[88,941,942],{"class":139}," enqueue_pending_orders",[88,944,945],{"class":98},"(\n",[88,947,949],{"class":90,"line":948},30,[88,950,951],{"class":98},"    queue: asyncio.Queue,\n",[88,953,955,958,960],{"class":90,"line":954},31,[88,956,957],{"class":98},"    tenant_id: ",[88,959,206],{"class":199},[88,961,862],{"class":98},[88,963,965,968,970,973,976],{"class":90,"line":964},32,[88,966,967],{"class":98},"    batch_size: ",[88,969,206],{"class":199},[88,971,972],{"class":94}," =",[88,974,975],{"class":199}," 1000",[88,977,862],{"class":98},[88,979,981,983,985],{"class":90,"line":980},33,[88,982,306],{"class":98},[88,984,309],{"class":199},[88,986,312],{"class":98},[88,988,990,992,994],{"class":90,"line":989},34,[88,991,318],{"class":98},[88,993,189],{"class":94},[88,995,323],{"class":98},[88,997,999],{"class":90,"line":998},35,[88,1000,1001],{"class":98},"        select(Order)\n",[88,1003,1005,1008,1010,1013,1015,1018],{"class":90,"line":1004},36,[88,1006,1007],{"class":98},"        .where(Order.tenant_id ",[88,1009,338],{"class":94},[88,1011,1012],{"class":98}," tenant_id, Order.state ",[88,1014,338],{"class":94},[88,1016,1017],{"class":192}," \"pending\"",[88,1019,226],{"class":98},[88,1021,1023],{"class":90,"line":1022},37,[88,1024,1025],{"class":98},"        .order_by(Order.id)\n",[88,1027,1029,1031,1033,1035],{"class":90,"line":1028},38,[88,1030,353],{"class":98},[88,1032,20],{"class":217},[88,1034,189],{"class":94},[88,1036,1037],{"class":98},"batch_size)\n",[88,1039,1041],{"class":90,"line":1040},39,[88,1042,368],{"class":98},[88,1044,1046,1048,1050,1053,1055],{"class":90,"line":1045},40,[88,1047,497],{"class":94},[88,1049,500],{"class":94},[88,1051,1052],{"class":98}," AsyncSessionLocal() ",[88,1054,506],{"class":94},[88,1056,1057],{"class":98}," session:\n",[88,1059,1061,1063,1065,1067,1069],{"class":90,"line":1060},41,[88,1062,514],{"class":94},[88,1064,500],{"class":94},[88,1066,503],{"class":98},[88,1068,506],{"class":94},[88,1070,509],{"class":98},[88,1072,1074,1077,1079,1082,1084],{"class":90,"line":1073},42,[88,1075,1076],{"class":94},"            async",[88,1078,517],{"class":94},[88,1080,1081],{"class":98}," order ",[88,1083,380],{"class":94},[88,1085,509],{"class":98},[88,1087,1089,1092],{"class":90,"line":1088},43,[88,1090,1091],{"class":94},"                await",[88,1093,1094],{"class":98}," queue.put(order)\n",[14,1096,1097,1098,1101],{},"The synchronous counterpart uses ",[18,1099,1100],{},"Result.partitions()"," for explicit chunking — useful when you need to send batches to a downstream consumer rather than individual rows:",[79,1103,1105],{"className":81,"code":1104,"language":83,"meta":84,"style":84},"from sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\n\ndef export_orders_in_chunks(\n    session: Session,\n    tenant_id: int,\n    batch_size: int = 1000,\n) -> None:\n    stmt = (\n        select(Order)\n        .where(Order.tenant_id == tenant_id)\n        .order_by(Order.id)\n        .execution_options(yield_per=batch_size)\n    )\n    result = session.execute(stmt)\n    for chunk in result.partitions(batch_size):\n        write_to_parquet(chunk)\n",[18,1106,1107,1117,1128,1132,1136,1145,1150,1158,1170,1178,1186,1190,1198,1202,1212,1216,1226,1238],{"__ignoreMap":84},[88,1108,1109,1111,1113,1115],{"class":90,"line":91},[88,1110,95],{"class":94},[88,1112,99],{"class":98},[88,1114,102],{"class":94},[88,1116,105],{"class":98},[88,1118,1119,1121,1123,1125],{"class":90,"line":108},[88,1120,95],{"class":94},[88,1122,113],{"class":98},[88,1124,102],{"class":94},[88,1126,1127],{"class":98}," Session\n",[88,1129,1130],{"class":90,"line":121},[88,1131,125],{"emptyLinePlaceholder":124},[88,1133,1134],{"class":90,"line":128},[88,1135,125],{"emptyLinePlaceholder":124},[88,1137,1138,1140,1143],{"class":90,"line":133},[88,1139,295],{"class":94},[88,1141,1142],{"class":139}," export_orders_in_chunks",[88,1144,945],{"class":98},[88,1146,1147],{"class":90,"line":152},[88,1148,1149],{"class":98},"    session: Session,\n",[88,1151,1152,1154,1156],{"class":90,"line":158},[88,1153,957],{"class":98},[88,1155,206],{"class":199},[88,1157,862],{"class":98},[88,1159,1160,1162,1164,1166,1168],{"class":90,"line":163},[88,1161,967],{"class":98},[88,1163,206],{"class":199},[88,1165,972],{"class":94},[88,1167,975],{"class":199},[88,1169,862],{"class":98},[88,1171,1172,1174,1176],{"class":90,"line":168},[88,1173,306],{"class":98},[88,1175,309],{"class":199},[88,1177,312],{"class":98},[88,1179,1180,1182,1184],{"class":90,"line":183},[88,1181,318],{"class":98},[88,1183,189],{"class":94},[88,1185,323],{"class":98},[88,1187,1188],{"class":90,"line":196},[88,1189,1001],{"class":98},[88,1191,1192,1194,1196],{"class":90,"line":229},[88,1193,1007],{"class":98},[88,1195,338],{"class":94},[88,1197,341],{"class":98},[88,1199,1200],{"class":90,"line":252},[88,1201,1025],{"class":98},[88,1203,1204,1206,1208,1210],{"class":90,"line":267},[88,1205,353],{"class":98},[88,1207,20],{"class":217},[88,1209,189],{"class":94},[88,1211,1037],{"class":98},[88,1213,1214],{"class":90,"line":282},[88,1215,368],{"class":98},[88,1217,1218,1221,1223],{"class":90,"line":287},[88,1219,1220],{"class":98},"    result ",[88,1222,189],{"class":94},[88,1224,1225],{"class":98}," session.execute(stmt)\n",[88,1227,1228,1230,1233,1235],{"class":90,"line":292},[88,1229,374],{"class":94},[88,1231,1232],{"class":98}," chunk ",[88,1234,380],{"class":94},[88,1236,1237],{"class":98}," result.partitions(batch_size):\n",[88,1239,1240],{"class":90,"line":315},[88,1241,1242],{"class":98},"        write_to_parquet(chunk)\n",[14,1244,1245,1248],{},[18,1246,1247],{},"Result.partitions(N)"," groups consecutive rows into lists of length N, which maps cleanly to file chunking, message broker batches, or database micro-transactions.",[29,1250,1252],{"id":1251},"state-management-session-boundaries","State Management & Session Boundaries",[14,1254,1255],{},"Streaming keeps a database connection checked out for the entire duration of the iteration. This has two important consequences for session and transaction design.",[14,1257,1258,1262,1263,1265,1266,1269,1270,1273,1274,1277],{},[1259,1260,1261],"strong",{},"Identity map accumulation."," The SQLAlchemy session maintains an identity map — a dictionary keyed by primary key that stores every loaded ORM instance. Without explicit management, streaming ten million rows into a long-lived session fills this map with ten million objects that are never evicted. Memory grows linearly even though ",[18,1264,20],{}," caps the in-flight DBAPI buffer. The fix is to call ",[18,1267,1268],{},"session.expunge_all()"," after each batch or to use ",[18,1271,1272],{},"expire_on_commit=False"," combined with an explicit ",[18,1275,1276],{},"session.expire(instance)"," per row when mutation is not needed.",[79,1279,1281],{"className":81,"code":1280,"language":83,"meta":84,"style":84},"from sqlalchemy import select\nfrom sqlalchemy.orm import Session\n\n\ndef stream_and_evict(session: Session, batch_size: int = 5000) -> None:\n    stmt = (\n        select(Invoice)\n        .order_by(Invoice.id)\n        .execution_options(yield_per=batch_size)\n    )\n    result = session.execute(stmt)\n    for chunk in result.partitions(batch_size):\n        for row in chunk:\n            (invoice,) = row\n            process(invoice)\n        session.expunge_all()  # prevent identity map growth\n",[18,1282,1283,1293,1303,1307,1311,1334,1342,1346,1350,1360,1364,1372,1382,1395,1405,1410],{"__ignoreMap":84},[88,1284,1285,1287,1289,1291],{"class":90,"line":91},[88,1286,95],{"class":94},[88,1288,99],{"class":98},[88,1290,102],{"class":94},[88,1292,105],{"class":98},[88,1294,1295,1297,1299,1301],{"class":90,"line":108},[88,1296,95],{"class":94},[88,1298,113],{"class":98},[88,1300,102],{"class":94},[88,1302,1127],{"class":98},[88,1304,1305],{"class":90,"line":121},[88,1306,125],{"emptyLinePlaceholder":124},[88,1308,1309],{"class":90,"line":128},[88,1310,125],{"emptyLinePlaceholder":124},[88,1312,1313,1315,1318,1321,1323,1325,1328,1330,1332],{"class":90,"line":133},[88,1314,295],{"class":94},[88,1316,1317],{"class":139}," stream_and_evict",[88,1319,1320],{"class":98},"(session: Session, batch_size: ",[88,1322,206],{"class":199},[88,1324,972],{"class":94},[88,1326,1327],{"class":199}," 5000",[88,1329,306],{"class":98},[88,1331,309],{"class":199},[88,1333,312],{"class":98},[88,1335,1336,1338,1340],{"class":90,"line":152},[88,1337,318],{"class":98},[88,1339,189],{"class":94},[88,1341,323],{"class":98},[88,1343,1344],{"class":90,"line":158},[88,1345,329],{"class":98},[88,1347,1348],{"class":90,"line":163},[88,1349,347],{"class":98},[88,1351,1352,1354,1356,1358],{"class":90,"line":168},[88,1353,353],{"class":98},[88,1355,20],{"class":217},[88,1357,189],{"class":94},[88,1359,1037],{"class":98},[88,1361,1362],{"class":90,"line":183},[88,1363,368],{"class":98},[88,1365,1366,1368,1370],{"class":90,"line":196},[88,1367,1220],{"class":98},[88,1369,189],{"class":94},[88,1371,1225],{"class":98},[88,1373,1374,1376,1378,1380],{"class":90,"line":229},[88,1375,374],{"class":94},[88,1377,1232],{"class":98},[88,1379,380],{"class":94},[88,1381,1237],{"class":98},[88,1383,1384,1387,1390,1392],{"class":90,"line":252},[88,1385,1386],{"class":94},"        for",[88,1388,1389],{"class":98}," row ",[88,1391,380],{"class":94},[88,1393,1394],{"class":98}," chunk:\n",[88,1396,1397,1400,1402],{"class":90,"line":267},[88,1398,1399],{"class":98},"            (invoice,) ",[88,1401,189],{"class":94},[88,1403,1404],{"class":98}," row\n",[88,1406,1407],{"class":90,"line":282},[88,1408,1409],{"class":98},"            process(invoice)\n",[88,1411,1412,1415],{"class":90,"line":287},[88,1413,1414],{"class":98},"        session.expunge_all()  ",[88,1416,1418],{"class":1417},"sJ8bj","# prevent identity map growth\n",[14,1420,1421,1424,1425,1428,1429,1432,1433,1436,1437,1440],{},[1259,1422,1423],{},"Transaction scope."," Server-side cursors in PostgreSQL require an open transaction. ",[18,1426,1427],{},"asyncpg"," begins a transaction implicitly on the first statement, so streaming without an explicit ",[18,1430,1431],{},"BEGIN"," still works — but autocommit mode breaks this assumption. Always wrap streaming queries in an explicit ",[18,1434,1435],{},"async with session.begin()"," block when your session factory uses ",[18,1438,1439],{},"autocommit=True",", or switch to the default transactional mode.",[14,1442,1443,1446],{},[1259,1444,1445],{},"Connection checkout duration."," A streaming query holds its connection for as long as iteration takes. If your event loop processes each row slowly — for instance, making additional database calls per row — you may exhaust the connection pool before the stream finishes. Design streaming consumers to be as fast as possible; push slow work to a background task or queue rather than performing it inline inside the iteration loop.",[29,1448,1450],{"id":1449},"advanced-streaming-patterns","Advanced Streaming Patterns",[1452,1453,1455],"h3",{"id":1454},"execution_optionsyield_pern-vs-stream_resultstrue","execution_options(yield_per=N) vs stream_results=True",[14,1457,1458,1460,1461,1464,1465,1468,1469,1471,1472,1475],{},[18,1459,50],{}," is the high-level ORM option. It sets both ",[18,1462,1463],{},"stream_results=True"," and the fetch size simultaneously. If you are working at the Core level with a raw ",[18,1466,1467],{},"Connection",", you can set ",[18,1470,1463],{}," independently and rely on ",[18,1473,1474],{},"cursor.fetchmany()"," manually:",[79,1477,1479],{"className":81,"code":1478,"language":83,"meta":84,"style":84},"from sqlalchemy import text\nfrom sqlalchemy.ext.asyncio import AsyncEngine\n\n\nasync def core_streaming(engine: AsyncEngine) -> None:\n    async with engine.connect() as conn:\n        result = await conn.execute(\n            text(\"SELECT id, amount_cents FROM invoices ORDER BY id\"),\n            execution_options={\"stream_results\": True, \"max_row_buffer\": 2000},\n        )\n        while True:\n            rows = result.fetchmany(2000)\n            if not rows:\n                break\n            for row in rows:\n                process_row(row)\n",[18,1480,1481,1492,1503,1507,1511,1527,1541,1554,1565,1596,1601,1611,1625,1636,1641,1652],{"__ignoreMap":84},[88,1482,1483,1485,1487,1489],{"class":90,"line":91},[88,1484,95],{"class":94},[88,1486,99],{"class":98},[88,1488,102],{"class":94},[88,1490,1491],{"class":98}," text\n",[88,1493,1494,1496,1498,1500],{"class":90,"line":108},[88,1495,95],{"class":94},[88,1497,417],{"class":98},[88,1499,102],{"class":94},[88,1501,1502],{"class":98}," AsyncEngine\n",[88,1504,1505],{"class":90,"line":121},[88,1506,125],{"emptyLinePlaceholder":124},[88,1508,1509],{"class":90,"line":128},[88,1510,125],{"emptyLinePlaceholder":124},[88,1512,1513,1515,1517,1520,1523,1525],{"class":90,"line":133},[88,1514,435],{"class":94},[88,1516,438],{"class":94},[88,1518,1519],{"class":139}," core_streaming",[88,1521,1522],{"class":98},"(engine: AsyncEngine) -> ",[88,1524,309],{"class":199},[88,1526,312],{"class":98},[88,1528,1529,1531,1533,1536,1538],{"class":90,"line":152},[88,1530,497],{"class":94},[88,1532,500],{"class":94},[88,1534,1535],{"class":98}," engine.connect() ",[88,1537,506],{"class":94},[88,1539,1540],{"class":98}," conn:\n",[88,1542,1543,1546,1548,1551],{"class":90,"line":158},[88,1544,1545],{"class":98},"        result ",[88,1547,189],{"class":94},[88,1549,1550],{"class":94}," await",[88,1552,1553],{"class":98}," conn.execute(\n",[88,1555,1556,1559,1562],{"class":90,"line":163},[88,1557,1558],{"class":98},"            text(",[88,1560,1561],{"class":192},"\"SELECT id, amount_cents FROM invoices ORDER BY id\"",[88,1563,1564],{"class":98},"),\n",[88,1566,1567,1570,1572,1575,1578,1581,1583,1586,1589,1591,1593],{"class":90,"line":168},[88,1568,1569],{"class":217},"            execution_options",[88,1571,189],{"class":94},[88,1573,1574],{"class":98},"{",[88,1576,1577],{"class":192},"\"stream_results\"",[88,1579,1580],{"class":98},": ",[88,1582,223],{"class":199},[88,1584,1585],{"class":98},", ",[88,1587,1588],{"class":192},"\"max_row_buffer\"",[88,1590,1580],{"class":98},[88,1592,360],{"class":199},[88,1594,1595],{"class":98},"},\n",[88,1597,1598],{"class":90,"line":183},[88,1599,1600],{"class":98},"        )\n",[88,1602,1603,1606,1609],{"class":90,"line":196},[88,1604,1605],{"class":94},"        while",[88,1607,1608],{"class":199}," True",[88,1610,312],{"class":98},[88,1612,1613,1616,1618,1621,1623],{"class":90,"line":229},[88,1614,1615],{"class":98},"            rows ",[88,1617,189],{"class":94},[88,1619,1620],{"class":98}," result.fetchmany(",[88,1622,360],{"class":199},[88,1624,226],{"class":98},[88,1626,1627,1630,1633],{"class":90,"line":252},[88,1628,1629],{"class":94},"            if",[88,1631,1632],{"class":94}," not",[88,1634,1635],{"class":98}," rows:\n",[88,1637,1638],{"class":90,"line":267},[88,1639,1640],{"class":94},"                break\n",[88,1642,1643,1646,1648,1650],{"class":90,"line":282},[88,1644,1645],{"class":94},"            for",[88,1647,1389],{"class":98},[88,1649,380],{"class":94},[88,1651,1635],{"class":98},[88,1653,1654],{"class":90,"line":287},[88,1655,1656],{"class":98},"                process_row(row)\n",[14,1658,1659,1660,1663],{},"Setting ",[18,1661,1662],{},"max_row_buffer"," caps how many rows asyncpg pre-fetches in its internal buffer. Without it, the driver may speculatively fetch ahead and partially defeat the memory savings.",[1452,1665,1667],{"id":1666},"asyncsessionstream-and-stream_scalars","AsyncSession.stream() and stream_scalars()",[14,1669,1670,1672,1673,1676,1677,1680,1681,1683],{},[18,1671,639],{}," returns an ",[18,1674,1675],{},"AsyncResult"," over ",[18,1678,1679],{},"Row"," objects — useful when selecting multiple columns or mixing ORM columns with aggregate expressions. ",[18,1682,643],{}," unwraps the first column automatically, which suits single-entity queries:",[79,1685,1687],{"className":81,"code":1686,"language":83,"meta":84,"style":84},"from sqlalchemy import select, func\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\n\nasync def revenue_by_tenant(session: AsyncSession) -> None:\n    stmt = (\n        select(Invoice.tenant_id, func.sum(Invoice.total_cents).label(\"revenue\"))\n        .group_by(Invoice.tenant_id)\n        .order_by(Invoice.tenant_id)\n        .execution_options(yield_per=500)\n    )\n    # stream() for multi-column results\n    async with session.stream(stmt) as result:\n        async for partition in result.partitions(500):\n            for row in partition:\n                record_revenue(row.tenant_id, row.revenue)\n",[18,1688,1689,1700,1710,1714,1718,1734,1742,1753,1758,1763,1776,1780,1785,1798,1816,1827],{"__ignoreMap":84},[88,1690,1691,1693,1695,1697],{"class":90,"line":91},[88,1692,95],{"class":94},[88,1694,99],{"class":98},[88,1696,102],{"class":94},[88,1698,1699],{"class":98}," select, func\n",[88,1701,1702,1704,1706,1708],{"class":90,"line":108},[88,1703,95],{"class":94},[88,1705,417],{"class":98},[88,1707,102],{"class":94},[88,1709,422],{"class":98},[88,1711,1712],{"class":90,"line":121},[88,1713,125],{"emptyLinePlaceholder":124},[88,1715,1716],{"class":90,"line":128},[88,1717,125],{"emptyLinePlaceholder":124},[88,1719,1720,1722,1724,1727,1730,1732],{"class":90,"line":133},[88,1721,435],{"class":94},[88,1723,438],{"class":94},[88,1725,1726],{"class":139}," revenue_by_tenant",[88,1728,1729],{"class":98},"(session: AsyncSession) -> ",[88,1731,309],{"class":199},[88,1733,312],{"class":98},[88,1735,1736,1738,1740],{"class":90,"line":152},[88,1737,318],{"class":98},[88,1739,189],{"class":94},[88,1741,323],{"class":98},[88,1743,1744,1747,1750],{"class":90,"line":158},[88,1745,1746],{"class":98},"        select(Invoice.tenant_id, func.sum(Invoice.total_cents).label(",[88,1748,1749],{"class":192},"\"revenue\"",[88,1751,1752],{"class":98},"))\n",[88,1754,1755],{"class":90,"line":163},[88,1756,1757],{"class":98},"        .group_by(Invoice.tenant_id)\n",[88,1759,1760],{"class":90,"line":168},[88,1761,1762],{"class":98},"        .order_by(Invoice.tenant_id)\n",[88,1764,1765,1767,1769,1771,1774],{"class":90,"line":183},[88,1766,353],{"class":98},[88,1768,20],{"class":217},[88,1770,189],{"class":94},[88,1772,1773],{"class":199},"500",[88,1775,226],{"class":98},[88,1777,1778],{"class":90,"line":196},[88,1779,368],{"class":98},[88,1781,1782],{"class":90,"line":229},[88,1783,1784],{"class":1417},"    # stream() for multi-column results\n",[88,1786,1787,1789,1791,1794,1796],{"class":90,"line":252},[88,1788,497],{"class":94},[88,1790,500],{"class":94},[88,1792,1793],{"class":98}," session.stream(stmt) ",[88,1795,506],{"class":94},[88,1797,509],{"class":98},[88,1799,1800,1802,1804,1807,1809,1812,1814],{"class":90,"line":267},[88,1801,514],{"class":94},[88,1803,517],{"class":94},[88,1805,1806],{"class":98}," partition ",[88,1808,380],{"class":94},[88,1810,1811],{"class":98}," result.partitions(",[88,1813,1773],{"class":199},[88,1815,149],{"class":98},[88,1817,1818,1820,1822,1824],{"class":90,"line":282},[88,1819,1645],{"class":94},[88,1821,1389],{"class":98},[88,1823,380],{"class":94},[88,1825,1826],{"class":98}," partition:\n",[88,1828,1829],{"class":90,"line":287},[88,1830,1831],{"class":98},"                record_revenue(row.tenant_id, row.revenue)\n",[1452,1833,1835],{"id":1834},"why-yield_per-is-incompatible-with-joinedload","Why yield_per is Incompatible with joinedload",[14,1837,1838,1839,1842,1843,1846],{},"This is the most common pitfall. When ",[18,1840,1841],{},"joinedload"," is applied to a one-to-many relationship, SQLAlchemy generates a ",[18,1844,1845],{},"JOIN"," that produces one row per child entity. A parent with twenty children appears as twenty rows in the raw result. SQLAlchemy normally de-duplicates these rows using the identity map, holding the complete set in memory until all rows for a given parent have arrived — which requires buffering the entire result.",[14,1848,1849,1851,1852,1854],{},[18,1850,20],{}," conflicts directly with this behaviour. SQLAlchemy cannot know whether the next batch contains more rows for the current parent without looking ahead, so it raises ",[18,1853,654],{}," at execution time:",[79,1856,1861],{"className":1857,"code":1859,"language":1860},[1858],"language-text","sqlalchemy.exc.InvalidRequestError: Can't use yield_per with joined eager loading.\n","text",[18,1862,1859],{"__ignoreMap":84},[14,1864,1865,1866,1868,1869,1872,1873,1876,1877,1879],{},"The fix is to replace ",[18,1867,1841],{}," with ",[18,1870,1871],{},"selectinload",", which issues a second ",[18,1874,1875],{},"SELECT ... WHERE parent_id IN (...)"," query for each batch of parents rather than joining at the row level. This is compatible with ",[18,1878,20],{}," and often faster for large collections because it avoids the row-multiplication overhead of the JOIN.",[79,1881,1883],{"className":81,"code":1882,"language":83,"meta":84,"style":84},"from sqlalchemy import select\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom typing import List\n\n\nclass Product(Base):\n    __tablename__ = \"products\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    name: Mapped[str] = mapped_column()\n\n\nclass OrderLine(Base):\n    __tablename__ = \"order_lines\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    order_id: Mapped[int] = mapped_column(index=True)\n    product_id: Mapped[int] = mapped_column()\n    quantity: Mapped[int] = mapped_column()\n\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    tenant_id: Mapped[int] = mapped_column(index=True)\n    amount_cents: Mapped[int] = mapped_column()\n    state: Mapped[str] = mapped_column()\n    lines: Mapped[List[OrderLine]] = relationship(\"OrderLine\", lazy=\"select\")\n\n\nasync def stream_orders_with_lines(session: AsyncSession) -> None:\n    stmt = (\n        select(Order)\n        .options(selectinload(Order.lines))  # CORRECT: compatible with yield_per\n        # .options(joinedload(Order.lines))   # WRONG: raises InvalidRequestError\n        .order_by(Order.id)\n        .execution_options(yield_per=500)\n    )\n    async with session.stream_scalars(stmt) as result:\n        async for order in result:\n            for line in order.lines:\n                process_line(order, line)\n",[18,1884,1885,1895,1906,1916,1927,1931,1935,1948,1957,1979,1992,1996,2000,2013,2022,2044,2065,2078,2091,2095,2099,2111,2119,2141,2161,2173,2185,2210,2214,2218,2233,2241,2245,2253,2258,2262,2274,2278,2290,2302,2314],{"__ignoreMap":84},[88,1886,1887,1889,1891,1893],{"class":90,"line":91},[88,1888,95],{"class":94},[88,1890,99],{"class":98},[88,1892,102],{"class":94},[88,1894,105],{"class":98},[88,1896,1897,1899,1901,1903],{"class":90,"line":108},[88,1898,95],{"class":94},[88,1900,113],{"class":98},[88,1902,102],{"class":94},[88,1904,1905],{"class":98}," Mapped, mapped_column, relationship, selectinload\n",[88,1907,1908,1910,1912,1914],{"class":90,"line":121},[88,1909,95],{"class":94},[88,1911,417],{"class":98},[88,1913,102],{"class":94},[88,1915,422],{"class":98},[88,1917,1918,1920,1922,1924],{"class":90,"line":128},[88,1919,95],{"class":94},[88,1921,677],{"class":98},[88,1923,102],{"class":94},[88,1925,1926],{"class":98}," List\n",[88,1928,1929],{"class":90,"line":133},[88,1930,125],{"emptyLinePlaceholder":124},[88,1932,1933],{"class":90,"line":152},[88,1934,125],{"emptyLinePlaceholder":124},[88,1936,1937,1939,1942,1944,1946],{"class":90,"line":158},[88,1938,136],{"class":94},[88,1940,1941],{"class":139}," Product",[88,1943,143],{"class":98},[88,1945,178],{"class":139},[88,1947,149],{"class":98},[88,1949,1950,1952,1954],{"class":90,"line":163},[88,1951,186],{"class":98},[88,1953,189],{"class":94},[88,1955,1956],{"class":192}," \"products\"\n",[88,1958,1959,1961,1963,1965,1967,1969,1971,1973,1975,1977],{"class":90,"line":168},[88,1960,200],{"class":199},[88,1962,203],{"class":98},[88,1964,206],{"class":199},[88,1966,209],{"class":98},[88,1968,189],{"class":94},[88,1970,214],{"class":98},[88,1972,218],{"class":217},[88,1974,189],{"class":94},[88,1976,223],{"class":199},[88,1978,226],{"class":98},[88,1980,1981,1984,1986,1988,1990],{"class":90,"line":183},[88,1982,1983],{"class":98},"    name: Mapped[",[88,1985,273],{"class":199},[88,1987,209],{"class":98},[88,1989,189],{"class":94},[88,1991,264],{"class":98},[88,1993,1994],{"class":90,"line":196},[88,1995,125],{"emptyLinePlaceholder":124},[88,1997,1998],{"class":90,"line":229},[88,1999,125],{"emptyLinePlaceholder":124},[88,2001,2002,2004,2007,2009,2011],{"class":90,"line":252},[88,2003,136],{"class":94},[88,2005,2006],{"class":139}," OrderLine",[88,2008,143],{"class":98},[88,2010,178],{"class":139},[88,2012,149],{"class":98},[88,2014,2015,2017,2019],{"class":90,"line":267},[88,2016,186],{"class":98},[88,2018,189],{"class":94},[88,2020,2021],{"class":192}," \"order_lines\"\n",[88,2023,2024,2026,2028,2030,2032,2034,2036,2038,2040,2042],{"class":90,"line":282},[88,2025,200],{"class":199},[88,2027,203],{"class":98},[88,2029,206],{"class":199},[88,2031,209],{"class":98},[88,2033,189],{"class":94},[88,2035,214],{"class":98},[88,2037,218],{"class":217},[88,2039,189],{"class":94},[88,2041,223],{"class":199},[88,2043,226],{"class":98},[88,2045,2046,2049,2051,2053,2055,2057,2059,2061,2063],{"class":90,"line":287},[88,2047,2048],{"class":98},"    order_id: Mapped[",[88,2050,206],{"class":199},[88,2052,209],{"class":98},[88,2054,189],{"class":94},[88,2056,214],{"class":98},[88,2058,243],{"class":217},[88,2060,189],{"class":94},[88,2062,223],{"class":199},[88,2064,226],{"class":98},[88,2066,2067,2070,2072,2074,2076],{"class":90,"line":292},[88,2068,2069],{"class":98},"    product_id: Mapped[",[88,2071,206],{"class":199},[88,2073,209],{"class":98},[88,2075,189],{"class":94},[88,2077,264],{"class":98},[88,2079,2080,2083,2085,2087,2089],{"class":90,"line":315},[88,2081,2082],{"class":98},"    quantity: Mapped[",[88,2084,206],{"class":199},[88,2086,209],{"class":98},[88,2088,189],{"class":94},[88,2090,264],{"class":98},[88,2092,2093],{"class":90,"line":326},[88,2094,125],{"emptyLinePlaceholder":124},[88,2096,2097],{"class":90,"line":332},[88,2098,125],{"emptyLinePlaceholder":124},[88,2100,2101,2103,2105,2107,2109],{"class":90,"line":344},[88,2102,136],{"class":94},[88,2104,753],{"class":139},[88,2106,143],{"class":98},[88,2108,178],{"class":139},[88,2110,149],{"class":98},[88,2112,2113,2115,2117],{"class":90,"line":350},[88,2114,186],{"class":98},[88,2116,189],{"class":94},[88,2118,768],{"class":192},[88,2120,2121,2123,2125,2127,2129,2131,2133,2135,2137,2139],{"class":90,"line":365},[88,2122,200],{"class":199},[88,2124,203],{"class":98},[88,2126,206],{"class":199},[88,2128,209],{"class":98},[88,2130,189],{"class":94},[88,2132,214],{"class":98},[88,2134,218],{"class":217},[88,2136,189],{"class":94},[88,2138,223],{"class":199},[88,2140,226],{"class":98},[88,2142,2143,2145,2147,2149,2151,2153,2155,2157,2159],{"class":90,"line":371},[88,2144,232],{"class":98},[88,2146,206],{"class":199},[88,2148,209],{"class":98},[88,2150,189],{"class":94},[88,2152,214],{"class":98},[88,2154,243],{"class":217},[88,2156,189],{"class":94},[88,2158,223],{"class":199},[88,2160,226],{"class":98},[88,2162,2163,2165,2167,2169,2171],{"class":90,"line":386},[88,2164,815],{"class":98},[88,2166,206],{"class":199},[88,2168,209],{"class":98},[88,2170,189],{"class":94},[88,2172,264],{"class":98},[88,2174,2175,2177,2179,2181,2183],{"class":90,"line":904},[88,2176,828],{"class":98},[88,2178,273],{"class":199},[88,2180,209],{"class":98},[88,2182,189],{"class":94},[88,2184,264],{"class":98},[88,2186,2187,2190,2192,2195,2198,2200,2203,2205,2208],{"class":90,"line":925},[88,2188,2189],{"class":98},"    lines: Mapped[List[OrderLine]] ",[88,2191,189],{"class":94},[88,2193,2194],{"class":98}," relationship(",[88,2196,2197],{"class":192},"\"OrderLine\"",[88,2199,1585],{"class":98},[88,2201,2202],{"class":217},"lazy",[88,2204,189],{"class":94},[88,2206,2207],{"class":192},"\"select\"",[88,2209,226],{"class":98},[88,2211,2212],{"class":90,"line":930},[88,2213,125],{"emptyLinePlaceholder":124},[88,2215,2216],{"class":90,"line":935},[88,2217,125],{"emptyLinePlaceholder":124},[88,2219,2220,2222,2224,2227,2229,2231],{"class":90,"line":948},[88,2221,435],{"class":94},[88,2223,438],{"class":94},[88,2225,2226],{"class":139}," stream_orders_with_lines",[88,2228,1729],{"class":98},[88,2230,309],{"class":199},[88,2232,312],{"class":98},[88,2234,2235,2237,2239],{"class":90,"line":954},[88,2236,318],{"class":98},[88,2238,189],{"class":94},[88,2240,323],{"class":98},[88,2242,2243],{"class":90,"line":964},[88,2244,1001],{"class":98},[88,2246,2247,2250],{"class":90,"line":980},[88,2248,2249],{"class":98},"        .options(selectinload(Order.lines))  ",[88,2251,2252],{"class":1417},"# CORRECT: compatible with yield_per\n",[88,2254,2255],{"class":90,"line":989},[88,2256,2257],{"class":1417},"        # .options(joinedload(Order.lines))   # WRONG: raises InvalidRequestError\n",[88,2259,2260],{"class":90,"line":998},[88,2261,1025],{"class":98},[88,2263,2264,2266,2268,2270,2272],{"class":90,"line":1004},[88,2265,353],{"class":98},[88,2267,20],{"class":217},[88,2269,189],{"class":94},[88,2271,1773],{"class":199},[88,2273,226],{"class":98},[88,2275,2276],{"class":90,"line":1022},[88,2277,368],{"class":98},[88,2279,2280,2282,2284,2286,2288],{"class":90,"line":1028},[88,2281,497],{"class":94},[88,2283,500],{"class":94},[88,2285,503],{"class":98},[88,2287,506],{"class":94},[88,2289,509],{"class":98},[88,2291,2292,2294,2296,2298,2300],{"class":90,"line":1040},[88,2293,514],{"class":94},[88,2295,517],{"class":94},[88,2297,1081],{"class":98},[88,2299,380],{"class":94},[88,2301,509],{"class":98},[88,2303,2304,2306,2309,2311],{"class":90,"line":1045},[88,2305,1645],{"class":94},[88,2307,2308],{"class":98}," line ",[88,2310,380],{"class":94},[88,2312,2313],{"class":98}," order.lines:\n",[88,2315,2316],{"class":90,"line":1060},[88,2317,2318],{"class":98},"                process_line(order, line)\n",[14,2320,2321,2322,2326,2327,2330,2331,2333],{},"The relationship-loading deep-dive at ",[23,2323,2325],{"href":2324},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fcomplex-joins-and-relationship-loading-strategies\u002F","Complex Joins and Relationship Loading Strategies"," covers when ",[18,2328,2329],{},"subqueryload"," is preferable to ",[18,2332,1871],{}," and how to diagnose N+1 patterns in streaming contexts.",[1452,2335,2337],{"id":2336},"choosing-the-right-batch-size","Choosing the Right Batch Size",[14,2339,2340,2341,2343,2344,2346],{},"The optimal ",[18,2342,20],{}," value balances memory consumption against round-trip overhead. Each batch requires one ",[18,2345,43],{}," command, so extremely small values (yield_per=1) generate chatty traffic. Very large values (yield_per=100000) approach full-buffering behaviour and defeat the purpose.",[14,2348,2349,2350,2353,2354,2357,2358,2361],{},"A practical formula: estimate your target peak memory budget for the streaming operation (say, 256 MB for a data export worker), divide by the average row size in bytes (measurable with ",[18,2351,2352],{},"pg_column_size()"," or Python's ",[18,2355,2356],{},"sys.getsizeof()"," on a sample instance), and use that quotient as your starting point. Then validate by measuring actual heap usage with ",[18,2359,2360],{},"tracemalloc"," under realistic load.",[14,2363,2364],{},"For rows averaging 500 bytes each and a 256 MB budget:",[79,2366,2369],{"className":2367,"code":2368,"language":1860},[1858],"256 * 1024 * 1024 \u002F 500 ≈ 536,870 rows\n",[18,2370,2368],{"__ignoreMap":84},[14,2372,2373,2374,2377],{},"Account for ORM hydration overhead (typically 3–5x the raw row bytes for instrumented instances) and you arrive at roughly 100,000 rows — still too large for most use cases once the identity map and Python object graph are counted. A practical starting range for typical ORM-hydrated rows is ",[1259,2375,2376],{},"1,000–10,000",", with 5,000 as a reasonable default for rows under 1 KB each. For wide rows (many TEXT or JSONB columns), start at 500.",[14,2379,2380,2381,2385],{},"The detailed async implementation walkthrough at ",[23,2382,2384],{"href":2383},"\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","Using yield_per to Stream Millions of Rows in Async"," benchmarks batch sizes against wall-clock throughput and memory profiles across asyncpg and psycopg3 drivers.",[1452,2387,2389],{"id":2388},"memory-profile-comparison","Memory Profile Comparison",[14,2391,2392,2393,2395],{},"The diagram below illustrates the difference in client-side memory usage between a buffered full-fetch and ",[18,2394,20],{}," streaming over the same ten-million-row result set.",[2397,2398,2401],"figure",{"className":2399},[2400],"diagram",[2402,2403,2407,2408,2407,2412,2407,2407,2416,2407,2407,2424,2407,2434,2407,2442,2407,2407,2447,2407,2452,2407,2457,2407,2460,2407,2463,2407,2466,2407,2470,2407,2473,2407,2407,2477,2407,2480,2407,2483,2407,2486,2407,2489,2407,2491,2407,2494,2407,2497,2407,2500,2407,2502,2407,2407,2505,2407,2508,2407,2510,2407,2407,2517,2407,2525,2407,2530,2407,2534,2407,2407,2539,2407,2544,2407,2550,2407,2553,2407,2557,2407,2562],"svg",{"viewBox":2404,"role":2405,"ariaLabel":2406},"0 0 760 340","img","Memory profile comparison: buffered full-fetch versus yield_per streaming","\n  ",[2409,2410,2411],"title",{},"Memory profile: buffered full-fetch vs yield_per streaming",[2413,2414,2415],"desc",{},"Two line charts comparing client memory usage over time. The buffered approach ramps continuously to a peak before dropping sharply after the query completes. The yield_per approach stays flat at a low level throughout processing.",[2417,2418],"rect",{"x":2419,"y":2419,"width":2420,"height":2421,"fill":2422,"rx":2423},"0","760","340","#f1f9f6","8",[2417,2425],{"x":2426,"y":2427,"width":2428,"height":2429,"fill":2430,"rx":2431,"stroke":2432,"style":2433},"70","30","610","220","#ffffff","4","rgba(15,118,110,0.28)","stroke-width:1",[1860,2435,2441],{"x":2436,"y":2437,"fill":2438,"transform":2439,"style":2440},"18","145","#3f4f4b","rotate(-90,18,145)","text-anchor:middle;font-size:13px","\nMemory (MB)\n",[1860,2443,2446],{"x":2444,"y":2445,"fill":2438,"style":2440},"375","320","\nTime →  (rows fetched)\n",[90,2448],{"x1":2426,"y1":2449,"x2":2450,"y2":2449,"stroke":2451,"style":2433},"230","680","rgba(15,118,110,0.15)",[1860,2453,2419],{"x":2454,"y":2455,"fill":2438,"style":2456},"60","234","text-anchor:end;font-size:13px",[90,2458],{"x1":2426,"y1":2459,"x2":2450,"y2":2459,"stroke":2451,"style":2433},"175",[1860,2461,1773],{"x":2454,"y":2462,"fill":2438,"style":2456},"179",[90,2464],{"x1":2426,"y1":2465,"x2":2450,"y2":2465,"stroke":2451,"style":2433},"120",[1860,2467,2469],{"x":2454,"y":2468,"fill":2438,"style":2456},"124","1000",[90,2471],{"x1":2426,"y1":2472,"x2":2450,"y2":2472,"stroke":2451,"style":2433},"65",[1860,2474,2476],{"x":2454,"y":2475,"fill":2438,"style":2456},"69","1500",[90,2478],{"x1":2426,"y1":2449,"x2":2426,"y2":2479,"stroke":2438,"style":2433},"238",[1860,2481,2419],{"x":2426,"y":2482,"fill":2438,"style":2440},"252",[90,2484],{"x1":2485,"y1":2449,"x2":2485,"y2":2479,"stroke":2438,"style":2433},"222",[1860,2487,2488],{"x":2485,"y":2482,"fill":2438,"style":2440},"2.5M",[90,2490],{"x1":2444,"y1":2449,"x2":2444,"y2":2479,"stroke":2438,"style":2433},[1860,2492,2493],{"x":2444,"y":2482,"fill":2438,"style":2440},"5M",[90,2495],{"x1":2496,"y1":2449,"x2":2496,"y2":2479,"stroke":2438,"style":2433},"527",[1860,2498,2499],{"x":2496,"y":2482,"fill":2438,"style":2440},"7.5M",[90,2501],{"x1":2450,"y1":2449,"x2":2450,"y2":2479,"stroke":2438,"style":2433},[1860,2503,2504],{"x":2450,"y":2482,"fill":2438,"style":2440},"10M",[90,2506],{"x1":2426,"y1":2427,"x2":2426,"y2":2449,"stroke":2438,"style":2507},"stroke-width:1.5",[90,2509],{"x1":2426,"y1":2449,"x2":2450,"y2":2449,"stroke":2438,"style":2507},[2511,2512],"polyline",{"points":2513,"fill":2514,"stroke":2515,"style":2516},"70,230 150,218 230,200 310,178 390,150 470,112 560,58 600,48 650,230 680,230","none","#e05252","stroke-width:2.5;stroke-linejoin:round",[2417,2518],{"x":2519,"y":2520,"width":2521,"height":2522,"fill":2515,"rx":2523,"opacity":2524},"540","35","90","22","3","0.15",[1860,2526,2529],{"x":2527,"y":2528,"fill":2515,"style":2440},"585","50","OOM risk",[2511,2531],{"points":2532,"fill":2514,"stroke":2533,"style":2516},"70,222 150,220 230,219 310,220 390,221 470,220 560,219 640,220 680,221","#0f766e",[2535,2536],"polygon",{"points":2537,"fill":2533,"opacity":2538},"70,222 150,220 230,219 310,220 390,221 470,220 560,219 640,220 680,221 680,230 70,230","0.12",[2417,2540],{"x":2521,"y":2541,"width":2542,"height":2431,"fill":2515,"rx":2543},"42","16","2",[1860,2545,2549],{"x":2546,"y":2547,"fill":2438,"style":2548},"112","48","text-anchor:start;font-size:13px","Buffered full-fetch (fetchall)",[2417,2551],{"x":2521,"y":2552,"width":2542,"height":2431,"fill":2533,"rx":2543},"62",[1860,2554,2556],{"x":2546,"y":2555,"fill":2438,"style":2548},"68","yield_per streaming (server-side cursor)",[1860,2558,2561],{"x":2559,"y":2560,"fill":2515,"style":2440},"598","40","\n~1.4 GB\n",[1860,2563,2565],{"x":2444,"y":2564,"fill":2533,"style":2440},"212","\n~50 MB constant\n",[14,2567,2568,2569,2571],{},"The buffered approach accumulates memory proportionally to total row count, peaking just before your code processes the first row. ",[18,2570,20],{}," keeps the footprint constant because each batch is discarded before the next is fetched.",[1452,2573,2575],{"id":2574},"keyset-pagination-as-a-complement","Keyset Pagination as a Complement",[14,2577,2578,2579,2583],{},"Server-side cursors are not the only tool for large result traversal. Cursor-based keyset pagination issues separate queries, each starting after the last primary key of the previous batch, rather than holding a cursor open. This avoids long-lived connections and works across stateless HTTP services where a single request cannot hold a database connection for minutes. The trade-offs between these approaches — and when to combine them — are explored in ",[23,2580,2582],{"href":2581},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per\u002Fpaginating-large-result-sets-with-keyset-pagination\u002F","Paginating Large Result Sets with Keyset Pagination",".",[29,2585,2587],{"id":2586},"hybrid-architectures-migration-strategies","Hybrid Architectures & Migration Strategies",[1452,2589,2591],{"id":2590},"mixing-core-and-orm-in-streaming-pipelines","Mixing Core and ORM in Streaming Pipelines",[14,2593,2594],{},"Some pipelines benefit from ORM-level streaming for the initial traversal (to leverage relationship loading and type coercion) but switch to Core for downstream writes (to bypass ORM overhead during bulk mutations). This hybrid pattern is fully supported in SQLAlchemy 2.0:",[79,2596,2598],{"className":81,"code":2597,"language":83,"meta":84,"style":84},"from sqlalchemy import select, update\nfrom sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine\n\n\nasync def recalculate_invoice_totals(\n    session: AsyncSession,\n    engine: AsyncEngine,\n    tenant_id: int,\n) -> None:\n    stmt = (\n        select(Invoice)\n        .where(Invoice.tenant_id == tenant_id, Invoice.status == \"draft\")\n        .order_by(Invoice.id)\n        .execution_options(yield_per=2000)\n    )\n\n    pending_updates: list[dict] = []\n\n    async with session.stream_scalars(stmt) as result:\n        async for invoice in result:\n            new_total = compute_total(invoice)\n            pending_updates.append({\"id\": invoice.id, \"total_cents\": new_total})\n\n            if len(pending_updates) >= 2000:\n                # Flush the accumulated batch via Core to avoid ORM overhead\n                async with engine.begin() as conn:\n                    await conn.execute(\n                        update(Invoice),\n                        pending_updates,\n                    )\n                pending_updates.clear()\n\n    # Flush remainder\n    if pending_updates:\n        async with engine.begin() as conn:\n            await conn.execute(update(Invoice), pending_updates)\n",[18,2599,2600,2611,2622,2626,2630,2641,2646,2651,2659,2667,2675,2679,2695,2699,2711,2715,2719,2734,2738,2750,2762,2772,2789,2793,2811,2816,2830,2837,2842,2847,2852,2857,2861,2866,2874,2886],{"__ignoreMap":84},[88,2601,2602,2604,2606,2608],{"class":90,"line":91},[88,2603,95],{"class":94},[88,2605,99],{"class":98},[88,2607,102],{"class":94},[88,2609,2610],{"class":98}," select, update\n",[88,2612,2613,2615,2617,2619],{"class":90,"line":108},[88,2614,95],{"class":94},[88,2616,417],{"class":98},[88,2618,102],{"class":94},[88,2620,2621],{"class":98}," AsyncSession, AsyncEngine\n",[88,2623,2624],{"class":90,"line":121},[88,2625,125],{"emptyLinePlaceholder":124},[88,2627,2628],{"class":90,"line":128},[88,2629,125],{"emptyLinePlaceholder":124},[88,2631,2632,2634,2636,2639],{"class":90,"line":133},[88,2633,435],{"class":94},[88,2635,438],{"class":94},[88,2637,2638],{"class":139}," recalculate_invoice_totals",[88,2640,945],{"class":98},[88,2642,2643],{"class":90,"line":152},[88,2644,2645],{"class":98},"    session: AsyncSession,\n",[88,2647,2648],{"class":90,"line":158},[88,2649,2650],{"class":98},"    engine: AsyncEngine,\n",[88,2652,2653,2655,2657],{"class":90,"line":163},[88,2654,957],{"class":98},[88,2656,206],{"class":199},[88,2658,862],{"class":98},[88,2660,2661,2663,2665],{"class":90,"line":168},[88,2662,306],{"class":98},[88,2664,309],{"class":199},[88,2666,312],{"class":98},[88,2668,2669,2671,2673],{"class":90,"line":183},[88,2670,318],{"class":98},[88,2672,189],{"class":94},[88,2674,323],{"class":98},[88,2676,2677],{"class":90,"line":196},[88,2678,329],{"class":98},[88,2680,2681,2683,2685,2688,2690,2693],{"class":90,"line":229},[88,2682,335],{"class":98},[88,2684,338],{"class":94},[88,2686,2687],{"class":98}," tenant_id, Invoice.status ",[88,2689,338],{"class":94},[88,2691,2692],{"class":192}," \"draft\"",[88,2694,226],{"class":98},[88,2696,2697],{"class":90,"line":252},[88,2698,347],{"class":98},[88,2700,2701,2703,2705,2707,2709],{"class":90,"line":267},[88,2702,353],{"class":98},[88,2704,20],{"class":217},[88,2706,189],{"class":94},[88,2708,360],{"class":199},[88,2710,226],{"class":98},[88,2712,2713],{"class":90,"line":282},[88,2714,368],{"class":98},[88,2716,2717],{"class":90,"line":287},[88,2718,125],{"emptyLinePlaceholder":124},[88,2720,2721,2724,2727,2729,2731],{"class":90,"line":292},[88,2722,2723],{"class":98},"    pending_updates: list[",[88,2725,2726],{"class":199},"dict",[88,2728,209],{"class":98},[88,2730,189],{"class":94},[88,2732,2733],{"class":98}," []\n",[88,2735,2736],{"class":90,"line":315},[88,2737,125],{"emptyLinePlaceholder":124},[88,2739,2740,2742,2744,2746,2748],{"class":90,"line":326},[88,2741,497],{"class":94},[88,2743,500],{"class":94},[88,2745,503],{"class":98},[88,2747,506],{"class":94},[88,2749,509],{"class":98},[88,2751,2752,2754,2756,2758,2760],{"class":90,"line":332},[88,2753,514],{"class":94},[88,2755,517],{"class":94},[88,2757,377],{"class":98},[88,2759,380],{"class":94},[88,2761,509],{"class":98},[88,2763,2764,2767,2769],{"class":90,"line":344},[88,2765,2766],{"class":98},"            new_total ",[88,2768,189],{"class":94},[88,2770,2771],{"class":98}," compute_total(invoice)\n",[88,2773,2774,2777,2780,2783,2786],{"class":90,"line":350},[88,2775,2776],{"class":98},"            pending_updates.append({",[88,2778,2779],{"class":192},"\"id\"",[88,2781,2782],{"class":98},": invoice.id, ",[88,2784,2785],{"class":192},"\"total_cents\"",[88,2787,2788],{"class":98},": new_total})\n",[88,2790,2791],{"class":90,"line":365},[88,2792,125],{"emptyLinePlaceholder":124},[88,2794,2795,2797,2800,2803,2806,2809],{"class":90,"line":371},[88,2796,1629],{"class":94},[88,2798,2799],{"class":199}," len",[88,2801,2802],{"class":98},"(pending_updates) ",[88,2804,2805],{"class":94},">=",[88,2807,2808],{"class":199}," 2000",[88,2810,312],{"class":98},[88,2812,2813],{"class":90,"line":386},[88,2814,2815],{"class":1417},"                # Flush the accumulated batch via Core to avoid ORM overhead\n",[88,2817,2818,2821,2823,2826,2828],{"class":90,"line":904},[88,2819,2820],{"class":94},"                async",[88,2822,500],{"class":94},[88,2824,2825],{"class":98}," engine.begin() ",[88,2827,506],{"class":94},[88,2829,1540],{"class":98},[88,2831,2832,2835],{"class":90,"line":925},[88,2833,2834],{"class":94},"                    await",[88,2836,1553],{"class":98},[88,2838,2839],{"class":90,"line":930},[88,2840,2841],{"class":98},"                        update(Invoice),\n",[88,2843,2844],{"class":90,"line":935},[88,2845,2846],{"class":98},"                        pending_updates,\n",[88,2848,2849],{"class":90,"line":948},[88,2850,2851],{"class":98},"                    )\n",[88,2853,2854],{"class":90,"line":954},[88,2855,2856],{"class":98},"                pending_updates.clear()\n",[88,2858,2859],{"class":90,"line":964},[88,2860,125],{"emptyLinePlaceholder":124},[88,2862,2863],{"class":90,"line":980},[88,2864,2865],{"class":1417},"    # Flush remainder\n",[88,2867,2868,2871],{"class":90,"line":989},[88,2869,2870],{"class":94},"    if",[88,2872,2873],{"class":98}," pending_updates:\n",[88,2875,2876,2878,2880,2882,2884],{"class":90,"line":998},[88,2877,514],{"class":94},[88,2879,500],{"class":94},[88,2881,2825],{"class":98},[88,2883,506],{"class":94},[88,2885,1540],{"class":98},[88,2887,2888,2890],{"class":90,"line":1004},[88,2889,528],{"class":94},[88,2891,2892],{"class":98}," conn.execute(update(Invoice), pending_updates)\n",[14,2894,2895,2896,2900,2901,2904],{},"This pattern reads with the ORM (gaining type safety and relationship traversal) and writes with Core (gaining bulk-execute throughput). For the bulk write half of this pattern, the performance guidance at ",[23,2897,2899],{"href":2898},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fhigh-performance-bulk-inserts-and-updates\u002F","High-Performance Bulk Inserts and Updates"," covers transaction batching, ",[18,2902,2903],{},"RETURNING"," clause usage, and deadlock avoidance.",[1452,2906,2908],{"id":2907},"migrating-from-sqlalchemy-14-to-20","Migrating from SQLAlchemy 1.4 to 2.0",[14,2910,2911,2912,2914,2915,2918,2919,2922],{},"In 1.4, the equivalent of ",[18,2913,20],{}," was ",[18,2916,2917],{},"Query.yield_per(N)"," on the legacy ",[18,2920,2921],{},"session.query()"," API. The 2.0 migration path is mechanical:",[551,2924,2925,2935],{},[554,2926,2927],{},[557,2928,2929,2932],{},[560,2930,2931],{},"1.4 pattern",[560,2933,2934],{},"2.0 equivalent",[570,2936,2937,2949,2961],{},[557,2938,2939,2944],{},[575,2940,2941],{},[18,2942,2943],{},"session.query(Invoice).yield_per(1000)",[575,2945,2946],{},[18,2947,2948],{},"session.scalars(select(Invoice).execution_options(yield_per=1000))",[557,2950,2951,2956],{},[575,2952,2953],{},[18,2954,2955],{},"session.query(Invoice).with_for_update().yield_per(1000)",[575,2957,2958],{},[18,2959,2960],{},"select(Invoice).with_for_update().execution_options(yield_per=1000)",[557,2962,2963,2969],{},[575,2964,2965,2968],{},[18,2966,2967],{},"for invoice in query.yield_per(1000)"," (sync)",[575,2970,2971,2974],{},[18,2972,2973],{},"async for invoice in result"," (async)",[14,2976,636,2977,2980,2981,2983,2984,2987],{},[18,2978,2979],{},"execution_options()"," approach is more composable: you can attach it to a subquery, pass it through a helper function that constructs the ",[18,2982,54],{},", or apply it conditionally based on an estimated row count without restructuring the query. The legacy ",[18,2985,2986],{},"Query.yield_per()"," was a method on the session-bound query object and could not be composed this way.",[14,2989,2990,2991,640,2994,2996,2997,3000,3001,3004,3005,3007,3008,3000,3011,3014,3015,3018,3019,3022],{},"One important 2.0 change: ",[18,2992,2993],{},"scalars()",[18,2995,643],{}," return ",[18,2998,2999],{},"ScalarResult"," \u002F ",[18,3002,3003],{},"AsyncScalarResult",", which iterate directly over the first column of each row. If you were iterating over ",[18,3006,1679],{}," objects in 1.4 and accessing columns by name, switch to ",[18,3009,3010],{},"session.execute()",[18,3012,3013],{},"session.stream()"," and unpack ",[18,3016,3017],{},"row.Invoice"," or ",[18,3020,3021],{},"row[0]"," explicitly.",[29,3024,3026],{"id":3025},"production-pitfalls-anti-patterns","Production Pitfalls & Anti-Patterns",[551,3028,3029,3042],{},[554,3030,3031],{},[557,3032,3033,3036,3039],{},[560,3034,3035],{},"Pitfall",[560,3037,3038],{},"Exception or Symptom",[560,3040,3041],{},"Fix",[570,3043,3044,3068,3086,3106,3120,3138],{},[557,3045,3046,3053,3058],{},[575,3047,3048,3049,1868,3051],{},"Using ",[18,3050,1841],{},[18,3052,20],{},[575,3054,3055],{},[18,3056,3057],{},"InvalidRequestError: Can't use yield_per with joined eager loading",[575,3059,3060,3061,1868,3063,3018,3065,3067],{},"Replace ",[18,3062,1841],{},[18,3064,1871],{},[18,3066,2329],{}," for all eager-loaded relationships",[557,3069,3070,3073,3076],{},[575,3071,3072],{},"Streaming without an active transaction in autocommit mode",[575,3074,3075],{},"Cursor closes prematurely; partial results silently truncated",[575,3077,3078,3079,3081,3082,3018,3084],{},"Wrap the stream in ",[18,3080,1435],{}," before calling ",[18,3083,639],{},[18,3085,643],{},[557,3087,3088,3091,3097],{},[575,3089,3090],{},"Identity map growth across the full stream",[575,3092,3093,3094],{},"Monotonically growing heap; eventual ",[18,3095,3096],{},"MemoryError",[575,3098,3099,3100,3102,3103],{},"Call ",[18,3101,1268],{}," after processing each batch, or use ",[18,3104,3105],{},"expire_on_commit=True",[557,3107,3108,3111,3117],{},[575,3109,3110],{},"Slow per-row processing blocking the connection checkout",[575,3112,3113,3114],{},"Pool exhaustion; ",[18,3115,3116],{},"TimeoutError: QueuePool limit reached",[575,3118,3119],{},"Move per-row side effects to an async queue; process batches not individual rows inside the stream",[557,3121,3122,3129,3132],{},[575,3123,3124,3125,3128],{},"Missing ",[18,3126,3127],{},"order_by"," on the streaming query",[575,3130,3131],{},"Non-deterministic result ordering; duplicate or skipped rows on cursor resume",[575,3133,3134,3135,3137],{},"Always add an ",[18,3136,3127],{}," clause tied to an indexed column (typically the primary key)",[557,3139,3140,3153,3158],{},[575,3141,3142,3143,3145,3146,3018,3149,3152],{},"Applying ",[18,3144,20],{}," to a query that uses ",[18,3147,3148],{},"DISTINCT",[18,3150,3151],{},"GROUP BY"," on non-identity columns",[575,3154,3155,3156],{},"ORM identity deduplication breaks batch boundaries; ",[18,3157,654],{},[575,3159,3160,3161,3163],{},"Use Core-level streaming (",[18,3162,1463],{},") for aggregated or deduplicated result sets",[29,3165,3167],{"id":3166},"frequently-asked-questions","Frequently Asked Questions",[14,3169,3170],{},[1259,3171,3172],{},"Does yield_per work with AsyncSession?",[14,3174,3175,3176,3179,3180,3182,3183,3018,3185,3187,3188,3190],{},"Yes. Apply ",[18,3177,3178],{},".execution_options(yield_per=N)"," to the ",[18,3181,54],{}," statement before passing it to ",[18,3184,625],{},[18,3186,609],{},". Both methods return asynchronous context managers that keep the DBAPI cursor open for server-side delivery. The ",[18,3189,62],{}," uses the asyncpg driver's native cursor protocol, so the memory savings are identical to the synchronous path — rows are delivered in batches of N without the full result ever being materialized on the client.",[14,3192,3193],{},[1259,3194,3195],{},"What happens if I use joinedload with yield_per?",[14,3197,3198,3199,3202,3203,3205,3206,3208,3209,3212],{},"SQLAlchemy raises ",[18,3200,3201],{},"sqlalchemy.exc.InvalidRequestError"," at execution time with the message \"Can't use yield_per with joined eager loading.\" This is a deliberate guard: joined eager loading multiplies rows at the SQL level and requires the ORM to buffer all rows for a given parent entity before yielding it. That buffering requirement is fundamentally incompatible with the fixed-batch guarantee that ",[18,3204,20],{}," provides. The correct replacement is ",[18,3207,1871],{},", which issues a separate ",[18,3210,3211],{},"SELECT ... IN (...)"," query per batch of parent IDs and is fully compatible with streaming.",[14,3214,3215],{},[1259,3216,3217],{},"How do I choose the right N for yield_per?",[14,3219,3220,3221,3223,3224,3227],{},"Start from your memory budget for the streaming operation, divide by the estimated per-row cost including ORM instrumentation (typically 3–5x raw row bytes), and round down to the nearest power of ten. Then validate empirically: stream 100,000 rows from production data with ",[18,3222,2360],{}," enabled and observe peak allocation. Adjust N until peak allocation fits your budget with a 30% safety margin. For most applications processing rows under 1 KB each, ",[18,3225,3226],{},"yield_per=5000"," is a reasonable starting point. For wide rows with large text or JSON columns, start at 500. Avoid values below 100 (too many round trips) or above 50,000 (approaches full-buffer behaviour).",[14,3229,3230],{},[1259,3231,3232],{},"Can I use yield_per with SQLAlchemy Core?",[14,3234,3235,3236,3238,3239,3242,3243,3245,3246,3018,3248,3251,3252,3018,3254,3257,3258,3260,3261,640,3263,3266,3267,3269],{},"Yes. Set ",[18,3237,1463],{}," in ",[18,3240,3241],{},"execution_options"," on any Core ",[18,3244,54],{}," statement executed against a ",[18,3247,1467],{},[18,3249,3250],{},"AsyncConnection",". You then iterate with ",[18,3253,1474],{},[18,3255,3256],{},"result.fetchmany()",". The ",[18,3259,20],{}," shorthand sets both ",[18,3262,1463],{},[18,3264,3265],{},"max_row_buffer=N"," simultaneously, which is why it is preferred at the ORM level — it encapsulates both settings in a single call. At the Core level you may want separate control over the two, for example setting a large ",[18,3268,1662],{}," for asyncpg pre-fetching while keeping your application batch size smaller.",[14,3271,3272],{},[1259,3273,3274],{},"Does yield_per prevent timeouts on long streams?",[14,3276,3277,3279,3280,3283,3284,3287,3288,3291,3292,3294,3295,3297,3298,3301,3302,3304,3305,3307,3308,3310,3311,3314,3315,3318],{},[18,3278,20],{}," itself does not manage statement timeouts. PostgreSQL's ",[18,3281,3282],{},"statement_timeout"," setting applies to the total execution time of the cursor's initial ",[18,3285,3286],{},"EXECUTE"," call, not to the duration of ",[18,3289,3290],{},"FETCH"," calls. If your database has a 30-second ",[18,3293,3282],{}," and the initial ",[18,3296,3286],{}," for a 100-million-row query takes 45 seconds to plan and begin streaming, you will receive a ",[18,3299,3300],{},"QueryCanceledError"," regardless of ",[18,3303,20],{},". To handle long streams safely: set ",[18,3306,3282],{}," to ",[18,3309,2419],{}," for the connection used for streaming (via ",[18,3312,3313],{},"connect_args","), use an indexed ",[18,3316,3317],{},"ORDER BY"," so PostgreSQL can begin streaming without a full table scan, and implement application-level checkpointing (storing the last processed primary key) so a failed stream can resume rather than restart from row zero.",[29,3320,3322],{"id":3321},"related","Related",[3324,3325,3326,3332,3337,3342,3347],"ul",{},[3327,3328,3329,3331],"li",{},[23,3330,26],{"href":25}," — the parent topic covering the full spectrum of SQLAlchemy 2.0 query and bulk-data techniques, including CTEs, window functions, and connection pool tuning.",[3327,3333,3334,3336],{},[23,3335,2384],{"href":2383}," — a step-by-step walkthrough benchmarking batch sizes and memory profiles across asyncpg and psycopg3 for production async streaming.",[3327,3338,3339,3341],{},[23,3340,2582],{"href":2581}," — covers cursor-based keyset pagination as a stateless complement to server-side cursor streaming, with comparisons of trade-offs by use case.",[3327,3343,3344,3346],{},[23,3345,2899],{"href":2898}," — details the write side of large data pipelines: Core-level bulk inserts, upsert conflict resolution, and chunked transaction strategies.",[3327,3348,3349,3351],{},[23,3350,2325],{"href":2324}," — explains when to use selectinload vs joinedload vs subqueryload, directly relevant to avoiding the joinedload\u002Fyield_per incompatibility.",[3353,3354,3355],"style",{},"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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":84,"searchDepth":108,"depth":108,"links":3357},[3358,3359,3360,3361,3369,3373,3374,3375],{"id":31,"depth":108,"text":32},{"id":541,"depth":108,"text":542},{"id":1251,"depth":108,"text":1252},{"id":1449,"depth":108,"text":1450,"children":3362},[3363,3364,3365,3366,3367,3368],{"id":1454,"depth":121,"text":1455},{"id":1666,"depth":121,"text":1667},{"id":1834,"depth":121,"text":1835},{"id":2336,"depth":121,"text":2337},{"id":2388,"depth":121,"text":2389},{"id":2574,"depth":121,"text":2575},{"id":2586,"depth":108,"text":2587,"children":3370},[3371,3372],{"id":2590,"depth":121,"text":2591},{"id":2907,"depth":121,"text":2908},{"id":3025,"depth":108,"text":3026},{"id":3166,"depth":108,"text":3167},{"id":3321,"depth":108,"text":3322},"SQLAlchemy's yield_per keeps memory flat when processing millions of rows — part of the broader Advanced Query Patterns and Bulk Data Operations toolkit.","md",{"date":3379},"2026-06-18","\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per",{"title":5,"description":3376},"advanced-query-patterns-and-bulk-data-operations\u002Fstreaming-large-result-sets-with-yield-per\u002Findex","Ty_qLZem28hCfSCZpVge5QJ9GSjtDkQb-gJSHAB5wxg",1781810028980]