Generate background migration modules for creating, dropping, or renaming database indexes on large tables using the Explorer.Migrator.HeavyDbIndexOperation framework. Automatically updates the BackgroundMigrations cache module with proper tracking. These migrations run in the background with progress tracking and dependency management. Use this skill for requests on creating background migrations to delete / create / rename indexes on large tables (logs, internal_transactions, token_transfers, addresses, transactions, blocks, etc.) to avoid blocking the database.
Install
npx skillscat add blockscout/blockscout/heavy-db-index-operation Install via the SkillsCat registry.
Overview
The heavy-db-index-operation skill helps you generate migration modules that create, drop, or rename database indexes on large tables in a controlled, non-blocking manner. These operations use the Explorer.Migrator.HeavyDbIndexOperation behavior and are tracked via Explorer.Migrator.MigrationStatus.
What this skill generates:
- Migration module files (create/drop/rename) in
apps/explorer/lib/explorer/migrator/heavy_db_index_operation/ - Updates to
apps/explorer/lib/explorer/chain/cache/background_migrations.ex:- Cache keys for tracking completion status
- Module aliases
- Fallback handlers for cache population
When to Use
- When creating new indexes on large tables (logs, internal_transactions, token_transfers, addresses, transactions, blocks, etc.)
- When dropping existing indexes as part of schema optimization
- When renaming indexes (typically as the final step in a create → drop → rename workflow)
- When the index operation might take significant time and should run in the background
- When you need to track the progress of index creation/deletion/rename
- When index operations need to depend on other completed migrations
- When you want CONCURRENT index operations on PostgreSQL
Module Structure
Each heavy index operation module must implement the Explorer.Migrator.HeavyDbIndexOperation behavior with these callbacks:
Required Callbacks
migration_name/0- Automatically generated fromoperation_typeandindex_name. Format:heavy_indexes_{operation_type}_{lowercase_index_name}table_name/0- Returns the table atom (:logs,:internal_transactions,:addresses, etc.)operation_type/0- Returns:create,:drop, or:renameindex_name/0- Returns the index name as a string (for renames, return the final/new index name)dependent_from_migrations/0- Returns list of migration names this depends on (or[])db_index_operation/0- Executes the actual index creation/deletion/renamecheck_db_index_operation_progress/0- Checks operation progressdb_index_operation_status/0- Returns operation statusrestart_db_index_operation/0- Restarts the operation if neededrunning_other_heavy_migration_exists?/1- Checks for conflicting migrationsupdate_cache/0- Updates the BackgroundMigrations cache when migration completes
Index Definition Methods
Method 1: Using @table_columns (Simple Indexes)
Use this for straightforward indexes on one or more columns:
@table_columns ["address_hash", "block_number DESC", "index DESC"]
@impl HeavyDbIndexOperation
def db_index_operation do
HeavyDbIndexOperationHelper.create_db_index(@index_name, @table_name, @table_columns)
endWhen to use:
- Simple multi-column indexes
- No WHERE clause needed
- Standard column ordering acceptable
- Most common use case
Method 2: Using @query_string (Complex Indexes)
Use this for more complex index definitions:
@query_string """
CREATE INDEX #{HeavyDbIndexOperationHelper.add_concurrently_flag?()} IF NOT EXISTS "#{@index_name}"
ON #{@table_name} ((1))
WHERE verified = true;
"""
@impl HeavyDbIndexOperation
def db_index_operation do
HeavyDbIndexOperationHelper.create_db_index(@query_string)
endWhen to use:
- Partial indexes (with WHERE clause)
- Expression indexes (e.g.,
((1))for existence check) - Custom index types (GIN, GIST, etc.)
- Fine-grained control over SQL
Naming Conventions
Module Names
Creation:
CreateTableNameColumnNameIndex- Example:
CreateLogsAddressHashBlockNumberDescIndexDescIndex - Example:
CreateAddressesVerifiedIndex
- Example:
Deletion:
DropTableNameIndexName- Example:
DropInternalTransactionsCreatedContractAddressHashPartialIndex - Example:
DropLogsAddressHashIndex
- Example:
Renaming:
RenameOldIndexNameToNewIndexNameorRenameTableNameIndexDescriptor- Example:
RenameTransactions2ndCreatedContractAddressHashWithPendingIndexA
- Example:
Index Names
- Follow PostgreSQL naming:
table_name_column_name_suffix_index - For partial indexes: include
_partialin the name - For descending columns: include
_descin the name - Examples:
logs_address_hash_block_number_DESC_index_DESC_indexaddresses_verified_indexinternal_transactions_created_contract_address_hash_partial_index
File Names
- Convert module name to snake_case
- Example:
CreateLogsAddressHashIndex→create_logs_address_hash_index.ex
Dependencies via dependent_from_migrations/0
Specify migrations that must complete before this one runs:
# No dependencies
@impl HeavyDbIndexOperation
def dependent_from_migrations, do: []
# Depends on another migration
alias Explorer.Migrator.EmptyInternalTransactionsData
@impl HeavyDbIndexOperation
def dependent_from_migrations do
[EmptyInternalTransactionsData.migration_name()]
end
# Multiple dependencies
alias Explorer.Migrator.HeavyDbIndexOperation.{
DropLogsIndexIndex,
DropLogsBlockNumberAscIndexAscIndex
}
@impl HeavyDbIndexOperation
def dependent_from_migrations do
[
DropLogsIndexIndex.migration_name(),
DropLogsBlockNumberAscIndexAscIndex.migration_name()
]
endComplete Example: Creating an Index
defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateLogsAddressHashBlockNumberDescIndexDescIndex do
@moduledoc """
Create B-tree index `logs_address_hash_block_number_DESC_index_DESC_index` on `logs` table
for (`address_hash`, `block_number DESC`, `index DESC`) columns.
"""
use Explorer.Migrator.HeavyDbIndexOperation
require Logger
alias Explorer.Chain.Cache.BackgroundMigrations
alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus}
alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper
@table_name :logs
@index_name "logs_address_hash_block_number_DESC_index_DESC_index"
@operation_type :create
@table_columns ["address_hash", "block_number DESC", "index DESC"]
@impl HeavyDbIndexOperation
def table_name, do: @table_name
@impl HeavyDbIndexOperation
def operation_type, do: @operation_type
@impl HeavyDbIndexOperation
def index_name, do: @index_name
@impl HeavyDbIndexOperation
def dependent_from_migrations, do: []
@impl HeavyDbIndexOperation
def db_index_operation do
HeavyDbIndexOperationHelper.create_db_index(@index_name, @table_name, @table_columns)
end
@impl HeavyDbIndexOperation
def check_db_index_operation_progress do
operation = HeavyDbIndexOperationHelper.create_index_query_string(@index_name, @table_name, @table_columns)
HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, operation)
end
@impl HeavyDbIndexOperation
def db_index_operation_status do
HeavyDbIndexOperationHelper.db_index_creation_status(@index_name)
end
@impl HeavyDbIndexOperation
def restart_db_index_operation do
HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name)
end
@impl HeavyDbIndexOperation
def running_other_heavy_migration_exists?(migration_name) do
MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name)
end
@impl HeavyDbIndexOperation
def update_cache do
BackgroundMigrations.set_heavy_indexes_create_logs_address_hash_block_number_desc_index_desc_index_finished(
true
)
end
endComplete Example: Dropping an Index
defmodule Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsCreatedContractAddressHashPartialIndex do
@moduledoc """
Drops index "internal_transactions_created_contract_address_hash_partial_index" on
internal_transactions(created_contract_address_hash, block_number DESC, transaction_index DESC, index DESC).
"""
use Explorer.Migrator.HeavyDbIndexOperation
alias Explorer.Chain.Cache.BackgroundMigrations
alias Explorer.Migrator.{EmptyInternalTransactionsData, HeavyDbIndexOperation, MigrationStatus}
alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper
@table_name :internal_transactions
@index_name "internal_transactions_created_contract_address_hash_partial_index"
@operation_type :drop
@impl HeavyDbIndexOperation
def table_name, do: @table_name
@impl HeavyDbIndexOperation
def operation_type, do: @operation_type
@impl HeavyDbIndexOperation
def index_name, do: @index_name
@impl HeavyDbIndexOperation
def dependent_from_migrations do
[EmptyInternalTransactionsData.migration_name()]
end
@impl HeavyDbIndexOperation
def db_index_operation do
HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name)
end
@impl HeavyDbIndexOperation
def check_db_index_operation_progress do
operation = HeavyDbIndexOperationHelper.drop_index_query_string(@index_name)
HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, operation)
end
@impl HeavyDbIndexOperation
def db_index_operation_status do
HeavyDbIndexOperationHelper.db_index_dropping_status(@index_name)
end
@impl HeavyDbIndexOperation
def restart_db_index_operation do
HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name)
end
@impl HeavyDbIndexOperation
def running_other_heavy_migration_exists?(migration_name) do
MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name)
end
@impl HeavyDbIndexOperation
def update_cache do
BackgroundMigrations.set_heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished(
true
)
end
endSupported Table Names
Valid values for @table_name (from behavior typespec):
:addresses:address_coin_balances:address_current_token_balances:address_token_balances:blocks:internal_transactions:logs:token_transfers:tokens:transactions
File Location
All generated modules must be placed in:
apps/explorer/lib/explorer/migrator/heavy_db_index_operation/Required Aliases
Standard aliases to include:
alias Explorer.Chain.Cache.BackgroundMigrations
alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus}
alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelperFor dependencies, add specific aliases:
alias Explorer.Migrator.EmptyInternalTransactionsDataHelper Functions Available
For Index Creation:
HeavyDbIndexOperationHelper.create_db_index/1- With query stringHeavyDbIndexOperationHelper.create_db_index/3- With index name, table, columnsHeavyDbIndexOperationHelper.create_index_query_string/3- Generate query stringHeavyDbIndexOperationHelper.db_index_creation_status/1- Check creation statusHeavyDbIndexOperationHelper.add_concurrently_flag?/0- For CONCURRENT keyword
For Index Deletion:
HeavyDbIndexOperationHelper.safely_drop_db_index/1- Drop with safety checksHeavyDbIndexOperationHelper.drop_index_query_string/1- Generate drop queryHeavyDbIndexOperationHelper.db_index_dropping_status/1- Check dropping status
Common:
HeavyDbIndexOperationHelper.check_db_index_operation_progress/2- Monitor progress
Cache Invalidation
When dropping indexes, you may need to invalidate caches as shown in the module docstring:
@impl HeavyDbIndexOperation
def restart_db_index_operation do
HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name)
BackgroundMigrations.invalidate_cache(__MODULE__.migration_name())
endComplete Example: Renaming an Index
For rename operations (typically used after create + drop to swap indexes):
defmodule Explorer.Migrator.HeavyDbIndexOperation.RenameTransactions2ndCreatedContractAddressHashWithPendingIndexA do
@moduledoc """
Renames index "transactions_2nd_created_contract_address_hash_with_pending_index_a"
to "transactions_created_contract_address_hash_with_pending_index_a".
"""
use Explorer.Migrator.HeavyDbIndexOperation
require Logger
alias Explorer.Chain.Cache.BackgroundMigrations
alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus}
alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper
alias Explorer.Migrator.HeavyDbIndexOperation.DropTransactionsCreatedContractAddressHashWithPendingIndexA
alias Explorer.Repo
@table_name :transactions
@old_index_name "transactions_2nd_created_contract_address_hash_with_pending_index_a"
@new_index_name "transactions_created_contract_address_hash_with_pending_index_a"
@operation_type :rename
# Note: migration_name will be:
# "heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a"
@impl HeavyDbIndexOperation
def table_name, do: @table_name
@impl HeavyDbIndexOperation
def operation_type, do: @operation_type
@impl HeavyDbIndexOperation
def index_name, do: @new_index_name
@impl HeavyDbIndexOperation
def dependent_from_migrations do
[DropTransactionsCreatedContractAddressHashWithPendingIndexA.migration_name()]
end
@impl HeavyDbIndexOperation
# sobelow_skip ["SQL"]
def db_index_operation do
case Repo.query(rename_index_query_string(), [], timeout: :infinity) do
{:ok, _} ->
:ok
{:error, error} ->
Logger.error("Failed to rename index from #{@old_index_name} to #{@new_index_name}: #{inspect(error)}")
:error
end
end
@impl HeavyDbIndexOperation
def check_db_index_operation_progress do
HeavyDbIndexOperationHelper.check_db_index_operation_progress(@new_index_name, rename_index_query_string())
end
@impl HeavyDbIndexOperation
def db_index_operation_status do
old_index_status = HeavyDbIndexOperationHelper.db_index_exists_and_valid?(@old_index_name)
new_index_status = HeavyDbIndexOperationHelper.db_index_exists_and_valid?(@new_index_name)
cond do
# Rename completed: old index doesn't exist, new index exists and is valid
old_index_status == %{exists?: false, valid?: nil} and new_index_status == %{exists?: true, valid?: true} ->
:completed
# Rename not started: old index exists, new index doesn't exist
old_index_status == %{exists?: true, valid?: true} and new_index_status == %{exists?: false, valid?: nil} ->
:not_initialized
# Unknown state
true ->
:unknown
end
end
@impl HeavyDbIndexOperation
def restart_db_index_operation do
# To restart, we need to rename back to the old name
case Repo.query(reverse_rename_index_query_string(), [], timeout: :infinity) do
{:ok, _} ->
:ok
{:error, error} ->
Logger.error("Failed to reverse rename index from #{@new_index_name} to #{@old_index_name}: #{inspect(error)}")
:error
end
end
@impl HeavyDbIndexOperation
def running_other_heavy_migration_exists?(migration_name) do
MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name)
end
@impl HeavyDbIndexOperation
def update_cache do
BackgroundMigrations.set_heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a_finished(
true
)
end
defp rename_index_query_string do
"ALTER INDEX #{@old_index_name} RENAME TO #{@new_index_name};"
end
defp reverse_rename_index_query_string do
"ALTER INDEX #{@new_index_name} RENAME TO #{@old_index_name};"
end
endWhen to use rename operations:
- After creating a new index and dropping an old one
- To swap temporary index names with permanent ones
- Part of a create → drop → rename workflow for index replacement
Important notes for rename operations:
- Use
@operation_type :rename(not:create) index_name/0should return the new (final) index name- The migration name will be
heavy_indexes_rename_{new_index_name_lowercase} - Example: For
index_name= "transactions_created_contract_address_hash_with_pending_index_a", the migration name is "heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a"
Updating BackgroundMigrations Cache
After creating migration modules, you must update the cache tracking inapps/explorer/lib/explorer/chain/cache/background_migrations.ex:
Step 1: Add Cache Keys
Add keys for each new migration at the top of the module:
use Explorer.Chain.MapCache,
name: :background_migrations_status,
# ... existing keys ...
key: :heavy_indexes_create_transactions_2nd_created_contract_address_hash_with_pending_index_a_finished,
key: :heavy_indexes_drop_transactions_created_contract_address_hash_with_pending_index_a_finished,
key: :heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a_finishedStep 2: Add Module Aliases
Add aliases in the HeavyDbIndexOperation alias block:
alias Explorer.Migrator.HeavyDbIndexOperation.{
# ... existing aliases ...
CreateTransactions2ndCreatedContractAddressHashWithPendingIndexA,
DropTransactionsCreatedContractAddressHashWithPendingIndexA,
RenameTransactions2ndCreatedContractAddressHashWithPendingIndexA
}Step 3: Add Fallback Handlers
Add handle_fallback/1 functions for each migration:
defp handle_fallback(:heavy_indexes_create_transactions_2nd_created_contract_address_hash_with_pending_index_a_finished) do
set_and_return_migration_status(
CreateTransactions2ndCreatedContractAddressHashWithPendingIndexA,
&set_heavy_indexes_create_transactions_2nd_created_contract_address_hash_with_pending_index_a_finished/1
)
end
defp handle_fallback(:heavy_indexes_drop_transactions_created_contract_address_hash_with_pending_index_a_finished) do
set_and_return_migration_status(
DropTransactionsCreatedContractAddressHashWithPendingIndexA,
&set_heavy_indexes_drop_transactions_created_contract_address_hash_with_pending_index_a_finished/1
)
end
defp handle_fallback(:heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a_finished) do
set_and_return_migration_status(
RenameTransactions2ndCreatedContractAddressHashWithPendingIndexA,
&set_heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a_finished/1
)
endCache key naming convention:
- Format:
heavy_indexes_{operation}_{snake_case_index_name}_finished - Operation:
create,drop,rename, etc. - Always ends with
_finished
Step 4: Add to Application Supervisor
Add each migration module to the application supervisor inapps/explorer/lib/explorer/application.ex:
Find the section with other heavy DB index operations and add:
configure_mode_dependent_process(
Explorer.Migrator.HeavyDbIndexOperation.CreateTransactions2ndCreatedContractAddressHashWithPendingIndexA,
:indexer
),
configure_mode_dependent_process(
Explorer.Migrator.HeavyDbIndexOperation.DropTransactionsCreatedContractAddressHashWithPendingIndexA,
:indexer
),
configure_mode_dependent_process(
Explorer.Migrator.HeavyDbIndexOperation.RenameTransactions2ndCreatedContractAddressHashWithPendingIndexA,
:indexer
),Important: These entries must be added to start the migration processes during application startup.
Update Cache Implementation
Each migration module must implement update_cache/0:
@impl HeavyDbIndexOperation
def update_cache do
BackgroundMigrations.set_heavy_indexes_create_my_index_finished(true)
endThe setter function name follows: set_heavy_indexes_{operation}_{index_name}_finished/1
Checklist for New Modules
- Module name follows
Create*/Drop*/Rename*convention - File name is snake_case version of module name
-
@moduledocdescribes the index and its columns -
use Explorer.Migrator.HeavyDbIndexOperationdeclared near module top - All required callbacks implemented
-
@table_name,@index_name,@operation_typemodule attributes defined - Index definition uses
@table_columnsOR@query_string(or custom for rename) - Dependencies specified via
dependent_from_migrations/0 - Proper aliases added at module top
- File saved in
apps/explorer/lib/explorer/migrator/heavy_db_index_operation/ -
update_cache/0implemented with correct setter name - BackgroundMigrations cache updated with key, alias, and fallback handler
- Application.ex updated with
configure_mode_dependent_processentry
Common Pitfalls
❌ Incorrect table name - Must be one of the supported atoms
❌ Missing dependencies - If index depends on other migrations, specify them
❌ Wrong helper function - Use creation helpers for :create, dropping helpers for :drop
❌ Inconsistent naming - Index name should match module name semantically
❌ Missing CONCURRENT - Use add_concurrently_flag?() in query strings
❌ No progress tracking - Always implement check_db_index_operation_progress/0
❌ Forgot cache updates - Must update BackgroundMigrations cache module
❌ Missing update_cache/0 - Every module must implement this callback
Workflow for Index Replacement (Create → Drop → Rename)
When replacing an existing index with a new version (e.g., adding a WHERE clause):
- Create the new index with a temporary name (e.g.,
_2nd_prefix)- Depends on: latest heavy DB operation on the table
- Drop the old index
- Depends on: the create operation completing
- Rename the new index to the old index name
- Depends on: the drop operation completing
This ensures zero downtime - the old index remains available until the new one is ready.