spb/ultra-sharp-agent-skills Public
Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.
Python 100%
1<!--2Author: Simon-Pierre Boucher3Contact: contact@spboucher.ai4-->56# NoSQL Data Modeling — Patterns78## Contents9- Access-pattern table template10- Embed vs reference — worked examples11- Bucket pattern for unbounded arrays12- Cursor pagination13- Redis key catalog14- Schema versioning / lazy migration15- Gotchas1617## Access-pattern table template1819| # | Operation | Fields needed | Freq | Consistency |20|---|---|---|---|---|21| 1 | Load order page | order, items, shipping status | 500 r/s | read-your-writes |22| 2 | Add item to cart | cart items | 50 w/s | strong (same doc) |23| 3 | Seller sales list | order ids, totals by seller | 5 r/s | eventual ok |2425Every pattern must map to one indexed query or key lookup in the final model.2627## Embed vs reference — worked examples2829```js30// EMBED: order + line items (1:few, read & updated together, bounded)31{32 _id: "ord_81",33 user_id: "u_42", // reference — user updates independently34 user_name: "Alice", // denormalized copy; truth: users collection,35 // refreshed: never (historical snapshot is correct here)36 status: "shipped",37 items: [ { sku: "A-1", qty: 2, unit_price_cents: 1250 } ],38 schema_version: 239}4041// REFERENCE: user ← posts (unbounded growth)42{ _id: "u_42", name: "Alice", schema_version: 1 }43{ _id: "p_777", author_id: "u_42", author_name: "Alice", // copy, refreshed on user rename via async job44 title: "...", created_at: ISODate("2026-08-01") }45// Index to serve "posts by user, newest first":46db.posts.createIndex({ author_id: 1, created_at: -1 })47```4849## Bucket pattern for unbounded arrays5051```js52// Instead of one sensor doc with an ever-growing readings[] array:53{54 _id: "sensor_9:2026-08-05", // one bucket per sensor per day55 sensor_id: "sensor_9",56 day: "2026-08-05",57 count: 1440,58 readings: [ { t: "00:00", v: 20.1 }, /* ≤ 1440 */ ]59}60// Bounded documents, efficient range reads, no 16MB ceiling risk.61```6263## Cursor pagination6465```js66// Page 1:67db.posts.find({ author_id: uid })68 .sort({ created_at: -1, _id: -1 }) // _id breaks timestamp ties69 .limit(20)70// Next page — cursor = (created_at, _id) of last item:71db.posts.find({72 author_id: uid,73 $or: [74 { created_at: { $lt: cursor.created_at } },75 { created_at: cursor.created_at, _id: { $lt: cursor._id } }76 ]77}).sort({ created_at: -1, _id: -1 }).limit(20)78// Offsets (skip) degrade linearly and break when rows shift — never for feeds.79```8081## Redis key catalog8283Document every key family in one table; every cache key has a TTL.8485| Key pattern | Type | TTL | Notes |86|---|---|---|---|87| `user:{id}:session` | hash | 30d sliding | auth token data |88| `cache:product:{id}` | string(json) | 300s | invalidate on product write |89| `rate:{ip}:{minute}` | int (INCR) | 120s | rate limiting window |90| `queue:emails` | list | none | worker queue (durable store elsewhere) |91| `counter:orders:{0..15}` | int | none | sharded hot counter; SUM on read |9293```94SET cache:product:9f3e '{"name":...}' EX 30095INCR rate:203.0.113.7:20260805121196EXPIRE rate:203.0.113.7:202608051211 120 NX97```9899## Schema versioning / lazy migration100101```js102// Reader handles current and previous version:103function readUser(doc) {104 if (doc.schema_version === 1) {105 doc.full_name = doc.name; // v1 → v2 shape106 doc.schema_version = 2;107 }108 return doc;109}110// Writer persists upgraded shape on next write ("lazy migration").111// Backfill job optional once v1 read-rate ≈ 0.112```113114## Gotchas115116- MongoDB's 16MB limit includes field names — long repeated keys in big arrays117 waste real space; short names matter at scale.118- An index on `{a: 1, b: 1}` serves filters on `a` and `a+b`, NOT `b` alone119 (prefix rule) — order compound indexes by equality → sort → range.120- `$lookup` (joins) run on unsharded/local data paths and get slow fast — a121 frequent `$lookup` is a modeling smell: embed or copy instead.122- DynamoDB: model EVERYTHING around partition key + sort key up front; there is123 no ad-hoc query escape hatch, only GSIs (each with its own cost).124- Redis `KEYS pattern` blocks the server — always `SCAN` in production.125- Firestore charges and limits per document read — bucket smallness matters126 differently: many tiny docs can cost more than fewer medium ones.127- Eventual consistency of denormalized copies must be a product decision128 ("name may be stale ≤5 min"), never an accident.129