وہ migration جو production کو بند کر دیتی ہے، تقریباً کبھی بھی وہ نہیں ہوتی جس کے بارے میں سب فکر مند تھے۔ یہ شاذونادر ہی DROP TABLE ہوتی ہے — ایسی چیزوں کا ہر کوئی احتیاط سے جائزہ لیتا ہے۔ مسئلہ عموماً وہ migration بنتی ہے جو “محفوظ دکھائی دیتی” ہے: جیسے default کے ساتھ NOT NULL column شامل کرنا، یا کاروباری اوقات میں مصروف table پر index بنانا، اور اسے کوئی ایسا شخص چلا دیتا ہے جس نے بجا طور پر یہ سمجھا ہوتا ہے کہ اگر migration tool نے اسے خودکار طور پر بنایا ہے تو اسے چلانا محفوظ ہی ہوگا۔
ناخوشگوار حقیقت یہ ہے کہ migration tool کی نظر میں “محفوظ” کا مطلب عموماً “syntax کے لحاظ سے درست” ہوتا ہے، نہ کہ “table کو block نہیں کرے گی”۔ Postgres کی DDL operations حقیقی locks حاصل کرتی ہیں، اور عام schema changes میں سے کئی ایک ایسا lock لیتی ہیں — ACCESS EXCLUSIVE — جو operation کے پورے دورانیے تک table کے خلاف ہر read اور write کو روک دیتا ہے۔ چھوٹی table پر یہ چند milliseconds ہوتے ہیں، یعنی نظر ہی نہیں آتے۔ لیکن دسیوں ملین rows اور مسلسل traffic والی table پر یہی چیز کئی منٹ کا production outage بن سکتی ہے، اور وجہ ایک ایسی migration ہوتی ہے جو صرف ایک لائن کے diff جیسی دکھائی دیتی تھی۔
آپ یہ سیکھیں گے:
- کون سی عام schema changes table-blocking lock لیتی ہیں، اور کون سی جدید Postgres میں پہلے سے default کے طور پر محفوظ ہیں
- پورا table دوبارہ لکھے بغیر default value کے ساتھ column کیسے شامل کیا جائے
- writes کو block کیے بغیر
NOT NULLconstraint شامل کرنے کا درست تین مرحلہ pattern CREATE INDEX CONCURRENTLYعام index-build lock سے کیسے بچاتا ہے، اور اس کی حقیقی failure mode کیا ہے- migration چلانے سے پہلے
pg_locksاورpg_stat_activityکو کیسے پڑھا جائے تاکہ معلوم ہو سکے کہ migration کس چیز کو block کرنے والی ہے - Alembic یا Django migration کو کیسے اس طرح بنایا جائے کہ یہ pattern default کے طور پر محفوظ انداز میں لاگو ہو
فہرستِ مضامین
- کیوں کچھ DDL پوری Table کو Block کرتی ہے
- Lock کیے بغیر Columns شامل کرنا
- Writes کو Block کیے بغیر NOT NULL شامل کرنا
- CONCURRENTLY کے ساتھ Indexes بنانا
- بڑی Tables کو محفوظ طریقے سے Backfill کرنا
- Migration چلانے سے پہلے pg_locks پڑھنا
- Alembic اور Django میں محفوظ Migrations
- عام غلطیاں
کیوں کچھ DDL پوری Table کو Block کرتی ہے
Postgres میں ہر ALTER TABLE operation کے پورے دورانیے کے لیے table پر lock لیتی ہے، اور lock کی سطح یہ طے کرتی ہے کہ ساتھ ساتھ اور کیا کچھ ہو سکتا ہے۔ سب سے سخت lock، ACCESS EXCLUSIVE، لفظی طور پر باقی سب کچھ روک دیتی ہے — reads، writes، حتیٰ کہ دوسری DDL بھی — جب تک اسے release نہ کر دیا جائے۔ کسی مخصوص ALTER TABLE کو یہی سخت ترین lock درکار ہے یا وہ اس سے کہیں ہلکی lock پر کام چلا سکتی ہے، اس کا انحصار خاص طور پر اس بات پر ہوتا ہے کہ تبدیلی کرنے کے لیے Postgres کو table کی موجودہ rows دوبارہ لکھنی پڑتی ہیں یا نہیں۔
وہ operations جو صرف metadata کو چھیڑتی ہیں — nullable column شامل کرنا، NOT VALID کے ساتھ check constraint شامل کرنا، column drop کرنا — یہ بھی ACCESS EXCLUSIVE لیتی ہیں، مگر بہت مختصر وقت کے لیے، کیونکہ دوبارہ لکھنے کے لیے کوئی data نہیں ہوتا؛ lock صرف اتنی دیر رہتی ہے کہ catalog update ہو جائے۔ وہ operations جنہیں ہر موجودہ row دوبارہ لکھنی پڑتی ہے — جیسے column type کو ایسے انداز میں بدلنا جو binary-compatible نہ ہو، پرانے Postgres versions میں volatile default کے ساتھ column شامل کرنا، یا NOT VALID کے بغیر کچھ خاص constraints شامل کرنا — یہی ACCESS EXCLUSIVE lock پورے rewrite کے دورانیے تک اپنے پاس رکھتی ہیں، اور یہ دورانیہ براہِ راست table size کے ساتھ بڑھتا ہے، چنانچہ حقیقی load کے تحت بڑی table پر یہ منٹوں یا اس سے زیادہ تک جا سکتا ہے۔
عملی اصول یہ ہے: کوئی بھی DDL statement جس کا دورانیہ row count کے ساتھ بڑھتا ہے، مصروف table پر حقیقی خطرہ ہے، اور کوئی بھی statement جس کا دورانیہ row count سے قطع نظر مستقل ہو (یعنی metadata-only changes)، تقریباً ہمیشہ محفوظ ہوتی ہے۔ کسی بھی ALTER TABLE کے بارے میں پہلے سے جان لینا کہ وہ کس category میں آتی ہے، یہی اس پورے مضمون کی اصل مہارت ہے۔
ان operations کے لیے ایک فوری حوالہ جو سب سے زیادہ سامنے آتی ہیں:
| Operation | Lock taken | Blocks reads? | Blocks writes? | Duration scales with rows? |
|---|---|---|---|---|
| Nullable column شامل کرنا، default کے بغیر | ACCESS EXCLUSIVE (مختصر) | مختصراً | مختصراً | نہیں |
| Column شامل کرنا، literal default کے ساتھ (PG 11+) | ACCESS EXCLUSIVE (مختصر) | مختصراً | مختصراً | نہیں |
| Column شامل کرنا، volatile default کے ساتھ | ACCESS EXCLUSIVE | ہاں | ہاں | ہاں |
براہِ راست SET NOT NULL | ACCESS EXCLUSIVE | ہاں | ہاں | ہاں |
ADD CONSTRAINT ... NOT VALID | ACCESS EXCLUSIVE (مختصر) | مختصراً | مختصراً | نہیں |
VALIDATE CONSTRAINT | SHARE UPDATE EXCLUSIVE | نہیں | نہیں | ہاں، مگر non-blocking |
عام CREATE INDEX | SHARE | نہیں | ہاں | ہاں |
CREATE INDEX CONCURRENTLY | SHARE UPDATE EXCLUSIVE | نہیں | نہیں | ہاں، مگر non-blocking |
| Column type تبدیل کرنا (incompatible) | ACCESS EXCLUSIVE | ہاں | ہاں | ہاں |
آخری دو columns میں جہاں بھی “نہیں” ہے، وہاں pattern ایک جیسا ہے: تقریباً ہر اس operation کے لیے جس کا دورانیہ table size کے ساتھ بڑھتا ہے، Postgres کے پاس ہلکی lock mode موجود ہوتی ہے، اور اسے استعمال کرنے کی واحد قیمت یہ ہے کہ operation کو مکمل ہونے میں wall-clock وقت زیادہ لگتا ہے — اور کسی ایسی table پر جسے دوسرے لوگ فعال طور پر استعمال کر رہے ہوں، یہ سودا تقریباً ہمیشہ فائدے کا ہوتا ہے۔
Lock کیے بغیر Columns شامل کرنا
بغیر default کے nullable column شامل کرنا ہمیشہ سے Postgres میں ایک تیز، metadata-only operation رہا ہے — یہ دراصل کبھی بھی خطرناک نہیں تھی:
ALTER TABLE orders ADD COLUMN discount_code TEXT;
default value کے ساتھ column شامل کرنا پہلے ایک کلاسک trap سمجھا جاتا تھا: Postgres 11 سے پہلے، یہ default بھرنے کے لیے ہر موجودہ row کو rewrite کرتا تھا، اور پورے rewrite کے دوران ACCESS EXCLUSIVE لیے رکھتا تھا۔ Postgres 11 کے بعد، non-volatile default (یعنی literal، نہ کہ now() یا random()) خالصتاً metadata کے طور پر store ہوتی ہے اور پرانی rows پر read کے وقت lazily apply کی جاتی ہے — نہ table rewrite، نہ لمبی lock، چاہے table کتنی ہی بڑی ہو:
-- Safe and fast on Postgres 11+, any table size
ALTER TABLE orders ADD COLUMN status TEXT DEFAULT 'pending';
جو استثنا اب بھی اہم ہے وہ یہ ہے: volatile default — DEFAULT now(), DEFAULT gen_random_uuid()، یا کوئی بھی چیز جسے ہر row کے لیے حقیقی طور پر مختلف value compute کرنا پڑے — جدید Postgres میں بھی مکمل table rewrite پر مجبور کرتی ہے، کیونکہ lazily-applied default تبھی کام کرتی ہے جب ہر row عین ایک ہی stored value share کر سکے۔ ایسی ہر چیز کے لیے جسے per-row computed value چاہیے، پہلے nullable column بغیر default کے شامل کریں، batches میں backfill کریں (نیچے)، پھر default صرف future rows کے لیے شامل کریں۔
Writes کو Block کیے بغیر NOT NULL شامل کرنا
ایک سادہ ALTER TABLE orders ALTER COLUMN status SET NOT NULL کو پوری table scan کرنی پڑتی ہے تاکہ یہ verify کیا جا سکے کہ کوئی موجودہ row نئی constraint کی خلاف ورزی نہیں کرتی، اور یہ مکمل scan کے دوران ACCESS EXCLUSIVE لیے رکھتی ہے۔ بڑی اور مصروف table پر، حقیقی traffic کے ساتھ مقابلہ کرتی یہ scan بالکل وہی قسم کی migration ہے جو outage کا سبب بنتی ہے۔ محفوظ pattern اسے تین steps میں تقسیم کرتا ہے جو کبھی بھی لمبی exclusive lock نہیں لیتے:
-- Step 1: add a CHECK constraint as NOT VALID — instant, metadata-only
ALTER TABLE orders ADD CONSTRAINT orders_status_not_null
CHECK (status IS NOT NULL) NOT VALID;
-- Step 2: validate it separately — takes a lock, but only ROW SHARE,
-- which permits concurrent reads and writes
ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null;
-- Step 3 (Postgres 12+): now SET NOT NULL is instant, because
-- the planner can prove it from the already-validated constraint
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;
DROP CONSTRAINT orders_status_not_null; -- optional cleanup
Step 1 فوری ہوتا ہے کیونکہ NOT VALID Postgres کو بتاتا ہے کہ constraint صرف ان rows پر enforce کی جائے جو اس point کے بعد لکھی جائیں، موجودہ rows کو check کیے بغیر۔ Step 2 مہنگی full-table check تو کرتی ہے، مگر ACCESS EXCLUSIVE سے کہیں ہلکی lock کے تحت — یہ چلتے وقت concurrent reads اور writes معمول کے مطابق جاری رہتی ہیں، البتہ validation کے دوران کچھ اضافی I/O contention کی قیمت پر۔ Postgres 12 اور بعد کے versions میں Step 3 واقعی metadata-only operation بن جاتی ہے، کیونکہ planner validated constraint سے ثابت کر سکتا ہے کہ column پہلے ہی non-null ہے، دوسری table scan کے بغیر۔ یہی تین مرحلوں والا pattern اس پورے مضمون کی سب سے زیادہ قیمتی ترکیب ہے، اور یہ صرف NOT NULL تک محدود نہیں بلکہ کسی بھی CHECK یا foreign key constraint پر لاگو ہوتی ہے۔
CONCURRENTLY کے ساتھ Indexes بنانا
ایک عام CREATE INDEX ایسی SHARE lock لیتی ہے جو table پر writes (اگرچہ reads نہیں) کو پورے build کے دوران روک دیتی ہے — بڑی table پر اس کا مطلب منٹوں تک INSERT/UPDATE/DELETE statements کا block ہونا ہو سکتا ہے، جو کسی بھی write-heavy service کے لیے عملی طور پر outage ہے۔ CREATE INDEX CONCURRENTLY اس سے بچنے کے لیے index کو کئی passes میں بناتی ہے، اور ان کے درمیان اپنی lock چھوڑ دیتی ہے تاکہ پورے وقت writes جاری رہ سکیں:
CREATE INDEX CONCURRENTLY idx_orders_status ON orders (status);
اس کی حقیقی قیمت کو ٹھیک ٹھیک جاننا ضروری ہے: build واضح طور پر زیادہ وقت لیتی ہے (اکثر اسی table پر عام CREATE INDEX کے مقابلے میں دو سے تین گنا) اور، اہم بات یہ کہ، یہ transaction block کے اندر نہیں چل سکتی — زیادہ تر migration frameworks default کے طور پر ہر migration کو transaction میں لپیٹتی ہیں، اس لیے CONCURRENTLY کو درست طریقے سے چلانے کا مطلب عموماً framework کو واضح طور پر بتانا ہوتا ہے کہ اس مخصوص migration کے لیے transactional wrapping کو چھوڑ دے۔
دوسری حقیقی failure mode یہ ہے: اگر build درمیان میں رک جائے — connection drop ہو جائے، statement cancel ہو جائے، server restart ہو جائے — تو یہ ایک invalid index چھوڑ سکتی ہے، جو \d tablename میں INVALID کے طور پر نظر آتی ہے، disk space بھی لیتی رہتی ہے اور ہر write پر maintain بھی ہوتی رہتی ہے، مگر queries کے لیے کبھی استعمال نہیں ہوتی۔ Postgres اسے خودکار طور پر صاف نہیں کرتا:
-- Check for invalid indexes across the database
SELECT indexrelid::regclass, indrelid::regclass
FROM pg_index WHERE NOT indisvalid;
-- Drop and rebuild
DROP INDEX CONCURRENTLY idx_orders_status;
CREATE INDEX CONCURRENTLY idx_orders_status ON orders (status);
DROP INDEX CONCURRENTLY خاص طور پر اسی cleanup case کے لیے موجود ہے اور create variant کی طرح عام DROP INDEX والی blocking lock سے بچتی ہے۔ جب index محفوظ طریقے سے بن جائے تو EXPLAIN ANALYZE کے ذریعے یہ ضرور confirm کریں کہ اسے حقیقتاً اسی طرح استعمال کیا جا رہا ہے جیسا آپ توقع کر رہے ہیں — Postgres query plans پڑھنے کی guide میں بالکل یہی بتایا گیا ہے کہ یہ کیسے verify کیا جائے کہ نئی index نے plan shape بدلی ہے، بجائے اس کے کہ صرف تیز query کو ثبوت سمجھ لیا جائے۔
بڑی Tables کو محفوظ طریقے سے Backfill کرنا
ملینز rows کے خلاف ایک واحد UPDATE orders SET status = 'pending' WHERE status IS NULL منطقی طور پر تو درست کام کرتی ہے، مگر operational طور پر غلط — یہ ایک بہت بڑی transaction بن جاتی ہے، commit تک ہر touched row پر row locks رکھتی ہے، WAL traffic کا اچانک دباؤ پیدا کرتی ہے، اور ایسی long-running transaction کا خطرہ بڑھاتی ہے جو اپنی پوری مدت کے دوران database میں دوسری جگہوں پر autovacuum کو dead tuples صاف کرنے سے روکے رکھتی ہے۔ Backfills کو اس کے بجائے چھوٹی، committed batches میں ہونا چاہیے:
import time
BATCH_SIZE = 5000
while True:
with connection.cursor() as cursor:
cursor.execute("""
UPDATE orders SET status = 'pending'
WHERE id IN (
SELECT id FROM orders
WHERE status IS NULL
LIMIT %s
FOR UPDATE SKIP LOCKED
)
""", [BATCH_SIZE])
rows_updated = cursor.rowcount
if rows_updated == 0:
break
time.sleep(0.1) # let replication and autovacuum keep up
FOR UPDATE SKIP LOCKED کا مطلب یہ ہے کہ concurrent backfill batches (یا وہ application writes جو انہی rows کو چھو رہی ہوں) ایک دوسرے کو block نہیں کرتیں — ایک batch بس ان rows کو skip کر دیتی ہے جنہیں کسی دوسرے process نے پہلے ہی lock کیا ہوا ہو اور اگلے pass میں انہیں اٹھا لیتی ہے۔ Batches کے درمیان مختصر sleep جان بوجھ کر رکھا جاتا ہے: یہ replication lag، autovacuum، اور connection pool contention کو bursts کے درمیان سنبھلنے کی مہلت دیتا ہے، بجائے اس کے کہ table کو مسلسل اسی طرح پیٹا جائے جیسے ایک بڑی transaction بہرحال کرتی، صرف یہ کہ وہ دباؤ ایک جگہ مرکوز ہونے کے بجائے پھیلا ہوا ہوتا ہے۔
Migration چلانے سے پہلے pg_locks پڑھنا
Postgres add column lock incidents کی حیرت انگیز تعداد کی جڑ یہ ہوتی ہے کہ migration شروع ہونے سے پہلے کسی نے یہ دیکھا ہی نہیں کہ table کے خلاف پہلے سے کیا چل رہا تھا — کوئی long-running report query یا کوئی غیر متعلقہ batch job ایسا lock پکڑے ہوتی ہے جس کے پیچھے migration queue ہو جاتی ہے، اور یوں جو چیز فوری metadata change ہونی چاہیے تھی وہ کئی منٹ کے wait میں بدل جاتی ہے، اور پھر اس کے پیچھے باقی سب بھی queue ہوتے جاتے ہیں۔ یہ اندازہ لگانے کے بجائے کہ migration محفوظ ہے یا نہیں، Postgres اس وقت locks کے hold اور wait ہونے کی درست صورتِ حال دکھا دیتا ہے:
SELECT
pg_locks.pid,
pg_locks.mode,
pg_locks.granted,
pg_stat_activity.query,
pg_stat_activity.state
FROM pg_locks
JOIN pg_stat_activity ON pg_locks.pid = pg_stat_activity.pid
WHERE pg_locks.relation = 'orders'::regclass;
granted = false والی rows وہ ہیں جو واقعی انتظار میں پھنسی ہوئی ہیں — یہی زندہ اشارہ ہے کہ اس وقت کچھ block ہو رہا ہے، صرف ایک مفروضہ نہیں۔ Migration شروع کرنے کے فوراً بعد ایک دوسری session میں یہ query چلانا، جب migration ابھی جاری ہو، حقیقی وقت میں یہ confirm کرنے کا تیز ترین طریقہ ہے کہ اس نے وہی lock level حاصل کی جس کی آپ توقع کر رہے تھے یا اس سے زیادہ سخت کوئی lock، اور یہ بھی کہ اس کے پیچھے اور کیا queue ہو چکا ہے۔ اسے pg_stat_activity.query_start کے ساتھ ملا کر دیکھیں تاکہ یہ معلوم ہو سکے کہ blocking statement کتنی دیر سے چل رہی ہے؛ اکثر یہی وہ فیصلہ کن بات ہوتی ہے جو طے کرتی ہے کہ migration کو مکمل ہونے دیا جائے یا حقیقی نقصان سے پہلے cancel کر دیا جائے۔ pg_blocking_pids(pid) کو جاننا بھی مفید ہے — stuck query کی PID دیں اور یہ ٹھیک ٹھیک بتا دے گا کہ کون سی دوسری sessions اسے block کر رہی ہیں، یوں “کوئی چیز میری migration کو block کر رہی ہے” ایک واضح، قابلِ عمل PID میں بدل جاتا ہے جس کی investigation یا termination کی جا سکے، اندازے میں نہیں۔
بڑی production table پر کسی واقعی خطرناک migration کے لیے، وہ session جو اسے چلا رہی ہو اس پر ایک conservative lock_timeout سیٹ کرنا مناسب ہے، تاکہ اگر migration اپنی lock جلدی حاصل نہ کر سکے تو وہ خاموشی سے دوسری traffic کے پیچھے queue ہونے اور پھر اپنے پیچھے آنے والی ہر چیز کو block کرنے کے بجائے فوراً اور واضح طور پر fail ہو جائے:
SET lock_timeout = '2s';
ALTER TABLE orders ADD CONSTRAINT ...;
ایسی migration جو واضح lock not available error کے ساتھ تیزی سے fail ہو جائے، اس migration سے کہیں بہتر ہے جو منٹوں تک queue میں بیٹھی رہے اور اس دوران اس table پر آنے والی ہر دوسری query بھی اس کے پیچھے پھنس جائے — failed migration کو آسانی سے دوبارہ چلایا جا سکتا ہے؛ blocked production queries کی قطار تو تب تک incident بن چکی ہوتی ہے جب تک کسی کی نظر پڑے۔
Alembic اور Django میں محفوظ Migrations
Alembic یہ patterns خودکار طور پر لاگو نہیں کرتا — autogenerated migration میں ایک سیدھا op.create_index(...) default کے طور پر عام، blocking CREATE INDEX بناتا ہے۔ Index کے لیے alembic safe migration لکھنے کا مطلب ہے کہ آپ postgresql_concurrently=True بھی شامل کریں اور migration کو واضح طور پر non-transactional بھی نشان زد کریں:
from alembic import op
def upgrade():
op.execute("COMMIT") # end the implicit transaction Alembic opened
op.create_index(
"idx_orders_status", "orders", ["status"],
postgresql_concurrently=True,
)
Django کے migration framework میں خاص اسی case کے لیے ایک dedicated، purpose-built flag موجود ہے — migration class پر atomic = False، اور اس کے ساتھ django.contrib.postgres.operations سے AddIndexConcurrently:
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
operations = [
AddIndexConcurrently(
"order",
models.Index(fields=["status"], name="idx_orders_status"),
),
]
دونوں frameworks constraint-splitting pattern کو بھی support کرتی ہیں جس کا ذکر اوپر ہوا، یعنی ایک autogenerated SET NOT NULL کے بجائے تین واضح، الگ migration steps۔ اپنی migration files میں جان بوجھ کر migration کو NOT VALID / VALIDATE / SET NOT NULL sequence میں تقسیم کریں، بجائے اس کے کہ موجودہ populated table پر NOT NULL change کے لیے کسی بھی tool کے default single-step output پر بھروسا کریں۔
عام غلطیاں
غلطی: بڑی موجودہ table پر autogenerated migration کو یہ دیکھے بغیر trust کرنا کہ یہ حقیقت میں کیا کرتی ہے۔ Migration tools درست SQL بناتی ہیں، مگر ضروری نہیں کہ ایسی SQL جو پہلے سے production traffic اٹھانے والی table کے لیے محفوظ بھی ہو۔ حل: production پر چلانے سے پہلے ہر autogenerated ALTER TABLE کو اس سوال کے خلاف پڑھیں: “کیا یہ table کو rewrite کرتی ہے؟”
غلطی: CREATE INDEX CONCURRENTLY کو transaction block کے اندر چلانا۔ Postgres اسے سیدھا رد کر دیتا ہے — یہ transaction کے اندر concurrent طور پر نہیں چل سکتی۔ حل: جس framework کو آپ استعمال کر رہے ہیں، اس میں اس migration step کے لیے transactional wrapping کو واضح طور پر توڑیں۔
غلطی: interrupted concurrent build کے بعد invalid index کو پیچھے چھوڑ دینا۔ یہ خاموشی سے disk space اور write overhead لیتی رہتی ہے جبکہ query performance میں کچھ بھی حصہ نہیں ڈالتی۔ حل: وقتاً فوقتاً pg_index.indisvalid چیک کریں اور ملنے والی ہر invalid index کو صاف کریں۔
غلطی: ایک ہی transaction میں ملینز rows کی backfill کرنا۔ یہ transaction کی پوری مدت تک locks پکڑے رکھتی ہے اور WAL pressure پیدا کرتی ہے، اور meanwhile autovacuum کو بھی روکے رکھتی ہے۔ حل: LIMIT اور FOR UPDATE SKIP LOCKED کے ساتھ backfill کو batches میں کریں، اور batches کے درمیان مختصر وقفہ رکھیں۔
غلطی: volatile-default column شامل کر کے یہ سمجھ لینا کہ Postgres 11 کی fast-default optimization اسے بھی cover کرتی ہے۔ DEFAULT now() یا اس جیسی چیزیں عام literal default کے برعکس اب بھی مکمل table rewrite پر مجبور کرتی ہیں۔ حل: column کو nullable شامل کریں، batches میں backfill کریں، پھر default صرف future rows پر apply کریں۔
اختتامیہ
Zero-downtime migrations عام schema changes سے الگ کوئی جدا discipline نہیں ہیں — یہ وہی عام schema changes ہیں، بس ان کے ساتھ ایک اضافی عادت جڑی ہوتی ہے: کچھ بھی چلانے سے پہلے یہ جان لینا کہ operation کا دورانیہ table size کے ساتھ بڑھتا ہے یا نہیں، اور پھر وہ pattern چننا (fast default، NOT VALID/VALIDATE، CONCURRENTLY، batched backfill) جو اسے مستقل رکھے۔ اس مضمون کے ہر pattern کی وجہ یہ ہے کہ تقریباً ہر خطرناک دکھائی دینے والی تبدیلی کے لیے Postgres پہلے ہی ایک ہلکا راستہ فراہم کرتا ہے — اصل مہارت یہ جاننے میں ہے کہ وہ راستہ موجود ہے، اور کسی بھی ایسی table پر جو اتنی بڑی یا اتنی مصروف ہو کہ naive version کا خطرہ نہ لیا جا سکے، default کے طور پر اسی کو اپنانا۔
آپ کی اگلی migration جب کسی production table کو چھیڑے، کیا آپ کو پہلے سے معلوم ہوگا کہ وہ کون سی lock لینے والی ہے، یا آپ یہ بات کسی incident سے جانیں گے؟
