mysql-patterns — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited mysql-patterns (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
@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. swapping LIKE for 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.@skills/mysql-problem-solver/SKILL.md. This skill is for designing features, not investigating regressions.@rules/laravel/laravel.mdc and @rules/laravel/architecture.mdc.@rules/security/backend.md — parameterized queries / ORM only, least-privilege DB users, never hardcode credentials.final classes, declare(strict_types=1), Pest tests for any data-access code added.SELECT VERSION();). MySQL 8 and MariaDB diverge on JSON, ON DUPLICATE KEY aliases, and SKIP LOCKED.These topics complement the query-tuning rules; they are not covered there.
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.INSERT ... ON DUPLICATE KEY UPDATE price = VALUES(price) (MariaDB) / ... AS new ON DUPLICATE KEY UPDATE price = new.price (MySQL 8, VALUES() deprecated).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();->> (or JSON_UNQUOTE(JSON_EXTRACT(...))) returns the unquoted scalar; index that, not the raw ->.VIRTUAL columns compute on read (no storage, index still allowed); STORED persists (faster read, costs disk + write). Default to VIRTUAL unless the column is read-heavy.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();innodb_ft_min_token_size is 3 — shorter terms are ignored unless you lower it and rebuild the index.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)');NOW(), RAND()).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)
);WHERE; a query without it scans every partition.DB::statement(...) with raw SQL.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 => true routes 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.Model::on('mysql::read') or DB::connection('mysql')->select(...).sticky or an explicit write-connection read.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.@rules/sql/optimalize.mdc for the transaction/locking fundamentals this builds on.// config/database.php 'mysql' → 'options'
PDO::ATTR_TIMEOUT => 3, // connect timeout (seconds)
PDO::ATTR_PERSISTENT => false, // avoid stale persistent conns behind PgBouncer-style poolerswait_timeout / interactive_timeout server-side to reclaim idle connections; size max_connections to (workers + queue workers + scheduler) × pool.slow_query_log = 1, long_query_time = 1) to surface candidates for @skills/mysql-problem-solver/SKILL.md.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(); -- sizes@rules/sql/optimalize.mdc "Performance Non-Regression on Query Changes").@rules/sql/optimalize.mdc).sticky on for read-after-write paths.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.