[{"data":1,"prerenderedAt":1864},["ShallowReactive",2],{"page-\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002Fadding-a-not-null-column-without-locking-in-postgres\u002F":3},{"id":4,"title":5,"body":6,"description":1856,"extension":1857,"meta":1858,"navigation":102,"path":1860,"seo":1861,"stem":1862,"__hash__":1863},"content\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002Fadding-a-not-null-column-without-locking-in-postgres\u002Findex.md","Adding a NOT NULL Column Without Locking in Postgres",{"type":7,"value":8,"toc":1842},"minimark",[9,13,32,37,43,48,205,209,790,794,805,815,1141,1152,1163,1167,1302,1306,1310,1327,1539,1554,1558,1561,1613,1696,1702,1706,1711,1714,1727,1763,1768,1784,1789,1805,1809,1838],[10,11,5],"h1",{"id":12},"adding-a-not-null-column-without-locking-in-postgres",[14,15,16,17,21,22,25,26,31],"p",{},"Adding a NOT NULL column to a large Postgres table without downtime requires a multi-step migration — add the column as nullable first, backfill in batches, then enforce the constraint — rather than a single ",[18,19,20],"code",{},"ALTER TABLE"," that holds an ",[18,23,24],{},"ACCESS EXCLUSIVE"," lock for the duration; this pattern is covered in depth as part of ",[27,28,30],"a",{"href":29},"\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002F","Zero-Downtime Schema Migration Strategies",".",[33,34,36],"h2",{"id":35},"quick-answer","Quick Answer",[14,38,39,40,42],{},"A naive single-step migration rewrites the entire table under an ",[18,41,24],{}," lock, blocking all reads and writes for minutes on a large table. The correct approach splits the operation into discrete steps, each taking only a brief or weaker lock.",[44,45,47],"h3",{"id":46},"before-the-wrong-approach-blocks-reads-and-writes","Before — the wrong approach (blocks reads and writes)",[49,50,55],"pre",{"className":51,"code":52,"language":53,"meta":54,"style":54},"language-python shiki shiki-themes github-light github-dark","# migrations\u002Fversions\u002Fxxxx_add_status_wrong.py\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade() -> None:\n    # DANGER: Takes ACCESS EXCLUSIVE lock for the full table rewrite.\n    # On a 50M-row table this can run for 30+ minutes.\n    op.add_column(\n        \"orders\",\n        sa.Column(\"status\", sa.String(50), nullable=False, server_default=\"pending\"),\n    )\n","python","",[18,56,57,66,83,97,104,109,129,135,141,147,157,199],{"__ignoreMap":54},[58,59,62],"span",{"class":60,"line":61},"line",1,[58,63,65],{"class":64},"sJ8bj","# migrations\u002Fversions\u002Fxxxx_add_status_wrong.py\n",[58,67,69,73,77,80],{"class":60,"line":68},2,[58,70,72],{"class":71},"szBVR","from",[58,74,76],{"class":75},"sVt8B"," alembic ",[58,78,79],{"class":71},"import",[58,81,82],{"class":75}," op\n",[58,84,86,88,91,94],{"class":60,"line":85},3,[58,87,79],{"class":71},[58,89,90],{"class":75}," sqlalchemy ",[58,92,93],{"class":71},"as",[58,95,96],{"class":75}," sa\n",[58,98,100],{"class":60,"line":99},4,[58,101,103],{"emptyLinePlaceholder":102},true,"\n",[58,105,107],{"class":60,"line":106},5,[58,108,103],{"emptyLinePlaceholder":102},[58,110,112,115,119,122,126],{"class":60,"line":111},6,[58,113,114],{"class":71},"def",[58,116,118],{"class":117},"sScJk"," upgrade",[58,120,121],{"class":75},"() -> ",[58,123,125],{"class":124},"sj4cs","None",[58,127,128],{"class":75},":\n",[58,130,132],{"class":60,"line":131},7,[58,133,134],{"class":64},"    # DANGER: Takes ACCESS EXCLUSIVE lock for the full table rewrite.\n",[58,136,138],{"class":60,"line":137},8,[58,139,140],{"class":64},"    # On a 50M-row table this can run for 30+ minutes.\n",[58,142,144],{"class":60,"line":143},9,[58,145,146],{"class":75},"    op.add_column(\n",[58,148,150,154],{"class":60,"line":149},10,[58,151,153],{"class":152},"sZZnC","        \"orders\"",[58,155,156],{"class":75},",\n",[58,158,160,163,166,169,172,175,179,182,185,188,191,193,196],{"class":60,"line":159},11,[58,161,162],{"class":75},"        sa.Column(",[58,164,165],{"class":152},"\"status\"",[58,167,168],{"class":75},", sa.String(",[58,170,171],{"class":124},"50",[58,173,174],{"class":75},"), ",[58,176,178],{"class":177},"s4XuR","nullable",[58,180,181],{"class":71},"=",[58,183,184],{"class":124},"False",[58,186,187],{"class":75},", ",[58,189,190],{"class":177},"server_default",[58,192,181],{"class":71},[58,194,195],{"class":152},"\"pending\"",[58,197,198],{"class":75},"),\n",[58,200,202],{"class":60,"line":201},12,[58,203,204],{"class":75},"    )\n",[44,206,208],{"id":207},"after-the-correct-multi-step-recipe","After — the correct multi-step recipe",[49,210,212],{"className":51,"code":211,"language":53,"meta":54,"style":54},"# migrations\u002Fversions\u002Fxxxx_add_status_safe.py\nfrom alembic import op\nimport sqlalchemy as sa\nfrom sqlalchemy import text\n\n\ndef upgrade() -> None:\n    conn = op.get_bind()\n\n    # Step 0: Set a short lock_timeout so this migration fails fast\n    # rather than queuing behind a long-running transaction.\n    conn.execute(text(\"SET lock_timeout = '2s'\"))\n    conn.execute(text(\"SET statement_timeout = '0'\"))  # No cap on batch UPDATEs\n\n    # Step 1: Add the column as nullable — instant, no table rewrite.\n    op.add_column(\n        \"orders\",\n        sa.Column(\"status\", sa.String(50), nullable=True),\n    )\n\n    # Step 2: Backfill in batches of 10,000 rows outside a long transaction.\n    # Each batch is its own short transaction — avoids long-held row locks.\n    batch_size = 10_000\n    while True:\n        result = conn.execute(\n            text(\n                \"UPDATE orders SET status = 'pending' \"\n                \"WHERE id IN (\"\n                \"  SELECT id FROM orders WHERE status IS NULL LIMIT :batch\"\n                \")\"\n            ),\n            {\"batch\": batch_size},\n        )\n        conn.commit()  # Commit each batch immediately\n        if result.rowcount == 0:\n            break  # No more NULL rows\n\n    # Step 3a (PG 11+, constant default): If a constant server_default is\n    # acceptable, this single statement is near-instant on PG 11+ because\n    # Postgres stores the default in pg_attribute — no table rewrite occurs.\n    # Use this path instead of Steps 3b\u002F3c when the default is a fixed literal.\n    #\n    # op.alter_column(\"orders\", \"status\", server_default=\"pending\", nullable=False)\n    #\n    # Step 3b: For PG 12+ or when you need the strongest guarantee, use the\n    # CHECK NOT VALID + VALIDATE CONSTRAINT trick (see Advanced section below).\n    conn.execute(\n        text(\n            \"ALTER TABLE orders \"\n            \"ADD CONSTRAINT orders_status_not_null \"\n            \"CHECK (status IS NOT NULL) NOT VALID\"\n        )\n    )\n    conn.commit()\n\n    # Step 3c: VALIDATE takes only ShareUpdateExclusiveLock — reads and writes\n    # continue unblocked during the full-table scan.\n    conn.execute(\n        text(\"ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null\")\n    )\n    conn.commit()\n\n    # Step 3d: On PG 12+ this is near-instant because Postgres finds the\n    # existing CHECK and skips re-scanning the table.\n    conn.execute(\n        text(\"ALTER TABLE orders ALTER COLUMN status SET NOT NULL\")\n    )\n    conn.commit()\n\n    # Step 4: Drop the now-redundant CHECK constraint and clean up the default.\n    conn.execute(\n        text(\"ALTER TABLE orders DROP CONSTRAINT orders_status_not_null\")\n    )\n    op.alter_column(\"orders\", \"status\", server_default=None)\n\n\ndef downgrade() -> None:\n    op.drop_column(\"orders\", \"status\")\n",[18,213,214,219,229,239,250,254,258,270,280,284,289,294,305,319,324,330,335,342,364,369,374,380,386,397,408,419,425,431,437,443,449,455,467,473,482,499,508,513,519,525,531,537,543,549,554,560,566,572,578,584,590,596,601,606,612,617,623,629,634,646,651,656,661,667,673,678,688,693,698,703,709,714,724,729,752,757,762,776],{"__ignoreMap":54},[58,215,216],{"class":60,"line":61},[58,217,218],{"class":64},"# migrations\u002Fversions\u002Fxxxx_add_status_safe.py\n",[58,220,221,223,225,227],{"class":60,"line":68},[58,222,72],{"class":71},[58,224,76],{"class":75},[58,226,79],{"class":71},[58,228,82],{"class":75},[58,230,231,233,235,237],{"class":60,"line":85},[58,232,79],{"class":71},[58,234,90],{"class":75},[58,236,93],{"class":71},[58,238,96],{"class":75},[58,240,241,243,245,247],{"class":60,"line":99},[58,242,72],{"class":71},[58,244,90],{"class":75},[58,246,79],{"class":71},[58,248,249],{"class":75}," text\n",[58,251,252],{"class":60,"line":106},[58,253,103],{"emptyLinePlaceholder":102},[58,255,256],{"class":60,"line":111},[58,257,103],{"emptyLinePlaceholder":102},[58,259,260,262,264,266,268],{"class":60,"line":131},[58,261,114],{"class":71},[58,263,118],{"class":117},[58,265,121],{"class":75},[58,267,125],{"class":124},[58,269,128],{"class":75},[58,271,272,275,277],{"class":60,"line":137},[58,273,274],{"class":75},"    conn ",[58,276,181],{"class":71},[58,278,279],{"class":75}," op.get_bind()\n",[58,281,282],{"class":60,"line":143},[58,283,103],{"emptyLinePlaceholder":102},[58,285,286],{"class":60,"line":149},[58,287,288],{"class":64},"    # Step 0: Set a short lock_timeout so this migration fails fast\n",[58,290,291],{"class":60,"line":159},[58,292,293],{"class":64},"    # rather than queuing behind a long-running transaction.\n",[58,295,296,299,302],{"class":60,"line":201},[58,297,298],{"class":75},"    conn.execute(text(",[58,300,301],{"class":152},"\"SET lock_timeout = '2s'\"",[58,303,304],{"class":75},"))\n",[58,306,308,310,313,316],{"class":60,"line":307},13,[58,309,298],{"class":75},[58,311,312],{"class":152},"\"SET statement_timeout = '0'\"",[58,314,315],{"class":75},"))  ",[58,317,318],{"class":64},"# No cap on batch UPDATEs\n",[58,320,322],{"class":60,"line":321},14,[58,323,103],{"emptyLinePlaceholder":102},[58,325,327],{"class":60,"line":326},15,[58,328,329],{"class":64},"    # Step 1: Add the column as nullable — instant, no table rewrite.\n",[58,331,333],{"class":60,"line":332},16,[58,334,146],{"class":75},[58,336,338,340],{"class":60,"line":337},17,[58,339,153],{"class":152},[58,341,156],{"class":75},[58,343,345,347,349,351,353,355,357,359,362],{"class":60,"line":344},18,[58,346,162],{"class":75},[58,348,165],{"class":152},[58,350,168],{"class":75},[58,352,171],{"class":124},[58,354,174],{"class":75},[58,356,178],{"class":177},[58,358,181],{"class":71},[58,360,361],{"class":124},"True",[58,363,198],{"class":75},[58,365,367],{"class":60,"line":366},19,[58,368,204],{"class":75},[58,370,372],{"class":60,"line":371},20,[58,373,103],{"emptyLinePlaceholder":102},[58,375,377],{"class":60,"line":376},21,[58,378,379],{"class":64},"    # Step 2: Backfill in batches of 10,000 rows outside a long transaction.\n",[58,381,383],{"class":60,"line":382},22,[58,384,385],{"class":64},"    # Each batch is its own short transaction — avoids long-held row locks.\n",[58,387,389,392,394],{"class":60,"line":388},23,[58,390,391],{"class":75},"    batch_size ",[58,393,181],{"class":71},[58,395,396],{"class":124}," 10_000\n",[58,398,400,403,406],{"class":60,"line":399},24,[58,401,402],{"class":71},"    while",[58,404,405],{"class":124}," True",[58,407,128],{"class":75},[58,409,411,414,416],{"class":60,"line":410},25,[58,412,413],{"class":75},"        result ",[58,415,181],{"class":71},[58,417,418],{"class":75}," conn.execute(\n",[58,420,422],{"class":60,"line":421},26,[58,423,424],{"class":75},"            text(\n",[58,426,428],{"class":60,"line":427},27,[58,429,430],{"class":152},"                \"UPDATE orders SET status = 'pending' \"\n",[58,432,434],{"class":60,"line":433},28,[58,435,436],{"class":152},"                \"WHERE id IN (\"\n",[58,438,440],{"class":60,"line":439},29,[58,441,442],{"class":152},"                \"  SELECT id FROM orders WHERE status IS NULL LIMIT :batch\"\n",[58,444,446],{"class":60,"line":445},30,[58,447,448],{"class":152},"                \")\"\n",[58,450,452],{"class":60,"line":451},31,[58,453,454],{"class":75},"            ),\n",[58,456,458,461,464],{"class":60,"line":457},32,[58,459,460],{"class":75},"            {",[58,462,463],{"class":152},"\"batch\"",[58,465,466],{"class":75},": batch_size},\n",[58,468,470],{"class":60,"line":469},33,[58,471,472],{"class":75},"        )\n",[58,474,476,479],{"class":60,"line":475},34,[58,477,478],{"class":75},"        conn.commit()  ",[58,480,481],{"class":64},"# Commit each batch immediately\n",[58,483,485,488,491,494,497],{"class":60,"line":484},35,[58,486,487],{"class":71},"        if",[58,489,490],{"class":75}," result.rowcount ",[58,492,493],{"class":71},"==",[58,495,496],{"class":124}," 0",[58,498,128],{"class":75},[58,500,502,505],{"class":60,"line":501},36,[58,503,504],{"class":71},"            break",[58,506,507],{"class":64},"  # No more NULL rows\n",[58,509,511],{"class":60,"line":510},37,[58,512,103],{"emptyLinePlaceholder":102},[58,514,516],{"class":60,"line":515},38,[58,517,518],{"class":64},"    # Step 3a (PG 11+, constant default): If a constant server_default is\n",[58,520,522],{"class":60,"line":521},39,[58,523,524],{"class":64},"    # acceptable, this single statement is near-instant on PG 11+ because\n",[58,526,528],{"class":60,"line":527},40,[58,529,530],{"class":64},"    # Postgres stores the default in pg_attribute — no table rewrite occurs.\n",[58,532,534],{"class":60,"line":533},41,[58,535,536],{"class":64},"    # Use this path instead of Steps 3b\u002F3c when the default is a fixed literal.\n",[58,538,540],{"class":60,"line":539},42,[58,541,542],{"class":64},"    #\n",[58,544,546],{"class":60,"line":545},43,[58,547,548],{"class":64},"    # op.alter_column(\"orders\", \"status\", server_default=\"pending\", nullable=False)\n",[58,550,552],{"class":60,"line":551},44,[58,553,542],{"class":64},[58,555,557],{"class":60,"line":556},45,[58,558,559],{"class":64},"    # Step 3b: For PG 12+ or when you need the strongest guarantee, use the\n",[58,561,563],{"class":60,"line":562},46,[58,564,565],{"class":64},"    # CHECK NOT VALID + VALIDATE CONSTRAINT trick (see Advanced section below).\n",[58,567,569],{"class":60,"line":568},47,[58,570,571],{"class":75},"    conn.execute(\n",[58,573,575],{"class":60,"line":574},48,[58,576,577],{"class":75},"        text(\n",[58,579,581],{"class":60,"line":580},49,[58,582,583],{"class":152},"            \"ALTER TABLE orders \"\n",[58,585,587],{"class":60,"line":586},50,[58,588,589],{"class":152},"            \"ADD CONSTRAINT orders_status_not_null \"\n",[58,591,593],{"class":60,"line":592},51,[58,594,595],{"class":152},"            \"CHECK (status IS NOT NULL) NOT VALID\"\n",[58,597,599],{"class":60,"line":598},52,[58,600,472],{"class":75},[58,602,604],{"class":60,"line":603},53,[58,605,204],{"class":75},[58,607,609],{"class":60,"line":608},54,[58,610,611],{"class":75},"    conn.commit()\n",[58,613,615],{"class":60,"line":614},55,[58,616,103],{"emptyLinePlaceholder":102},[58,618,620],{"class":60,"line":619},56,[58,621,622],{"class":64},"    # Step 3c: VALIDATE takes only ShareUpdateExclusiveLock — reads and writes\n",[58,624,626],{"class":60,"line":625},57,[58,627,628],{"class":64},"    # continue unblocked during the full-table scan.\n",[58,630,632],{"class":60,"line":631},58,[58,633,571],{"class":75},[58,635,637,640,643],{"class":60,"line":636},59,[58,638,639],{"class":75},"        text(",[58,641,642],{"class":152},"\"ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null\"",[58,644,645],{"class":75},")\n",[58,647,649],{"class":60,"line":648},60,[58,650,204],{"class":75},[58,652,654],{"class":60,"line":653},61,[58,655,611],{"class":75},[58,657,659],{"class":60,"line":658},62,[58,660,103],{"emptyLinePlaceholder":102},[58,662,664],{"class":60,"line":663},63,[58,665,666],{"class":64},"    # Step 3d: On PG 12+ this is near-instant because Postgres finds the\n",[58,668,670],{"class":60,"line":669},64,[58,671,672],{"class":64},"    # existing CHECK and skips re-scanning the table.\n",[58,674,676],{"class":60,"line":675},65,[58,677,571],{"class":75},[58,679,681,683,686],{"class":60,"line":680},66,[58,682,639],{"class":75},[58,684,685],{"class":152},"\"ALTER TABLE orders ALTER COLUMN status SET NOT NULL\"",[58,687,645],{"class":75},[58,689,691],{"class":60,"line":690},67,[58,692,204],{"class":75},[58,694,696],{"class":60,"line":695},68,[58,697,611],{"class":75},[58,699,701],{"class":60,"line":700},69,[58,702,103],{"emptyLinePlaceholder":102},[58,704,706],{"class":60,"line":705},70,[58,707,708],{"class":64},"    # Step 4: Drop the now-redundant CHECK constraint and clean up the default.\n",[58,710,712],{"class":60,"line":711},71,[58,713,571],{"class":75},[58,715,717,719,722],{"class":60,"line":716},72,[58,718,639],{"class":75},[58,720,721],{"class":152},"\"ALTER TABLE orders DROP CONSTRAINT orders_status_not_null\"",[58,723,645],{"class":75},[58,725,727],{"class":60,"line":726},73,[58,728,204],{"class":75},[58,730,732,735,738,740,742,744,746,748,750],{"class":60,"line":731},74,[58,733,734],{"class":75},"    op.alter_column(",[58,736,737],{"class":152},"\"orders\"",[58,739,187],{"class":75},[58,741,165],{"class":152},[58,743,187],{"class":75},[58,745,190],{"class":177},[58,747,181],{"class":71},[58,749,125],{"class":124},[58,751,645],{"class":75},[58,753,755],{"class":60,"line":754},75,[58,756,103],{"emptyLinePlaceholder":102},[58,758,760],{"class":60,"line":759},76,[58,761,103],{"emptyLinePlaceholder":102},[58,763,765,767,770,772,774],{"class":60,"line":764},77,[58,766,114],{"class":71},[58,768,769],{"class":117}," downgrade",[58,771,121],{"class":75},[58,773,125],{"class":124},[58,775,128],{"class":75},[58,777,779,782,784,786,788],{"class":60,"line":778},78,[58,780,781],{"class":75},"    op.drop_column(",[58,783,737],{"class":152},[58,785,187],{"class":75},[58,787,165],{"class":152},[58,789,645],{"class":75},[33,791,793],{"id":792},"execution-context-async-workflow-integration","Execution Context & Async Workflow Integration",[14,795,796,797,800,801,804],{},"Alembic migrations typically run in a synchronous context invoked from the command line or a deployment script, but in an async SQLAlchemy project you may be calling ",[18,798,799],{},"run_sync"," from an async engine. The multi-step recipe above is event-loop safe because each ",[18,802,803],{},"conn.commit()"," releases locks and returns the connection to a clean state between batch iterations. There is no long-held transaction that could starve the async connection pool.",[14,806,807,808,811,812,814],{},"When Alembic is wired to an ",[18,809,810],{},"AsyncEngine"," via ",[18,813,799],{},", the pattern requires wrapping the synchronous migration logic correctly:",[49,816,818],{"className":51,"code":817,"language":53,"meta":54,"style":54},"# env.py — async Alembic environment configuration\nimport asyncio\nfrom logging.config import fileConfig\n\nfrom alembic import context\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nfrom myapp.db import Base\n\nconfig = context.config\nfileConfig(config.config_file_name)\ntarget_metadata = Base.metadata\n\n\ndef run_migrations_online() -> None:\n    async def _run() -> None:\n        connectable = create_async_engine(\n            config.get_main_option(\"sqlalchemy.url\"),\n            # Keep pool_size small for migration runners — they are sequential.\n            pool_size=2,\n            max_overflow=0,\n        )\n        async with connectable.connect() as connection:\n            await connection.run_sync(do_run_migrations)\n        await connectable.dispose()\n\n    asyncio.run(_run())\n\n\ndef do_run_migrations(connection: sa.engine.Connection) -> None:\n    context.configure(\n        connection=connection,\n        target_metadata=target_metadata,\n        # Required so individual op.* calls use the same connection.\n        compare_type=True,\n    )\n    with context.begin_transaction():\n        context.run_migrations()\n\n\nrun_migrations_online()\n",[18,819,820,825,832,844,848,859,871,875,887,891,901,906,916,920,924,937,954,964,974,979,991,1003,1007,1023,1031,1039,1043,1048,1052,1056,1070,1075,1085,1095,1100,1111,1115,1123,1128,1132,1136],{"__ignoreMap":54},[58,821,822],{"class":60,"line":61},[58,823,824],{"class":64},"# env.py — async Alembic environment configuration\n",[58,826,827,829],{"class":60,"line":68},[58,828,79],{"class":71},[58,830,831],{"class":75}," asyncio\n",[58,833,834,836,839,841],{"class":60,"line":85},[58,835,72],{"class":71},[58,837,838],{"class":75}," logging.config ",[58,840,79],{"class":71},[58,842,843],{"class":75}," fileConfig\n",[58,845,846],{"class":60,"line":99},[58,847,103],{"emptyLinePlaceholder":102},[58,849,850,852,854,856],{"class":60,"line":106},[58,851,72],{"class":71},[58,853,76],{"class":75},[58,855,79],{"class":71},[58,857,858],{"class":75}," context\n",[58,860,861,863,866,868],{"class":60,"line":111},[58,862,72],{"class":71},[58,864,865],{"class":75}," sqlalchemy.ext.asyncio ",[58,867,79],{"class":71},[58,869,870],{"class":75}," create_async_engine\n",[58,872,873],{"class":60,"line":131},[58,874,103],{"emptyLinePlaceholder":102},[58,876,877,879,882,884],{"class":60,"line":137},[58,878,72],{"class":71},[58,880,881],{"class":75}," myapp.db ",[58,883,79],{"class":71},[58,885,886],{"class":75}," Base\n",[58,888,889],{"class":60,"line":143},[58,890,103],{"emptyLinePlaceholder":102},[58,892,893,896,898],{"class":60,"line":149},[58,894,895],{"class":75},"config ",[58,897,181],{"class":71},[58,899,900],{"class":75}," context.config\n",[58,902,903],{"class":60,"line":159},[58,904,905],{"class":75},"fileConfig(config.config_file_name)\n",[58,907,908,911,913],{"class":60,"line":201},[58,909,910],{"class":75},"target_metadata ",[58,912,181],{"class":71},[58,914,915],{"class":75}," Base.metadata\n",[58,917,918],{"class":60,"line":307},[58,919,103],{"emptyLinePlaceholder":102},[58,921,922],{"class":60,"line":321},[58,923,103],{"emptyLinePlaceholder":102},[58,925,926,928,931,933,935],{"class":60,"line":326},[58,927,114],{"class":71},[58,929,930],{"class":117}," run_migrations_online",[58,932,121],{"class":75},[58,934,125],{"class":124},[58,936,128],{"class":75},[58,938,939,942,945,948,950,952],{"class":60,"line":332},[58,940,941],{"class":71},"    async",[58,943,944],{"class":71}," def",[58,946,947],{"class":117}," _run",[58,949,121],{"class":75},[58,951,125],{"class":124},[58,953,128],{"class":75},[58,955,956,959,961],{"class":60,"line":337},[58,957,958],{"class":75},"        connectable ",[58,960,181],{"class":71},[58,962,963],{"class":75}," create_async_engine(\n",[58,965,966,969,972],{"class":60,"line":344},[58,967,968],{"class":75},"            config.get_main_option(",[58,970,971],{"class":152},"\"sqlalchemy.url\"",[58,973,198],{"class":75},[58,975,976],{"class":60,"line":366},[58,977,978],{"class":64},"            # Keep pool_size small for migration runners — they are sequential.\n",[58,980,981,984,986,989],{"class":60,"line":371},[58,982,983],{"class":177},"            pool_size",[58,985,181],{"class":71},[58,987,988],{"class":124},"2",[58,990,156],{"class":75},[58,992,993,996,998,1001],{"class":60,"line":376},[58,994,995],{"class":177},"            max_overflow",[58,997,181],{"class":71},[58,999,1000],{"class":124},"0",[58,1002,156],{"class":75},[58,1004,1005],{"class":60,"line":382},[58,1006,472],{"class":75},[58,1008,1009,1012,1015,1018,1020],{"class":60,"line":388},[58,1010,1011],{"class":71},"        async",[58,1013,1014],{"class":71}," with",[58,1016,1017],{"class":75}," connectable.connect() ",[58,1019,93],{"class":71},[58,1021,1022],{"class":75}," connection:\n",[58,1024,1025,1028],{"class":60,"line":399},[58,1026,1027],{"class":71},"            await",[58,1029,1030],{"class":75}," connection.run_sync(do_run_migrations)\n",[58,1032,1033,1036],{"class":60,"line":410},[58,1034,1035],{"class":71},"        await",[58,1037,1038],{"class":75}," connectable.dispose()\n",[58,1040,1041],{"class":60,"line":421},[58,1042,103],{"emptyLinePlaceholder":102},[58,1044,1045],{"class":60,"line":427},[58,1046,1047],{"class":75},"    asyncio.run(_run())\n",[58,1049,1050],{"class":60,"line":433},[58,1051,103],{"emptyLinePlaceholder":102},[58,1053,1054],{"class":60,"line":439},[58,1055,103],{"emptyLinePlaceholder":102},[58,1057,1058,1060,1063,1066,1068],{"class":60,"line":445},[58,1059,114],{"class":71},[58,1061,1062],{"class":117}," do_run_migrations",[58,1064,1065],{"class":75},"(connection: sa.engine.Connection) -> ",[58,1067,125],{"class":124},[58,1069,128],{"class":75},[58,1071,1072],{"class":60,"line":451},[58,1073,1074],{"class":75},"    context.configure(\n",[58,1076,1077,1080,1082],{"class":60,"line":457},[58,1078,1079],{"class":177},"        connection",[58,1081,181],{"class":71},[58,1083,1084],{"class":75},"connection,\n",[58,1086,1087,1090,1092],{"class":60,"line":469},[58,1088,1089],{"class":177},"        target_metadata",[58,1091,181],{"class":71},[58,1093,1094],{"class":75},"target_metadata,\n",[58,1096,1097],{"class":60,"line":475},[58,1098,1099],{"class":64},"        # Required so individual op.* calls use the same connection.\n",[58,1101,1102,1105,1107,1109],{"class":60,"line":484},[58,1103,1104],{"class":177},"        compare_type",[58,1106,181],{"class":71},[58,1108,361],{"class":124},[58,1110,156],{"class":75},[58,1112,1113],{"class":60,"line":501},[58,1114,204],{"class":75},[58,1116,1117,1120],{"class":60,"line":510},[58,1118,1119],{"class":71},"    with",[58,1121,1122],{"class":75}," context.begin_transaction():\n",[58,1124,1125],{"class":60,"line":515},[58,1126,1127],{"class":75},"        context.run_migrations()\n",[58,1129,1130],{"class":60,"line":521},[58,1131,103],{"emptyLinePlaceholder":102},[58,1133,1134],{"class":60,"line":527},[58,1135,103],{"emptyLinePlaceholder":102},[58,1137,1138],{"class":60,"line":533},[58,1139,1140],{"class":75},"run_migrations_online()\n",[14,1142,1143,1144,1147,1148,1151],{},"The batched ",[18,1145,1146],{},"UPDATE"," loop inside ",[18,1149,1150],{},"upgrade()"," commits each batch independently. This is intentional: in an async web server running concurrently, long-held row locks from a single large UPDATE would queue async tasks waiting for those rows, causing cascading timeouts in the connection pool. Batching keeps individual lock windows under a few milliseconds.",[14,1153,1154,1155,1158,1159,1162],{},"The ",[18,1156,1157],{},"lock_timeout = '2s'"," set at migration start is a circuit breaker. If a long-running OLAP query or a vacuum is holding a conflicting lock, the migration fails immediately with a ",[18,1160,1161],{},"LockNotAvailable"," error rather than queuing silently and blocking the entire connection pool for minutes. Retry with exponential back-off from your deployment tooling.",[33,1164,1166],{"id":1165},"resolving-warnings-errors-common-mistakes","Resolving Warnings, Errors & Common Mistakes",[1168,1169,1170,1186],"table",{},[1171,1172,1173],"thead",{},[1174,1175,1176,1180,1183],"tr",{},[1177,1178,1179],"th",{},"Exact error \u002F warning string",[1177,1181,1182],{},"Root cause",[1177,1184,1185],{},"Production fix",[1187,1188,1189,1210,1230,1254,1271],"tbody",{},[1174,1190,1191,1197,1203],{},[1192,1193,1194],"td",{},[18,1195,1196],{},"ERROR: column \"status\" of relation \"orders\" contains null values",[1192,1198,1199,1202],{},[18,1200,1201],{},"ALTER COLUMN ... SET NOT NULL"," executed before all rows were backfilled.",[1192,1204,1205,1206,1209],{},"Verify backfill completed: ",[18,1207,1208],{},"SELECT COUNT(*) FROM orders WHERE status IS NULL",". Re-run the batch loop until it returns 0.",[1174,1211,1212,1217,1220],{},[1192,1213,1214],{},[18,1215,1216],{},"ERROR: canceling statement due to lock timeout",[1192,1218,1219],{},"Another session holds a conflicting lock (e.g., a long-running SELECT, autovacuum, or another DDL).",[1192,1221,1222,1223,1226,1227,31],{},"Retry the migration during low-traffic. Increase ",[18,1224,1225],{},"lock_timeout"," only as a last resort; investigate what holds the lock with ",[18,1228,1229],{},"SELECT * FROM pg_stat_activity WHERE wait_event_type = 'Lock'",[1174,1231,1232,1237,1248],{},[1192,1233,1234],{},[18,1235,1236],{},"ERROR: cannot use a subquery in a check constraint",[1192,1238,1239,1240,1243,1244,1247],{},"Attempted ",[18,1241,1242],{},"CHECK (status IN (SELECT ...))"," or similar correlated subquery inside the ",[18,1245,1246],{},"ADD CONSTRAINT"," statement.",[1192,1249,1250,1251,31],{},"Check constraints must use only simple expressions referencing the current row. Replace subqueries with explicit value lists: ",[18,1252,1253],{},"CHECK (status IN ('pending', 'active', 'closed'))",[1174,1255,1256,1261,1264],{},[1192,1257,1258],{},[18,1259,1260],{},"ERROR: deadlock detected",[1192,1262,1263],{},"Batch UPDATE transactions are large enough to intersect with each other (parallel migration runs) or with OLTP writes touching the same rows.",[1192,1265,1266,1267,1270],{},"Reduce batch size (try 1,000 rows). Add an index on the column being backfilled (",[18,1268,1269],{},"CREATE INDEX CONCURRENTLY orders_status_null_idx ON orders (id) WHERE status IS NULL",") so each batch scan is an index seek, not a sequential scan. Ensure only one migration worker runs at a time.",[1174,1272,1273,1278,1287],{},[1192,1274,1275],{},[18,1276,1277],{},"WARNING: there is already a transaction in progress",[1192,1279,1280,1282,1283,1286],{},[18,1281,803],{}," called inside an Alembic ",[18,1284,1285],{},"begin_transaction()"," context that auto-manages the transaction.",[1192,1288,1289,1290,1293,1294,1297,1298,1301],{},"Use ",[18,1291,1292],{},"op.get_bind()"," and call ",[18,1295,1296],{},"conn.execute(text(\"COMMIT\"))"," directly, or restructure the migration to use ",[18,1299,1300],{},"connection.execution_options(no_begin=True)"," for the batch loop.",[33,1303,1305],{"id":1304},"advanced-not-null-constraint-optimization","Advanced NOT NULL Constraint Optimization",[44,1307,1309],{"id":1308},"postgresql-11-fast-default-path","PostgreSQL 11+ Fast Default Path",[14,1311,1312,1313,1315,1316,1319,1320,1322,1323,1326],{},"On PostgreSQL 11 and later, when you add a column with a constant ",[18,1314,190],{}," and mark it ",[18,1317,1318],{},"NOT NULL"," in a single ",[18,1321,20],{},", Postgres stores the default value in ",[18,1324,1325],{},"pg_attribute"," (the system catalog) rather than physically writing it into every row. The table itself is not rewritten. The operation is essentially instantaneous regardless of table size.",[49,1328,1330],{"className":51,"code":1329,"language":53,"meta":54,"style":54},"# migrations\u002Fversions\u002Fxxxx_add_priority_pg11.py\nfrom alembic import op\nimport sqlalchemy as sa\nfrom sqlalchemy import text\n\n\ndef upgrade() -> None:\n    conn = op.get_bind()\n    conn.execute(text(\"SET lock_timeout = '2s'\"))\n\n    # On PG 11+: near-instant even on a 100M-row table.\n    # Postgres records the default in pg_attribute; rows read the default\n    # lazily until they are next modified (VACUUM or UPDATE rewrites them).\n    op.add_column(\n        \"orders\",\n        sa.Column(\n            \"priority\",\n            sa.Integer(),\n            nullable=False,\n            server_default=\"0\",\n        ),\n    )\n    # Remove the server_default so new inserts must supply an explicit value.\n    op.alter_column(\"orders\", \"priority\", server_default=None)\n\n\ndef downgrade() -> None:\n    op.drop_column(\"orders\", \"priority\")\n",[18,1331,1332,1337,1347,1357,1367,1371,1375,1387,1395,1403,1407,1412,1417,1422,1426,1432,1437,1444,1449,1460,1472,1477,1481,1486,1507,1511,1515,1527],{"__ignoreMap":54},[58,1333,1334],{"class":60,"line":61},[58,1335,1336],{"class":64},"# migrations\u002Fversions\u002Fxxxx_add_priority_pg11.py\n",[58,1338,1339,1341,1343,1345],{"class":60,"line":68},[58,1340,72],{"class":71},[58,1342,76],{"class":75},[58,1344,79],{"class":71},[58,1346,82],{"class":75},[58,1348,1349,1351,1353,1355],{"class":60,"line":85},[58,1350,79],{"class":71},[58,1352,90],{"class":75},[58,1354,93],{"class":71},[58,1356,96],{"class":75},[58,1358,1359,1361,1363,1365],{"class":60,"line":99},[58,1360,72],{"class":71},[58,1362,90],{"class":75},[58,1364,79],{"class":71},[58,1366,249],{"class":75},[58,1368,1369],{"class":60,"line":106},[58,1370,103],{"emptyLinePlaceholder":102},[58,1372,1373],{"class":60,"line":111},[58,1374,103],{"emptyLinePlaceholder":102},[58,1376,1377,1379,1381,1383,1385],{"class":60,"line":131},[58,1378,114],{"class":71},[58,1380,118],{"class":117},[58,1382,121],{"class":75},[58,1384,125],{"class":124},[58,1386,128],{"class":75},[58,1388,1389,1391,1393],{"class":60,"line":137},[58,1390,274],{"class":75},[58,1392,181],{"class":71},[58,1394,279],{"class":75},[58,1396,1397,1399,1401],{"class":60,"line":143},[58,1398,298],{"class":75},[58,1400,301],{"class":152},[58,1402,304],{"class":75},[58,1404,1405],{"class":60,"line":149},[58,1406,103],{"emptyLinePlaceholder":102},[58,1408,1409],{"class":60,"line":159},[58,1410,1411],{"class":64},"    # On PG 11+: near-instant even on a 100M-row table.\n",[58,1413,1414],{"class":60,"line":201},[58,1415,1416],{"class":64},"    # Postgres records the default in pg_attribute; rows read the default\n",[58,1418,1419],{"class":60,"line":307},[58,1420,1421],{"class":64},"    # lazily until they are next modified (VACUUM or UPDATE rewrites them).\n",[58,1423,1424],{"class":60,"line":321},[58,1425,146],{"class":75},[58,1427,1428,1430],{"class":60,"line":326},[58,1429,153],{"class":152},[58,1431,156],{"class":75},[58,1433,1434],{"class":60,"line":332},[58,1435,1436],{"class":75},"        sa.Column(\n",[58,1438,1439,1442],{"class":60,"line":337},[58,1440,1441],{"class":152},"            \"priority\"",[58,1443,156],{"class":75},[58,1445,1446],{"class":60,"line":344},[58,1447,1448],{"class":75},"            sa.Integer(),\n",[58,1450,1451,1454,1456,1458],{"class":60,"line":366},[58,1452,1453],{"class":177},"            nullable",[58,1455,181],{"class":71},[58,1457,184],{"class":124},[58,1459,156],{"class":75},[58,1461,1462,1465,1467,1470],{"class":60,"line":371},[58,1463,1464],{"class":177},"            server_default",[58,1466,181],{"class":71},[58,1468,1469],{"class":152},"\"0\"",[58,1471,156],{"class":75},[58,1473,1474],{"class":60,"line":376},[58,1475,1476],{"class":75},"        ),\n",[58,1478,1479],{"class":60,"line":382},[58,1480,204],{"class":75},[58,1482,1483],{"class":60,"line":388},[58,1484,1485],{"class":64},"    # Remove the server_default so new inserts must supply an explicit value.\n",[58,1487,1488,1490,1492,1494,1497,1499,1501,1503,1505],{"class":60,"line":399},[58,1489,734],{"class":75},[58,1491,737],{"class":152},[58,1493,187],{"class":75},[58,1495,1496],{"class":152},"\"priority\"",[58,1498,187],{"class":75},[58,1500,190],{"class":177},[58,1502,181],{"class":71},[58,1504,125],{"class":124},[58,1506,645],{"class":75},[58,1508,1509],{"class":60,"line":410},[58,1510,103],{"emptyLinePlaceholder":102},[58,1512,1513],{"class":60,"line":421},[58,1514,103],{"emptyLinePlaceholder":102},[58,1516,1517,1519,1521,1523,1525],{"class":60,"line":427},[58,1518,114],{"class":71},[58,1520,769],{"class":117},[58,1522,121],{"class":75},[58,1524,125],{"class":124},[58,1526,128],{"class":75},[58,1528,1529,1531,1533,1535,1537],{"class":60,"line":433},[58,1530,781],{"class":75},[58,1532,737],{"class":152},[58,1534,187],{"class":75},[58,1536,1496],{"class":152},[58,1538,645],{"class":75},[14,1540,1541,1545,1546,1549,1550,1553],{},[1542,1543,1544],"strong",{},"When this applies:"," The default must be a constant literal (an integer, a string, a function call like ",[18,1547,1548],{},"now()"," counts as non-constant and triggers a rewrite on older PG versions). Verify your Postgres version with ",[18,1551,1552],{},"SELECT version()"," before relying on this. On PG 10 and earlier, the same statement rewrites every row.",[44,1555,1557],{"id":1556},"the-check-not-valid-validate-constraint-trick-in-detail","The CHECK NOT VALID + VALIDATE CONSTRAINT Trick in Detail",[14,1559,1560],{},"This is the most broadly applicable technique because it works across PG 10–17 and handles non-constant defaults:",[1562,1563,1564,1578,1601],"ol",{},[1565,1566,1567,1570,1571,1573,1574,1577],"li",{},[18,1568,1569],{},"ADD CONSTRAINT ... CHECK (...) NOT VALID"," — acquires ",[18,1572,24],{}," lock only briefly to record the constraint in the catalog. It does ",[1542,1575,1576],{},"not"," scan existing rows. New rows and updated rows are validated immediately. Existing rows remain unchecked.",[1565,1579,1580,1583,1584,1587,1588,187,1591,187,1594,1596,1597,1600],{},[18,1581,1582],{},"VALIDATE CONSTRAINT"," — scans all existing rows but takes only ",[18,1585,1586],{},"SHARE UPDATE EXCLUSIVE"," lock. This lock is compatible with concurrent ",[18,1589,1590],{},"SELECT",[18,1592,1593],{},"INSERT",[18,1595,1146],{},", and ",[18,1598,1599],{},"DELETE",". Your application continues serving traffic during the scan.",[1565,1602,1603,1605,1606,1609,1610,1612],{},[18,1604,1201],{}," — on PG 12+, Postgres inspects the catalog and finds the validated ",[18,1607,1608],{},"CHECK (status IS NOT NULL)"," constraint. It concludes a full table scan is unnecessary and the operation takes only a brief ",[18,1611,24],{}," lock to update column metadata.",[49,1614,1616],{"className":51,"code":1615,"language":53,"meta":54,"style":54},"# Demonstration of the three lock levels\n# Step 3b lock: ACCESS EXCLUSIVE (brief, catalog write only)\nconn.execute(text(\n    \"ALTER TABLE orders \"\n    \"ADD CONSTRAINT orders_status_not_null \"\n    \"CHECK (status IS NOT NULL) NOT VALID\"\n))\n\n# Step 3c lock: SHARE UPDATE EXCLUSIVE (long scan, but non-blocking for reads\u002Fwrites)\nconn.execute(text(\n    \"ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null\"\n))\n\n# Step 3d lock: ACCESS EXCLUSIVE (near-instant on PG 12+ — constraint already verified)\nconn.execute(text(\n    \"ALTER TABLE orders ALTER COLUMN status SET NOT NULL\"\n))\n",[18,1617,1618,1623,1628,1633,1638,1643,1648,1652,1656,1661,1665,1670,1674,1678,1683,1687,1692],{"__ignoreMap":54},[58,1619,1620],{"class":60,"line":61},[58,1621,1622],{"class":64},"# Demonstration of the three lock levels\n",[58,1624,1625],{"class":60,"line":68},[58,1626,1627],{"class":64},"# Step 3b lock: ACCESS EXCLUSIVE (brief, catalog write only)\n",[58,1629,1630],{"class":60,"line":85},[58,1631,1632],{"class":75},"conn.execute(text(\n",[58,1634,1635],{"class":60,"line":99},[58,1636,1637],{"class":152},"    \"ALTER TABLE orders \"\n",[58,1639,1640],{"class":60,"line":106},[58,1641,1642],{"class":152},"    \"ADD CONSTRAINT orders_status_not_null \"\n",[58,1644,1645],{"class":60,"line":111},[58,1646,1647],{"class":152},"    \"CHECK (status IS NOT NULL) NOT VALID\"\n",[58,1649,1650],{"class":60,"line":131},[58,1651,304],{"class":75},[58,1653,1654],{"class":60,"line":137},[58,1655,103],{"emptyLinePlaceholder":102},[58,1657,1658],{"class":60,"line":143},[58,1659,1660],{"class":64},"# Step 3c lock: SHARE UPDATE EXCLUSIVE (long scan, but non-blocking for reads\u002Fwrites)\n",[58,1662,1663],{"class":60,"line":149},[58,1664,1632],{"class":75},[58,1666,1667],{"class":60,"line":159},[58,1668,1669],{"class":152},"    \"ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null\"\n",[58,1671,1672],{"class":60,"line":201},[58,1673,304],{"class":75},[58,1675,1676],{"class":60,"line":307},[58,1677,103],{"emptyLinePlaceholder":102},[58,1679,1680],{"class":60,"line":321},[58,1681,1682],{"class":64},"# Step 3d lock: ACCESS EXCLUSIVE (near-instant on PG 12+ — constraint already verified)\n",[58,1684,1685],{"class":60,"line":326},[58,1686,1632],{"class":75},[58,1688,1689],{"class":60,"line":332},[58,1690,1691],{"class":152},"    \"ALTER TABLE orders ALTER COLUMN status SET NOT NULL\"\n",[58,1693,1694],{"class":60,"line":337},[58,1695,304],{"class":75},[14,1697,1698,1699,1701],{},"On PG 11 and older, step 3d still performs a full table scan even with the validated CHECK constraint, so the safe option on older versions is to leave the CHECK constraint in place instead of converting it to a ",[18,1700,1318],{}," column attribute.",[33,1703,1705],{"id":1704},"frequently-asked-questions","Frequently Asked Questions",[14,1707,1708],{},[1542,1709,1710],{},"Does the batch UPDATE need to run inside the Alembic migration, or can it be a separate script?",[14,1712,1713],{},"It can be a separate script run before the migration — this is often preferable for very large tables (hundreds of millions of rows) where the backfill takes hours. In that case, step 1 (add nullable column) is one migration deployed first, the backfill script runs separately and can be monitored, and steps 3–4 (enforce constraint) are a second migration deployed after the backfill completes. Splitting it this way avoids migration timeout issues in deployment pipelines.",[14,1715,1716],{},[1542,1717,1718,1719,1722,1723,1726],{},"Why use ",[18,1720,1721],{},"WHERE id IN (SELECT id FROM orders WHERE status IS NULL LIMIT :batch)"," rather than a plain ",[18,1724,1725],{},"UPDATE ... WHERE status IS NULL LIMIT :batch","?",[14,1728,1729,1730,1733,1734,1736,1737,1739,1740,1743,1744,1747,1748,1751,1752,1755,1756,1759,1760,31],{},"Postgres does not support ",[18,1731,1732],{},"LIMIT"," in ",[18,1735,1146],{}," statements directly. The subquery pattern is the idiomatic workaround. The inner ",[18,1738,1590],{}," uses an index on ",[18,1741,1742],{},"id"," and a filter on ",[18,1745,1746],{},"status IS NULL",", making each batch a bounded operation. Ensure there is an index that supports the ",[18,1749,1750],{},"WHERE status IS NULL"," filter — either the primary key index (if ",[18,1753,1754],{},"status"," is the only NULL column and a partial index exists) or a dedicated ",[18,1757,1758],{},"CREATE INDEX CONCURRENTLY"," on ",[18,1761,1762],{},"(id) WHERE status IS NULL",[14,1764,1765],{},[1542,1766,1767],{},"What happens to rows inserted during the backfill window?",[14,1769,1770,1771,1773,1774,1777,1778,1780,1781,1783],{},"Application code inserting rows during the backfill window should always supply a value for ",[18,1772,1754],{}," explicitly (since it has no ",[18,1775,1776],{},"DEFAULT"," at this point — step 1 added it as nullable with no default). If application code cannot be deployed simultaneously, add a ",[18,1779,190],{}," in step 1 so the database fills in the default for any insert that omits the column, then remove the ",[18,1782,190],{}," at step 4.",[14,1785,1786],{},[1542,1787,1788],{},"Can I combine steps 3b–3d into a single transaction?",[14,1790,1791,1792,1794,1795,1797,1798,1801,1802,1804],{},"No. ",[18,1793,1582],{}," must be committed before ",[18,1796,1201],{}," for PG 12+ to recognize the existing validated CHECK and skip the rescan. If both statements run in the same transaction, the planner has not yet written the validated constraint to the catalog, and ",[18,1799,1800],{},"SET NOT NULL"," will perform a full table scan under ",[18,1803,24],{}," lock — exactly what you wanted to avoid.",[33,1806,1808],{"id":1807},"related","Related",[1810,1811,1812,1817,1831],"ul",{},[1565,1813,1814,1816],{},[27,1815,30],{"href":29}," — parent section covering the full range of lock-safe migration patterns",[1565,1818,1819,1823,1824,1826,1827,1830],{},[27,1820,1822],{"href":1821},"\u002Falembic-async-migrations-and-schema-evolution\u002Fconfiguring-alembic-with-async-sqlalchemy-engines\u002F","Configuring Alembic with Async SQLAlchemy Engines"," — how to wire ",[18,1825,810],{}," into ",[18,1828,1829],{},"env.py"," so migrations run in async projects",[1565,1832,1833,1837],{},[27,1834,1836],{"href":1835},"\u002Falembic-async-migrations-and-schema-evolution\u002F","Alembic Async Migrations and Schema Evolution"," — pillar covering all migration topics for async SQLAlchemy projects",[1839,1840,1841],"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 .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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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":54,"searchDepth":68,"depth":68,"links":1843},[1844,1848,1849,1850,1854,1855],{"id":35,"depth":68,"text":36,"children":1845},[1846,1847],{"id":46,"depth":85,"text":47},{"id":207,"depth":85,"text":208},{"id":792,"depth":68,"text":793},{"id":1165,"depth":68,"text":1166},{"id":1304,"depth":68,"text":1305,"children":1851},[1852,1853],{"id":1308,"depth":85,"text":1309},{"id":1556,"depth":85,"text":1557},{"id":1704,"depth":68,"text":1705},{"id":1807,"depth":68,"text":1808},"Adding a NOT NULL column to a large Postgres table without downtime requires a multi-step migration — add the column as nullable first, backfill in batches, then enforce the constraint — rather than a single ALTER TABLE that holds an ACCESS EXCLUSIVE lock for the duration; this pattern is covered in depth as part of Zero-Downtime Schema Migration Strategies.","md",{"date":1859},"2026-06-18","\u002Falembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002Fadding-a-not-null-column-without-locking-in-postgres",{"title":5,"description":1856},"alembic-async-migrations-and-schema-evolution\u002Fzero-downtime-schema-migration-strategies\u002Fadding-a-not-null-column-without-locking-in-postgres\u002Findex","-IIpwIOZjTyLinGNqXbEUXp-Rh_sk6qXpebNri3Ln5g",1781810028982]