agentsclimarketplace

Eloquent performance

Skill HamzaAlayed/laravel-claude-agents/skills/eloquent-performance

The query-performance cookbook — reading EXPLAIN plans, diagnosing + fixing N+1 without overfetching, pagination and chunking choices, the caching decision tree (remember vs flexible vs lock), measurement discipline. Use when an endpoint or query is slow, before adding an index or cache, or when reviewing query-shape changes. Diagnosis recipes — the fix lands with the owning builder (backend / database developer).From its SKILL.md

Install
npx -y skills add HamzaAlayed/laravel-claude-agents --skill eloquent-performance

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

4.5 KB, as published. Nobody here has run it

Eloquent + Query Performance

Measure first. A number without a baseline is a guess; p95/p99, not mean. Reproduce under realistic volume — a plan on 100 rows lies about 10M. One probe decides run-vs-static: no vendor/ (or the first bootstrap fails) → static analysis only, declared as such — never retry-flail an unrunnable app.

Reading EXPLAIN

EXPLAIN ANALYZE <query> (MySQL 8+ / Postgres). Red flags, in order of blood pressure:

  • rows examined ≫ rows returned — the query scans and discards; index or rewrite.
  • type=ALL / Seq Scan on a large table — full scan. Check: is there an index the planner can't use? Function-on-column (WHERE DATE(created_at) = …) and leading-wildcard LIKE '%x' kill index use — rewrite as a range (created_at BETWEEN …).
  • filesort / temporary table — sort not served by an index; composite index matching WHERE + ORDER BY order.
  • Postgres: Rows Removed by Filter high → same discard problem; check work_mem spills on sorts/hashes.

Index handoff to database-developer: the EXPLAIN plan, the query pattern + frequency, and the target verdict — never "add an index" bare.

N+1

  • Detect: Model::preventLazyLoading(! app()->isProduction()) (plus preventSilentlyDiscardingAttributes()) in AppServiceProvider::boot throws on lazy loads in non-prod; Pulse Slow Queries / Telescope Queries tab show the repeat pattern; DB::listen(fn ($q) => Log::info('sql', ['sql' => $q->sql, 'ms' => $q->time])) around a reproduction counts them.
  • Fix at source: with(), withCount, withExists, withAggregate on the list query; loadMissing() post-hoc; whereRelation() for single-column constraints; multi-column OR chains → whereAny / whereAll / whereNone instead of PHP filtering.
  • L13 safety net: Model::automaticallyEagerLoadRelationships() globally, or $collection->withRelationshipAutoloading() per collection — auto lazy-eager-loads for the whole collection instead of per row. A net, not a substitute for explicit with() on known paths.
  • Check overfetch after the fix: eager-loading a fat relation to render one field, on 10k rows, trades N+1 for a memory bomb. Constrain: with('author:id,name'). Measure both ways.

Reading big, writing big

  • Iterate large sets: chunkById() (safe under mutation — chunk() is not), lazy(), cursor() (single query, no eager loads).
  • Bulk writes: upsert(), insertOrIgnore() — no model events fire; if Observers matter, loop in a transaction and say why.
  • Select only needed columns. Cursor pagination for infinite scroll / large offsets; offset pagination only when total counts matter and the table is small.

Caching decision tree

  1. Recomputed per request, cheap to invalidate → Cache::remember($key, $ttl, $fn).
  2. Stampede-prone (hot key, expensive recompute) → Cache::flexible($key, [$fresh, $stale], $fn) — serves stale, recomputes in background.
    • Same key hit repeatedly within one request/job → Cache::memo()->get($key) (L13) — resolves once, then from memory. Sliding expiration → Cache::touch($key, $ttl) extends TTL without get+put.
  3. Staleness unacceptable → Cache::lock($key)->block($seconds, $fn) around the recompute.

Every cache entry states three things or it doesn't ship: key, TTL, exact invalidation trigger. A cache without an invalidation story is a bug with latency. Capture hit ratio for any cache added or touched.

Redis: pipeline batched ops, tag caches for group invalidation, know the eviction policy (maxmemory-policy) before trusting TTLs.

Measurement discipline

  • Wall-clock endpoints: wrk -t4 -c50 -d30s <url> or a k6 script → p50/p95/p99 + req/s. Local or dedicated staging only; shared/prod targets need human sign-off.
  • PHP-level: Xdebug → KCachegrind, or Blackfire / Tideways for call graph + memory.
  • State every result as a number with units, before and after. No before/after delta → it didn't happen.
  • Cheapest win first: eager-load > index > cache > rewrite > scale. Exhaust a layer before escalating.

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.