db-antipatterns (M19) — unified anti-pattern catalog
M19 is the catch-all for recognized anti-patterns that span paradigms and don't sit cleanly inside one
structural module. It does not own a weight of its own. Findings inherit the category of the most
natural module: an EAV smell is a modeling/normalization problem → it scores under M1 (Modelado,
design); a Mongo unbounded-array smell scores under document Crecimiento-doc (performance); polymorphic
associations without an FK score under M3 (Integridad referencial, both). The finding's module stays
M19 for provenance, but expected_impact.axis and the rule id's intent route it to the owning
category at scoring time. This avoids double-counting — a single smell contributes to exactly one
category per axis. See references/scoring-model.md for the category map per paradigm.
What it checks (catalog)
Relational / SQL:
- EAV (entity-attribute-value) generic
(entity, attribute, value) tables replacing real columns →
re-homes to M1 (design). Defeats types, constraints, indexing.
- God table — one table with dozens-to-hundreds of columns, many nullable → M1 (design).
- CSV-in-a-column —
tags VARCHAR holding "a,b,c" instead of a child table/array/jsonb → M1/M4.
- Polymorphic association without FK (
commentable_type + commentable_id, no constraint) → M3 (both).
- Boolean-flag soup — many
is_* booleans that should be a state enum/lookup → M4 (design).
- Magic catch-all
jsonb/text blob used as schema evasion → re-homes to M4 (design).
NoSQL:
- Mongo unbounded array growth — arrays that grow without bound toward the 16 MB doc cap → document
Crecimiento-doc (performance).
- Deep nesting / massive embedded docs that should be referenced collections → document
Access-pattern & embedding (design).
- App-side join / fan-out — N reads to stitch documents that a different model would co-locate →
document Query (performance, directional/structural).
- Redis as durable primary store — relied-on data with no AOF/RDB durability path → key-value
Durabilidad (perf). Sev-5 only with live evidence of durability config; else directional.
Tier-0 static checks
Parse the schema/ORM (scripts/parse-schema.mjs, parse-orm-python.py). Flag: a table whose columns
are literally (entity_id, attribute, value); column count above a high threshold with mostly-nullable
columns; string columns named tags/roles/csv; *_type + *_id pairs with no matching FK; ≥N
is_*/has_* booleans on one table; Mongoose schemas with array-of-subdocument fields and no cap.
Tier-1 verification query
- Postgres god table:
SELECT relname, count(*) AS cols FROM information_schema.columns JOIN pg_class ON relname=table_name GROUP BY relname ORDER BY cols DESC;
- Mongo array growth (Tier-1/2):
db.coll.aggregate([{$project:{n:{$size:"$items"}}},{$group:{_id:null,max:{$max:"$n"},avg:{$avg:"$n"}}}])
and db.coll.stats() for avg doc size trending toward 16 MB.
- Redis durability:
CONFIG GET save and CONFIG GET appendonly (needs live; else needs_api).
Findings
Emit per schema/finding.schema.json. Example ids:
M19.events.eav_table — generic EAV table (warn, severity 3, axis design, confidence directional,
re-homes to Modelado/M1).
M19.comments.polymorphic_no_fk — commentable_type/commentable_id with no FK (fail, severity 4,
axis both, re-homes to M3).
M19.feed.unbounded_array — Mongo array trending toward 16 MB (warn, severity 4, axis performance,
re-homes to document Crecimiento-doc; sev-5 only with live size evidence).
Each finding: evidence.observed quotes the real DDL/Mongoose schema/query verbatim with secrets
redacted; verification.reproduce is one of the runnable commands above (referencing $DATABASE_URL);
verification.method is ddl_parse, schema_introspect, or manual_review;
expected_impact is {axis, confidence, magnitude (high|medium|low), rationale} — banded, never a naked %.
Honesty
- Never call a pattern an anti-pattern without naming the workload it harms; EAV, denormalized blobs,
and embedded arrays are legitimate in the right context. State the assumption.
- A
directional structural smell (e.g. app-side fan-out inferred from ORM source) never caps a
score. Redis-undurable and unbounded-array sev-5 require live evidence; otherwise directional or
needs_api, never a silent pass.
1---2name: db-antipatterns3description: db-antipatterns (M19) — unified anti-pattern catalog4---56# db-antipatterns (M19) — unified anti-pattern catalog78M19 is the catch-all for recognized anti-patterns that span paradigms and don't sit cleanly inside one9structural module. It does **not** own a weight of its own. **Findings inherit the category of the most10natural module**: an EAV smell is a modeling/normalization problem → it scores under M1 (Modelado,11design); a Mongo unbounded-array smell scores under document Crecimiento-doc (performance); polymorphic12associations without an FK score under M3 (Integridad referencial, both). The finding's `module` stays13`M19` for provenance, but `expected_impact.axis` and the rule id's intent route it to the owning14category at scoring time. This avoids double-counting — a single smell contributes to exactly one15category per axis. See `references/scoring-model.md` for the category map per paradigm.1617## What it checks (catalog)18**Relational / SQL:**19- **EAV (entity-attribute-value)** generic `(entity, attribute, value)` tables replacing real columns →20 re-homes to M1 (design). Defeats types, constraints, indexing.21- **God table** — one table with dozens-to-hundreds of columns, many nullable → M1 (design).22- **CSV-in-a-column** — `tags VARCHAR` holding `"a,b,c"` instead of a child table/array/jsonb → M1/M4.23- **Polymorphic association** without FK (`commentable_type` + `commentable_id`, no constraint) → M3 (both).24- **Boolean-flag soup** — many `is_*` booleans that should be a state enum/lookup → M4 (design).25- **Magic catch-all `jsonb`/`text` blob** used as schema evasion → re-homes to M4 (design).26**NoSQL:**27- **Mongo unbounded array growth** — arrays that grow without bound toward the 16 MB doc cap → document28 Crecimiento-doc (performance).29- **Deep nesting / massive embedded docs** that should be referenced collections → document30 Access-pattern & embedding (design).31- **App-side join / fan-out** — N reads to stitch documents that a different model would co-locate →32 document Query (performance, directional/structural).33- **Redis as durable primary store** — relied-on data with no AOF/RDB durability path → key-value34 Durabilidad (perf). **Sev-5 only with live evidence** of durability config; else directional.3536## Tier-0 static checks37Parse the schema/ORM (`scripts/parse-schema.mjs`, `parse-orm-python.py`). Flag: a table whose columns38are literally `(entity_id, attribute, value)`; column count above a high threshold with mostly-nullable39columns; string columns named `tags`/`roles`/`csv`; `*_type` + `*_id` pairs with no matching FK; ≥N40`is_*`/`has_*` booleans on one table; Mongoose schemas with array-of-subdocument fields and no cap.4142## Tier-1 verification query43- Postgres god table: `SELECT relname, count(*) AS cols FROM information_schema.columns44 JOIN pg_class ON relname=table_name GROUP BY relname ORDER BY cols DESC;`45- Mongo array growth (Tier-1/2): `db.coll.aggregate([{$project:{n:{$size:"$items"}}},{$group:{_id:null,max:{$max:"$n"},avg:{$avg:"$n"}}}])`46 and `db.coll.stats()` for avg doc size trending toward 16 MB.47- Redis durability: `CONFIG GET save` and `CONFIG GET appendonly` (needs live; else `needs_api`).4849## Findings50Emit per `schema/finding.schema.json`. Example ids:51- `M19.events.eav_table` — generic EAV table (warn, severity 3, axis `design`, confidence `directional`,52 re-homes to Modelado/M1).53- `M19.comments.polymorphic_no_fk` — `commentable_type`/`commentable_id` with no FK (fail, severity 4,54 axis `both`, re-homes to M3).55- `M19.feed.unbounded_array` — Mongo array trending toward 16 MB (warn, severity 4, axis `performance`,56 re-homes to document Crecimiento-doc; **sev-5 only with live size evidence**).57Each finding: `evidence.observed` quotes the real DDL/Mongoose schema/query verbatim with secrets58redacted; `verification.reproduce` is one of the runnable commands above (referencing `$DATABASE_URL`);59`verification.method` is `ddl_parse`, `schema_introspect`, or `manual_review`;60`expected_impact` is `{axis, confidence, magnitude (high|medium|low), rationale}` — banded, never a naked %.6162## Honesty63- Never call a pattern an anti-pattern without naming the workload it harms; EAV, denormalized blobs,64 and embedded arrays are legitimate in the right context. State the assumption.65- A `directional` structural smell (e.g. app-side fan-out inferred from ORM source) **never caps** a66 score. Redis-undurable and unbounded-array sev-5 require live evidence; otherwise `directional` or67 `needs_api`, never a silent pass.