Skip to content

Commit

Permalink
Moved sections [skip ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
ankane committed Mar 11, 2024
1 parent 7382ac9 commit f30608e
Showing 1 changed file with 71 additions and 71 deletions.
142 changes: 71 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,14 @@ An operation is classified as dangerous if it either:
Potentially dangerous operations:

- [removing a column](#removing-a-column)
- [adding a column with a default value](#adding-a-column-with-a-default-value)
- [backfilling data](#backfilling-data)
- [adding a stored generated column](#adding-a-stored-generated-column)
- [changing the type of a column](#changing-the-type-of-a-column)
- [renaming a column](#renaming-a-column)
- [renaming a table](#renaming-a-table)
- [creating a table with the force option](#creating-a-table-with-the-force-option)
- [adding a check constraint](#adding-a-check-constraint)
- [executing SQL directly](#executing-SQL-directly)
- [backfilling data](#backfilling-data)

Postgres-specific checks:

Expand All @@ -79,6 +78,7 @@ Postgres-specific checks:
- [adding an exclusion constraint](#adding-an-exclusion-constraint)
- [adding a json column](#adding-a-json-column)
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
- [adding a column with a default value](#adding-a-column-with-a-default-value)

Config-specific checks:

Expand Down Expand Up @@ -128,75 +128,6 @@ end
4. Deploy and run the migration
5. Remove the line added in step 1

### Adding a column with a default value

#### Bad

In earlier versions of Postgres, adding a column with a default value to an existing table causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres.

```ruby
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :some_column, :text, default: "default_value"
end
end
```

In Postgres 11+, this no longer requires a table rewrite and is safe (except for volatile functions like `gen_random_uuid()`).

#### Good

Instead, add the column without a default value, then change the default.

```ruby
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
def up
add_column :users, :some_column, :text
change_column_default :users, :some_column, "default_value"
end

def down
remove_column :users, :some_column
end
end
```

See the next section for how to backfill.

### Backfilling data

#### Bad

Active Record creates a transaction around each migration, and backfilling in the same transaction that alters a table keeps the table locked for the [duration of the backfill](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/).

```ruby
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :some_column, :text
User.update_all some_column: "default_value"
end
end
```

Also, running a single query to update data can cause issues for large tables.

#### Good

There are three keys to backfilling safely: batching, throttling, and running it outside a transaction. Use the Rails console or a separate migration with `disable_ddl_transaction!`.

```ruby
class BackfillSomeColumn < ActiveRecord::Migration[7.1]
disable_ddl_transaction!

def up
User.unscoped.in_batches do |relation|
relation.update_all some_column: "default_value"
sleep(0.01) # throttle
end
end
end
```

### Adding a stored generated column

#### Bad
Expand Down Expand Up @@ -397,6 +328,40 @@ class ExecuteSQL < ActiveRecord::Migration[7.1]
end
```

### Backfilling data

#### Bad

Active Record creates a transaction around each migration, and backfilling in the same transaction that alters a table keeps the table locked for the [duration of the backfill](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/).

```ruby
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :some_column, :text
User.update_all some_column: "default_value"
end
end
```

Also, running a single query to update data can cause issues for large tables.

#### Good

There are three keys to backfilling safely: batching, throttling, and running it outside a transaction. Use the Rails console or a separate migration with `disable_ddl_transaction!`.

```ruby
class BackfillSomeColumn < ActiveRecord::Migration[7.1]
disable_ddl_transaction!

def up
User.unscoped.in_batches do |relation|
relation.update_all some_column: "default_value"
sleep(0.01) # throttle
end
end
end
```

### Adding an index non-concurrently

:turtle: Safe by default available
Expand Down Expand Up @@ -666,6 +631,41 @@ class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
end
```

### Adding a column with a default value

#### Bad

In earlier versions of Postgres, adding a column with a default value to an existing table causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres.

```ruby
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :some_column, :text, default: "default_value"
end
end
```

In Postgres 11+, this no longer requires a table rewrite and is safe (except for volatile functions like `gen_random_uuid()`).

#### Good

Instead, add the column without a default value, then change the default.

```ruby
class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
def up
add_column :users, :some_column, :text
change_column_default :users, :some_column, "default_value"
end

def down
remove_column :users, :some_column
end
end
```

Then [backfill the data](#backfilling-data).

### Changing the default value of a column

#### Bad
Expand Down

0 comments on commit f30608e

Please sign in to comment.