Migraciones de Laravel: añadir una columna, revertir y eliminar

Introducción Añadir columnas o tablas a mano a tu base de datos puede resultar intimidante y, la mayoría de las veces, acaba provocando incoherencias entre tus distintos entornos. Las migraciones de Laravel te permiten versionar tu base de datos para que todos los miembros de tu equipo dispongan de un

Migraciones de Laravel: añadir una columna, revertir y eliminar
Respuesta rápida

Una migración se elimina borrando su archivo, pero solo si nunca se ha ejecutado. Compruébalo con php artisan migrate:status: si está en Ran, lanza primero php artisan migrate:rollback --step=1 y borra el archivo después. Para añadir una columna a una tabla existente, no modifiques nunca la migración antigua: crea una nueva con --table.

Las migraciones versionan el esquema de tu base igual que Git versiona tu código: cada cambio es un archivo, reproducible en cualquier entorno. Tres operaciones cubren casi todo el día a día: añadir una columna a una tabla que ya existe, revertir una migración y eliminar un archivo que nunca se debería haber creado.

Artículo del itinerario Desarrollo web. Todos los comandos y las salidas que verás a continuación se han ejecutado en Laravel 13.30.1 con PHP 8.4.25.

Crear una migración

El comando sigue una convención de nombres que determina el contenido del archivo generado:

bash
php artisan make:migration create_tasks_table

El nombre empieza por create, seguido del nombre de la tabla en plural, seguido de table. Laravel reconoce este patrón y rellena de antemano Schema::create() con id() y timestamps(), la columna title se añade aquí para el ejemplo, y se han quitado los comentarios generados:

database/migrations/2026_09_02_100000_create_tasks_table.php
<?php

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::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};
Si tus migraciones no se parecen a esto

Desde Laravel 8, las migraciones son clases anónimas devueltas por return new class extends Migration. Las antiguas migraciones con nombre, del tipo class CreateTasksTable extends Migration, siguen funcionando pero ya no se generan. Del mismo modo, $table->bigIncrements('id') ha dejado paso a $table->id(), y los métodos up() y down() declaran ahora un tipo de retorno void.

Si la tabla tiene que llevar fechas de creación y de modificación, $table->timestamps() las añade: sin ellas aparece el error «Unknown column ‘updated_at’».

Ejecutar la migración crea la tabla:

bash
php artisan migrate

Añadir una columna a una tabla existente

Una vez montado el esquema, las consultas Eloquent habituales hacen el resto. No modifiques nunca una migración que ya se ha ejecutado: tus compañeros y tus servidores la han lanzado y no la van a volver a lanzar. Crea una migración nueva, indicando la tabla afectada con --table:

bash
php artisan make:migration add_notes_to_tasks_table --table=tasks

El archivo generado contiene un Schema::table() vacío en los dos sentidos. Rellénalos: la columna en up() y su eliminación en down().

database/migrations/2026_09_02_194036_add_notes_to_tasks_table.php
public function up(): void
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->text('notes')->nullable()->after('title');
    });
}

public function down(): void
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->dropColumn('notes');
    });
}
bash
php artisan migrate
code
  2026_09_02_194036_add_notes_to_tasks_table .................... 47.43ms DONE

Aquí conviene recordar dos precauciones.

Haz que la columna sea nullable, o dale un valor por defecto. En una tabla que ya contiene filas, una columna NOT NULL sin valor por defecto hace fallar la migración: la base no sabe qué escribir en las filas existentes.

Escribe siempre el down(). Es lo que hace reversible la migración. Una migración sin down() convierte cualquier marcha atrás en una intervención manual.

after() no funciona en todas partes

Colocar una columna con after() es propio de MySQL y MariaDB. En SQLite la instrucción se acepta sin error, pero se ignora: la columna se añade al final de la tabla. Verificado en SQLite 3.46.1, donde notes acaba después de updated_at pese al after('title'). El orden de las columnas no tiene ninguna consecuencia funcional.

Revertir una migración ya ejecutada

Empieza por mirar en qué punto estás. migrate:status lista cada migración con su lote y su estado:

bash
php artisan migrate:status
code
  2026_09_02_100000_create_tasks_table ............................... [1] Ran
  2026_09_02_194036_add_notes_to_tasks_table ......................... Pending

Una migración Pending no se ha ejecutado nunca: su archivo se puede borrar directamente. Una migración Ran hay que revertirla antes.

bash
# revierte solo la última migración
php artisan migrate:rollback --step=1

# revierte todo el último lote
php artisan migrate:rollback

El rollback ejecuta el método down(). En el ejemplo anterior, la columna notes desaparece efectivamente de la tabla y la migración vuelve a Pending. Es en ese momento, y no antes, cuando puedes borrar el archivo.

Existen dos comandos vecinos, reservados al desarrollo:

bash
# revierte todo y lo vuelve a ejecutar todo
php artisan migrate:refresh

# elimina todas las tablas y luego lo vuelve a ejecutar todo
php artisan migrate:fresh
migrate:fresh borra los datos

migrate:fresh elimina las tablas, incluidas las que no gestiona ninguna migración. En un servidor de producción, el comando destruye la base sin confirmación posible una vez lanzado. Resérvalo estrictamente al desarrollo local.

Eliminar una migración

No existe ningún make:migration --delete: una migración se elimina borrando su archivo, en database/migrations, la carpeta que devuelve database_path() entre las rutas de la aplicación. La única pregunta es si ya se ha ejecutado.

Nunca se ha ejecutado (estado Pending): borra el archivo, y nada más.

bash
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.php

Ya se ha ejecutado (estado Ran): reviértela primero y luego borra el archivo.

bash
php artisan migrate:rollback --step=1
rm database/migrations/2026_09_02_194036_add_notes_to_tasks_table.php
Si la migración ya ha salido a otro entorno

Borrar el archivo no elimina la fila correspondiente en la tabla migrations de las bases donde ya se ha ejecutado. Esos entornos conservarán la columna, sin ninguna migración que la explique. En ese caso no borres nada: escribe una migración nueva que deshaga el cambio. El principio es el mismo que con Git, donde se prefiere un commit de reversión a reescribir un historial ya publicado.

Modificar una columna existente

Para cambiar un tipo, una longitud o la nulabilidad, vuelve a declarar la columna y añade change():

php
public function up(): void
{
    Schema::table('tasks', function (Blueprint $table) {
        $table->string('title', 500)->change();
    });
}

Buena noticia para quien vuelva de una versión antigua: el paquete doctrine/dbal, obligatorio durante mucho tiempo para esta operación, ya no lo es desde Laravel 11. Verificado en Laravel 13.30.1, donde change() funciona sin ninguna dependencia adicional.

change() reescribe toda la definición

Los atributos que no repitas se pierden. Una columna ->nullable()->default('x') modificada con un simple ->string('title', 500)->change() vuelve a ser no nullable y sin valor por defecto. Vuelve a declarar la definición completa cada vez.

Errores frecuentes

Modificar una migración ya ejecutada Los demás entornos no la volverán a lanzar y conservarán el esquema antiguo. Crea siempre una migración nueva.
Columna NOT NULL sin valor por defecto En una tabla que ya contiene filas, la migración falla: la base no sabe qué escribir en lo existente. Añade nullable() o default().
Olvidar el método down() La migración se vuelve irreversible y cualquier marcha atrás pasa por una intervención manual.
migrate:fresh en producción El comando elimina todas las tablas, incluidas las que no gestiona ninguna migración, y sin confirmación.
after() ignorado fuera de MySQL En SQLite la columna se añade al final de la tabla sin error. No tiene efecto funcional, pero el esquema no se corresponde con lo que está escrito.
change() reescribe toda la definición Los atributos que no se repiten se pierden: una columna nullable con valor por defecto los pierde si no vuelves a declararlos.

LaravelMigrationsPHPSQL

Damien Flandrin Desarrollador web desde 2010, creador de Gekkode y de Email Impact. Cada artículo se prueba en un proyecto real antes de publicarse. Contacto
Newsletter

Las nuevas pruebas, tutoriales y proyectos, por correo.

Pruebas reproducibles, código versionado, resultados fechados. Nunca spam.