mysql-patterns
Skill10 starsChanged 3 months agoMITView on GitHub
Use when designing MySQL schema features or applying advanced MySQL patterns in Laravel — upserts, JSON columns, full-text search, partitioning, replication/read-write splitting, and deadlock handling — beyond the query tuning already in the SQL rules.
We split this skill into the rules it gives an agent
7 rules. 0 are common, 3 are distinctive, and 4 are very specific to this project.
0
Common
Most of the 9 edited versions we read keep these.
3
Distinctive
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 14
If the project uses Laravel, also apply @rules/laravel/laravel.mdc and @rules/laravel/architecture.mdc.
- Very specific: line 17
Verify the engine/version before using a version-specific feature (SELECT VERSION();).
- Very specific: line 130
Default innodb_ft_min_token_size is 3 — shorter terms are ignored unless you lower it and rebuild the index.
- Distinctive: line 144
Use them to enforce an invariant in one place instead of recomputing it in every query.
- Distinctive: line 169
Every UNIQUE / PRIMARY key must include the partition column — this often forces a composite PK and reshapes the schema.
- Distinctive: line 198
Keep transactions short, lock rows in a consistent order across code paths, and touch the fewest rows possible.
- Very specific: line 230
Read/write splitting keeps sticky on for read-after-write paths.
“Common” is measured: kept by at least 6 in 10 of the 9 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
MySQL Patterns
Constraints
- Apply
@rules/sql/optimalize.mdc— it already owns indexing, SARGable WHERE, seek/keyset pagination, EXPLAIN, transactions/locking basics, batch-over-per-row, CTE/window/recursive queries, schema basics, DB-level caching, and the Performance Non-Regression on Query Changes gate. Do not re-explain those here; defer to it. When a pattern in this skill changes an existing query (e.g. swappingLIKEfor a FULLTEXT match, moving a filter onto a generated column, introducing partition pruning), capture the original query's baseline and confirm the new shape is equal or faster — if it is slower, document the reason and the remaining optimization options per that gate. - For diagnosing an existing slow query, use
@skills/mysql-problem-solver/SKILL.md. This skill is for designing features, not investigating regressions. - If the project uses Laravel, also apply
@rules/laravel/laravel.mdcand@rules/laravel/architecture.mdc. - Apply
@rules/security/backend.md— parameterized queries / ORM only, least-privilege DB users, never hardcode credentials. finalclasses,declare(strict_types=1), Pest tests for any data-access code added.- Verify the engine/version before using a version-specific feature (
SELECT VERSION();). MySQL 8 and MariaDB diverge on JSON,ON DUPLICATE KEYaliases, andSKIP LOCKED.
Use when
- Adding upserts, JSON columns, full-text search, generated columns, or partitioning to a Laravel app.
- Writing a migration that creates a new table and needs the schema standard expressed in Blueprint.
- Setting up read/write splitting against replicas, or hardening against deadlocks and connection exhaustion.
- Reviewing a migration that introduces any of the above on a large table.
These topics complement the query-tuning rules; they are not covered there.
Schema Standard in Laravel Migrations
@rules/sql/optimalize.mdc Schema Design owns the standard (singular names, NOT NULL defaults, _at / _date suffixes, DATETIME over TIMESTAMP, adjective booleans, justified VARCHAR widths, DECIMAL money, ON DELETE by relation nature, CHECK invariants, utf8mb4_0900_*, PK width). This section only maps it onto Blueprint, where several defaults contradict it.
Schema::create('post', function (Blueprint $table): void { // singular — Eloquent needs $table = 'post';
$table->increments('id'); // INT UNSIGNED; ->id() would give BIGINT
$table->string('slug', 100)->charset('utf8mb4')->collation('utf8mb4_0900_bin');
$table->string('meta_title', 160)->default(''); // NOT NULL + domain default
$table->boolean('published')->default(false); // adjective, positive polarity
$table->decimal('price', 9, 2); // money is never float/double
$table->date('invoice_date'); // calendar day → DATE
$table->dateTime('published_at')->nullable();
$table->dateTime('created_at');
$table->dateTime('updated_at')->useCurrent()->useCurrentOnUpdate();
$table->unsignedInteger('author_id'); // same width as user.id
$table->foreign('author_id', 'fk_post_author') // name the constraint yourself
->references('id')->on('user')
->restrictOnDelete()->cascadeOnUpdate();
$table->index(['author_id', 'published_at'], 'idx_post_author_published');
});$table->timestamps()emitsTIMESTAMPcolumns — the 2038 + session-zone trap. DeclaredateTime('created_at')/dateTime('updated_at')explicitly.- Pair the FK width to the PK it references.
$table->id()and$table->foreignId()are bothBIGINT UNSIGNED;increments()isINT UNSIGNED. Soid()pairs withforeignId()->constrained(), andincrements()pairs withunsignedInteger()+foreign(). A mixed pair makes MySQL reject the constraint (errno 3780, incompatible column types). constrained()/foreign()generate a name likepost_author_id_foreign— pass the constraint name (constrained(table: 'user', indexName: 'fk_post_author'), or the second argument offoreign()) so the schema keeps the naming the rule requires.- FK actions read directly off the relation:
cascadeOnDelete()(composition),restrictOnDelete()(association),nullOnDelete()(meaningful detachment). - Blueprint has no DSL for
CHECKconstraints, triggers, orCOMMENTon a magically maintained column — useDB::statement(...)and name the constraint after the rule:DB::statement('ALTER TABLE `customer_order` ADD CONSTRAINT `chk_order_shipped_needs_date` CHECK (`status` <> \'shipped\' OR `shipped_at` IS NOT NULL)'); - Set the connection charset/collation and strict mode once in
config/database.php:'charset' => 'utf8mb4','collation' => 'utf8mb4_0900_ai_ci','strict' => true(or an explicit'modes' => [...]list). MariaDB has noutf8mb4_0900_*family — verify withSELECT VERSION();before pinning it. - An
updated_atmaintained byON UPDATE CURRENT_TIMESTAMPand one maintained by Eloquent are two mechanisms for one column — pick one (public $timestamps = false;when the DB owns it) so the row version is not written twice with different values.
Upserts
Pick the narrowest tool. All run as single statements — never loop per row.
// Insert-or-update many rows in ONE statement.
// 2nd arg = unique-by columns; 3rd = columns to overwrite on conflict.
Product::upsert(
[['sku' => 'A1', 'price' => 10], ['sku' => 'B2', 'price' => 20]],
uniqueBy: ['sku'],
update: ['price'],
);
// Insert, silently skip rows that violate a unique key. No update.
DB::table('tags')->insertOrIgnore([['name' => 'php'], ['name' => 'sql']]);
// Single row: fetch-or-create-then-update. Fires model events; runs SELECT + INSERT/UPDATE.
Product::updateOrCreate(['sku' => 'A1'], ['price' => 10]);upsert()/insertOrIgnore()are bulk and do not fire model events or touch timestamps automatically (set them yourself). Prefer them for large batches.updateOrCreate()is per-row, fires events, and is race-prone under concurrency unless a unique index backs the match columns. Wrap in a deadlock retry (below) when concurrent.- Raw form:
INSERT ... ON DUPLICATE KEY UPDATE price = VALUES(price)(MariaDB) /... AS new ON DUPLICATE KEY UPDATE price = new.price(MySQL 8,VALUES()deprecated). - A unique index on the conflict columns is mandatory or the upsert degrades to plain inserts.
JSON Columns + Indexes
JSON columns cannot be indexed directly. Index a generated column extracted from the JSON, or a multi-valued index for arrays.
Schema::table('products', function (Blueprint $table): void {
$table->json('attributes');
// Stored generated column promotes attributes->'$.color' to an indexable scalar.
$table->string('color')->storedAs("attributes->>'$.color'");
$table->index('color');
});// Querying JSON paths in Eloquent:
Product::where('attributes->color', 'red')->get();
Product::whereJsonContains('attributes->tags', 'sale')->get();->>(orJSON_UNQUOTE(JSON_EXTRACT(...))) returns the unquoted scalar; index that, not the raw->.VIRTUALcolumns compute on read (no storage, index still allowed);STOREDpersists (faster read, costs disk + write). Default toVIRTUALunless the column is read-heavy.- Validate JSON shape in the application; MySQL only enforces well-formedness, not structure.
Full-Text Search
For natural-language search on TEXT columns, a FULLTEXT index beats LIKE '%term%' (which is non-SARGable, see @rules/sql/optimalize.mdc).
Schema::table('articles', function (Blueprint $table): void {
$table->fullText(['title', 'body']);
});// Boolean mode supports +required -excluded "phrase" operators.
Article::whereFullText(['title', 'body'], 'laravel +redis', ['mode' => 'boolean'])->get();
// Or raw:
Article::whereRaw(
'MATCH(title, body) AGAINST(? IN NATURAL LANGUAGE MODE)',
['laravel redis'],
)->get();- Default
innodb_ft_min_token_sizeis 3 — shorter terms are ignored unless you lower it and rebuild the index. - FULLTEXT covers basic relevance ranking only. For typo tolerance, facets, or large corpora, reach for a dedicated engine (Meilisearch/Elasticsearch via Laravel Scout). Note the tradeoff; do not over-engineer for small tables.
Generated / Virtual Columns
Beyond JSON, generated columns normalize derived values so they stay consistent and become indexable.
$table->decimal('net', 15, 2);
$table->decimal('vat_rate', 5, 4);
$table->decimal('gross', 15, 2)->virtualAs('net * (1 + vat_rate)');- A generated column cannot reference another generated column defined after it, nor non-deterministic functions (
NOW(),RAND()). - Use them to enforce an invariant in one place instead of recomputing it in every query.
Partitioning
Partitioning splits one logical table across physical segments to prune scans and cheapen bulk drops. Use it for genuinely large, time- or range-sliced tables — not by default.
-- RANGE: time-series; drop old data instantly with ALTER TABLE ... DROP PARTITION.
CREATE TABLE events (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at DATETIME NOT NULL,
PRIMARY KEY (id, created_at) -- partition key MUST be in every unique key
) PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- LIST: discrete buckets (e.g. region/tenant).
PARTITION BY LIST (region_id) (
PARTITION eu VALUES IN (1, 2, 3),
PARTITION us VALUES IN (4, 5)
);- Every UNIQUE / PRIMARY key must include the partition column — this often forces a composite PK and reshapes the schema. Decide early.
- Partition pruning only triggers when the partition key appears in the
WHERE; a query without it scans every partition. - Laravel migrations have no partition DSL — use
DB::statement(...)with raw SQL.
Replication & Read/Write Splitting
Route reads to replicas, writes to the primary, with a single connection config.
// config/database.php — 'mysql' connection
'read' => ['host' => ['10.0.0.2', '10.0.0.3']],
'write' => ['host' => ['10.0.0.1']],
'sticky' => true,sticky => trueroutes reads back to the write host for the rest of the request after any write, so a just-written row is read back consistently despite replication lag. Keep it on for typical web flows.- Force a specific connection when needed:
Model::on('mysql::read')orDB::connection('mysql')->select(...). - Replicas are eventually consistent. Never read a balance/counter you just wrote from a replica without
stickyor an explicit write-connection read.
Deadlock Retry
InnoDB aborts one transaction in a deadlock (SQLSTATE 40001, error 1213). The correct response is to retry the whole transaction, not to catch-and-continue.
DB::transaction(function (): void {
// ... ordered, consistent locking; lock rows in a stable order to reduce deadlocks
}, attempts: 3); // Laravel retries the closure on deadlock up to `attempts` times.- Keep transactions short, lock rows in a consistent order across code paths, and touch the fewest rows possible.
- Idempotency matters: the closure runs up to N times, so it must be safe to re-run.
- See
@rules/sql/optimalize.mdcfor the transaction/locking fundamentals this builds on.
Connection Config & Timeouts
// config/database.php 'mysql' → 'options'
PDO::ATTR_TIMEOUT => 3, // connect timeout (seconds)
PDO::ATTR_PERSISTENT => false, // avoid stale persistent conns behind PgBouncer-style poolers- Set
wait_timeout/interactive_timeoutserver-side to reclaim idle connections; sizemax_connectionsto(workers + queue workers + scheduler) × pool. - Enable the slow query log (
slow_query_log = 1,long_query_time = 1) to surface candidates for@skills/mysql-problem-solver/SKILL.md.
Diagnostics Cheatsheet
SHOW ENGINE INNODB STATUS\G -- LATEST DETECTED DEADLOCK + lock waits
SHOW FULL PROCESSLIST; -- live queries, state, time, lock waits
SHOW INDEX FROM orders; -- existing indexes (verify before adding one)
SELECT * FROM information_schema.innodb_trx; -- open transactions
SELECT * FROM performance_schema.data_lock_waits; -- who blocks whom (MySQL 8)
SELECT table_name, data_length, index_length
FROM information_schema.tables WHERE table_schema = DATABASE(); -- sizesDone when
- The chosen feature is verified against the actual engine/version.
- Any pattern that changed an existing query was benchmarked against its baseline and is equal or faster; a slower result carries the documented reason and remaining optimization options (
@rules/sql/optimalize.mdc"Performance Non-Regression on Query Changes"). - Upserts run as single statements with a backing unique index; concurrent ones are deadlock-retried.
- JSON / FULLTEXT / partition queries are confirmed to hit the intended index (EXPLAIN — see
@rules/sql/optimalize.mdc). - Read/write splitting keeps
stickyon for read-after-write paths. - Pest tests cover the new data-access paths; no secrets are hardcoded.
Other versions9 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 9
- Fmarzochi/EGC★ 61same name
- gongyijie85/dsh-ecc★ 8same name
- ronmkr/PromptBook★ 2same name
- Ronnasayd/aipp★ 1same name
- SamuelNDCE/claude-super-skill-library★ 1same name
- anbanmv/claude-db-patterns★ 0same name
- davidanthonyn/claude-skills★ 0same name
- lintangknitto/agent-skills★ 0same name
- renepf/claude-skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository62 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- analyze-problemSkill
- api-reviewSkill
- article-writingSkill
- assignment-compliance-checkSkill
- automation-audit-opsSkill
- autonomous-loopsSkill
- benchmark-optimization-loopSkill
- benchmarkSkill
- blueprintSkill
- class-refactoringSkill
- cleanup-local-branchesSkill
- codebase-simplification-auditSkill
- code-review-bugsnagSkill
- code-review-githubSkill
- code-review-jiraSkill
- code-reviewSkill
- composer-updateSkill
- create-issues-from-textSkill
- create-issueSkill
- create-missing-tests-in-prSkill
- create-testSkill
- design-systemSkill
- docker-patternsSkill
- e2e-testingSkill
- frontend-a11ySkill
- frontend-design-directionSkill
- frontend-patternsSkill
- frontend-slidesSkill
- git-workflowSkill
- laravel-authorization-reviewSkill
- laravel-securitySkill
- laravel-telescopeSkill
- latency-critical-systemsSkill
- merge-github-prSkill
- mysql-problem-solverSkill
- penetration-testerSkill
- postgres-patternsSkill
- prepare-issue-contextSkill
- process-code-reviewSkill
- product-capabilitySkill
- production-auditSkill
- pr-staged-merge-planSkill
- pr-summarySkill
- record-project-memorySkill
- redis-patternsSkill
- refactor-entry-point-to-actionSkill
- refresh-claude-mdSkill
- resolve-issueSkill
- rewrite-tests-pestSkill
- security-bounty-hunterSkill
- security-reviewSkill
- security-threat-analysisSkill
- seoSkill
- skill-creatorSkill
- slack-messagingSkill
- smartest-project-additionSkill
- test-driven-developmentSkill
- tester-cookbookSkill
- test-like-humanSkill
- The first 60 shown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

