编写、审阅、调优 MySQL、SQLite、MariaDB、SQL Server 上的 SQL,并安全迁移。
记忆
MongoDB
围绕 explain 执行计划与具体规则,给出 MongoDB 模式设计、查询调优与故障处置方案。
它能做什么
围绕 MongoDB 文档建模、复合索引设计(ESR 顺序)、聚合管道构建与连接池配置展开。能解读 explain 执行计划(COLLSCAN、totalDocsExamined 与 nReturned 比值),识别模式陷阱(无限增长的数组、整文档重写开销),并列出常见错误码的首步处置。覆盖副本集、分片、变更流、时序集合、带重试的事务、Atlas Search 与向量搜索、数据迁移、备份与安全。适用于排查慢查询、处置生产故障(无主节点、oplog 窗口收缩、缓存停滞),以及在不引入 SQL 范式思维的前提下建模文档。
什么时候用它
- 文档模式设计:嵌入还是引用、数组增长、多租户布局
- 排查慢查询,解读 explain("executionStats") 输出
- 处置生产故障:主节点切换、副本滞后、连接风暴、oplog 窗口
- 构建聚合管道并选择合适的复合索引
技能文档
User preferences and memory live in ~/Clawic/data/mongodb/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/mongodb/ or ~/clawic/mongodb/), move it to ~/Clawic/data/mongodb/.
When To Use
- Designing or reviewing document schemas: embed vs reference, growing arrays, multi-tenant layout, versioned shapes
- A query, pipeline, or dashboard is slow: reading explain plans, designing compound indexes, killing a scan
- Building or debugging aggregation pipelines,
$lookupjoins, materialized views, window functions - Connecting an application: connection strings, pooling, timeouts, retries, ODM behavior, serverless clients
- Operating production: replica sets, sharding, write and read concerns, backups, upgrades, security, Atlas
- A live incident: no primary, connection storm, disk full, runaway operation, cache stall, blown oplog window
- Not for SQL or relational databases — normalization instincts from relational design actively mislead here (see Related Skills)
Quick Reference
| Situation | Play |
|---|---|
| Query is slow, cause unknown | explain("executionStats") before touching anything; triage order in slow-queries.md |
| explain shows COLLSCAN | No usable index, or the index exists in the wrong field order — check ESR before creating another (→ indexes.md) |
totalDocsExamined far above nReturned | Index is not selective enough for this shape; the ratio target is Core Rule 1 |
| Embed or reference this relationship? | Three questions in order: read together, bounded, independently updated (→ schema.md) |
| An array grows once per user action | Never embed it — child collection or time-series collection (Core Rule 3) |
| Pipeline aborts "exceeded memory limit" | A blocking stage hit its 100MB budget: index the sort or reshape upstream (→ aggregation.md) |
find().sort() fails on memory | Different limit: 32MB in-memory sort for find, not the pipeline's 100MB (→ slow-queries.md) |
$lookup inside a hot query | A schema signal, not a tuning problem: extended reference or embed (→ schema.md) |
Deep pagination (skip in the tens of thousands) | Range on the sort key: {_id: {$gt: lastSeenId}} — constant cost per page |
| "not writable primary", server selection timed out | Topology or failover, not your query (→ errors.md, then connections.md) |
| Latency spikes right after a deploy | Pool math: pods × maxPoolSize (Core Rule 8) or Mongoose autoIndex rebuilding on boot (→ connections.md) |
| Cursor died mid-iteration (code 43) | 10-minute idle cursor timeout — re-query by range per batch (→ connections.md) |
| Duplicate key error on an optional field | Plain unique index counts two missing fields as duplicate nulls — partial unique index (→ indexes.md) |
| Secondary lagging, oplog window shrinking | → replication.md; flow control and the resync threshold live there |
| Majority writes hang after losing one node | PSA topology trap (→ replication.md) |
| One shard absorbs all the writes | Monotonic shard key (→ sharding.md) |
| Full-text, fuzzy, faceted, or vector search | The built-in text index is the wrong tool past basic keyword match (→ search.md) |
| React to writes: CDC, cache invalidation, outbox | Change streams, resume tokens, oplog window (→ change-streams.md) |
| Changing a field's shape under live traffic | Expand → dual-write → backfill in batches → contract (→ migrations.md) |
| Metrics, events, or sensor readings by timestamp | Time-series collection, not a normal collection with a date index (→ time-series.md) |
| Multi-document invariant across collections | Try to co-locate it first; transactions with a retry loop if you cannot (→ transactions.md) |
| Database reachable from the internet | Auth, bindIp, and TLS audit before anything else (→ security.md) |
| An error code or message to decode | Error Codes below; full catalog with fixes in errors.md |
| Anything else | Reproduce with the smallest find() that still shows it, explain() before and after every change; if production is stuck, read db.currentOp() before you change a thing |
Depth on demand, by phase:
- Diagnose —
slow-queries.mdexplain plans and triage ·errors.mdcode to cause ·incidents.mdno primary, disk full, connection storm, cache stall ·monitoring.mdwhat to watch and alert on ·mongosh.mdthe shell toolkit - Design —
schema.mdembed vs reference and the pattern catalog ·indexes.mdESR, index types, lifecycle ·aggregation.mdpipeline craft ·time-series.mdmeasurements over time ·search.mdtext and vector search - Change —
migrations.mdonline schema change, backfills, bulk loading ·transactions.mdsessions and retry loops ·change-streams.mdreacting to writes - Operate —
connections.mddrivers, pools, timeouts, ODMs ·replication.mdreplica sets, concerns, oplog, upgrades ·sharding.mdshard keys and the balancer ·tuning.mdWiredTiger cache and host settings ·backups.mddumps, snapshots, PITR, drills ·security.mdauth, roles, TLS, injection, encryption ·atlas.mdthe managed platform
Core Rules
- Read
explain("executionStats"), never guess. Healthy ratio:totalDocsExamined / nReturned≈ 1. Worked example: 1,240,000 examined for 25 returned = 49,600:1 — missing or wrong index (Atlas's Query Targeting alert fires at 1000:1 by default). A ratio near 1 with a slow query is a different bug: look atnReturneditself, then at the sort. - 16MB is a ceiling, not a budget. WiredTiger rewrites the whole document on every update: a 5MB document taking a 20-byte
$incstill costs a 5MB rewrite in cache. Keep working documents in the KB range; a document you routinely read in full should fit in a single-digit number of 4KB pages. - Unbounded arrays are the #1 schema failure. Anything that grows per event goes to its own collection or a time-series collection (MongoDB >=5.0). A multikey index adds one entry per element: a 10,000-element array = 10,000 index entries for one document, paid on every write to that document.
- Set write and read concern explicitly in the connection string. Implicit defaults changed across versions (→
replication.md); code relying on them silently changes durability semantics on upgrade. Baseline URI:?retryWrites=true&w=majority. - One compound index per query shape, ordered Equality → Sort → Range. Prefix rule:
{a: 1, b: 1, c: 1}also serves queries on{a}and{a, b}— delete those redundant single-field indexes. Index intersection exists but the planner rarely picks it; never design for it. - Single-document atomicity is the concurrency primitive. Model invariants inside one document before reaching for transactions; when you do use them, stay under 1,000 modified documents and well inside the 60s default transaction lifetime (→
transactions.md). - Read-your-own-writes requires primary reads or a causal-consistency session. Secondary lag is usually sub-second but unbounded under load — never assume freshness on a secondary; bound it with
maxStalenessSecondswhen you read one deliberately. - One
MongoClientper process, sized from concurrency, not from the default. Total server connections = processes ×maxPoolSize(driver default 100): 40 pods × 100 = 4,000 connections ≈ 4GB of mongod RAM before a single document is served. Creating a client per request or per serverless invocation is the same bug at higher speed (→connections.md).
Query Semantics That Bite
{tags: "red"}matches docs wheretagsIS "red" or an array CONTAINING "red" — implicit array traversal, both directions, and you cannot turn it off.{qty: {$gte: 5, $lte: 10}}on an array field matches[3, 12]: different elements satisfy each bound. One element must satisfy all conditions →$elemMatch.{field: null}matches explicit null AND missing field. Distinguish with{field: {$type: "null"}}vs{field: {$exists: false}}.$ne,$nin,$nottechnically use the index but scan most of it — near-collection-scan cost; restructure with a positive predicate or a status field.skip(100000).limit(20)walks 100,020 index entries. Paginate by range on the sort key instead:{_id: {$gt: lastSeenId}}— constant cost per page.- Anchored case-sensitive regex
/^abc/uses the index;/^abc/icannot. Case-insensitive lookups need a collation index (→indexes.md). - Comparisons cross BSON types in a fixed order rather than erroring: the string
"5"is never$gt: 4. Mixed types in one field silently split every range query — enforce the type with a$jsonSchemavalidator. $inwith a large array is a range, not an equality, for index-order purposes; a short$ingets exploded into parallel index scans and keeps the sort order (→indexes.md).- Dotted paths into arrays are positional-ambiguous:
{"items.0.sku": x}targets a position,{"items.sku": x}targets any element.$and$[]update operators follow the same split.
Consistency Model
- Acknowledged ≠ durable: a
w: 1write vanishes into a rollback file if the primary fails before replication — someone must reconcile those by hand, and nothing in the application will ever know. - Failovers happen; retryable writes (default on in modern drivers) hide most of them — but
updateManyanddeleteManyare NOT retryable writes. Wrap multi-doc mutations in idempotent logic or a transaction. - Causal sessions give read-your-own-writes even across secondaries; use them instead of forcing
primaryeverywhere. - Read concern
localcan return writes that later roll back;majoritycannot.availableon a sharded cluster skips orphan filtering and can return documents twice — never use it for correctness-sensitive reads. - Arbiters are a durability trap, not a cheap third node: the PSA topology stalls majority writes when one data node is down (→
replication.md).
ObjectId and _id
- 12 bytes: 4-byte unix timestamp + 5-byte random + 3-byte counter.
ObjectId.getTimestamp()recovers creation time; sorting by_idapproximates insertion order (per-process counter, not a global clock). - Predictable enough to enumerate — never use as a security token or unguessable URL.
- Monotonic growth makes
_ida hotspotting shard key (→sharding.md). _idis immutable: changing it means insert-new + delete-old, which is a migration, not an update (error 66 inerrors.md).- Custom
_idis legitimate and free of a second index when the natural key is stable and short ({_id: ":"}). Random UUIDv4 as_idcosts B-tree locality on inserts; UUIDv7 or a prefixed key keeps writes clustered.
Error Codes
Codes are stable; message text is not. Match on the code. Full catalog with recovery steps in errors.md.
| Code | Name | First move |
|---|---|---|
| 11000 | DuplicateKey | Genuine duplicate, a race, or a unique index counting missing fields as null (→ indexes.md) |
| 112 | WriteConflict | Two writers hit the same document; inside a transaction, retry the whole transaction (→ transactions.md) |
| 50 | MaxTimeMSExpired | Your maxTimeMS fired as designed — the query is slow, not broken (→ slow-queries.md) |
| 43 | CursorNotFound | Cursor idled past 10 minutes; batch and re-query by range (→ connections.md) |
| 292 | QueryExceededMemoryLimitNoDiskUseAllowed | Blocking stage over 100MB with disk use off (→ aggregation.md) |
| 10107 | NotWritablePrimary | You are talking to a secondary or a stepped-down node — connect with the full replica set URI |
| 189 | PrimarySteppedDown | Failover in progress; retryable writes cover single-document writes only |
| 133 | FailedToSatisfyReadPreference | No member matches the read preference or maxStalenessSeconds (→ replication.md) |
| 251 | NoSuchTransaction | The transaction expired (60s default) or the session was lost (→ transactions.md) |
| 121 | DocumentValidationFailure | A $jsonSchema validator rejected the write — read errInfo for the failing path |
| 18 / 13 | AuthenticationFailed / Unauthorized | Credentials vs privileges: 18 is who you are, 13 is what you may do (→ security.md) |
| 66 | ImmutableField | Something tried to modify _id or a shard key field the wrong way |
Configuration
User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/mongodb/config.yaml.
| Variable | Type | Default | Effect |
|---|---|---|---|
| server_version | number (4.4-8.0) | 7.0 | Which version-gated advice applies (feature >=X lines) when the live server version is unknown; also gates upgrade recommendations |
| deployment | atlas | self-hosted | docker | documentdb | cosmos-mongo | atlas | Switches between Atlas console recipes and mongod.conf/setParameter, and suppresses features the emulation layers lack (→ atlas.md) |
| driver | mongosh | node | python | java | go | csharp | mongosh | Language of every emitted code example and connection string |
| odm | none | mongoose | prisma | beanie | spring-data | none | Whether examples are raw driver or ODM-shaped, and which ODM traps get surfaced (→ connections.md) |
| id_style | objectid | uuid | natural | objectid | _id type in generated schemas, migrations, and shard-key advice |
| field_naming | camelCase | snake_case | camelCase | Field names in generated documents, index specs, and pipelines |
| write_concern_default | majority | w1 | majority | Write concern written into emitted URIs and examples (Core Rule 4) |
| slow_ms | number (ms, 20-500) | 100 | Profiler threshold in monitoring.md recipes and what counts as "slow" when reporting |
| backfill_batch | number (docs, 100-10000) | 1000 | Batch size in every backfill and migration loop (→ migrations.md) |
| destructive_confirm | bool | true | drop, dropDatabase, dropIndex, unfiltered deleteMany/updateMany, and killOp are emitted for review instead of run |
Preference areas — customizable dimensions; a stated preference is recorded in config.yaml and applied from then on:
- Tooling — shell vs Compass vs driver REPL, migration framework (plain scripts, migrate-mongo, Mongock), diagram and schema-analysis tooling
- Thresholds — profiler
slowms, the examined:returned ratio worth reporting, pool size, replication-lag alarm, index-size budget per collection - Conventions — collection naming (plural/singular), timestamp field names, soft-delete policy,
schema_versionusage, index naming, enum-as-string vs code - Platform — server version, deployment target, storage class, instance memory and core count — affects every number in
tuning.md - Risk posture — whether to run mutations directly or hand back reviewed scripts, whether secondary reads are allowed, how aggressive index changes may be on a live collection
- Output format — snippets only vs snippets plus an explain walkthrough, how much plan detail to narrate, whether to include the rollback path by default
- Integrations — monitoring stack (Atlas metrics, Prometheus exporter, FTDC), backup tooling, search backend (Atlas Search vs external engine)
- Restrictions — compliance regimes that mandate field-level encryption or auditing, collections that must never be touched online, features the platform forbids
- Cadence — restore-drill frequency, index-usage review cycle, oplog-window review, upgrade windows
Output Gates
Before emitting a schema, an index, a pipeline, or a migration:
- Is every array in this schema bounded by something other than optimism?
- Does each new index correspond to a real query shape, in ESR order, with its redundant single-field prefixes removed?
- Was the query run through
explain("executionStats"), with examined:returned checked, and not just read? - Does the pipeline's first stage reach an index, and does no blocking stage sit ahead of the filter?
- Is the write concern explicit, and does it match the data class (telemetry vs money)?
- Is the backfill batched, resumable, and safe to run twice?
- Does the destructive step (
drop, unfiltereddeleteMany, index drop) ship separately from the code change, after the code that stopped using it? - For anything user-supplied reaching a query object: is it type-checked so
{$ne: null}cannot arrive where a string was expected (→security.md)?
Traps
| Trap | Why it fails | Do instead |
|---|---|---|
| Treating "schemaless" as "no schema" | Schema moved into app code, unenforced; shapes drift per deploy | $jsonSchema validator + schema_version field (→ schema.md) |
$push growing an array forever | Full-document rewrite per push, then the 16MB wall | $slice cap or a child collection |
countDocuments({}) for dashboard totals | Runs a scan-backed count on every load | estimatedDocumentCount() — metadata, O(1); accepts orphan drift on sharded clusters |
| One collection per tenant or per day | Each collection and index is a WiredTiger file; thousands degrade checkpoints and startup | tenant_id field + compound indexes (→ schema.md) |
$where / $function in hot paths | JavaScript per document, no index use — and an injection surface | Rewrite with native operators (→ security.md) |
| Ignoring write concern on "unimportant" writes | Data appears written, lost on failover | Pick a concern per data class (→ replication.md) |
| Building an index on a live collection at peak | Non-blocking since 4.2, but it still burns IO and cache on every member | Schedule off-peak, or roll it member by member (→ indexes.md) |
A new MongoClient per request or per Lambda invocation | Every invocation opens a fresh pool; the server hits its connection cap while the app looks idle | One client per process, cached outside the handler (Core Rule 8) |
| Iterating a huge cursor while doing slow work per document | The cursor idles out at 10 minutes mid-loop, code 43 | Page by range key, one query per page (→ connections.md) |
find() in a loop instead of one $in or $lookup | N round trips at network latency each; the database is not the bottleneck, the trips are | Batch the keys, or fix the shape so one read serves the page |
Trusting retryWrites to cover everything | updateMany/deleteMany and multi-statement work are not retryable | Idempotent logic or a transaction (→ transactions.md) |
Deleting millions of documents with one deleteMany | Long-running write, oplog flood, replication lag, no resume point | Batched deletes by range, or a TTL index that does it continuously |
Where Experts Disagree
- Embed-first vs reference-first. MongoDB's own guidance is embed-first; teams from relational or microservice backgrounds reference-first for independent lifecycles. Boundary: embed when the child is always read with the parent AND bounded; reference otherwise.
- Transactions. One school treats a multi-document transaction as a schema-design smell (redesign so the invariant fits one document); the other uses them freely since 4.0. Boundary: cross-entity invariants you cannot co-locate (ledger + balance) justify them; convenience joins do not.
- Secondary reads for scale. Often called a myth — every secondary applies every write anyway, so they add read capacity only. Still legitimate for analytics isolation and geo-local latency with a bounded
maxStalenessSeconds. - Schema validators. One camp enforces
$jsonSchemaat the database because application-only validation always drifts; the other keeps validation in code because a validator change is a live DDL-shaped operation. Testable boundary: more than one writer (services, scripts, humans in the shell) means the database has to hold the rule. - Aggregation in the database vs the application. Pushing analytics into pipelines competes with the operational workload for the same WiredTiger cache. Boundary: sub-second, index-backed, user-facing aggregations belong in MongoDB; anything scanning history belongs in a materialized collection, an analytics node, or a warehouse (→
aggregation.md).
Related Skills
More Clawic skills, get them at https://clawic.com/skills/mongodb (install if the user confirms):
pg— PostgreSQL; jump there when the model is genuinely relational or you need cross-row constraints and SQLdb— general database operations, reliability, and scaling patternsdatabase-indexing— index theory beyond MongoDB specifics: write-cost budgets, structures, composite orderingvector-databases— comparing Atlas Vector Search against dedicated vector stores by scale and filter needselasticsearch— when text search outgrows Atlas Search: analyzers, relevance tuning, aggregations
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/mongodb.
常见问题
- 怎么决定一个关联文档是嵌入还是引用?
- 按顺序问三个问题:读取时是否一起拉取、数组是否有限、子文档是否独立更新。只要有一个答案是否,就该用引用;如果它按事件增长,就用时序集合。
- 查询很慢,第一步该做什么?
- 先跑 explain("executionStats"),在动手改任何东西之前看输出。如果 totalDocsExamined 与 nReturned 的比值远大于 1,说明索引缺失或字段顺序不对;按 Equality → Sort → Range 顺序核对,再考虑新增索引。
- DuplicateKey(错误码 11000)通常意味着什么?
- 三种可能:真实的重复写入、并发写入竞争,或者唯一索引把缺失字段都当作 null 计入重复。第三种情况需要改用 partial 唯一索引。
- 为什么会看到 "not writable primary"?
- 客户端连到了从节点或已下台的主节点,通常是连接串不完整。用完整的副本集 URI 连接,让驱动自动跟随故障切换。
相关技能
诊断 PostgreSQL 慢查询、设计表结构与索引,并在生产库上安全执行迁移。
诊断 GraphQL 缺陷并加固 schema、resolver 与客户端,处理 N+1、null 传播、订阅失效与联邦拆分。
为 Node 与 TypeScript 项目设计 Prisma schema、编写类型安全查询,并解决迁移、连接池与 N+1 关联加载问题。
针对 Node.js 运行时、打包与线上问题,给出可落地的诊断依据与修复规则。
围绕运行语义、异步行为与特性版本下限,为 Node 与浏览器场景调试与编写 JavaScript。