Skip to content

Database engineering

Database & Eloquent Engineering

The query work behind a Laravel application that has outgrown its schema - N+1 elimination, indexing from real query plans, and migrations that do not lock a live table.

Almost every Laravel application that becomes slow becomes slow in the database, and almost every one of those was fine in development. A page that runs eleven queries against forty rows runs eleven queries against four million rows the same way, and only one of those is survivable.

Where the time actually goes

Across the engagements we run, the causes cluster into a short list.

An N+1 that only appears in production. The index page eager-loads its relation. The detail page does not, because it only ever loads one record - and then somebody reuses the detail component inside a loop. The query count goes from two to two hundred and nothing in the diff looks wrong.

A relation loaded inside an accessor. getFullAddressAttribute() touches $this->country, the accessor is called during serialisation, and the application issues one query per row while looking like it is formatting a string.

An index that exists and is not used. A column wrapped in a function, an implicit type conversion between a string column and an integer parameter, a leading wildcard on a LIKE. The index is there, EXPLAIN says it is not being read, and everyone trusts the schema instead of the query plan.

Aggregation done in PHP. The collection pipeline is expressive enough that ->get()->groupBy()->map() reads better than the SQL would, so a hundred thousand rows are hydrated into models to produce six numbers.

A migration that locked a live table. Adding an index, adding a column with a default, or changing a column type, on a table large enough that the lock outlasts the request timeout. The deploy succeeded; the site was down.

How we work

Measure first, from the real system. Slow query logs, the query count per endpoint, and EXPLAIN on the queries that matter. Not a profiler on a laptop with a seeded database, which reliably tells you about a problem you do not have.

Fix the cause rather than the symptom. A cache in front of a bad query is a way of paying for the bad query less often. Sometimes that is the right call and we will say so; more often the query wants an index, a join, or to not be issued at run time at all.

Make the fix permanent. Lazy loading disabled outside production, so the N+1 you just removed cannot be reintroduced quietly. A query-count assertion on the endpoints that matter, so a future eager-load removal fails a test rather than a support ticket. Both are a few lines and they are the difference between a fix and a fix that stays.

Schema work on a system that cannot stop

Most of what we are asked to fix is not a query but a shape: a status column that should be a state table, a polymorphic relation that made two rows impossible to constrain, an audit log growing without a retention policy, a table that is doing three jobs because splitting it seemed expensive two years ago.

That work is done in steps that are individually safe:

  1. Add the new structure alongside the old one, with nothing reading it.
  2. Backfill in batches, on a queue, with progress that survives a deploy.
  3. Write to both, read from the old, and reconcile until the difference is zero.
  4. Switch reads. Wait. Then stop writing the old one.
  5. Drop it, in a separate release, once nothing has touched it for a week.

It is more steps than a single migration and it is the difference between a schema change and a scheduled outage.

What you receive

A findings document with each issue traced to the query or the migration that causes it and sized by effort, the fixes as reviewable pull requests, the index changes as migrations that are safe to run on your data volume, and the CI assertions that stop the regressions coming back.

Where the answer is structural rather than a query rewrite, the findings say so and put both numbers side by side: what changing it costs, and what leaving it costs over the next year. You should be reading that in the first week rather than hearing it in a closing call.

Before starting, people usually want to read two things: what a locking schema change costs on a live table, and where MySQL and Postgres actually differ for a Laravel application. If the slow thing turns out to be the request path and not the query, performance is the neighbouring engagement.

Scope and terms

Engagement model
Fixed scope, agreed in writing before work starts. Not a day rate against an open backlog.
Price and timeline
Both are set per project, once the scope is. Quoted together, before anything is built.
What we need from you
One person who can approve decisions, and access to your repository and issue tracker.
Not included
Anything outside the agreed scope. It becomes its own scope rather than a variation order.
Third-party costs
Hosting, licences, API fees and SaaS subscriptions are contracted and paid by you.
Invoicing
Codefacture Yazılım A.Ş., Türkiye. EUR, USD or GBP by bank transfer, with no Turkish VAT on exported services.

Frequently asked questions

Is this the same as performance work?
Overlapping but not the same. Performance and scaling covers the whole request path - caching, queues, workers, the web tier. This is the database specifically, which is where the majority of Laravel slowness turns out to live but not all of it. If you do not know which you have, the audit says so.
Will you replace Eloquent with raw SQL?
Rarely, and never wholesale. Eloquent is not the problem in most slow applications: how it is being used is. Where a report genuinely wants a query builder or raw SQL we will say so, but replacing an ORM to fix an N+1 is treating a habit as an architecture.
Can you work on a database you are not allowed to copy?
Yes, and it is common. We work from query logs, EXPLAIN output and a schema dump with no rows in it. Where we need data shapes rather than data, an anonymised subset is enough.
Do you change the schema on a live system?
Only with a written plan and a rollback, and usually in more steps than it looks like it needs: add, backfill in batches, switch reads, switch writes, drop. A migration that appears to be one line is frequently five deploys when the table is large and the application cannot stop.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page