19 PostgreSQL & Node.js Interview Questions for 2026

·21 min read
By ·Updated
nodejspostgresqldatabasesqlinterview-questionsbackend

PostgreSQL and Node.js are a common backend combination. Interviewers use it to test whether you understand session ownership, transaction boundaries, SQL safety, query plans, failure handling, and schema rollout beyond basic CRUD.

This update targets PostgreSQL 18 and current node-postgres behavior as of September 2026.

This guide covers the essential PostgreSQL questions you'll face in Node.js backend interviews, from basic connections to advanced optimization techniques.

Table of Contents

  1. Connection and Pooling Questions
  2. Parameterized Queries and Security Questions
  3. Transaction Questions
  4. Migration Questions
  5. Query Optimization Questions
  6. PostgreSQL Features Questions
  7. Error Handling Questions

Connection and Pooling Questions

Connection management is fundamental to building performant Node.js applications with PostgreSQL.

How do you connect to PostgreSQL from Node.js?

The standard low-level approach uses the pg (node-postgres) library with a connection pool. The pool creates clients on demand and reuses released clients; the application still owns sizing, timeout, cancellation, health, and failure policy.

Create a pool once per process rather than connecting per request. Establishing a session requires network, optional TLS, authentication, and PostgreSQL setup; the cost depends on topology. Reuse helps latency and, more importantly, bounds database concurrency.

// db.js - Database connection setup
const { Pool } = require('pg');
 
const pool = new Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT || 5432,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 10,                // Example only: size from total deployment capacity
  idleTimeoutMillis: 30000,  // Close idle connections after 30s
  connectionTimeoutMillis: 2000, // Fail if can't connect in 2s
  ssl: process.env.DB_CA
    ? { ca: process.env.DB_CA, rejectUnauthorized: true }
    : undefined
});
 
// Verify connection on startup
pool.on('error', (err) => {
  console.error('Unexpected pool error', err);
  // Mark the instance unready and let the service's graceful-shutdown policy run.
});
 
module.exports = pool;

What is connection pooling and why is it important?

Connection pooling keeps a bounded set of reusable PostgreSQL sessions. Without pooling, each operation repeats connection, optional TLS, authentication, and session setup. The exact cost is topology-dependent, so measure rather than quoting a universal millisecond saving.

The pool creates clients on demand up to max, returns checked-out clients after release, and closes idle or expired clients according to configuration. It does not choose a safe pool size for your entire deployment or provide application-level admission control automatically.

// What happens under the hood
const pool = new Pool({ max: 10 });
 
// Request 1: No idle connections, creates a new session
await pool.query('SELECT 1');
 
// Request 2: Can reuse the idle session
await pool.query('SELECT 2');
 
// 11 concurrent requests: 10 run immediately, 1 waits in queue
const promises = Array(11).fill().map(() =>
  pool.query('SELECT pg_sleep(1)')  // 1 second each
);
await Promise.all(promises);  // Roughly two waves, plus scheduling/database overhead
 
// Check pool status
console.log({
  total: pool.totalCount,      // Total connections created
  idle: pool.idleCount,        // Available connections
  waiting: pool.waitingCount   // Queued requests
});

What happens when all pool connections are busy?

When all connections are in use, new checkouts wait in a FIFO queue. connectionTimeoutMillis bounds how long connection establishment or checkout can wait. Also propagate request cancellation/deadlines where supported and expose waitingCount; otherwise a saturated database turns into an unbounded latency queue.

Do not automatically increase max: more active queries can reduce throughput through memory pressure, lock contention, cache churn, and I/O saturation. Budget connections across every process, replica, worker, migration job, and administrative reserve. PgBouncer can multiplex client sessions, but transaction pooling changes session semantics for prepared statements, temporary tables, advisory locks, and session settings.

When should you use Pool versus Client?

The Pool should be your default choice for most database operations because it handles connection management automatically. When you call pool.query(), it acquires a connection, executes the query, and releases the connection back to the pool.

You need a dedicated Client only when you require the same connection across multiple operations—specifically for transactions. Transactions must execute all their queries on the same connection to maintain isolation and atomicity.

// Pool - for most operations (automatic connection management)
await pool.query('SELECT * FROM users');
 
// Client - for transactions (need same connection throughout)
const client = await pool.connect();
try {
  await client.query('BEGIN');
  // ... multiple queries on same connection
  await client.query('COMMIT');
} catch (err) {
  await client.query('ROLLBACK');
  throw err;
} finally {
  client.release();  // CRITICAL: always release
}

Critical warning: Forgetting client.release() leaks a checkout. The pool eventually exhausts, and new work queues until a client returns or a configured timeout/deadline fails it.


Parameterized Queries and Security Questions

Security awareness with database queries is essential for any backend developer.

How do you prevent SQL injection in Node.js?

Parameterized queries separate SQL structure from data values. With node-postgres, use $1, $2 placeholders and pass a values array; the driver sends the query and values through PostgreSQL's protocol rather than asking you to escape strings manually.

Parameters work only for values. PostgreSQL does not accept a parameter in place of an identifier, keyword, table name, or sort direction. Map those choices through a strict allowlist (or a trusted identifier-quoting/query-builder API), and treat ORM “raw/unsafe” escape hatches as SQL. Least-privilege roles and query timeouts reduce impact if a defect remains.

// ✅ CORRECT: Parameterized query
async function getUserById(id) {
  const result = await pool.query(
    'SELECT id, name, email FROM users WHERE id = $1',
    [id]
  );
  return result.rows[0];
}
 
// ❌ WRONG: SQL injection vulnerability
async function getUserByIdUnsafe(id) {
  const result = await pool.query(
    `SELECT * FROM users WHERE id = ${id}`  // NEVER DO THIS
  );
  return result.rows[0];
}

How do you write parameterized queries for different operations?

Parameterized queries work for all SQL operations—SELECT, INSERT, UPDATE, DELETE. The placeholder numbers correspond to the position in the values array: $1 is the first element, $2 is the second, and so on.

JavaScript-to-PostgreSQL type conversion deserves explicit tests. By default, node-postgres returns PostgreSQL int8 and arbitrary-precision numeric values as strings to avoid silent precision loss; timestamps and time zones also need a documented application policy. Do not coerce them to Number unless the range/precision is proven safe.

For INSERT operations, you can use the RETURNING clause to get the inserted data back without a second query. This is particularly useful for getting auto-generated IDs or default values.

// Insert with returning
async function createUser(name, email, passwordHash) {
  const result = await pool.query(
    `INSERT INTO users (name, email, password_hash, created_at)
     VALUES ($1, $2, $3, NOW())
     RETURNING id, name, email, created_at`,
    [name, email, passwordHash]
  );
  return result.rows[0];
}
 
// Search with multiple conditions
async function searchUsers(searchTerm, limit = 20, offset = 0) {
  const result = await pool.query(
    `SELECT id, name, email
     FROM users
     WHERE name ILIKE $1 OR email ILIKE $1
     ORDER BY created_at DESC
     LIMIT $2 OFFSET $3`,
    [`%${searchTerm}%`, limit, offset]
  );
  return result.rows;
}

Transaction Questions

Understanding transactions demonstrates knowledge of data integrity and concurrent operations.

How do you handle transactions in Node.js with PostgreSQL?

Transactions ensure that multiple database operations either all succeed or all fail together. You need a dedicated client from the pool because all queries in a transaction must run on the same connection. The pattern is: acquire client, BEGIN, execute queries, COMMIT on success or ROLLBACK on failure, then always release the client.

The try/catch/finally pattern is essential. The finally block guarantees the client is released back to the pool regardless of whether the transaction succeeded or failed.

async function transferMoney(fromAccount, toAccount, amount) {
  if (fromAccount === toAccount || amount <= 0) {
    throw new TypeError('Transfer requires distinct accounts and a positive amount');
  }
 
  const client = await pool.connect();
 
  try {
    await client.query('BEGIN');
 
    // Debit from source account
    const debitResult = await client.query(
      `UPDATE accounts
       SET balance = balance - $1
       WHERE id = $2 AND balance >= $1
       RETURNING balance`,
      [amount, fromAccount]
    );
 
    if (debitResult.rowCount === 0) {
      throw new Error('Insufficient funds or account not found');
    }
 
    // Credit to destination account
    const creditResult = await client.query(
      `UPDATE accounts
       SET balance = balance + $1
       WHERE id = $2
       RETURNING balance`,
      [amount, toAccount]
    );
 
    if (creditResult.rowCount !== 1) {
      throw new Error('Destination account not found');
    }
 
    // Record the transfer
    await client.query(
      `INSERT INTO transfers (from_account, to_account, amount, created_at)
       VALUES ($1, $2, $3, NOW())`,
      [fromAccount, toAccount, amount]
    );
 
    await client.query('COMMIT');
    return { success: true };
 
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.release();
  }
}

Key insight: The conditional debit makes the balance check and update atomic for that row, and the destination update must also affect exactly one row. Add database constraints such as CHECK (balance >= 0), use an exact money representation (numeric or integer minor units), define authorization/idempotency, and choose a consistent lock order to reduce deadlocks.

How do you create a reusable transaction helper?

For cleaner code and consistent transaction handling across your application, create a reusable wrapper function. This helper encapsulates the BEGIN/COMMIT/ROLLBACK pattern and ensures proper client release, reducing boilerplate and preventing connection leaks.

The callback pattern allows you to pass any set of queries to execute within the transaction context.

async function withTransaction(callback) {
  const client = await pool.connect();
  let discardClient = false;
  try {
    await client.query('BEGIN');
    const result = await callback(client);
    await client.query('COMMIT');
    return result;
  } catch (err) {
    try {
      await client.query('ROLLBACK');
    } catch (rollbackError) {
      discardClient = true;
      throw new AggregateError([err, rollbackError], 'Transaction and rollback failed');
    }
    throw err;
  } finally {
    client.release(discardClient ? new Error('Discard client after failed rollback') : undefined);
  }
}
 
// Usage
await withTransaction(async (client) => {
  await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 1]);
  await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 2]);
  return { transferred: 100 };
});

Migration Questions

Migration knowledge demonstrates understanding of team workflows and deployment processes.

What are database migrations and why are they important?

Migrations are ordered, version-controlled changes that move a known schema state forward. A migration runner records which files have run and must serialize concurrent deployers. Generated SQL is a draft to review for locks, table rewrites, long transactions, data backfills, and compatibility with both the old and new application versions.

Migrations provide an auditable history, but “rollback” is not guaranteed: dropping a column or transforming data may be irreversible, and rolling back code while leaving an additive schema change is often safer. For online changes, use expand-and-contract: add compatible structure, deploy dual-read/write or backfill, switch traffic, then remove the old structure in a later release. Test backups and restore separately.

// Using Knex migrations
// migrations/20240115120000_create_users.js
 
exports.up = function(knex) {
  return knex.schema.createTable('users', (table) => {
    table.increments('id').primary();
    table.string('email').unique().notNullable();
    table.string('password_hash').notNullable();
    table.string('name');
    table.timestamp('created_at').defaultTo(knex.fn.now());
    table.timestamp('updated_at').defaultTo(knex.fn.now());
 
    // UNIQUE already creates the supporting unique index in PostgreSQL.
  });
};
 
exports.down = function(knex) {
  return knex.schema.dropTable('users');
};
# Run migrations
npx knex migrate:latest
 
# Rollback last migration
npx knex migrate:rollback
 
# Create new migration
npx knex migrate:make add_user_roles

How do you manage migrations with Prisma 8?

Prisma 8 is the current major line as of September 2026 and introduces a contract-based migration workflow. Emit the contract, plan an on-disk migration, review its TypeScript/operation data and SQL preview, then apply it. Prisma 7 remains supported but uses the older migrate dev/migrate deploy workflow, so pin the major version in documentation and CI.

Migration tooling does not make a blocking DDL change or destructive transformation zero-downtime. Rehearse the exact generated plan on production-like data, review pre/post checks, and still use expand-and-contract for cross-version application compatibility.

// prisma/schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
 
}
 
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
 
  @@index([authorId])
}
# Prisma 8: emit, plan, inspect, and apply
npx prisma@latest contract emit
npx prisma@latest migration plan --name add_posts_table
npx prisma@latest migration show <migration-directory>
npx prisma@latest db migrate
 
# CI additionally verifies checked-in migration artifacts
npx prisma@latest migration check

Query Optimization Questions

Understanding query optimization separates mid-level from senior developers.

How do you use EXPLAIN ANALYZE to diagnose slow queries?

EXPLAIN shows the planned operations; EXPLAIN ANALYZE executes the statement and adds actual rows, loops, and timing. Add BUFFERS to see cache reads/hits and temp activity. Never prepend arbitrary application SQL and run it in production: writes will execute, and even a SELECT can be expensive or lock-sensitive.

The output shows each step of query execution, the method used (index scan vs sequential scan), and the actual time spent. This information guides your optimization decisions.

async function analyzeQuery(query, params) {
  const result = await pool.query(
    `EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query}`,
    params
  );
  return result.rows.map(r => r['QUERY PLAN']).join('\n');
}
 
// Usage
const plan = await analyzeQuery(
  'SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC',
  [123]
);
console.log(plan);
-- Output shows what's happening
Index Scan using idx_orders_user_date on orders
  Index Cond: (user_id = 123)
  Rows Removed by Filter: 0
  Planning Time: 0.150 ms
  Execution Time: 0.045 ms

Red flags to look for:

  • Large estimate/actual differences → investigate statistics, skew, correlations, and parameters
  • Inner nodes with high loops → multiply work before judging nested-loop cost
  • High buffer reads or temp reads/writes → investigate I/O and spills
  • Rows removed late in an expensive plan → see whether a predicate/index can reduce work earlier

A sequential scan or sort can be optimal when many rows are needed or the table is small. Optimize end-to-end latency and resource cost, not the presence of a particular node name.

How do you create effective indexes?

Indexes can reduce search and ordering work, but they also consume storage/cache and add write, vacuum, and planning cost. PostgreSQL may correctly prefer a sequential scan when a predicate returns much of a table.

Design indexes for important query shapes: predicates, ordering, projected columns, selectivity, and frequency. Composite column order matters; equality constraints on leading columns and then a range/order column are a useful starting point, not a substitute for checking the plan.

-- Candidate lookup query; measure it on representative data
SELECT * FROM users WHERE email = 'john@example.com';
 
-- Prefer a unique constraint when email is a business key
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);
 
-- Composite index for common query patterns
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC);
 
-- This query uses the composite index efficiently
SELECT * FROM orders
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 10;

How do you find slow queries in production?

PostgreSQL provides the pg_stat_statements extension that tracks execution statistics for all queries. This helps identify which queries consume the most time and resources, allowing you to focus optimization efforts where they'll have the greatest impact.

pg_stat_statements must be loaded through shared_preload_libraries and created as an extension. Protect access because normalized query text can still reveal sensitive structure. Rank by total time as well as mean latency: a moderately slow query called millions of times may matter more than a rare outlier.

// Finding slow queries in production
async function getSlowQueries() {
  // Requires pg_stat_statements extension
  const result = await pool.query(`
    SELECT query, calls, mean_exec_time, total_exec_time,
           rows, shared_blks_read, temp_blks_written
    FROM pg_stat_statements
    ORDER BY total_exec_time DESC
    LIMIT 10
  `);
  return result.rows;
}

How do you solve the N+1 query problem?

The N+1 problem occurs when you execute one query to fetch a list of items, then N additional queries to fetch related data for each item. This is extremely inefficient—10 users with their posts becomes 11 queries instead of 1 or 2.

Solve it with an eager-loading JOIN, one batched WHERE ... = ANY($1) query, a data-loader pattern, or aggregation. One giant JOIN is not automatically best: it can duplicate parent columns and create a large result. Compare payload size, plan, pagination semantics, and consistency needs.

// N+1 Problem: 1 query for users + N queries for posts
const users = await pool.query('SELECT * FROM users LIMIT 10');
for (const user of users.rows) {
  // This runs 10 separate queries!
  const posts = await pool.query(
    'SELECT * FROM posts WHERE user_id = $1',
    [user.id]
  );
}
 
// Solution: JOIN or batch query
const result = await pool.query(`
  SELECT u.*,
         COALESCE(
           jsonb_agg(to_jsonb(p)) FILTER (WHERE p.id IS NOT NULL),
           '[]'::jsonb
         ) AS posts
  FROM users u
  LEFT JOIN posts p ON p.user_id = u.id
  GROUP BY u.id
  LIMIT 10
`);

In real pagination, select the limited user IDs in a subquery/CTE before joining so the LIMIT applies to parents, not joined rows, and add a deterministic ORDER BY.


PostgreSQL Features Questions

Knowledge of PostgreSQL-specific features demonstrates depth beyond basic SQL.

How do you use JSONB for flexible data storage?

JSONB (Binary JSON) allows you to store semi-structured data within a relational database. Unlike regular JSON, JSONB is stored in a decomposed binary format that supports indexing and efficient querying. This gives you schema flexibility for certain fields while maintaining the benefits of relational structure.

JSONB is useful for metadata, preferences, or event payloads whose shape legitimately varies. Keep stable identifiers, relationships, constraints, money, and frequently filtered/joined attributes relational. Validate JSON shape at the application and, where appropriate, database boundary; choose expression or GIN indexes for actual operators rather than indexing every document by default.

// Store and query JSON data
await pool.query(`
  CREATE TABLE events (
    id SERIAL PRIMARY KEY,
    type VARCHAR(50),
    data JSONB,
    created_at TIMESTAMP DEFAULT NOW()
  )
`);
 
// Insert JSON
await pool.query(
  `INSERT INTO events (type, data) VALUES ($1, $2)`,
  ['user_signup', { userId: 123, source: 'google', plan: 'pro' }]
);
 
// Query inside JSON
const result = await pool.query(`
  SELECT * FROM events
  WHERE data->>'source' = 'google'
  AND (data->>'plan')::text = 'pro'
`);
 
// Index JSON fields for performance
await pool.query(`
  CREATE INDEX idx_events_source ON events ((data->>'source'))
`);

How do you work with PostgreSQL arrays?

PostgreSQL supports array columns for bounded values that belong to one row and do not need their own identity, metadata, referential integrity, or independent updates. Use a junction table when elements are entities or participate in relationships.

Arrays support operators for containment checks, overlaps, and element access, making queries concise and efficient.

// PostgreSQL arrays are powerful
await pool.query(`
  CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255),
    tags TEXT[]
  )
`);
 
// Insert with array
await pool.query(
  `INSERT INTO articles (title, tags) VALUES ($1, $2)`,
  ['PostgreSQL Tips', ['database', 'postgresql', 'backend']]
);
 
// Query arrays
const result = await pool.query(`
  SELECT * FROM articles
  WHERE 'postgresql' = ANY(tags)
`);

What is the difference between PostgreSQL and MySQL?

Both PostgreSQL and MySQL/InnoDB provide ACID transactions, MVCC, CTEs, window functions, JSON, full-text search, replication, and mature Node.js drivers. Neither is universally faster, simpler, or “for reads/writes”: results depend on schema, query mix, configuration, storage, and operations.

Choose PostgreSQL for a requirement it specifically satisfies—such as PostGIS, its extension/type/index ecosystem, or its concurrency semantics—not because the application uses Node.js. Choose MySQL when its existing ecosystem, InnoDB behavior, managed platform, replication/HA model, or team expertise reduces risk. Benchmark representative workload and rehearse backup, restore, failover, and upgrades for either choice.


Error Handling Questions

Robust error handling is essential for production database applications.

How do you handle database connection errors?

Database operations can fail because of deadlines, network partitions, failover, pool saturation, deadlocks, serialization conflicts, constraint violations, or bad SQL. Classify by SQLSTATE and operation semantics; do not label every connection error transient or hide a prolonged outage inside a user request.

Retry only when the operation is idempotent or protected by an idempotency key. After a connection loss, a write outcome may be unknown—the server might have committed before the acknowledgement was lost. Retry serialization failure (40001) or deadlock (40P01) by rerunning the whole transaction with fresh state, not only the last statement. Bound attempts and total deadline, add jitter, and use metrics/backpressure or a circuit breaker during outages.

import { setTimeout as delay } from 'node:timers/promises';
 
// Caller must guarantee that operation is safe to repeat.
async function retryIdempotent(operation, { maxAttempts = 3, signal } = {}) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await operation();
    } catch (err) {
      const retryableConnectionFailure =
        err.code === 'ECONNREFUSED' ||
        err.code === 'ECONNRESET' ||
        err.code === 'ETIMEDOUT' ||
        err.code === '57P01' ||  // admin_shutdown
        err.code === '57P02';    // crash_shutdown
 
      if (!retryableConnectionFailure || attempt === maxAttempts) {
        throw err;
      }
 
      const maxBackoffMs = Math.min(100 * 2 ** (attempt - 1), 1000);
      await delay(Math.random() * maxBackoffMs, undefined, { signal });
    }
  }
}

How do you handle database credentials securely?

Database credentials should never be hardcoded or committed. Environment variables are only a delivery mechanism and can leak through diagnostics, child processes, or operator tooling. Prefer workload identity or a secret manager with short-lived/scoped credentials, rotation, and audit logs. Use a least-privilege database role per service and separate migration privileges from runtime privileges.

For local development, keep .env files out of source control and provide a sanitized example file. In CI/CD, inject secrets at runtime and prevent them from appearing in logs. Configure TLS with CA verification (and hostname verification through Node TLS); encryption with rejectUnauthorized: false is vulnerable to an active man-in-the-middle. Rotate credentials and test pool reconnection behavior after rotation.


Quick Reference

ConceptWhat to Remember
ConnectionUse Pool, not Client for general queries
PoolingBounds and reuses sessions; size across the entire deployment
Parameterized$1, $2 protect values, not identifiers or SQL fragments
TransactionsBEGIN → queries → COMMIT/ROLLBACK, release client
MigrationsReviewed, ordered changes; prefer expand-and-contract
EXPLAIN ANALYZEExecutes SQL; inspect estimates, loops, buffers, I/O and spills
IndexingDesign for measured query shapes and account for write cost
N+1Batch, eager-load, or aggregate with correct pagination semantics
JSONBSemi-structured fields with validation and operator-specific indexes

Official Sources


Frequently Asked Questions

How do you connect to PostgreSQL from Node.js?

node-postgres ('pg') is a common low-level driver. Create one bounded Pool per process for ordinary queries; pool.query() checks out and releases a client automatically. Use pool.connect() when several statements must share one session, especially a transaction. Size all application pools against database capacity, set timeouts, and use TLS with certificate verification rather than merely enabling encryption.

What is connection pooling and why is it important?

A pool reuses established database sessions and bounds concurrent connections. It avoids repeated TCP/TLS/authentication setup, but latency varies by network and provider. node-postgres creates clients on demand up to max and queues checkout requests. Monitor total, idle, and waiting counts; apply request deadlines and backpressure. Do not simply raise max, because every process/replica contributes to PostgreSQL's total concurrency.

How do you prevent SQL injection in Node.js?

Use $1/$2 value parameters and pass values separately. Parameters protect data values, not SQL identifiers, keywords, sort directions, or arbitrary fragments; choose those through strict allowlists or a trusted query builder. Avoid unsafe raw-query ORM APIs, validate limits/types, use a least-privilege database role, and never concatenate untrusted input into SQL structure.

How do you handle transactions in Node.js with PostgreSQL?

Get a dedicated client from the pool, run BEGIN, execute queries, then COMMIT or ROLLBACK. Use try/catch/finally to ensure the client is released back to the pool. For complex transactions, consider using a transaction helper function that handles BEGIN/COMMIT/ROLLBACK automatically. Always release the client in a finally block to prevent connection leaks.

What are database migrations and how do you manage them?

Migrations are ordered, version-controlled schema changes applied once and recorded in a history table. Review generated SQL, serialize runners, test on production-like data, back up and rehearse recovery. Prefer expand-and-contract changes for zero-downtime compatibility. A down migration is not always safe or desirable after data loss or application rollout; use forward fixes and an explicit recovery plan when reversal is destructive.

How do you optimize slow PostgreSQL queries?

Start with workload evidence such as pg_stat_statements, then inspect representative plans. EXPLAIN ANALYZE executes the statement, so use it carefully; include BUFFERS and compare estimates, actual rows, loops, I/O, and temp spills. Sequential scans and nested loops can be optimal. Fix the measured cause through schema/query changes, statistics, an appropriate index, batching, or configuration—not by applying an index or JOIN rewrite mechanically.

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides