В итоге сделал view в mysql, содержащий нужные данные (заодно переделал exists на join, потому что судя по explain так чуть лучше используются индексы, хотя, конечно, надо в боевых условиях сравнивать):
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$prefix = DB::getTablePrefix();
$agreementTypesTable = "agreement_types";
$profilesTable = "profiles";
$agreementProfileTable = "agreement_profile";
$sqlQuery = DB::table($agreementTypesTable)
->select([
"$agreementTypesTable.id as agreement_type_id",
"$agreementTypesTable.last_agreement_id as last_agreement_id",
"$profilesTable.id as profile_id",
DB::raw("!ISNULL($agreementProfileTable.profile_id) as last_agreement_approved"),
])
->join($profilesTable, DB::raw('1'), '=', DB::raw('1'))
->leftJoin($agreementProfileTable, function (JoinClause $join) use ($agreementProfileTable, $agreementTypesTable, $profilesTable) {
$join->on("$agreementProfileTable.agreement_id", "=", "$agreementTypesTable.last_agreement_id")
->on("$agreementProfileTable.profile_id", "=", "$profilesTable.id");
})
->toSql();
DB::statement("CREATE VIEW ${prefix}agreement_type_profile AS ($sqlQuery)");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$prefix = DB::getTablePrefix();
DB::statement("DROP VIEW ${prefix}agreement_type_profile");
}
};