A MongoDB collection has no schema. To query it with SQL, something has to work one out — and almost every tool does that by reading a sample and guessing. This page explains what can be recovered exactly, what cannot, and why the difference matters more than the accuracy number.
SQL requires one schema per table. MongoDB permits one schema per document. That gap is the object-relational impedance mismatch, and it is genuinely old — people have been writing about it since the 1980s.
It is usually discussed as one problem. It is actually three, and they have very different answers:
STRUCT, LIST and JSON. The mapping is total.The common approach is to read the first few hundred documents and treat their fields as the schema. Two things go wrong:
That last failure is the dangerous one: data that exists but is invisible, with no error. Measured against an exhaustive scan of 25,000 real GitHub events, sampling the first 500 documents missed 154 of 346 rare fields.
Varan asks the server for the answer rather than estimating it. One aggregation per nesting level returns the exact field set and an exact type histogram, without transferring documents:
// exact field names and type counts, computed server-side
[
{ $project: { kv: { $objectToArray: "$$ROOT" } } },
{ $unwind: "$kv" },
{ $group: { _id: { k: "$kv.k", t: { $type: "$kv.v" } }, n: { $sum: 1 } } }
]
From that you get, for the whole collection and not a sample:
customer.addr.geo.latint ×4850, string ×150Anything the scan proves exists but the sample happened to miss is then fetched directly. In a test collection of 2,000 documents with a 50-document sample, a field present in one document was reported at exactly 0.05%.
Once a collection lands as a table, it is just a table:
SELECT c.name, c."addr.city" AS city, COUNT(o.id) AS orders
FROM mongo_customers c -- MongoDB
JOIN postgres_orders o ON o.customer_id = c._id -- PostgreSQL
GROUP BY 1, 2;
addr.city needs no special syntax.unnest(tags) works.If a field is an integer in 4,850 documents and a string in 150, what should it be?
Nobody can answer that from the data. It could be a bug, a deliberate union type, or a migration that never finished. Reading more documents makes the estimate sharper and the decision no easier — the information lives in the head of whoever wrote the writer, not in the collection.
So Varan does not pretend. Fields like that are held as text, marked as needing a decision, ranked so the least certain come first, and shown with the counts behind them. You answer once; the answer is stored and survives every later re-scan.
On 25,000 real GitHub events — a genuinely polymorphic collection where push, pull-request and issue events share nothing — that came to 17 fields out of 909.
There is no machine learning here. No training, no weights, no API call. Discovery is counting; the remaining uncertainty is quantified with Wilson and Clopper–Pearson intervals, both of which are ordinary textbook statistics. Every decision displays the counts it was based on, so you can check any of them yourself.
That also means it works offline, costs nothing per query, and cannot hallucinate a column that does not exist.
Try it on your own collection →