You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PostgreSQL+Elixir Phoenix环境下低停机Ecto数据库迁移方案咨询

Low-Downtime Column Addition with Default Value in Ecto (PostgreSQL)

Hey there! Let's walk through a safe, low-downtime approach to add a column with a default value to your ~1000-row table using Ecto for your Phoenix app. This method avoids long table locks and keeps your site available throughout the process.

Key Background: PostgreSQL's Behavior

First, a quick context note: PostgreSQL 11+ doesn’t rewrite all rows when you add a column with a constant default value. Instead, it stores the default as metadata and returns it dynamically for existing rows on read. This makes the initial DDL operation nearly instant. For older PostgreSQL versions (<11), we’ll still use a phased approach to minimize lock time.

Step-by-Step Implementation

1. Add the Column (Nullable with Default)

Start by creating a migration that adds the column as nullable with your desired default value. This avoids an immediate full-table rewrite—critical for low downtime.

defmodule MyApp.Repo.Migrations.AddNewColumnToMyTable do
  use Ecto.Migration

  def change do
    # Replace :new_column, :my_table, :string, and default value with your actual details
    alter table(:my_table) do
      add :new_column, :string, default: "default_value", null: true
    end
  end
end

Run this with mix ecto.migrate—it’ll complete in milliseconds even for 1000 rows, since no row-level updates happen yet.

While PostgreSQL returns the default value for existing rows on read, if you plan to add a NOT NULL constraint later, you’ll need to backfill the column for all existing rows. Do this asynchronously in batches to avoid locking the entire table.

You can create a Mix task for this, or use a background job (like Oban) if you have one set up:

defmodule Mix.Tasks.MyApp.BackfillNewColumn do
  use Mix.Task
  import Ecto.Query
  alias MyApp.Repo
  alias MyApp.MyTable

  @batch_size 100

  def run(_args) do
    Mix.Task.run("app.start")

    from(t in MyTable, where: is_nil(t.new_column))
    |> Repo.stream(max_rows: @batch_size)
    |> Enum.each(fn row ->
      row
      |> MyTable.changeset(%{new_column: "default_value"})
      |> Repo.update!()
    end)

    IO.puts("Backfill complete!")
  end
end

Run this with mix my_app.backfill_new_column. For 1000 rows, this takes just a few seconds, and batched operations won’t block other database traffic.

3. Add NOT NULL Constraint (If Needed)

Once all rows have the column filled, you can safely add the NOT NULL constraint. This is instant because PostgreSQL doesn’t need to recheck every row (we already backfilled them):

defmodule MyApp.Repo.Migrations.MakeNewColumnNotNull do
  use Ecto.Migration

  def change do
    alter table(:my_table) do
      modify :new_column, :string, default: "default_value", null: false
    end
  end
end

Run this with mix ecto.migrate—again, it’ll finish in milliseconds.

Expected Migration Timings

For your ~1000-row table:

  • Step 1: < 1 second (instant DDL in PostgreSQL 11+; even older versions finish quickly for this row count)
  • Step 2: 2-5 seconds (batched updates with minimal locking)
  • Step 3: < 1 second (instant constraint addition)

Total downtime risk is effectively zero, as each step is non-blocking or completes in milliseconds.

Additional Tips

  • If you don’t need the column to be NOT NULL, skip steps 2 and 3—the default value will work for existing rows without any backfill.
  • Always test migrations in a staging environment first to match your exact PostgreSQL version and table characteristics.

内容的提问来源于stack exchange,提问作者relentless

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 08:42:42