M
MeshWorld.
Laravel Database Migrations PHP 6 min read

Drop Composite Indexes in Laravel Migrations

Vishnu
By Vishnu
| Updated: Mar 27, 2026

dropIndex() removes a composite index from a table in a Laravel migration. You pass either the column array that defines the index or the auto-generated index name. Laravel figures out which index to drop either way. This is the counterpart to creating composite indexes — same migration, opposite direction.

:::note[TL;DR]

  • $table->dropIndex(['col1', 'col2']) — drop by passing the column array
  • $table->dropIndex('table_col1_col2_index') — drop by the auto-generated name
  • Laravel generates index names as {table}_{columns}_index
  • Put the drop logic in the down() method so migrate:rollback cleans up correctly
  • Use anonymous class syntax (return new class extends Migration) in Laravel 9+ :::

How does Laravel name a composite index automatically?

When you create an index with $table->index(['email', 'phone_number']), Laravel generates the name automatically. The pattern is:

{table_name}_{column1}_{column2}_index

For a users table with email and phone_number:

users_email_phone_number_index

That name is what Laravel uses internally and what you’d pass to dropIndex() by name. You can verify any index name with SHOW INDEXES FROM users in MySQL, or by checking the migration output.

The scenario: You added a composite index three months ago during a performance fix. Now the query pattern changed and the index is actually slowing down inserts. You need to drop it. The column-array form of dropIndex() is safest here — you don’t have to remember or look up the generated name.

How do you drop a composite index by column array?

Pass the same column array you used when creating the index. Laravel reconstructs the generated name from the table name and column list:

$table->dropIndex(['email', 'phone_number']);

This is the preferred form. If you rename the table or add the migration to a different database, the reconstructed name stays correct.

How do you drop a composite index by name?

Pass the full index name as a string. Useful when the index was created with a custom name or when you’re working with a database you didn’t create:

$table->dropIndex('users_email_phone_number_index');

Both forms call the same underlying logic. The string form is just more explicit about which index you’re targeting.

What does a complete migration look like?

This migration adds a composite index on email and phone_number in the up() method and drops it in down(). The down() method is what runs during php artisan migrate:rollback.

The anonymous class format is used here — this is standard in Laravel 9 and required in Laravel 12’s generated migrations:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            // Creates the composite index users_email_phone_number_index
            $table->index(['email', 'phone_number']);
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            // Drops the same composite index
            $table->dropIndex(['email', 'phone_number']);
        });
    }
};

Run the migration:

php artisan migrate

Roll it back:

php artisan migrate:rollback

Laravel handles the index name generation in both directions. You don’t specify the name anywhere — you just describe the columns.

:::warning Older Laravel tutorials show named migration classes like class AlterUsersTable extends Migration. That syntax still works in Laravel 12, but php artisan make:migration now generates anonymous classes by default. New migrations should use return new class extends Migration to stay consistent with the generated format. :::

What if the index has a custom name?

If the index was originally created with an explicit name, you must drop it by that name:

// How it was created
$table->index(['email', 'phone_number'], 'users_contact_lookup');

// How to drop it
$table->dropIndex('users_contact_lookup');

Passing the column array here won’t work — Laravel would reconstruct the auto-generated name, which doesn’t match the custom one. Use the string form when the index name was explicitly set.

What’s the difference between dropIndex() and dropUnique()?

Both methods remove an index, but they’re semantically different:

  • dropIndex() removes a regular (non-unique) index
  • dropUnique() removes a unique constraint (which is also an index)

If you created the index with $table->unique(['email', 'phone_number']), drop it with $table->dropUnique(['email', 'phone_number']). Using dropIndex() on a unique index will cause a database error.

The scenario: You’re reviewing a failed rollback in CI. The error says the index doesn’t exist. Check whether the original migration used ->index() or ->unique(). If it was ->unique(), your down() method needs dropUnique(), not dropIndex().

See Create Composite Indexes with Migration in Laravel for the full creation-side reference — including how to create named indexes and unique composite constraints.

Summary

  • dropIndex(['col1', 'col2']) reconstructs the auto-generated name from the table and column list. Use this by default.
  • dropIndex('custom_name') drops by explicit name — required when the index was created with a custom name.
  • Auto-generated names follow the pattern {table}_{col1}_{col2}_index.
  • Use dropUnique() for unique composite constraints. Using dropIndex() on them errors.
  • Anonymous class syntax is the standard in Laravel 9+ and all new Laravel 12 migrations.

FAQ

What happens if I call dropIndex() and the index doesn’t exist? You get a database exception. Guard against it with if ($sm->hasIndex('users', 'users_email_phone_number_index')) before dropping, or use a try/catch if you’re writing a defensive rollback.

Can I drop multiple indexes in one migration? Yes. Call dropIndex() multiple times inside the same Schema::table() closure. Each call is a separate operation.

Does dropping an index affect the data in the table? No. Indexes are separate structures. Dropping one only removes the performance optimization — no rows are changed or deleted.

How do I find the name of an existing index in my database? In MySQL: SHOW INDEXES FROM table_name;. In PostgreSQL: \d table_name in psql. In Laravel, you can also use DB::select("SHOW INDEXES FROM users") to inspect programmatically.

Do I need to drop indexes before dropping columns? Yes, in most databases. If a column is part of an index, you must drop the index first, then drop the column. Laravel doesn’t handle this automatically — a migration that drops a column still covered by an index will fail.