Migrations are Laravel's version control for database schemas. Generate one with php artisan make:migration create_posts_table, define schema changes in up(), and put the inverse in down(). Run with php artisan migrate, roll back with migrate:rollback, refresh with migrate:refresh (rollback plus re-run, keeping tables), or use migrate:fresh to drop all tables and re-run from scratch, which is preferred when the schema is broken. Add a column to an existing table with make:migration add_email_to_users_table --table=users, then $table->string('email')->after('name'). Renaming columns worked via the doctrine/dbal package on older Laravel but is native from Laravel 10 onward.
The schema builder exposes column types including string, integer, json, timestamp, softDeletes, uuid, and morphs, plus modifiers such as nullable, default, unique, index, and after. Foreign keys are concise with foreignId('user_id')->constrained()->cascadeOnDelete(), and constrained('users') overrides the inferred table name. Add nullOnDelete(), restrictOnDelete(), or cascadeOnUpdate() for finer control. Composite indexes ($table->unique(['user_id', 'slot'])) accelerate queries that filter by their leftmost columns. Drop columns with dropColumn('is_admin') or rename with renameColumn('old', 'new'). For database features the builder does not expose, fall back to DB::statement() for raw SQL inside migrations, and write the inverse in down().
Seeders populate the database with sample or fixture data, and factories generate model blueprints using Faker. Create a seeder with make:seeder and call other seeders from DatabaseSeeder::run() via $this->call([UserSeeder::class, PostSeeder::class]); run them with php artisan db:seed or combine with migrate --seed for a one-step rebuild. A factory's definition() returns the attribute defaults, and returning another factory as a value (e.g., 'user_id' => User::factory()) automatically creates and links the parent. Define states like suspended() with return $this->state(['suspended_at' => now()]), then chain User::factory()->admin()->suspended()->create(). Use truncate() to remove all rows and reset auto-increment, but note that it cannot be rolled back inside a MySQL transaction.
Laravel's database layer also handles transactions, multiple connections, and query inspection. Wrap a closure in DB::transaction($cb, $attempts) to roll back automatically on any exception and optionally retry on serialization failures (deadlocks). For more granular control, use DB::beginTransaction(), commit(), and rollBack() manually, and defer side effects until commit with DB::afterCommit(fn() => ...). Configure additional connections in config/database.php and access them via DB::connection('pgsql') or set \(connection on a model. Read/write splits use nested arrays under the connection key. To debug query traffic, register DB::listen(fn (\)q) => logger()->info($q->sql, $q->bindings)) in a service provider during development. The DB facade also supports raw table queries (DB::table('users')->where(...)->get()) returning stdClass, plus UNIONs via DB::table('b')->union($q)->get() and subqueries through addSelect(['post_count' => DB::table(...)->selectRaw('count(*)')->whereColumn(...)]).