Zero-Downtime Database Migrations with Drizzle ORM

Run database migrations without downtime using Drizzle ORM. Covers expand-contract pattern, backward-compatible schema changes, rollback strategies, and CI/CD integration for PostgreSQL.

E
ECOSIRE Research and Development Team
|19 मार्च 202610 मिनट पढ़ें2.3k शब्द|

#ड्रिज़ल ओआरएम के साथ जीरो-डाउनटाइम डेटाबेस माइग्रेशन

उत्पादन परिनियोजन में डेटाबेस माइग्रेशन सबसे खतरनाक ऑपरेशन है। एक माइग्रेशन जो एक टेबल को लॉक कर देता है - यहां तक ​​कि 200 एमएस के लिए भी - कनेक्शन टाइमआउट का कारण बनता है जो 500 त्रुटियों में बदल जाता है जो 2 बजे अलर्ट तूफान में बदल जाता है। अधिकांश ओआरएम माइग्रेशन ट्यूटोरियल आपको "आसान" रास्ता दिखाते हैं: ऐप बंद करें, माइग्रेशन चलाएं, पुनरारंभ करें। वह शून्य-डाउनटाइम नहीं है; वह निर्धारित रखरखाव है।

वास्तविक शून्य-डाउनटाइम माइग्रेशन के लिए एक अलग मानसिक मॉडल की आवश्यकता होती है: आपकी स्कीमा तैनाती प्रक्रिया के दौरान वर्तमान में चल रहे एप्लिकेशन कोड के साथ बैकवर्ड-संगत होनी चाहिए। इसका मतलब है कि रोलआउट विंडो के दौरान पुराने कोड और नए कोड दोनों को एक साथ काम करना होगा। विस्तार-अनुबंध पैटर्न इसे संभव बनाता है। ड्रिज़ल ओआरएम का एसक्यूएल-प्रथम दृष्टिकोण आपको इसे सही ढंग से लागू करने का नियंत्रण देता है।

मुख्य बातें

  • उत्पादन में कभी भी drizzle-kit push न चलाएं - SQL उत्पन्न करें और समीक्षा करें, फिर नियंत्रण के साथ लागू करें
  • विस्तार-अनुबंध पैटर्न: जोड़ें (विस्तृत करें) → नया कोड तैनात करें → पुराना हटाएं (अनुबंध)
  • एक निरर्थक कॉलम जोड़ना सुरक्षित है; डिफ़ॉल्ट के बिना NOT NULL जोड़ना खतरनाक है
  • किसी कॉलम का नाम बदलने के लिए आवश्यक है: नया कॉलम जोड़ें → बैकफ़िल → कोड अपडेट करें → पुराना कॉलम हटाएं (4 चरण)
  • इंडेक्स निर्माण के दौरान टेबल लॉक से बचने के लिए PostgreSQL CREATE INDEX CONCURRENTLY का उपयोग करें
  • उत्पादन पर आवेदन करने से पहले अपने डेटाबेस की उत्पादन-आकार की प्रतिलिपि पर माइग्रेशन का परीक्षण करें
  • हमेशा एक रोलबैक योजना रखें: प्रत्येक माइग्रेशन के लिए, रोलबैक SQL लिखें और उसका परीक्षण करें
  • स्कीमा परिवर्तन जिनके लिए तालिका को फिर से लिखने की आवश्यकता होती है (बड़ी तालिका पर प्रकार बदलें) रखरखाव विंडो की आवश्यकता होती है

बूंदा बांदी ओआरएम स्कीमा सेटअप

// packages/db/src/schema/contacts.ts
import { pgTable, uuid, varchar, text, timestamp, pgEnum, index } from 'drizzle-orm/pg-core';

export const contactStatusEnum = pgEnum('contact_status', ['active', 'inactive', 'archived']);

export const contacts = pgTable('contacts', {
  id:             uuid('id').primaryKey().defaultRandom(),
  organizationId: uuid('organization_id').notNull(),
  name:           varchar('name', { length: 255 }).notNull(),
  email:          varchar('email', { length: 255 }),
  phone:          varchar('phone', { length: 50 }),
  status:         contactStatusEnum('status').default('active').notNull(),
  notes:          text('notes'),
  createdAt:      timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  updatedAt:      timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
  orgIdIdx:  index('contacts_org_id_idx').on(table.organizationId),
  emailIdx:  index('contacts_email_idx').on(table.email),
  statusIdx: index('contacts_status_idx').on(table.status),
}));

माइग्रेशन उत्पन्न करें:

# Generate SQL migration (never auto-apply to production)
npx drizzle-kit generate

# Review the generated SQL before applying
cat drizzle/0001_add_contact_status.sql

# Apply to development database
npx drizzle-kit migrate

# For production: apply via your deployment pipeline

विस्तार-अनुबंध पैटर्न

मुख्य सिद्धांत: प्रत्येक स्कीमा परिवर्तन को कई पिछड़े-संगत तैनाती में विभाजित किया जाता है।

चरण 1: विस्तार करें (बिना तोड़े जोड़ें)

ऐसे स्कीमा परिवर्तन तैनात करें जो केवल योगात्मक हों - डिफ़ॉल्ट के साथ नए कॉलम, नई तालिकाएँ, नई अनुक्रमणिकाएँ।

-- Migration: 0010_expand_add_company.sql
-- Safe to apply while old code is running: nullable column with a default

ALTER TABLE contacts
  ADD COLUMN IF NOT EXISTS company_id UUID,
  ADD COLUMN IF NOT EXISTS company_name VARCHAR(255);

-- Old code ignores these columns; new code uses them
-- Both versions coexist during rolling deployment
// Drizzle schema after expansion
export const contacts = pgTable('contacts', {
  // ... existing columns ...
  companyId:   uuid('company_id'),           // nullable — old code ignores it
  companyName: varchar('company_name', { length: 255 }), // nullable — safe
});

चरण 2: नया एप्लिकेशन कोड परिनियोजित करें

नया ऐप संस्करण परिनियोजित करें जो पुराने और नए दोनों कॉलमों को पढ़ता और लिखता है। रोलिंग परिनियोजन के दौरान, पुराने इंस्टेंस (कोई कंपनी समर्थन नहीं) और नए इंस्टेंस (कंपनी समर्थन) एक साथ चलते हैं - दोनों काम करते हैं क्योंकि कॉलम शून्य है।

चरण 3: बैकफ़िल

टेबल लॉक से बचने के लिए मौजूदा पंक्तियों के लिए छोटे बैचों में नया कॉलम भरें:

-- Migration: 0011_backfill_company_name.sql
-- Run in small batches to avoid locking

DO $$
DECLARE
  batch_size INTEGER := 1000;
  offset_val INTEGER := 0;
  rows_updated INTEGER;
BEGIN
  LOOP
    UPDATE contacts
    SET company_name = 'Unknown'
    WHERE company_name IS NULL
      AND id IN (
        SELECT id FROM contacts
        WHERE company_name IS NULL
        ORDER BY id
        LIMIT batch_size
        OFFSET offset_val
      );

    GET DIAGNOSTICS rows_updated = ROW_COUNT;
    EXIT WHEN rows_updated = 0;

    offset_val := offset_val + batch_size;
    -- Brief pause between batches to reduce I/O pressure
    PERFORM pg_sleep(0.1);
  END LOOP;
END $$;

चरण 4: अनुबंध (पुराना कॉलम हटाएँ)

सभी एप्लिकेशन कोड अद्यतन होने और बैकफ़िल पूर्ण होने के बाद, पुराने कॉलम या बाधा को हटा दें।


सुरक्षित प्रवासन पैटर्न

एक शून्य कॉलम जोड़ना

-- WRONG — will fail if table has rows (no default, no nullable)
ALTER TABLE contacts ADD COLUMN tier VARCHAR(20) NOT NULL;

-- WRONG — locks table while it writes the default to every row
ALTER TABLE contacts ADD COLUMN tier VARCHAR(20) NOT NULL DEFAULT 'free';

-- CORRECT — three-step approach
-- Step 1: Add nullable column
ALTER TABLE contacts ADD COLUMN tier VARCHAR(20);

-- Step 2: Backfill existing rows
UPDATE contacts SET tier = 'free' WHERE tier IS NULL;

-- Step 3 (next deployment): Add NOT NULL constraint (instant if no NULLs exist)
ALTER TABLE contacts ALTER COLUMN tier SET NOT NULL;
ALTER TABLE contacts ALTER COLUMN tier SET DEFAULT 'free';

किसी कॉलम का नाम बदलना (4-चरणीय प्रक्रिया)

-- Step 1: Add the new column
ALTER TABLE contacts ADD COLUMN full_name VARCHAR(255);

-- Step 2: Backfill + keep in sync with a trigger
UPDATE contacts SET full_name = name;

CREATE OR REPLACE FUNCTION sync_full_name() RETURNS trigger AS $$
BEGIN
  NEW.full_name := NEW.name;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER contacts_sync_full_name
  BEFORE INSERT OR UPDATE OF name ON contacts
  FOR EACH ROW EXECUTE FUNCTION sync_full_name();

-- Step 3: Deploy new code that writes to full_name, reads from both
-- Step 4 (next deployment, after all instances updated):
DROP TRIGGER contacts_sync_full_name ON contacts;
DROP FUNCTION sync_full_name;
ALTER TABLE contacts DROP COLUMN name;

एनम प्रकार बदलना

PostgreSQL पूर्ण तालिका पुनर्लेखन के बिना एनम मानों को हटाने की अनुमति नहीं देता है। सुरक्षित एनुम परिवर्तन के लिए:

-- Adding a new enum value is safe (instant, no lock)
ALTER TYPE contact_status ADD VALUE IF NOT EXISTS 'pending';

-- Removing an enum value requires a full table rewrite
-- Use the expand-contract pattern: add a new enum type, migrate, drop old
CREATE TYPE contact_status_new AS ENUM ('active', 'inactive', 'archived', 'pending');

-- Migrate data
ALTER TABLE contacts
  ALTER COLUMN status TYPE contact_status_new
  USING status::text::contact_status_new;

DROP TYPE contact_status;
ALTER TYPE contact_status_new RENAME TO contact_status;

बिना लॉक किए इंडेक्स बनाना

नियमित CREATE INDEX एक ShareLock प्राप्त करता है जो अवधि के लिए सभी लेखन को अवरुद्ध करता है। एक बड़ी मेज पर, इसमें कुछ मिनट लग सकते हैं।

-- WRONG — locks writes during index creation
CREATE INDEX contacts_email_idx ON contacts(email);

-- CORRECT — concurrent index creation; no write lock
CREATE INDEX CONCURRENTLY IF NOT EXISTS contacts_email_idx
  ON contacts(email)
  WHERE email IS NOT NULL; -- Partial index for better performance

-- Drizzle note: drizzle-kit does not generate CONCURRENTLY by default
-- Edit the generated SQL migration to add CONCURRENTLY before applying

अपनी ड्रिज़ल माइग्रेशन फ़ाइल में, पीढ़ी के बाद मैन्युअल रूप से CONCURRENTLY जोड़ें:

-- drizzle/0012_add_email_index.sql (edited after generation)
CREATE INDEX CONCURRENTLY IF NOT EXISTS contacts_email_idx
  ON contacts (email)
  WHERE email IS NOT NULL;

उत्पादन सीआई/सीडी में बूंदाबांदी प्रवासन

उत्पादन में कभी भी drizzle-kit push का उपयोग न करें - यह समीक्षा चरण के बिना माइग्रेशन लागू करता है। जेनरेट की गई SQL फ़ाइलों के साथ drizzle-kit migrate का उपयोग करें जो संस्करण-नियंत्रित हैं:

# scripts/deploy-production.sh

echo "=== Running database migrations ==="

# Check if there are pending migrations
PENDING=$(npx drizzle-kit migrate --dry-run 2>&1 | grep "pending")
if [ -n "$PENDING" ]; then
  echo "Pending migrations detected:"
  echo "$PENDING"

  # Apply migrations
  npx drizzle-kit migrate
  echo "Migrations applied successfully"
else
  echo "No pending migrations"
fi
// Programmatic migration in main.ts (NestJS)
import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';

async function runMigrations() {
  const migrationClient = postgres(process.env.DATABASE_URL!, { max: 1 });
  const db = drizzle(migrationClient);

  await migrate(db, {
    migrationsFolder: join(__dirname, '..', '..', '..', 'drizzle'),
  });

  await migrationClient.end();
}

// In bootstrap(), before app.listen():
if (process.env.RUN_MIGRATIONS === 'true') {
  await runMigrations();
}

रोलबैक रणनीतियाँ

प्रत्येक माइग्रेशन में तैनाती से पहले एक सहयोगी रोलबैक माइग्रेशन लिखा और परीक्षण किया जाना चाहिए:

// drizzle/0013_add_company_id.sql (forward migration)
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS company_id UUID;
CREATE INDEX CONCURRENTLY IF NOT EXISTS contacts_company_id_idx
  ON contacts(company_id);

// drizzle/rollbacks/0013_rollback_add_company_id.sql
DROP INDEX CONCURRENTLY IF EXISTS contacts_company_id_idx;
ALTER TABLE contacts DROP COLUMN IF EXISTS company_id;

आपातकालीन रोलबैक के लिए:

# Emergency rollback script
# Run the rollback SQL, then redeploy the previous app version

psql "$DATABASE_URL" < drizzle/rollbacks/0013_rollback_add_company_id.sql
git checkout HEAD~1
pnpm build && pm2 restart ecosystem.config.cjs --update-env

प्रवासन परीक्षण रणनीति

// packages/db/src/tests/migration.spec.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import postgres from 'postgres';
import { drizzle } from 'drizzle-orm/postgres-js';

describe('Database Migrations', () => {
  let sql: ReturnType<typeof postgres>;

  beforeAll(async () => {
    sql = postgres(process.env.TEST_DATABASE_URL!);
  });

  it('all migrations apply cleanly on a fresh database', async () => {
    const db = drizzle(sql);
    await expect(migrate(db, { migrationsFolder: './drizzle' }))
      .resolves.not.toThrow();
  });

  it('migrations are idempotent (can be applied twice safely)', async () => {
    const db = drizzle(sql);
    await migrate(db, { migrationsFolder: './drizzle' });
    // Running again should be a no-op, not an error
    await expect(migrate(db, { migrationsFolder: './drizzle' }))
      .resolves.not.toThrow();
  });
});

खतरनाक संचालन के लिए विंडोज़ के रखरखाव की आवश्यकता होती है

कुछ परिचालनों को बहुत बड़ी तालिकाओं (सैकड़ों लाखों पंक्तियों) पर शून्य-डाउनटाइम नहीं किया जा सकता है:

ऑपरेशनजोखिमशमन
बड़े कॉलम पर ALTER TYPE USINGपूर्ण तालिका पुनर्लेखन, लंबा लॉकशेड्यूल रखरखाव विंडो
ADD COLUMN NOT NULL बिना डिफ़ॉल्ट केयदि पंक्तियाँ मौजूद हैं तो तत्काल विफल हो जाएँतीन-चरणीय विस्तार-अनुबंध का उपयोग करें
CREATE INDEX (बिना समवर्ती)अवधि के लिए लॉक लिखेंहमेशा CONCURRENTLY
ALTER TABLE ... RENAME TOतुरंत, लेकिन पुराने कोड को तोड़ देता हैकोड अपडेट होने के बाद केवल विस्तार चरण में
DROP TABLEअपरिवर्तनीयहमेशा एक बैकअप रखें; 30 दिन की सॉफ्ट-डिलीट पॉलिसी
VACUUM FULLअवधि के लिए टेबल लॉकरखरखाव के दौरान नियमित VACUUM + शेड्यूल फुल का उपयोग करें

अक्सर पूछे जाने वाले प्रश्न

drizzle-kit push और drizzle-kit migrate के बीच क्या अंतर है?

push माइग्रेशन फ़ाइलें उत्पन्न किए बिना आपके वर्तमान स्कीमा को सीधे डेटाबेस पर लागू करता है - विकास के लिए उपयोगी जहां आपको माइग्रेशन इतिहास की परवाह नहीं है। migrate आपकी drizzle/ निर्देशिका में SQL फ़ाइलों को क्रम में लागू करता है, जिन्हें लागू किया गया है। उत्पादन में हमेशा migrate का उपयोग करें ताकि आपके पास प्रत्येक स्कीमा परिवर्तन का नियंत्रित, समीक्षा योग्य, संस्करण-नियंत्रित इतिहास हो।

ड्रिज़ल drizzle_migrations ट्रैकिंग टेबल को कैसे संभालता है?

ड्रिज़ल एक drizzle_migrations (या __drizzle_migrations) तालिका बनाता है जो रिकॉर्ड करता है कि कौन सी माइग्रेशन फ़ाइलें लागू की गई हैं और कब। माइग्रेशन लागू करने से पहले, यह इस तालिका की जाँच करता है। इस तालिका से पंक्तियों को मैन्युअल रूप से सम्मिलित या हटाएं नहीं - यह ड्रिज़ल का राज्य ट्रैकिंग तंत्र है। यदि आपको पिछली माइग्रेशन स्थिति पर वापस लौटने की आवश्यकता है, तो अपना रोलबैक SQL चलाएँ और संबंधित पंक्ति को मैन्युअल रूप से हटा दें।

मैं उत्पादन-आकार वाले डेटाबेस के विरुद्ध माइग्रेशन का परीक्षण कैसे करूं?

अपने उत्पादन डेटाबेस की एक साफ-सुथरी (पीआईआई-हटाई गई) प्रति को स्टेजिंग सर्वर पर पुनर्स्थापित करें। वहां माइग्रेशन लागू करें और मापें: दीवार घड़ी का समय, लॉक प्रतीक्षा समय (pg_stat_activity से), टेबल ब्लोट (pg_stat_user_tables से), और क्वेरी प्रदर्शन पर प्रभाव (EXPLAIN ANALYZE से)। यदि माइग्रेशन को स्टेजिंग में 5 सेकंड से अधिक समय लगता है, तो इसे फिर से डिज़ाइन करें।

क्या मैं डेटाबेस माइग्रेशन के लिए लेनदेन का उपयोग कर सकता हूं?

PostgreSQL में अधिकांश DDL लेन-देन संबंधी है - आप लेन-देन में ALTER TABLE, CREATE TABLE, CREATE INDEX (लेकिन CREATE INDEX CONCURRENTLY नहीं) लपेट सकते हैं और यदि कोई चरण विफल हो जाता है तो वापस रोल कर सकते हैं। ड्रिज़ल प्रत्येक माइग्रेशन फ़ाइल को डिफ़ॉल्ट रूप से लेनदेन में लपेटता है। CONCURRENTLY का उपयोग करने वाले माइग्रेशन के लिए, आपको उन्हें एक अलग माइग्रेशन फ़ाइल में विभाजित करना होगा जो लेनदेन के बाहर चलती है।

मैं टाइपस्क्रिप्ट में ड्रिज़ल एनम तुलनाओं को कैसे प्रबंधित करूं?

ड्रिज़ल के एनम कॉलम डेटाबेस से स्ट्रिंग मान लौटाते हैं। तुलना करते समय, टाइपस्क्रिप्ट प्रकार को सही ढंग से सीमित नहीं कर सकता है। स्पष्ट रूप से कास्ट करें: if ((contact.status as ContactStatus) === 'active') या अपने ड्रिज़ल where क्लॉज में एक प्रकार के दावे का उपयोग करें: where(eq(contacts.status, 'active' as ContactStatus))। यह टाइपस्क्रिप्ट को खुश रखते हुए रनटाइम त्रुटियों से बचाता है।


अगले चरण

शून्य-डाउनटाइम डेटाबेस माइग्रेशन के लिए अनुशासन की आवश्यकता होती है - लेकिन विकल्प (डाउनटाइम, डेटा भ्रष्टाचार, 3 बजे आपातकालीन रोलबैक) बहुत खराब है। विस्तार-अनुबंध पैटर्न, CREATE INDEX CONCURRENTLY, और उचित रोलबैक तैयारी स्कीमा परिवर्तनों को एक नियमित, सुरक्षित संचालन बनाती है।

ECOSIRE, Drizzle ORM, PostgreSQL 17 और 65+ स्कीमा फ़ाइलों और 300+ माइग्रेशन में परीक्षण की गई शून्य-डाउनटाइम परिनियोजन पाइपलाइन के साथ NestJS अनुप्रयोगों के लिए डेटाबेस स्कीमा का प्रबंधन करता है। हमारी बैकएंड इंजीनियरिंग सेवाओं का अन्वेषण करें यह जानने के लिए कि हम बड़े पैमाने पर डेटाबेस संचालन को सुरक्षित रूप से कैसे संभालते हैं।

शेयर करें:
E

लेखक

ECOSIRE Research and Development Team

ECOSIRE में एंटरप्राइज़-ग्रेड डिजिटल उत्पाद बना रहे हैं। Odoo एकीकरण, ई-कॉमर्स ऑटोमेशन, और AI-संचालित व्यावसायिक समाधानों पर अंतर्दृष्टि साझा कर रहे हैं।

WhatsApp पर चैट करें