[{"data":1,"prerenderedAt":1167},["ShallowReactive",2],{"header-counts":3,"playbook-migration\u002Fai-db-migration":6,"footer-counts":1166},{"tools":4,"reviews":5},65,7,{"id":7,"title":8,"body":9,"category":1147,"cover":1148,"description":1149,"extension":1150,"meta":1151,"navigation":143,"path":1152,"published":1153,"relatedTools":1154,"seo":1157,"stem":1158,"tags":1159,"updated":1153,"__hash__":1165},"playbook\u002Fplaybook\u002Fmigration\u002Fai-db-migration.md","用 AI 做数据库迁移：零停机 schema 变更工作流",{"type":10,"value":11,"toc":1133},"minimark",[12,16,32,35,63,66,77,81,85,91,94,119,122,265,269,275,278,282,287,627,630,854,857,912,916,1015,1019,1022,1058,1123,1129],[13,14,15],"h2",{"id":15},"适用场景",[17,18,19,23,26,29],"ul",{},[20,21,22],"li",{},"生产数据库需要 schema 变更",[20,24,25],{},"大表（千万行+）加列 \u002F 改类型 \u002F 加索引",[20,27,28],{},"需要零停机迁移",[20,30,31],{},"想让 AI 生成迁移文件 + 回滚方案",[13,33,34],{"id":34},"迁移原则",[36,37,38,45,51,57],"ol",{},[20,39,40,44],{},[41,42,43],"strong",{},"永远可回滚","——每个迁移文件都有对应的 down migration",[20,46,47,50],{},[41,48,49],{},"分阶段执行","——大变更拆成多步，每步都可独立回滚",[20,52,53,56],{},[41,54,55],{},"先兼容后破坏","——先让代码兼容新 schema，再删旧字段",[20,58,59,62],{},[41,60,61],{},"AI 生成 + 人工审查","——AI 写迁移，人审 SQL",[13,64,65],{"id":65},"工作流",[67,68,73],"pre",{"className":69,"code":71,"language":72},[70],"language-text","需求：给 users 表加 phone 字段\n  → Step 1: Claude 生成迁移文件（含 up + down）\n  → Step 2: Claude 检查兼容性（是否破坏现有代码）\n  → Step 3: 在 shadow DB 测试迁移\n  → Step 4: 生产分阶段执行\n  → Step 5: 验证 + 清理\n","text",[74,75,71],"code",{"__ignoreMap":76},"",[13,78,80],{"id":79},"step-1-ai-生成迁移","Step 1: AI 生成迁移",[82,83,84],"p",{},"用 Claude Code 连数据库（通过 MCP），描述需求：",[67,86,89],{"className":87,"code":88,"language":72},[70],"给 users 表加一个 phone 字段，varchar(20)，可空，加唯一索引。\n用 Drizzle migration 格式生成，含 up 和 down。\n检查现有代码是否有依赖。\n",[74,90,88],{"__ignoreMap":76},[82,92,93],{},"Claude 会：",[36,95,96,99,102,105,108],{},[20,97,98],{},"查当前 users 表结构",[20,100,101],{},"生成 Drizzle schema 改动",[20,103,104],{},"生成 SQL migration 文件",[20,106,107],{},"生成回滚 SQL",[20,109,110,111,114,115,118],{},"检查代码里的 ",[74,112,113],{},"select *"," 和 ",[74,116,117],{},"insert"," 是否受影响",[82,120,121],{},"生成的文件：",[67,123,127],{"className":124,"code":125,"language":126,"meta":76,"style":76},"language-sql shiki shiki-themes github-light github-dark","-- migrations\u002F0024_add_user_phone.sql\n\n-- UP\nALTER TABLE users ADD COLUMN phone VARCHAR(20);\nCREATE UNIQUE INDEX idx_users_phone ON users(phone) WHERE phone IS NOT NULL;\n\n-- DOWN\nDROP INDEX IF EXISTS idx_users_phone;\nALTER TABLE users DROP COLUMN IF EXISTS phone;\n","sql",[74,128,129,138,145,151,184,215,220,225,243],{"__ignoreMap":76},[130,131,134],"span",{"class":132,"line":133},"line",1,[130,135,137],{"class":136},"sJ8bj","-- migrations\u002F0024_add_user_phone.sql\n",[130,139,141],{"class":132,"line":140},2,[130,142,144],{"emptyLinePlaceholder":143},true,"\n",[130,146,148],{"class":132,"line":147},3,[130,149,150],{"class":136},"-- UP\n",[130,152,154,158,161,165,168,171,174,177,181],{"class":132,"line":153},4,[130,155,157],{"class":156},"szBVR","ALTER",[130,159,160],{"class":156}," TABLE",[130,162,164],{"class":163},"sVt8B"," users ",[130,166,167],{"class":156},"ADD",[130,169,170],{"class":163}," COLUMN phone ",[130,172,173],{"class":156},"VARCHAR",[130,175,176],{"class":163},"(",[130,178,180],{"class":179},"sj4cs","20",[130,182,183],{"class":163},");\n",[130,185,187,190,193,197,200,203,206,209,212],{"class":132,"line":186},5,[130,188,189],{"class":156},"CREATE",[130,191,192],{"class":156}," UNIQUE INDEX",[130,194,196],{"class":195},"sScJk"," idx_users_phone",[130,198,199],{"class":156}," ON",[130,201,202],{"class":163}," users(phone) ",[130,204,205],{"class":156},"WHERE",[130,207,208],{"class":163}," phone ",[130,210,211],{"class":156},"IS NOT NULL",[130,213,214],{"class":163},";\n",[130,216,218],{"class":132,"line":217},6,[130,219,144],{"emptyLinePlaceholder":143},[130,221,222],{"class":132,"line":5},[130,223,224],{"class":136},"-- DOWN\n",[130,226,228,231,234,237,240],{"class":132,"line":227},8,[130,229,230],{"class":156},"DROP",[130,232,233],{"class":156}," INDEX",[130,235,236],{"class":156}," IF",[130,238,239],{"class":156}," EXISTS",[130,241,242],{"class":163}," idx_users_phone;\n",[130,244,246,248,250,252,254,257,260,262],{"class":132,"line":245},9,[130,247,157],{"class":156},[130,249,160],{"class":156},[130,251,164],{"class":163},[130,253,230],{"class":156},[130,255,256],{"class":163}," COLUMN ",[130,258,259],{"class":156},"IF",[130,261,239],{"class":156},[130,263,264],{"class":163}," phone;\n",[13,266,268],{"id":267},"step-2-兼容性检查","Step 2: 兼容性检查",[67,270,273],{"className":271,"code":272,"language":72},[70],"检查这次迁移会影响哪些代码：\n1. 哪些 INSERT 语句需要加 phone 字段\n2. 哪些 SELECT * 会导致返回字段变化\n3. 哪些 API 响应 schema 会变\n",[74,274,272],{"__ignoreMap":76},[82,276,277],{},"Claude 输出影响清单，你确认后再执行。",[13,279,281],{"id":280},"step-3-高危操作安全模式","Step 3: 高危操作安全模式",[283,284,286],"h3",{"id":285},"大表加列千万行","大表加列（千万行+）",[67,288,290],{"className":124,"code":289,"language":126,"meta":76,"style":76},"-- ❌ 错误：直接加（锁表）\nALTER TABLE users ADD COLUMN phone VARCHAR(20);\n\n-- ✅ 正确：分阶段\n-- Phase 1: 加可空字段（不锁表，PostgreSQL 11+）\nALTER TABLE users ADD COLUMN phone VARCHAR(20);\n\n-- Phase 2: 回填数据（分批，不锁表）\n-- Claude 生成批处理脚本\nDO $$\nDECLARE\n  batch_size INT := 10000;\n  offset_val INT := 0;\nBEGIN\n  LOOP\n    UPDATE users SET phone = '' WHERE id IN (\n      SELECT id FROM users WHERE phone IS NULL LIMIT batch_size\n    );\n    GET DIAGNOSTICS batch_size = ROW_COUNT;\n    EXIT WHEN batch_size = 0;\n    PERFORM pg_sleep(0.1);  -- 给主从复制留时间\n  END LOOP;\nEND $$;\n\n-- Phase 3: 加约束（先检查再加）\nALTER TABLE users ADD CONSTRAINT chk_phone CHECK (phone ~ '^\\+?[0-9]{6,20}$') NOT VALID;\nALTER TABLE users VALIDATE CONSTRAINT chk_phone;\n",[74,291,292,297,317,321,326,331,351,355,360,365,371,377,397,414,420,426,457,486,492,506,524,545,556,565,570,576,611],{"__ignoreMap":76},[130,293,294],{"class":132,"line":133},[130,295,296],{"class":136},"-- ❌ 错误：直接加（锁表）\n",[130,298,299,301,303,305,307,309,311,313,315],{"class":132,"line":140},[130,300,157],{"class":156},[130,302,160],{"class":156},[130,304,164],{"class":163},[130,306,167],{"class":156},[130,308,170],{"class":163},[130,310,173],{"class":156},[130,312,176],{"class":163},[130,314,180],{"class":179},[130,316,183],{"class":163},[130,318,319],{"class":132,"line":147},[130,320,144],{"emptyLinePlaceholder":143},[130,322,323],{"class":132,"line":153},[130,324,325],{"class":136},"-- ✅ 正确：分阶段\n",[130,327,328],{"class":132,"line":186},[130,329,330],{"class":136},"-- Phase 1: 加可空字段（不锁表，PostgreSQL 11+）\n",[130,332,333,335,337,339,341,343,345,347,349],{"class":132,"line":217},[130,334,157],{"class":156},[130,336,160],{"class":156},[130,338,164],{"class":163},[130,340,167],{"class":156},[130,342,170],{"class":163},[130,344,173],{"class":156},[130,346,176],{"class":163},[130,348,180],{"class":179},[130,350,183],{"class":163},[130,352,353],{"class":132,"line":5},[130,354,144],{"emptyLinePlaceholder":143},[130,356,357],{"class":132,"line":227},[130,358,359],{"class":136},"-- Phase 2: 回填数据（分批，不锁表）\n",[130,361,362],{"class":132,"line":245},[130,363,364],{"class":136},"-- Claude 生成批处理脚本\n",[130,366,368],{"class":132,"line":367},10,[130,369,370],{"class":163},"DO $$\n",[130,372,374],{"class":132,"line":373},11,[130,375,376],{"class":156},"DECLARE\n",[130,378,380,383,386,389,392,395],{"class":132,"line":379},12,[130,381,382],{"class":163},"  batch_size ",[130,384,385],{"class":156},"INT",[130,387,388],{"class":163}," :",[130,390,391],{"class":156},"=",[130,393,394],{"class":179}," 10000",[130,396,214],{"class":163},[130,398,400,403,405,407,409,412],{"class":132,"line":399},13,[130,401,402],{"class":163},"  offset_val ",[130,404,385],{"class":156},[130,406,388],{"class":163},[130,408,391],{"class":156},[130,410,411],{"class":179}," 0",[130,413,214],{"class":163},[130,415,417],{"class":132,"line":416},14,[130,418,419],{"class":156},"BEGIN\n",[130,421,423],{"class":132,"line":422},15,[130,424,425],{"class":156},"  LOOP\n",[130,427,429,432,434,437,439,441,445,448,451,454],{"class":132,"line":428},16,[130,430,431],{"class":156},"    UPDATE",[130,433,164],{"class":163},[130,435,436],{"class":156},"SET",[130,438,208],{"class":163},[130,440,391],{"class":156},[130,442,444],{"class":443},"sZZnC"," ''",[130,446,447],{"class":156}," WHERE",[130,449,450],{"class":163}," id ",[130,452,453],{"class":156},"IN",[130,455,456],{"class":163}," (\n",[130,458,460,463,465,468,470,472,474,477,480,483],{"class":132,"line":459},17,[130,461,462],{"class":156},"      SELECT",[130,464,450],{"class":163},[130,466,467],{"class":156},"FROM",[130,469,164],{"class":163},[130,471,205],{"class":156},[130,473,208],{"class":163},[130,475,476],{"class":156},"IS",[130,478,479],{"class":156}," NULL",[130,481,482],{"class":156}," LIMIT",[130,484,485],{"class":163}," batch_size\n",[130,487,489],{"class":132,"line":488},18,[130,490,491],{"class":163},"    );\n",[130,493,495,498,501,503],{"class":132,"line":494},19,[130,496,497],{"class":156},"    GET",[130,499,500],{"class":163}," DIAGNOSTICS batch_size ",[130,502,391],{"class":156},[130,504,505],{"class":163}," ROW_COUNT;\n",[130,507,509,512,515,518,520,522],{"class":132,"line":508},20,[130,510,511],{"class":163},"    EXIT ",[130,513,514],{"class":156},"WHEN",[130,516,517],{"class":163}," batch_size ",[130,519,391],{"class":156},[130,521,411],{"class":179},[130,523,214],{"class":163},[130,525,527,530,533,536,539,542],{"class":132,"line":526},21,[130,528,529],{"class":163},"    PERFORM pg_sleep(",[130,531,532],{"class":179},"0",[130,534,535],{"class":163},".",[130,537,538],{"class":179},"1",[130,540,541],{"class":163},");  ",[130,543,544],{"class":136},"-- 给主从复制留时间\n",[130,546,548,551,554],{"class":132,"line":547},22,[130,549,550],{"class":156},"  END",[130,552,553],{"class":156}," LOOP",[130,555,214],{"class":163},[130,557,559,562],{"class":132,"line":558},23,[130,560,561],{"class":156},"END",[130,563,564],{"class":163}," $$;\n",[130,566,568],{"class":132,"line":567},24,[130,569,144],{"emptyLinePlaceholder":143},[130,571,573],{"class":132,"line":572},25,[130,574,575],{"class":136},"-- Phase 3: 加约束（先检查再加）\n",[130,577,579,581,583,585,587,590,593,596,599,602,605,608],{"class":132,"line":578},26,[130,580,157],{"class":156},[130,582,160],{"class":156},[130,584,164],{"class":163},[130,586,167],{"class":156},[130,588,589],{"class":156}," CONSTRAINT",[130,591,592],{"class":163}," chk_phone ",[130,594,595],{"class":156},"CHECK",[130,597,598],{"class":163}," (phone ~ ",[130,600,601],{"class":443},"'^\\+?[0-9]{6,20}$'",[130,603,604],{"class":163},") ",[130,606,607],{"class":156},"NOT",[130,609,610],{"class":163}," VALID;\n",[130,612,614,616,618,621,624],{"class":132,"line":613},27,[130,615,157],{"class":156},[130,617,160],{"class":156},[130,619,620],{"class":163}," users VALIDATE ",[130,622,623],{"class":156},"CONSTRAINT",[130,625,626],{"class":163}," chk_phone;\n",[283,628,629],{"id":629},"改字段类型",[67,631,633],{"className":124,"code":632,"language":126,"meta":76,"style":76},"-- ❌ 错误：直接改（锁表 + 重写全表）\nALTER TABLE users ALTER COLUMN phone TYPE BIGINT USING phone::BIGINT;\n\n-- ✅ 正确：新字段 + 回填 + 切换 + 删旧\n-- Phase 1: 加新字段\nALTER TABLE users ADD COLUMN phone_int BIGINT;\n\n-- Phase 2: 双写（代码同时写旧和新）\n-- Claude 生成代码改动：INSERT\u002FUPDATE 同时写 phone 和 phone_int\n\n-- Phase 3: 回填\nUPDATE users SET phone_int = phone::BIGINT WHERE phone_int IS NULL AND phone ~ '^[0-9]+$';\n\n-- Phase 4: 验证数据一致\nSELECT COUNT(*) FROM users WHERE phone IS NOT NULL AND phone_int IS NULL;\n-- 必须为 0\n\n-- Phase 5: 代码切到读 phone_int\n\n-- Phase 6: 删旧字段（等一个发布周期后）\nALTER TABLE users DROP COLUMN phone;\nALTER TABLE users RENAME COLUMN phone_int TO phone;\n",[74,634,635,640,669,673,678,683,700,704,709,714,718,723,760,764,769,804,809,813,818,822,827,840],{"__ignoreMap":76},[130,636,637],{"class":132,"line":133},[130,638,639],{"class":136},"-- ❌ 错误：直接改（锁表 + 重写全表）\n",[130,641,642,644,646,648,650,652,655,658,661,664,667],{"class":132,"line":140},[130,643,157],{"class":156},[130,645,160],{"class":156},[130,647,164],{"class":163},[130,649,157],{"class":156},[130,651,170],{"class":163},[130,653,654],{"class":156},"TYPE",[130,656,657],{"class":156}," BIGINT",[130,659,660],{"class":156}," USING",[130,662,663],{"class":163}," phone::",[130,665,666],{"class":156},"BIGINT",[130,668,214],{"class":163},[130,670,671],{"class":132,"line":147},[130,672,144],{"emptyLinePlaceholder":143},[130,674,675],{"class":132,"line":153},[130,676,677],{"class":136},"-- ✅ 正确：新字段 + 回填 + 切换 + 删旧\n",[130,679,680],{"class":132,"line":186},[130,681,682],{"class":136},"-- Phase 1: 加新字段\n",[130,684,685,687,689,691,693,696,698],{"class":132,"line":217},[130,686,157],{"class":156},[130,688,160],{"class":156},[130,690,164],{"class":163},[130,692,167],{"class":156},[130,694,695],{"class":163}," COLUMN phone_int ",[130,697,666],{"class":156},[130,699,214],{"class":163},[130,701,702],{"class":132,"line":5},[130,703,144],{"emptyLinePlaceholder":143},[130,705,706],{"class":132,"line":227},[130,707,708],{"class":136},"-- Phase 2: 双写（代码同时写旧和新）\n",[130,710,711],{"class":132,"line":245},[130,712,713],{"class":136},"-- Claude 生成代码改动：INSERT\u002FUPDATE 同时写 phone 和 phone_int\n",[130,715,716],{"class":132,"line":367},[130,717,144],{"emptyLinePlaceholder":143},[130,719,720],{"class":132,"line":373},[130,721,722],{"class":136},"-- Phase 3: 回填\n",[130,724,725,728,730,732,735,737,739,741,743,745,747,749,752,755,758],{"class":132,"line":379},[130,726,727],{"class":156},"UPDATE",[130,729,164],{"class":163},[130,731,436],{"class":156},[130,733,734],{"class":163}," phone_int ",[130,736,391],{"class":156},[130,738,663],{"class":163},[130,740,666],{"class":156},[130,742,447],{"class":156},[130,744,734],{"class":163},[130,746,476],{"class":156},[130,748,479],{"class":156},[130,750,751],{"class":156}," AND",[130,753,754],{"class":163}," phone ~ ",[130,756,757],{"class":443},"'^[0-9]+$'",[130,759,214],{"class":163},[130,761,762],{"class":132,"line":399},[130,763,144],{"emptyLinePlaceholder":143},[130,765,766],{"class":132,"line":416},[130,767,768],{"class":136},"-- Phase 4: 验证数据一致\n",[130,770,771,774,777,779,782,784,786,788,790,792,794,796,798,800,802],{"class":132,"line":422},[130,772,773],{"class":156},"SELECT",[130,775,776],{"class":179}," COUNT",[130,778,176],{"class":163},[130,780,781],{"class":156},"*",[130,783,604],{"class":163},[130,785,467],{"class":156},[130,787,164],{"class":163},[130,789,205],{"class":156},[130,791,208],{"class":163},[130,793,211],{"class":156},[130,795,751],{"class":156},[130,797,734],{"class":163},[130,799,476],{"class":156},[130,801,479],{"class":156},[130,803,214],{"class":163},[130,805,806],{"class":132,"line":428},[130,807,808],{"class":136},"-- 必须为 0\n",[130,810,811],{"class":132,"line":459},[130,812,144],{"emptyLinePlaceholder":143},[130,814,815],{"class":132,"line":488},[130,816,817],{"class":136},"-- Phase 5: 代码切到读 phone_int\n",[130,819,820],{"class":132,"line":494},[130,821,144],{"emptyLinePlaceholder":143},[130,823,824],{"class":132,"line":508},[130,825,826],{"class":136},"-- Phase 6: 删旧字段（等一个发布周期后）\n",[130,828,829,831,833,835,837],{"class":132,"line":526},[130,830,157],{"class":156},[130,832,160],{"class":156},[130,834,164],{"class":163},[130,836,230],{"class":156},[130,838,839],{"class":163}," COLUMN phone;\n",[130,841,842,844,846,849,852],{"class":132,"line":547},[130,843,157],{"class":156},[130,845,160],{"class":156},[130,847,848],{"class":163}," users RENAME COLUMN phone_int ",[130,850,851],{"class":156},"TO",[130,853,264],{"class":163},[283,855,856],{"id":856},"加索引",[67,858,860],{"className":124,"code":859,"language":126,"meta":76,"style":76},"-- ❌ 错误：直接加（锁表写操作）\nCREATE INDEX idx_users_email ON users(email);\n\n-- ✅ 正确：CONCURRENTLY（不锁表，但慢）\nCREATE INDEX CONCURRENTLY idx_users_email ON users(email);\n-- 注意：CONCURRENTLY 不能在事务里跑\n",[74,861,862,867,881,885,890,907],{"__ignoreMap":76},[130,863,864],{"class":132,"line":133},[130,865,866],{"class":136},"-- ❌ 错误：直接加（锁表写操作）\n",[130,868,869,871,873,876,878],{"class":132,"line":140},[130,870,189],{"class":156},[130,872,233],{"class":156},[130,874,875],{"class":195}," idx_users_email",[130,877,199],{"class":156},[130,879,880],{"class":163}," users(email);\n",[130,882,883],{"class":132,"line":147},[130,884,144],{"emptyLinePlaceholder":143},[130,886,887],{"class":132,"line":153},[130,888,889],{"class":136},"-- ✅ 正确：CONCURRENTLY（不锁表，但慢）\n",[130,891,892,894,896,899,902,905],{"class":132,"line":186},[130,893,189],{"class":156},[130,895,233],{"class":156},[130,897,898],{"class":195}," CONCURRENTLY",[130,900,901],{"class":163}," idx_users_email ",[130,903,904],{"class":156},"ON",[130,906,880],{"class":163},[130,908,909],{"class":132,"line":217},[130,910,911],{"class":136},"-- 注意：CONCURRENTLY 不能在事务里跑\n",[13,913,915],{"id":914},"step-4-执行-监控","Step 4: 执行 + 监控",[67,917,921],{"className":918,"code":919,"language":920,"meta":76,"style":76},"language-bash shiki shiki-themes github-light github-dark","# 1. 在 shadow DB 测试\npsql $SHADOW_DB -f migrations\u002F0024_add_user_phone.sql\n\n# 2. 生产执行（维护窗口）\npsql $PROD_DB -f migrations\u002F0024_add_user_phone.sql\n\n# 3. 监控（Claude 帮你写监控脚本）\nwatch -n 5 'psql $PROD_DB -c \"\n  SELECT\n    count(*) AS total,\n    count(phone) AS with_phone,\n    count(*) - count(phone) AS without_phone\n  FROM users\n\"'\n","bash",[74,922,923,928,942,946,951,962,966,971,985,990,995,1000,1005,1010],{"__ignoreMap":76},[130,924,925],{"class":132,"line":133},[130,926,927],{"class":136},"# 1. 在 shadow DB 测试\n",[130,929,930,933,936,939],{"class":132,"line":140},[130,931,932],{"class":195},"psql",[130,934,935],{"class":163}," $SHADOW_DB ",[130,937,938],{"class":179},"-f",[130,940,941],{"class":443}," migrations\u002F0024_add_user_phone.sql\n",[130,943,944],{"class":132,"line":147},[130,945,144],{"emptyLinePlaceholder":143},[130,947,948],{"class":132,"line":153},[130,949,950],{"class":136},"# 2. 生产执行（维护窗口）\n",[130,952,953,955,958,960],{"class":132,"line":186},[130,954,932],{"class":195},[130,956,957],{"class":163}," $PROD_DB ",[130,959,938],{"class":179},[130,961,941],{"class":443},[130,963,964],{"class":132,"line":217},[130,965,144],{"emptyLinePlaceholder":143},[130,967,968],{"class":132,"line":5},[130,969,970],{"class":136},"# 3. 监控（Claude 帮你写监控脚本）\n",[130,972,973,976,979,982],{"class":132,"line":227},[130,974,975],{"class":195},"watch",[130,977,978],{"class":179}," -n",[130,980,981],{"class":179}," 5",[130,983,984],{"class":443}," 'psql $PROD_DB -c \"\n",[130,986,987],{"class":132,"line":245},[130,988,989],{"class":443},"  SELECT\n",[130,991,992],{"class":132,"line":367},[130,993,994],{"class":443},"    count(*) AS total,\n",[130,996,997],{"class":132,"line":373},[130,998,999],{"class":443},"    count(phone) AS with_phone,\n",[130,1001,1002],{"class":132,"line":379},[130,1003,1004],{"class":443},"    count(*) - count(phone) AS without_phone\n",[130,1006,1007],{"class":132,"line":399},[130,1008,1009],{"class":443},"  FROM users\n",[130,1011,1012],{"class":132,"line":416},[130,1013,1014],{"class":443},"\"'\n",[13,1016,1018],{"id":1017},"step-5-回滚方案","Step 5: 回滚方案",[82,1020,1021],{},"每个迁移执行前，Claude 生成回滚 checklist：",[67,1023,1027],{"className":1024,"code":1025,"language":1026,"meta":76,"style":76},"language-markdown shiki shiki-themes github-light github-dark","## 回滚步骤（如需）\n\n1. 确认 down migration 安全：\n   ```sql\n   SELECT count(*) FROM users WHERE phone IS NOT NULL;\n   -- 如果 > 0，回滚会丢数据，确认是否可接受\n","markdown",[74,1028,1029,1034,1038,1043,1048,1053],{"__ignoreMap":76},[130,1030,1031],{"class":132,"line":133},[130,1032,1033],{},"## 回滚步骤（如需）\n",[130,1035,1036],{"class":132,"line":140},[130,1037,144],{"emptyLinePlaceholder":143},[130,1039,1040],{"class":132,"line":147},[130,1041,1042],{},"1. 确认 down migration 安全：\n",[130,1044,1045],{"class":132,"line":153},[130,1046,1047],{},"   ```sql\n",[130,1049,1050],{"class":132,"line":186},[130,1051,1052],{},"   SELECT count(*) FROM users WHERE phone IS NOT NULL;\n",[130,1054,1055],{"class":132,"line":217},[130,1056,1057],{},"   -- 如果 > 0，回滚会丢数据，确认是否可接受\n",[36,1059,1060,1079,1107],{"start":140},[20,1061,1062,1063],{},"执行回滚：",[67,1064,1066],{"className":918,"code":1065,"language":920,"meta":76,"style":76},"psql $PROD_DB -f migrations\u002F0024_add_user_phone_down.sql\n",[74,1067,1068],{"__ignoreMap":76},[130,1069,1070,1072,1074,1076],{"class":132,"line":133},[130,1071,932],{"class":195},[130,1073,957],{"class":163},[130,1075,938],{"class":179},[130,1077,1078],{"class":443}," migrations\u002F0024_add_user_phone_down.sql\n",[20,1080,1081,1082],{},"代码回滚到上一个版本：",[67,1083,1085],{"className":918,"code":1084,"language":920,"meta":76,"style":76},"git revert \u003Cmerge-commit>\n",[74,1086,1087],{"__ignoreMap":76},[130,1088,1089,1092,1095,1098,1101,1104],{"class":132,"line":133},[130,1090,1091],{"class":195},"git",[130,1093,1094],{"class":443}," revert",[130,1096,1097],{"class":156}," \u003C",[130,1099,1100],{"class":443},"merge-commi",[130,1102,1103],{"class":163},"t",[130,1105,1106],{"class":156},">\n",[20,1108,1109,1110],{},"验证：",[67,1111,1113],{"className":124,"code":1112,"language":126,"meta":76,"style":76},"\\d users  -- 确认 phone 字段已删\n",[74,1114,1115],{"__ignoreMap":76},[130,1116,1117,1120],{"class":132,"line":133},[130,1118,1119],{"class":163},"\\d users  ",[130,1121,1122],{"class":136},"-- 确认 phone 字段已删\n",[67,1124,1127],{"className":1125,"code":1126,"language":72},[70],"\n## 踩坑记录\n\n1. **PostgreSQL 11+ 加可空字段才是即时**——低版本加字段仍会锁表，先升级。\n2. **CONCURRENTLY 索引失败要手动清理**——失败后留 invalid index，`DROP INDEX` 后重建。\n3. **回填脚本要分批 + sleep**——大批量 UPDATE 会撑爆 WAL 和主从延迟。\n4. **NOT VALID + VALIDATE 两步走**——直接加 CHECK 会全表扫描锁表。\n5. **Claude 生成 SQL 必须 review**——AI 偶尔会忘加 WHERE 条件，删数据操作尤其要审。\n",[74,1128,1126],{"__ignoreMap":76},[1130,1131,1132],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":76,"searchDepth":147,"depth":147,"links":1134},[1135,1136,1137,1138,1139,1140,1145,1146],{"id":15,"depth":140,"text":15},{"id":34,"depth":140,"text":34},{"id":65,"depth":140,"text":65},{"id":79,"depth":140,"text":80},{"id":267,"depth":140,"text":268},{"id":280,"depth":140,"text":281,"children":1141},[1142,1143,1144],{"id":285,"depth":147,"text":286},{"id":629,"depth":147,"text":629},{"id":856,"depth":147,"text":856},{"id":914,"depth":140,"text":915},{"id":1017,"depth":140,"text":1018},"migration","\u002Fog\u002Fplaybook\u002Fai-db-migration.png","数据库 schema 迁移最怕搞挂生产。用 Claude Code + Drizzle migrations 搭一套安全迁移流程——AI 生成迁移文件 → 自动检查兼容性 → 分阶段执行 → 验证 → 回滚方案。含大表加列、改类型等高危操作。","md",{},"\u002Fplaybook\u002Fmigration\u002Fai-db-migration","2026-06-21",[1155,1156],"coding\u002Fcli\u002Fclaude-code","coding\u002Fide\u002Fcursor",{"title":8,"description":1149},"playbook\u002Fmigration\u002Fai-db-migration",[1160,1161,1162,1163,1164],"数据库","迁移","Drizzle","零停机","Claude Code","tzaBre4cpBUCVMaO1qaWEjIwBDWReTAlQ9UMCOMI54A",{"tools":4,"reviews":5,"playbooks":367,"news":227},1782316489335]