My Journey from Traditional Monolithic Architecture to Distributed SQL

https://static.pingcap.com/files/2022/01/21141638/Blog-Twitter-Banner.png

Editor’s note: Bill Kendall wrote this post during his time at PingCAP. It was originally published in January 2022 and updated in August 2026 to reflect TiDB’s agent state stack.

Key Takeaways

  • Monolithic databases hit a hard ceiling: one machine, one failure domain, and a cost curve that steepens as you scale up.
  • Distributed database architecture scales out instead of up, spreading data across nodes that tolerate failure and rebalance automatically.
  • Distributed SQL keeps transactions, joins, and relational modeling on top of that architecture, so you don’t trade correctness for scale.
  • AI agents face the same gap now. Durable memory, state, and files need the same distributed foundation, not a fourth system bolted on.

Like many data management engineers, I started my career on Oracle. It was a pragmatic choice. Oracle was mature, reliable, and feature-rich, and I could build fast, very large, vertically scalable systems with it. When a database needed more resources, I scaled up: a bigger machine, all the way to Oracle’s flagship engineered system, Exadata.

Then the cloud arrived and the assumption underneath all of that work stopped holding. Distributed database architecture stopped being a topic I read about and became the job. I spent years moving large on-premises applications and their databases onto cloud infrastructure, and the database was always the hard part. That work convinced me the shift is not a stylistic preference. For any workload that has to scale out, span regions, and stay online through failure, distribution is the design that fits.

Four years after I first wrote this, the same shift is happening again in a different domain. AI agents are hitting the wall my applications hit in 2014: they have nowhere durable to keep what they know. This is the story of how I got from Oracle to distributed SQL, and where that road leads next.

Why Monolithic Database Architecture Worked for Its Era

Monolithic database architecture places compute and storage on a single server and scales vertically. One machine holds the data, runs the optimizer, executes transactions, and enforces consistency. When it runs out of headroom, you buy a larger machine. For decades that was the correct answer, and it is worth saying why before explaining why it stopped being enough.

Traditional relational databases were built for workloads that were largely predictable. A general ledger, an ERP install, or an order-entry system had a known user population, a known peak, and a growth curve a DBA could budget a year ahead. Centralizing compute and storage on one node made those workloads simpler to reason about. Transactions had exactly one place to commit. Consistency was a property of the machine rather than a property of a protocol. Tuning, backups, and capacity planning all pointed at a single target.

Oracle-style enterprise environments took that model close to its limit. Exadata paired the database with purpose-built storage and networking so one system could carry workloads that would have needed a room full of hardware a decade earlier. I built plenty of those, and they worked. When the workload fits inside one machine and the machine is engineered well, scale-up remains the simplest and often the cheapest architecture available.

Monolithic databases were not badly built. The shape of the workload changed underneath them.

Where Monolithic Systems Start to Break Down

Monolithic systems break down when the workload stops fitting on one machine, and no amount of tuning fixes that. Vertical scaling has a hard ceiling set by the largest server you can buy, and the cost curve toward that ceiling is not linear. The last increment of capacity is always the most expensive one.

Moving the applications to the cloud was relatively trivial. I could distribute them geographically, and most of my clients had already moved to microservices designed to be cloud-native and horizontally distributed. The Oracle database was far harder. It was designed from the beginning to scale vertically. I could scale up, but not out to where the cloud required me to be. Traditional relational systems were becoming the bottleneck for mission-critical applications, so I pivoted in a big way.

The failure modes are consistent across the migrations I have worked on:

  • Hardware ceilings. Once you are on the largest available instance, the only lever left is rewriting the application.
  • The cost of vertical scale. Each doubling of capacity costs more than the last, and you pay for peak around the clock.
  • Availability risk. A single primary is a single point of failure. Failover means a window where writes stop.
  • Operational complexity. Teams that outgrow one node shard by hand, pushing routing, rebalancing, and cross-shard joins into application code.
  • Global application demands. Users on three continents reading from one region pay for that geography in every request.
  • Mixed workloads. Analytics against a transactional primary degrades both, so most teams add a warehouse and accept a lag measured in tens of minutes.

None of these stay technical for long. Manual sharding slows releases, peak-provisioned hardware inflates cost, and failover windows become customer-facing incidents. The architecture sets the ceiling on what the product can do.

What Distributed Database Architecture Changes

Distributed database architecture spreads data and query processing across many nodes that behave as one logical database. Instead of replacing a server to grow, you add nodes and the system rebalances data across them. That single change, scale out rather than scale up, cascades into how availability, sharding, and consistency all work.

In early 2014 I was sent a newly released Google Research paper, Spanner: Google’s Globally-Distributed Database. It described a clustered distributed database that could span one data center or several. Data was sharded automatically. Copies were distributed and balanced across the cluster. Applications could connect to any node and reach any data. Nodes could be added or removed to scale in either direction. It was the first design I had read that treated the database the way my applications were already being treated.

The mechanics matter more than the label:

  • Horizontal scaling and data distribution. The data set is divided into ranges spread across nodes, and the cluster moves ranges to keep load even as nodes join or leave.
  • Automatic sharding. Splitting and placement happen inside the database, not in application code.
  • Replication and fault tolerance. Each range keeps multiple replicas. A consensus protocol, Raft in TiDB’s case, elects a leader per range so the cluster keeps serving when a node dies.
  • Coordination of compute and storage. Separating the SQL layer from the storage layer lets each scale on its own axis, which makes elastic capacity practical.
  • Consistency models. Eventual consistency is cheaper to build and harder to reason about. Strong consistency costs coordination and returns correctness.
  • Operational resilience. Rolling upgrades, node replacement, and region failure become routine operations rather than change-control events.

Monolithic vs. Distributed Architecture

Set side by side, the two architectures differ on nearly every axis that matters in production:

Dimension Monolithic architecture Distributed architecture
Scaling model Scale up. Replace the server with a larger one. Scale out. Add nodes and let the cluster rebalance.
Data placement One copy on one machine, plus backups and standbys. Data sharded and replicated across many nodes.
Failure domain The server is the failure domain. Losing it means failover or downtime. A node is the failure domain. The cluster keeps serving on surviving replicas.
Capacity ceiling The largest machine you can buy and afford. Practically bounded by cluster size, not by any single machine.
Mixed workloads Analytics competes with transactions for the same resources, or moves to a separate warehouse. Row and column stores can serve transactions and analytics from the same cluster.
Operational model Tune one instance. Shard manually at the application layer if you outgrow it. Manage a cluster. Sharding, rebalancing, and failover happen inside the database.

The Rise of Distributed SQL

Distributed SQL is the branch of distributed database architecture that keeps SQL, relational modeling, and transactional consistency while distributing data across nodes. It sits one level below the broader category: every distributed SQL database is a distributed database, but not every distributed database gives you transactions and joins.

That distinction exists because the first wave of distributed systems dropped the relational model to get scale. NoSQL stores solved real problems, and for append-heavy or schema-loose workloads they are still the right tool. What many teams discovered is that they had traded away things they could not do without. Multi-row transactions moved into application code, where they became a source of subtle bugs. Ad hoc queries required a second system. Referential integrity became a convention rather than a guarantee. For a payments ledger or an inventory system, “the write probably landed” is not an acceptable answer.

SQL survived because the relational model is a good fit for how businesses describe their data, and because thirty years of tooling, drivers, ORMs, and hiring pools sit on top of it. Distributed SQL is the attempt to keep that surface while changing what runs underneath: familiar relational semantics and ACID transactions on an architecture that scales horizontally and survives node loss.

At PingCAP we took the multi-node auto-sharding architecture at the core of distributed SQL and extended it for flexibility beyond what Spanner and other distributed SQL databases offered. That database is TiDB: an open source, MySQL-compatible distributed SQL database. The MySQL wire protocol compatibility is deliberate. It means an existing application, driver, or DBA workflow arrives already knowing how to talk to the database.

TiDB Design Tenets for Distributed Database Architecture

TiDB operationalizes the properties described above rather than simply claiming the distributed label. In 2015 our founders built TiDB from scratch alongside a large and active open source community on GitHub, working from a set of design tenets meant to produce a cloud-native distributed SQL architecture. These are the core tenets:

  • Scale without complexity. The database scales horizontally without operator intervention. Nodes join the cluster and TiDB rebalances data onto them automatically. Scaling in works the same way.
  • Resiliency always on. TiDB survives node failures while applications continue processing transactions on the surviving nodes, which drives time to recovery toward zero.
  • Consistent ACID transactions. For transactional systems you must always read the most recent correct copy of the data. That matters most in financial services and inventory. TiDB guarantees high isolation levels at the transaction level to preserve consistency.
  • Support for both OLTP and OLAP workloads. Many distributed SQL databases handle only transactional traffic. TiDB serves transactional, analytical, and mixed workloads by adding a columnar store, TiFlash, alongside the row store to handle large aggregations and analytical functions. This is hybrid transactional/analytical processing, or HTAP, and the cost-based optimizer decides which store answers each query.
  • A common API interface. TiDB was built by developers for developers, so access happens through a widely understood SQL interface. We chose wire compatibility with MySQL, which lets application developers and DBAs use one of the most established SQL protocols in production today.
  • Install and run anywhere. The database deploys on premises, in a single cloud, or across multiple clouds, on VMs or containerized with Kubernetes. Deployment architecture can change as the organization’s requirements change.

Operational Features That Make the Tenets Usable

Design tenets produce a capable database. Operational features make it something a team can actually run. TiDB exposes rich metrics and monitoring through Prometheus and Grafana plus a built-in TiDB dashboard. Role-based access control and TLS encryption protect data in flight and at rest. Built-in asynchronous change data capture moves data into and out of the cluster, which matters for migrations and for feeding downstream systems. TiDB Cloud takes the same architecture and runs it as a managed service, which is how most teams start today.

Why Distributed Database Architecture Matters for AI Agents

AI agents are stateless by default, the same architectural gap monolithic databases left behind. A model call takes input and returns output. Nothing about it remembers the last call. The context window looks like memory, but it is a buffer: bounded, scoped to one session, and gone when the process ends. An agent that ran for six hours yesterday starts today knowing nothing about it.

Production agents need more than a transcript. The state that must survive falls into categories:

  • Facts and preferences learned in earlier sessions, which must persist across machines and users.
  • Session and task history, so the agent can answer what it did, when, and why.
  • Tool outputs and intermediate results, expensive to regenerate and needed several steps later.
  • Files and artifacts: repositories, generated code, logs, and documents produced during a run.
  • Retrieval metadata tying embeddings to their source, version, and access rules.

Two access patterns run against that state, and both have to work. Agents need exact lookups with filters, joins, and transactions: which user owns this record, has this step already run. They also need semantic retrieval when nobody knows the exact key. A single agent turn frequently needs both.

The common workaround is to stitch systems together: Postgres for records, a vector database for embeddings, object storage for files, and Redis for session state. Every boundary adds a consistency problem. When memory is revised, superseded, or deleted, the change has to land everywhere or the agent acts on stale facts. This is why we treat agent memory and state as a database problem rather than a retrieval add-on. A distributed SQL database with native vector search can hold structured records, embeddings, and searchable text in one place, run hybrid retrieval in a single query, and give mutable memory transactional guarantees.

The TiDB Agent State Stack: Zero, mem9, and drive9

The TiDB agent state stack is three layers that together give an agent durable state, all powered by TiDB Cloud. TiDB Cloud Zero provides instant SQL state, mem9 provides persistent memory, and drive9 provides a persistent workspace for files. Because they share one backend, memory, operational records, retrieval metadata, files, and execution history stay connected as a deployment grows.

TiDB Cloud Zero for Instant SQL State

TiDB Cloud Zero is a zero sign-up, zero configuration way to provision a real MySQL-compatible TiDB Cloud database in seconds. There is no registration step and no cluster to size. The instance is TLS-secured and supports joins, transactions, and standard SQL, along with native vector search and full-text search, so an agent can store embeddings next to relational data instead of splitting them across systems.

Zero instances are disposable by design, with a 30-day lifespan. That fits agent sessions, CI runs, demos, and prototypes. When a prototype turns into something you want to keep, a three-click claim converts the instance into a persistent TiDB Cloud Starter database and carries the data and schema over. Zero also works well as the database behind an MCP server or custom agent tooling; it is not itself an MCP server, but it gives MCP tools a SQL backend they can provision on demand. TiDB Cloud Zero is currently in public preview.

mem9 for Persistent Agent Memory

mem9 is a persistent memory layer for AI agents. It gives coding agents, custom tools, and multi-agent systems a shared memory space that survives across sessions, machines, and users, so what one agent learns is available to the next one. It supports hybrid retrieval, combining vector similarity and keyword search in the same query, and generates embeddings server-side so there is no separate embedding pipeline to run.

Two design decisions matter more than the API surface. First, mem9 treats memory as a system rather than a store: ingestion, deduplication, ranking, retrieval, and evaluation are all part of it, because cognitive memory gets revised, superseded, expired, and purged. Mutable memory needs strong consistency, which is why it runs on TiDB Cloud. Second, memory is inspectable. The mem9 interface lets you visualize, manage, import, and export what an agent remembers, which is what makes it possible to trust and correct. mem9 integrates with agent harnesses including OpenClaw, OpenCode, Claude Code, Codex, and Dify apps, and any HTTP client can read and write to the same memory space.

drive9 for Persistent Agent Files

drive9 is a durable filesystem for agents that do real work on disk. Coding agents clone repositories, install dependencies, edit files, run tests, produce logs, and try several approaches in parallel. The sandbox they run in may last minutes; the work usually needs to last longer. drive9 keeps that workspace state outside the sandbox, so it survives resets and can be mounted from a different environment entirely.

You mount a drive9 workspace as a local filesystem and use normal tools against it. Workspaces can be forked for parallel attempts, checkpointed, rolled back, and committed with server-side conflict detection. Files are embedded and indexed automatically on write, so an agent can search its own workspace by meaning instead of guessing at paths. Local disks run the process and Git stores the final result; drive9 holds the working state in between.

Framed against the rest of this article, none of the three is a departure. They are the same architectural shift applied to a new workload: state moves out of a fragile, session-bound, single-machine home and into a distributed system that expects to outlive any one process.

From Monolithic Databases to Agent-Ready Architecture

After more than twenty years around Oracle, I can say plainly that distributed SQL changed what I am able to build. I have watched teams solve problems that were not solvable under a scale-up architecture, and I have watched that unlock a level of creativity in the community that keeps surprising me.

The lesson that has held up is that architectural decisions compound. Choosing a scale-up database in 2010 was defensible, and it still set the ceiling on what those systems could do in 2020. The same compounding is underway right now in AI infrastructure. Teams building agents on ephemeral state are making a decision whose cost arrives later, when the agent has to answer what it knew last Tuesday, or when memory has to move across machines, or when a sandbox reset takes an afternoon of work with it. The architecture that let databases scale for the cloud era is what makes durable agent state practical now, and for the same reasons: distribution, replication, consistency, and retrieval in one place instead of four.

If you want to see what that feels like in practice, spin up a TiDB Cloud Zero instance. It takes seconds, requires no sign-up, and gives you a MySQL-compatible database with vector search that you can point an agent at immediately.

FAQ

What Is Distributed Database Architecture?

Distributed database architecture spreads data and processing across multiple nodes instead of concentrating them on a single centralized server. This architecture improves scalability, availability, fault tolerance, and performance for applications that need to serve users or workloads at scale.

How Is Distributed Database Architecture Different from Monolithic Database Architecture?

Monolithic database architecture typically relies on a centralized system that scales vertically, while distributed database architecture scales horizontally across multiple nodes. Distributed systems handle larger workloads, tolerate failures, and support more flexible deployment models.

Why Did Companies Move from Monolithic Databases to Distributed Databases?

Companies moved toward distributed databases because modern applications require higher availability, global scale, real-time data access, and resilience than traditional scale-up architectures can easily provide. Distributed databases reduce single points of failure and make it easier to scale as workloads grow.

Where Does Distributed SQL Fit into Distributed Database Architecture?

Distributed SQL is a type of distributed database architecture that preserves SQL, relational data modeling, and transactional consistency while distributing data across multiple nodes. It serves teams that need horizontal scale without giving up familiar relational database capabilities.

Why Do AI Agents Need a Distributed Database for Memory?

AI agents are stateless between calls, and a context window is a temporary buffer rather than durable memory. Agents need persistent state that outlives a session: facts learned earlier, task history, tool outputs, files, and retrieval metadata. A distributed database can hold structured records and embeddings together, serve exact lookups and semantic search from one query, and apply transactional guarantees to memory that gets updated, superseded, or deleted.

Planet for the MySQL Community

Curated MySQL Data Sets for Realistic Testing

https://ronaldbradford.com/images/blog/mysql-curated-datasets.pngSynthetic benchmarks have their place, but I have always preferred working with real data. Not client production data — that stays private — but publicly available datasets that reflect the messy shapes, skewed distributions, and indexing challenges you encounter in the wild.Planet MySQL

Laravel NaturalQuery: ask your database questions in English, by voice or text

https://opengraph.githubassets.com/3f074d12b607463f214c915379044d7a1ad86276c55e5b80c10393bd76739318/jay123anta/laravel-natural-query

Let people ask your database questions in English – by voice or by typing –
without your data ever leaving your server.

Tests
License: MIT

Packagist
Downloads
PHP

The AI is sent your schema structure only – table names, column names,
types, and the words your users use for them. It returns SQL. Your server
validates that SQL, runs it locally, and formats the rows. Not one row is
ever sent upstream
, and that is enforced by tests, not by intent.

Any model, hosted or your own. Gemini, Claude, OpenAI, DeepSeek, Mistral,
Groq, OpenRouter – or a model you run yourself on Ollama, vLLM, LM Studio or
llama.cpp. One config block, no code changes. Self-hosting goes further still:
nothing leaves your network at all, not even the schema.

$result = NaturalQuery::query("top 5 customers by revenue");

Or drop the whole UI – chat thread, microphone, charts – into any Blade view:

<x-naturalquery::widget />

Requires PHP 8.2+ and Laravel 12 or 13. Works on PostgreSQL, MySQL,
MariaDB and SQLite.

composer require jayanta/laravel-natural-query
php artisan naturalquery:install
php artisan migrate

Choose a model in .env. A model you run yourself is a first-class choice, not
a fallback:

# Local, no API key, nothing leaves your machine
NATURALQUERY_LLM_DRIVER=ollama
OLLAMA_MODEL=llama3.3

# Or a hosted API
NATURALQUERY_LLM_DRIVER=gemini
GEMINI_API_KEY=your-key-here

Built-in drivers: ollama, gemini, openai, claude. Any other
OpenAI-compatible service – DeepSeek, Groq, Mistral, OpenRouter, vLLM,
LM Studio, LocalAI – plugs in with a base_url and a model; see
docs/PROVIDERS.md.

php artisan naturalquery:discover --ai

This is the whole adaptation step. The package knows nothing about your
application: this reads your database and writes one plain PHP file per table
into config/naturalquery-schemas/. Those files are the only thing that makes
it understand your domain – no code changes, no subclassing.

--ai also fills in the human layer that cannot be read from a database:
descriptions, the words your users actually say, business rules, and computed
metrics like averages. Worth doing – without it, a question like "average
amount" costs an extra API call to answer.

Then check it:

php artisan naturalquery:doctor

It names the real cause of any problem and prints the exact fix. Run it first
whenever something is wrong.

use Jayanta\NaturalQuery\Facades\NaturalQuery;

$result = NaturalQuery::query('total revenue by region last month');

Show parsed_query to your users. It states which measure, breakdown,
filters and dates were actually used – the difference between someone catching
a misreading and believing a number that answers a different question.

Or over HTTP, which is what the widget uses:

POST /naturalquery/text          {"text": "top 5 customers by revenue"}
POST /naturalquery/conversation  {"session_id": "abc", "text": "only in West"}

The browser listens. Your server only ever receives text.

<x-naturalquery::widget />   

There is nothing to configure and no audio endpoint. The widget uses the
browser’s SpeechRecognition to turn speech into English text on the device,
then posts that text exactly as if it had been typed. Three things follow from
that one decision:

  • It works with every model – Gemini, Claude, Ollama, anything – because by
    the time the model is involved it is reading a sentence, not hearing a
    recording.
  • No audio leaves the device. Not to your server, not to a provider. There
    is no upload path in the package at all.
  • Nothing extra to set up or pay for – no transcription service, no second
    API key, no added latency.

Answers carry a speech_text field phrased for reading aloud, and the widget
speaks it. Chrome, Edge and Safari support recognition; Firefox does not, so
the microphone is hidden there and people type – which is why text input is
never optional.

language picks which English accent to listen for – en-IN recognises
Indian English far more accurately than en-US does:

<x-naturalquery::widget language="en-IN" />

English only, on purpose. Multilingual belongs to a separate package with a
speech pipeline of its own; this one stays an English natural-language-to-SQL
assistant. → docs/WIDGET.md

These endpoints spend your API key, so they are not open by default.

Who gets in
A viewNaturalQuery gate you define Whatever the gate says
No gate, local or testing Everyone – so it works the moment you install it
No gate, anywhere else Signed-in users only
// AppServiceProvider::boot()
Gate::define('viewNaturalQuery', fn ($user) => $user->isAdmin());

Define the gate as soon as this is more than you: an ungated endpoint in
production is an LLM proxy for the internet.


What it is good at, and what it is not

It works well on datasets you have described. Told that revenue is a
measure to total, that users say "client" for customer_name, and that
cancelled orders do not count, it is reliable for the questions those datasets
are meant to answer.

It is not magic. Pointed at an undescribed database and asked something
vague, any text-to-SQL system will sometimes produce a confident, wrong answer.

Measured against the Spider benchmark – real questions, unfamiliar databases,
no curation – it answers 29 of 36 (81%). Read that as: roughly one question
in five is wrong on an uncurated schema. On a described one it is far better,
which is why the schema files matter more than anything else you will do.

The honest framing is a fast analyst for datasets you have curated, not an
oracle for arbitrary databases. Every mitigation here follows from that: SQL is
SELECT-only and restricted to your tables, doctor catches schema drift, and
every answer shows the query it understood.

Seventeen cases whose answers are arithmetic on three seeded rows – totals,
filters, averages, periods, a decomposed comparison, and a conversation that
narrows, drills down and rewinds:

Model Result
Gemini 2.5 Flash 17/17
Claude Sonnet 5 17/17
DeepSeek v4 Flash 17/17
Mistral Large 17/17
Llama 3.3 70B (open weights) 17/17
Llama 3.1 8B (open weights) 12/17

Model size matters more than vendor. The 70B open-weight model scores the
same as the four frontier hosted ones, and runs on a single good GPU. The 8B
drops filters and ignores date periods – asked for July it returns the whole
table, confidently – so use a 70B-class model or better wherever a wrong
number matters.

Conversation state is the exception worth noting: narrowing, drill-down and
rewind pass even on the 8B, because they are resolved in PHP rather than left
to the model.

NATURALQUERY_CONFORMANCE=1 NATURALQUERY_LLM_DRIVER=claude \
NATURALQUERY_CONFORMANCE_KEY=sk-... vendor/bin/phpunit --testsuite Conformance

Run any battery more than once before believing it. On a free tier the first
pass often measures the rate limit rather than the model – add
NATURALQUERY_CONFORMANCE_DELAY=15 to space the calls out.


docs/SCHEMA.md Schema files in full – metrics, aliases, joins, many tables
docs/API.md Every endpoint, field and error code – plus events and token cost
docs/CONVERSATIONS.md Follow-ups, drill-downs, rewind, multi-step answers
docs/PROVIDERS.md Every LLM driver, and adding your own
docs/WIDGET.md The bundled UI and browser voice input
docs/SECURITY.md The privacy wall, SQL validation, prompt-injection guard
docs/TROUBLESHOOTING.md What each error means and how to fix it
php artisan naturalquery:doctor      # diagnose setup problems, print the fix
php artisan naturalquery:discover    # generate schema files from your database
php artisan naturalquery:install     # publish config and migrations
php artisan naturalquery:debug ""   # show the exact prompt sent to the AI
php artisan naturalquery:cache-cleanup

vendor/bin/phpunit must pass and the widget must pass node --check. New
behaviour gets a test; every failure a real user hits becomes a regression
test.

MIT. See LICENSE.

Laravel News Links

Enforcing enterprise conventions with Spatie guidelines skills and Laravel Boost

https://origin-main.com/wp-content/uploads/2026/08/feat-8813-1.png

🕒  6 Minute Read    📅  Date Published: August 9, 2026

AI coding agents write functional PHP fast, but speed isn’t the same as adherence. Left alone, an agent will happily produce verbose control flow, stale docblocks, and framework anti-patterns that any senior reviewer would bounce in code review. Spatie tackled this by packaging their production coding standards as spatie guidelines skills for Laravel Boost, a machine-readable format that agents like Claude Code, Cursor, and GitHub Copilot can load on demand instead of guessing at your team’s conventions.

This article is part of our wider AI Architecture Module coverage of agent tooling, where Laravel Boost and MCP infrastructure provide coding agents the context they need to ensure AI-generated code follows vendor-specified conventions, not just that it compiles.

If you haven’t wired up the transport layer yet, our guide to rolling out Boost across a Laravel 13 project covers the installation and agent registration steps this article assumes.

The skill activation pipeline

Why machine-readable skills beat static prompt files

A .cursorrules file or a bloated CLAUDE.md gets loaded into every single turn whether it’s relevant or not. That’s wasted context on a JavaScript formatting rule while you’re editing a migration. Skills packages solve this differently.

  • Context on demand. Agents activate spatie-laravel-php or spatie-security only when the file being touched matches, not on every prompt.
  • Team-wide consistency. Installing through Composer means every engineer, and every CI runner, pulls the identical ruleset. No copy-pasted markdown drifting between machines.
  • One-command updates. Spatie ships changes to the package; your team pulls them with a single sync command instead of manually re-pasting guideline files.

[Architect’s Note] The distinction matters more than it sounds. A prompt file is advisory, the agent reads it and might follow it. A skill is conditional, it fires deterministically on file type and task. That’s the difference between a suggestion and a guardrail.

Installing Spatie’s guidelines skills via Laravel Boost

Laravel Boost is the primary transport for registering skill packages inside a Laravel 13 project. Install both dev dependencies through Composer:

composer require laravel/boost --dev
composer require spatie/guidelines-skills --dev

Link the repository immediately, before you run anything else: spatie/guidelines-skills on GitHub. That’s the source of truth for the skill definitions you’re about to install, and it’s the first place to check when a rule looks off.

Running the installer

php artisan boost:install

Select Spatie guidelines from the vendor list, then choose your active coding agent. Boost writes the agent binding files, one skill per convention area: spatie-laravel-php, spatie-javascript, spatie-version-control, and spatie-security.

[Production Pitfall] Don’t skip .boost/skills/ in your .gitignore by habit. Teams that treat generated agent config as disposable end up with every developer running a different skill version locally, which defeats the entire point of pulling this through Composer in the first place.

Running outside Laravel Boost: skills.sh and global Claude Code

Not every project runs Boost. A standalone JavaScript service, a non-Laravel PHP microservice, or a developer who just wants Spatie’s PHP conventions available everywhere still needs a path in.

Global Claude Code integration

To apply the conventions across every local repository without touching individual projects, register them in your user home directory:

mkdir -p ~/.claude && touch ~/.claude/CLAUDE.md
curl -o ~/.claude/laravel-php-guidelines.md https://spatie.be/laravel-php-ai-guidelines.md
echo -e '\n## Coding standards\nWhen working with Laravel/PHP projects, first read the coding guidelines at @~/.claude/laravel-php-guidelines.md' >> ~/.claude/CLAUDE.md

Runtime-agnostic install via skills.sh

For non-Laravel services, install through the cross-runtime skills.sh registry directly:

npx skills add spatie/guidelines-skills

This gets you the same four skills without a Composer dependency, and it’s the path that works across Claude Code, Cursor, Codex, and Copilot uniformly.

[Word to the Wise] If your team runs a mixed stack, decide up front which install method is canonical for which repo type. Having half your services pull skills through Boost and the other half through skills.sh isn’t wrong, but it does mean two update commands to remember, and someone will forget the second one.

Keeping guidelines current: Context7 and CI sync

Enforcing syntax conventions only solves half the problem. When an agent generates code against a specific Spatie package, spatie/laravel-permission, spatie/laravel-medialibrary, it needs accurate, current API signatures, not whatever the model’s training data happened to capture. That’s a separate failure mode from style drift, and it’s the one that produces confidently wrong method calls.

To close that gap, wire in the Context7 MCP server. For background on building or extending your own protocol gateways, see our guide on production MCP server infrastructure in Laravel.

{
  "mcpServers": {
    "context7": {
      "command": "npx",
      "args": ["-y", "@upstash/context7-mcp@latest"]
    }
  }
}

With this configured, an agent implementing a Spatie package feature queries Context7 for live documentation instead of relying on stale training data.

Automating the sync

Guidelines evolve. Keeping every developer’s local skill definitions current, and catching drift in CI before it ships, means treating the sync as a scripted step rather than a reminder in Slack:

{
    "scripts": {
        "update-ai-skills": [
            "composer update spatie/guidelines-skills",
            "php artisan boost:update"
        ]
    }
}

Run composer run update-ai-skills during local setup and again as a pull-request check. This is the same discipline we cover in our CI pipeline guide for AI applications: the goal isn’t just passing tests, it’s guaranteeing the agent instructions in front of every contributor match the security and architectural rules currently in force.

[Efficiency Gain] Bundling boost:update into the same CI step you already run for schema validation costs you nothing extra in pipeline time and removes an entire class of “works on my machine, why is the agent doing something different” tickets.

Operational guardrails for teams at scale

A few practices separate a clean rollout from one that quietly rots over six months.

  • Prune unused skill drivers. If your application is a pure API backend, disable spatie-javascript. Every active skill costs agent context; loading rules for code you don’t write is pure waste.
  • Audit generated output, don’t trust it blindly. Skills reduce style violations, they don’t eliminate the need for review. Check that agent-produced code actually respects PSR-12 and Spatie’s return-type conventions rather than assuming the skill fired correctly.
  • Keep automated formatting as your final gate. AI guidelines catch structural and naming issues; a vendor/bin/pint pre-commit hook is still your deterministic backstop for whitespace and formatting drift that skills won’t always reach.
  • Match your local environment to what CI enforces. If your team’s dev containers differ from what agents run against locally, guideline enforcement gets inconsistent fast. Our breakdown of a 2026 Laravel AI development stack covers keeping that parity intact.

[Edge Case Alert] Watch for skill collisions when a project pulls in guidelines from more than one vendor package. If a second guidelines package also defines a spatie-laravel-php-equivalent skill, Boost doesn’t merge them, it’s whichever one installs last. Audit .boost/skills/ after any multi-package install, not just after a single-package one.

Where this fits in your AI governance stack

Guidelines skills solve a narrower problem than most of what we cover in Production-Grade AI Architecture in Laravel: they’re about what the agent writes, not what it decides or how much it costs to run. But narrow doesn’t mean small. Style and convention drift is one of the quieter failure modes in agentic development, it doesn’t throw an exception, it just accumulates until a codebase written by five different agent sessions looks like it was written by five different developers who never talked to each other.

The pattern here generalizes past Spatie’s specific rules. Any team with an opinionated internal style guide can package it the same way: distilled into skills, distributed through Boost or skills.sh, kept current through Context7 and a CI sync step. The specific package changes. The architecture, deterministic, versioned, and enforced at the point of generation rather than the point of review, doesn’t.

Additional external references

– Laravel Boost documentation (https://spatie.be/guidelines/ai)
– Context7 MCP server on GitHub (https://github.com/upstash/context7)


Frequently Asked Questions

Does spatie/guidelines-skills replace spatie/boost-spatie-guidelines?

Yes. The older package was Boost-only; the new one works with both Boost and skills.sh, and covers everything the old package did.

Do I need Laravel Boost to use Spatie’s guidelines?

No. The skills.sh install path (npx skills add spatie/guidelines-skills) works independently of Boost, which matters for non-Laravel services in a polyglot stack.

Will these skills conflict with my own custom AI guideline files?

Not automatically, but they can collide if another package defines a similarly named skill. Audit .boost/skills/ after installing, especially in multi-package setups.

Does this replace code review?

No. Skills reduce the volume of style and convention violations an agent introduces; they don’t replace human review or your automated formatting gate (Pint).

Dewald Hugo

A software architect with 15+ years of experience in the PHP and Laravel ecosystem. Dewald created Origin Main to provide the engineering rigour required to integrate AI into professional, high-concurrency production systems. He writes for developers who care less about "getting it to work" and more about "getting it to last".

Laravel News Links

Laravel Truss 1.8: feed your real schema to a coding agent (MCP + context export)

https://albertoarena.it/images/posts/my-coding-agent-kept-inventing-columns/cover.jpg

Ask a coding agent to write a query against a table it hasn’t seen this session, and it will guess. Confidently, plausibly, and wrong: a foreign key named author_id when the column is actually created_by, a status treated as a free-text string when it’s a tinyint enum, a table it’s sure exists because a table like it usually does. It isn’t lying, it just doesn’t know, and nothing forces it to say so.

The usual fix is pasting a schema dump into the chat at the start of a session. That works until the next migration, at which point it’s just a different kind of wrong: confidently out of date instead of confidently invented.

There’s an obvious third option: let the agent run queries itself, most coding agents can already execute SQL. But that means handing over real database credentials, and a connection that can query can also see rows, not just structure. That’s a bigger grant than the problem needs.

Truss has always been a live, zoomable ER diagram of your Laravel app’s real database schema, structure only, never a row of data. Version 1.8 points that same live structure at a coding agent instead of a browser tab.

Give it meaning a type can’t carry

A column tells an agent its name and type, not what it means. status = 1 doesn’t say “paid” on its own. Annotate it once and every export carries it:

// config/truss.php
'annotations' => [
 'source' => ['config', 'database'],
 'tables' => [
 'orders' => [
 'note' => 'One row per checkout attempt, not per completed order.',
 'columns' => [
 'status' => 'tinyint: 0 pending, 1 paid, 2 refunded',
 ],
 ],
 ],
],

If your database already carries COMMENT strings on tables and columns, leave 'database' in annotations.source and Truss reads those directly, no duplicate config to keep in sync. Either way, a comment is part of the CREATE TABLE definition, not a row: still structure only. Strip them from any single export with --no-annotations when you just want the bare shape.

Trim it to what the question needs

A forty-table schema is a lot of tokens to spend on a question about one table. --compact drops column defaults and non-unique indexes without dropping a single table, column, or foreign key. --focus=orders --depth=1 narrows the export to one table and its foreign-key neighbourhood, the same idea as the dashboard’s focus mode, now available from the command line. And there’s a new llm format alongside the existing five (DBML, JSON, CSV, Markdown, Mermaid), a dense plaintext export tuned for a token budget rather than for a human reading a data dictionary:

php artisan truss:export --format=llm --focus=orders --depth=1 --compact

That’s what the export produces. Calling it is just as direct, in code or over HTTP.

Building the same thing in code goes through a new fluent, immutable Truss facade instead of the command:

Truss::snapshot()->focus('orders', depth: 1)->compact()->toDbml();

And a gated GET {prefix}/export/{format} route serves the identical output to any HTTP client, behind the same viewTruss gate as the dashboard. Command, facade, route, dashboard download: one pipeline underneath all four, so they can never quietly disagree with each other.

Ask it live, instead of pasting a snapshot

The part I actually wanted, though, wasn’t a better export. It was not exporting anything at all.

Truss 1.8 adds an optional server for MCP, the Model Context Protocol that Claude Code, Claude Desktop, and Cursor use to reach outside tools. Built on laravel/mcp, it talks to a coding agent directly over local stdio:

composer require laravel/mcp
php artisan mcp:start truss

Point one at it and the agent gets five tools, list_tables, describe_table, get_schema, focus_table, and get_structural_review, plus a truss://schema resource, all reading the live schema on demand. Every tool advertises MCP’s readOnlyHint, so a client can present them as read-only instead of prompting for write approval on a call that was never going to write anything. No row data, ever, and the same exclusion and managed-connection safeguards as the rest of Truss apply here too, opt-in and off by default behind truss.mcp.enabled.

I pointed it at a real project I’ve worked on for a while, in Claude Desktop, and the difference was immediate: instead of me pasting a schema dump at the start of the conversation, or the agent asking me to run a query to check a column name, it just called describe_table before it wrote anything, the same check that would have caught the guessed author_id from the start of this post. No staleness, because there’s nothing to go stale, it’s reading the same live introspection the diagram uses.

Try it

Update with composer update albertoarena/laravel-truss, and if you want the MCP server too, composer require laravel/mcp on top.

What’s next

More truss:doctor rules and CI-native output formats are next on the roadmap, followed by reading Eloquent relationships for semantic edge labels instead of raw foreign keys, and navigation aids for schemas with a hundred tables or more. If a tool the agent needs isn’t there yet, or an annotation source you’d want isn’t supported, open a discussion.

Laravel News Links

Laravel Discount: Coupon Codes, Usage Limits, and Stacking

https://laravelnews.s3.amazonaws.com/featured-images/Laravel-Discount-LN.png

Some Laravel applications that sell products or services need more than a flat percentage off. An e-commerce store might schedule a seasonal sale, or a SaaS product might hand out a launch coupon, and a discount that starts at 10% off can be capped at a maximum, tied to a minimum order total, a per-customer limit, and an expiry date. Laravel Discount by Milwad Khosravi stores all of that on Eloquent models and evaluates it through a single facade.

Here’s what the package covers:

  • Two discount types: DiscountType::Percentage and DiscountType::Fixed, with an optional max_discount_amount cap.
  • Coupon codes: Discounts with a code act as coupons, while codeless discounts apply automatically.
  • Time windows: starts_at and expires_at columns, paired with a valid() query scope.
  • Usage limits: Total usage_limit and per-customer usage_limit_per_user enforcement.
  • Guest support: Pass a session ID to enforce per-user limits for unauthenticated users.
  • Stacking rules: Mark discounts is_stackable and the package works out which combination saves the customer the most.
  • Discountable models: The HasDiscounts trait attaches discounts to any Eloquent model.
  • Cart integration: A CartDiscount service that applies codes to a Laravel Cart total or to a single cart item.

Percentage and Fixed Discounts

A discount is a standard Eloquent model, so you create one like any other record:

use Binafy\LaravelDiscount\Enums\DiscountType;

use Binafy\LaravelDiscount\Models\Discount;

 

$discount = Discount::query()->create([

'name' => 'Summer Sale',

'type' => DiscountType::Percentage,

'value' => 20,

]);

Applying it goes through the LaravelDiscount facade, which validates the discount before calculating anything and hands back a DiscountResult:

use Binafy\LaravelDiscount\Facades\LaravelDiscount;

 

$result = LaravelDiscount::apply($discount, 200);

 

$result->originalAmount; // 200.0

$result->discountAmount; // 40.0

$result->payableAmount(); // 160.0

The DiscountResult object holds the applied discounts, the original total, and the calculated discount value. payableAmount() subtracts the discount from the original total, clamping the result to a minimum of zero so that fixed discounts never yield negative totals.

A discount of either type can carry a ceiling through max_discount_amount, which is what covers the "20% off, up to $100" case that otherwise ends up hardcoded in a controller:

$discount = Discount::query()->create([

'code' => 'SAVE20',

'type' => DiscountType::Percentage,

'value' => 20,

'max_discount_amount' => 100,

]);

 

LaravelDiscount::apply($discount, 300)->discountAmount; // 60.0

LaravelDiscount::apply($discount, 1000)->discountAmount; // 100.0

Discount Codes, Expiry, and Usage Limits

Setting a code column turns a discount into a promotional coupon. applyCode() validates the code and throws DiscountNotFoundException if no match exists:

$result = LaravelDiscount::applyCode('WELCOME10', 200, $user);

For campaigns where every customer needs their own code, the package builds them with random_int() and leaves out ambiguous characters like 0/O and 1/I, so nobody misreads a code off a printed card:

LaravelDiscount::generateCode(); // "8FJ2K9QW"

LaravelDiscount::generateCodes(100, 'VIP'); // Collection of 100 unique codes

Length, alphabet, prefix, and separator all live under the codes key in config/laravel-discount.php.

Time-limited offers use starts_at and expires_at. Applying before the window throws DiscountNotStartedException, applying after it throws DiscountExpiredException and fires a DiscountExpired event. Querying for what is live right now is a scope:

Discount::query()->valid()->get();

usage_limit caps total redemptions, and usage_limit_per_user caps them per customer, but neither is settled at the time of application alone. You call redeem() when the order is actually finalised:

LaravelDiscount::redeem($discount, $user, $result->discountAmount);

That runs within a transaction and increments used_count by the limit in the where clause, so the database decides whether the increment occurs at all. Update zero rows and the limit was already reached, which surfaces as DiscountUsageLimitReachedException. Two customers claiming the hundredth spot at the same moment cannot both win it.

Guests get the same treatment through a session ID instead of a user model, tracked in the session_id column of discount_usages alongside the nullable user_id:

$result = LaravelDiscount::applyCode('GUEST10', $total, sessionId: session()->getId());

 

LaravelDiscount::redeem($discount, amount: $result->discountAmount, sessionId: session()->getId());

Conditional and Stackable Discounts

A min_order_value column gates a discount behind a spending threshold and throws MinimumOrderValueException when the total falls short. There’s also a conditions JSON column for storing your own condition data, which is where you’d hang anything the package doesn’t model natively. If you want conditions expressed as composable PHP objects instead, the Discountify package takes that approach.

Discounts can also attach to models. Add the HasDiscounts trait, and you get a polymorphic relationship backed by the discountables table:

use Binafy\LaravelDiscount\Traits\HasDiscounts;

 

class Product extends Model

{

use HasDiscounts;

}

$product->discounts()->attach($discount);

$product->validDiscounts();

$product->hasDiscount('TECH10');

 

$result = $product->applyDiscounts($product->price);

That last call routes through applyMany(), which is the stacking resolver. It drops any discount that fails validation, splits the rest into stackable and non-stackable, sums the stackable ones (capped at the order total), finds the single best non-stackable one, and returns whichever side saves more:

$result = LaravelDiscount::applyMany([$tenPercent, $tenFixed, $bigSolo], 100);

 

$result->discounts; // the ones that actually applied

$result->discountAmount; // the winning total

The discounts collection on the result matters here. After a stacking decision, you often need to show the customer which codes made the cut, and that collection is the answer.

Laravel Cart Integration

Install binafy/laravel-cart, the cart package from the same author that we covered previously, and a CartDiscount service becomes available for cart-level and item-level discounts:

use Binafy\LaravelDiscount\Integrations\LaravelCart\CartDiscount;

 

$cartDiscount = app(CartDiscount::class);

 

$result = $cartDiscount->applyToCart($cart, 'SUMMER-8FJ2K9QW');

 

$result = $cartDiscount->applyToItem($cartItem, $discount);

 

$result = $cartDiscount->applyItemDiscounts($cart);

applyToCart() checks the cart total against min_order_value and pulls the cart’s user for the per-user limit, so you don’t pass either one yourself. applyToItem() works against price multiplied by quantity for a single line. applyItemDiscounts() walks the cart and applies whatever each item’s underlying model has attached through HasDiscounts, which is how you run a product-level sale across a whole basket without touching the cart total.

Validation, Exceptions, and Events

Checkout forms need to reject invalid code before anything else happens, and the package includes a ValidDiscountCode rule for that. Its message names the actual reason rather than failing generically:

use Binafy\LaravelDiscount\Rules\ValidDiscountCode;

 

public function rules(): array

{

return [

'code' => ['required', new ValidDiscountCode(

orderAmount: $this->cartTotal(),

user: $this->user(),

)],

];

}

It’s a standard rule object, so it composes with the rest of a form request the same way any custom validation rule does.

Outside validation, every failure case has its own exception extending DiscountException: DiscountNotFoundException, DiscountNotActiveException, DiscountNotStartedException, DiscountExpiredException, DiscountUsageLimitReachedException, and MinimumOrderValueException. Each one exposes the discount that failed through getDiscount(), so you can catch the specific case you want to message differently and let the base class handle the rest:

try {

$result = LaravelDiscount::applyCode($code, $total, $user);

} catch (DiscountExpiredException $e) {

return back()->withErrors("Code {$e->getDiscount()->code} has expired.");

} catch (DiscountException $e) {

return back()->withErrors($e->getMessage());

}

Three events cover the lifecycle: DiscountApplied when discounts are applied to an amount, DiscountRedeemed after a redemption transaction commits, and DiscountExpired when validation hits an expired discount. The redeemed event carries both the discount and the usage row, which is enough to drive analytics or a notification without querying again.

Installation

Laravel Discount requires PHP 8.1+ and Laravel 9-13. Install the package via Composer and run the migrations:

composer require binafy/laravel-discount

php artisan migrate

The service provider registers itself, and the migrations create discounts, discount_usages, and discountables. Publishing the config is optional, and only worth doing if you need to change table names, point at a different user model, or adjust code generation defaults:

php artisan vendor:publish --tag="laravel-discount-config"

Two Artisan commands come along with it. discount:generate produces codes from the terminal, and discount:prune deletes expired discounts along with their usage records, which is a reasonable thing to put on the scheduler:

Schedule::command('discount:prune --days=30')->daily();

The full documentation, including the config reference, is on the Laravel Discount GitHub repository.

Laravel News

Slashdot Reader Builds a Photo-Verification App for iPhones

Long-time Slashdot reader BrianFagioli is announcing that he’s released a new iPhone app that creates a cryptographic witness for photos without uploading the original image.
The app hashes the file, signs the hash using a dedicated identity stored in Apple Keychain, and publishes the witness to Nostr relays while keeping the photo inside the app unless the user chooses to export it. Rather than trying to determine whether a scene was genuine, the app — named Veridenz — answers a narrower question, by verifying whether a photo file matches the one that was originally witnessed.
Users do not need a Nostr account because the app creates its own signing identity, keeping private documentation separate from a public Nostr profile. "The developer does not collect any data from this app," says its page in Apple’s iPhone store. The app’s tagline is "Capture. Prove. Verify."


Read more of this story at Slashdot.

Slashdot

MySQL Index Optimization: How to Speed Up Your Database Queries

https://webyog.com/wp-content/uploads/2026/08/Index-optimization.png

If there is one change that can turn a 10-second MySQL query into a millisecond one, it is adding the right index. And if there is one change that can quietly slow down your write heavy database over time, it is adding too many. 

Indexes are the most powerful performance tool in MySQL. Understanding how they work — and how they fail — is foundational to managing database performance at any scale.

How Indexes Work

An index is a separate data structure MySQL maintains alongside your table. Think of it like a book’s index: instead of reading every page to find a topic, you go directly to the right page number. 

Without an index on a filtered column, MySQL reads every row in the table on every query. With the right index, it locates matching rows in a fraction of the reads.

Reading EXPLAIN

EXPLAIN is your primary tool for understanding whether MySQL is using an index and how.

EXPLAIN SELECT * FROM customers WHERE email = ‘[email protected]’; 

The most important column in the output is type.

Other columns to watch: 

•  key — which index MySQL chose. NULL means no index used. 

•  rows — estimated rows MySQL will examine. Lower is better. 

•  Extra: Using filesort — sorting without an index. Can be expensive on large result sets.

•  Extra: Using index — the query was satisfied entirely from the index. Very efficient.

Index Types

Creating Effective Indexes

Single-Column Indexes 

The simplest case: a column you filter on frequently.

ALTER TABLE customers ADD UNIQUE INDEX idx_email (email);

Before and after:

Composite Indexes and the Left-Prefix Rule

When queries filter on multiple columns, a composite index is usually more effective than separate single-column indexes.

ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);

MySQL can use this index for queries that start with the leftmost column:

Order composite index columns by how they appear in your most common queries, with the most selective column first.

Covering Indexes

A covering index includes all the columns a query needs. MySQL satisfies the entire query from the index without reading the table at all — shown as Using index in EXPLAIN’s Extra column.

— Query only needs these three columns 
SELECT id, email, status FROM customers WHERE status = ‘active’; 

— Covering index includes all three 
ALTER TABLE customers ADD INDEX idx_covering (status, id, email);

On large tables, the performance difference between a regular index lookup and a covering index can be significant.

Common Mistakes

Over-Indexing

Every index you add slows down writes. MySQL updates all indexes on every INSERT, UPDATE, and DELETE. 

Find and remove unused indexes regularly:

SELECT * FROM sys.schema_unused_indexes;
SELECT * FROM sys.schema_redundant_indexes;

Functions on Indexed Columns

Using a function on an indexed column prevents MySQL from using the index:

Low-Cardinality Columns

Columns with very few distinct values (such as a boolean or a status with three possible values) are rarely worth indexing on their own. MySQL may choose a full table scan if it estimates the index would return a large fraction of rows anyway.

Maintaining Indexes

Want to analyze query performance visually? SQLyog’s Query Profiler shows detailed index usage and execution metrics for every query you run. Try SQLyog free.

Frequently Asked Questions

How do I know if a column needs an index?

Check your slow query log for queries with type: ALL in EXPLAIN. Any column that appears frequently in WHERE, JOIN, or ORDER BY clauses is a candidate. The test is always: run EXPLAIN before and after adding the index to confirm it is being used.

How many indexes should a table have?

There is no universal number, but most well-optimized tables have 3–8 indexes. Start with the primary key, add indexes for your most common query patterns, and stop before you over-index. Audit unused indexes quarterly.

Will adding an index lock my table in production?

In MySQL 5.6+ with InnoDB, most index additions are online and do not block reads or writes during the build. There is a brief metadata lock at the start and end. For very large tables, run the operation during low-traffic windows as a precaution.

My query shows an index in EXPLAIN but is still slow — why?

A few possibilities: the index has low cardinality and MySQL still reads many rows through it; you need a covering index (MySQL uses the index for filtering but reads full table rows for each match); or there is a Using filesort in EXPLAIN’s Extra column indicating additional sorting work after the index lookup.

What is the difference between a composite index and two separate indexes?

A composite index on (col1, col2) allows MySQL to filter on both columns in a single index lookup. Two separate indexes on col1 and col2 require MySQL to use one or perform a merge operation. For queries filtering on multiple columns together, a composite index almost always performs better.

How do I find duplicate indexes?

Query sys.schema_redundant_indexes . It shows indexes made unnecessary by another index on the same table — for example, if you have indexes on (a) and (a, b) , the first is redundant because all queries it can satisfy can also be satisfied by (a, b) .

Does MySQL automatically create indexes?

MySQL automatically indexes the primary key and any column declared with the UNIQUE constraint. It does not create indexes on foreign key columns or frequently queried columns automatically — those you must add manually based on your query patterns.

What does Using index in EXPLAIN’s Extra column mean?

It means MySQL satisfied the entire query from the index without reading the actual table rows. This is called a covering index and is one of the most efficient query execution patterns. When you see Using index , the query is well-optimized for index usage.

Planet for the MySQL Community