mongosh is the command-line shell for MongoDB, and it is the fastest way to learn how a document database works. If you know SQL Server, think of it as sqlcmd that speaks JavaScript. We will connect, create a database, store a document and look closely at what is inside it.

What a Document Database Stores
A relational database stores rows in tables. A document database stores documents in collections. A document is a set of field and value pairs, written like JSON. MongoDB keeps it in a binary format called BSON. BSON adds types that plain JSON lacks, such as dates and separate integer and decimal numbers.
The vocabulary maps cleanly. A database is still a database, and a collection is like a table. A document is like a row, and a field is like a column. My post on SQL terms vs MongoDB terms lists the rest. One difference matters from day one: documents in the same collection don’t have to share the same fields.
The model also keeps related data together. An order and its line items can live in one document, so one read returns the whole thing. In a relational design, you would join several tables to get the same answer. That is the main reason people choose a document database. Understand it before you design one.
Connect and Look Around
I ran every example below on MongoDB 9.0.2 with mongosh 2.13.0. If you don’t have a server yet, my post on how to install MongoDB on Windows covers the setup. With a local server running, type the shell’s name and press Enter. It connects to the default address, localhost on port 27017, and opens a database called test. The shell is a JavaScript environment, so loops, variables and functions all work in it.
mongosh
The shell prompt shows the current database. The command use switches to another one, and it works even for a name that doesn’t exist yet. Try it with a database called mongoshDemo, then ask the shell what exists.
use mongoshDemo
// switched to db mongoshDemo
db.getName()
// mongoshDemo
db.getCollectionNames()
// []
db.adminCommand({ listDatabases: 1, nameOnly: true }).databases.map(d => d.name).includes('mongoshDemo')
// falseThe shell has shortcuts for the last two checks, called show collections and show dbs. I used the longer commands because they return plain values I can compare between runs. The result is the interesting part: the database is selected, but it isn’t listed anywhere. Nothing exists yet.
Databases and Collections Appear on the First Write
In SQL Server, you run CREATE DATABASE and CREATE TABLE before you store anything. MongoDB has no such step. The first insert creates the database and the collection for you. This one stores a pantry item in a collection called pantry.
db.pantry.insertOne({
name: 'Red lentils',
quantity: 12,
price: 2.49,
inStock: true,
addedOn: ISODate('2026-10-01'),
tags: ['legume', 'dry goods'],
supplier: { name: 'Green Valley Farm', city: 'Fresno' }
})
// { acknowledged: true, insertedId: ObjectId('...') }The dots stand for a value that is different every time you run it. Now repeat the two checks from before. The collection exists, and the database shows up in the list.
db.getCollectionNames()
// [ 'pantry' ]
db.adminCommand({ listDatabases: 1, nameOnly: true }).databases.map(d => d.name).includes('mongoshDemo')
// true
db.pantry.findOne({ name: 'Red lentils' }, { _id: 0 })
// {
// name: 'Red lentils',
// quantity: 12,
// price: 2.49,
// inStock: true,
// addedOn: ISODate('2026-10-01T00:00:00.000Z'),
// tags: [ 'legume', 'dry goods' ],
// supplier: { name: 'Green Valley Farm', city: 'Fresno' }
// }The { _id: 0 } part hides the _id field to keep the output short. The next section shows what it holds.
The _id and the ObjectId
Every document needs an _id that is unique in its collection. It plays the role of a primary key. When you don’t supply one, MongoDB creates an ObjectId, a 12-byte value that the shell shows as 24 hexadecimal characters. The insertedId in the result above is one. I checked its length in the test, and it was 24.
The first four bytes of an ObjectId hold a time stamp in seconds. That makes new ids roughly sortable by creation time, and you can read the time back. The example uses a fixed id, so the answer never changes.
new ObjectId('6523f9800000000000000001').getTimestamp()
// ISODate('2023-10-09T13:00:48.000Z')BSON Types in Plain Words
The $type operator tells you how MongoDB stored each field. The aggregate below asks about every field of the pantry document and returns the type names in one result.
db.pantry.aggregate([{ $project: {
_id: 0,
name: { $type: '$name' },
quantity: { $type: '$quantity' },
price: { $type: '$price' },
inStock: { $type: '$inStock' },
addedOn: { $type: '$addedOn' },
tags: { $type: '$tags' },
supplier: { $type: '$supplier' }
} }])
// [ { name: 'string', quantity: 'int', price: 'double', inStock: 'bool',
// addedOn: 'date', tags: 'array', supplier: 'object' } ]| Field | Stored as | In plain words |
|---|---|---|
| name | string | Text |
| quantity | int | A whole number, 32 bits |
| price | double | A decimal number, 64 bits |
| inStock | bool | True or false |
| addedOn | date | A point in time, in UTC |
| tags | array | An ordered list of values |
| supplier | object | An embedded document, a document inside a document |
Whole numbers become int and numbers with a decimal point become double. Writing 12 stored an int, but the Double helper forces a double. I stored a quantity of Double(6) in a second document, and $type reported double. A plain 3 in the price field of that document was reported as int.
db.pantry.insertOne({ name: 'Oat milk', quantity: Double(6), price: 3, volumeLiters: 1.5 })
db.pantry.aggregate([{ $match: { name: 'Oat milk' } }, { $project: { _id: 0, quantity: { $type: '$quantity' }, price: { $type: '$price' } } }])
// [ { quantity: 'double', price: 'int' } ]The embedded document is the feature SQL people notice first. The supplier lives inside the item, so one read returns both. A dotted path reaches inside it: the filter { ‘supplier.city’: ‘Fresno’ } found the lentils in my test.
Where SQL Server People Get Confused
The two documents in pantry have different fields. The lentils have tags, and the oat milk has volumeLiters. MongoDB accepted both without a word, because a collection has no fixed column list.
db.pantry.find({}, { _id: 0, name: 1, tags: 1, volumeLiters: 1 })
// { name: 'Red lentils', tags: [ 'legume', 'dry goods' ] }
// { name: 'Oat milk', volumeLiters: 1.5 }The same freedom has two sharp edges, and both come from the first-write rule. Reading a collection that doesn’t exist is not an error. It returns zero and creates nothing. Writing to a misspelled name is not an error either. It creates a brand-new collection.
db.pantri.countDocuments({})
// 0
db.getCollectionNames()
// [ 'pantry' ]
db.panty.insertOne({ name: 'Oops' })
db.getCollectionNames()
// [ 'pantry', 'panty' ]
db.panty.drop()
// trueIn SQL Server, the same typo gives an invalid object name error right away. In MongoDB, the typo is a valid command. You find out later, when the data sits in the wrong place.
You could say flexible documents are a gift, and fixed schemas are a chore. Fair point. Flexibility is why teams pick MongoDB. But it moves the checking from the database to your code. A typo is the cheapest way to see that cost. MongoDB can enforce rules when you want them, through schema validation on a collection.
A Short Checklist
Check the current database with db.getName() before every write. Copy collection names instead of typing them, or keep them in variables. Use one type per field, so a price is always a double and a quantity is always an int. When you finish experimenting, drop the test database so nothing is left behind.
use mongoshDemo db.dropDatabase()
mongosh is not a toy console for developers, it is a clear window onto how MongoDB stores your data.
Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.
Discover more from SQL Authority with Pinal Dave
Subscribe to get the latest posts sent to your email.




