Levelwise
English
Data and storage

NoSQL and MongoDB

NoSQL databases store data in the shape the app reads it, not in normalized tables. In MongoDB each record is a complete document. Start the design from the queries, and choose NoSQL only when the shape of the data or the scale really needs it.

Not reviewedWritten with AI helpReading time: 12 minOnline shop catalog exampleC# code with MongoDB.Driver

Author: bezzad

The problem: products with different shapes

Our shop sells phones, shirts and books. Each category has its own attributes:

  1. Phone: memory, screen size, color.
  2. Shirt: size, fabric.
  3. Book: author, number of pages.

In a relational (SQL) database we have two simple ways, and both have problems:

  1. One table with hundreds of columns. Most columns stay empty for each product, and the table changes with each new category.
  2. An “attribute and value” table. Each attribute is one row. Building one product page needs several JOINs, and queries get complex.

In a document database, each product is one document with just the attributes it needs. The phone document and the shirt document do not need the same shape.

But note: Modern relational databases also have JSON columns (for example jsonb in PostgreSQL). For a few changing attributes, this is often enough, and you do not need to add a new database.

NoSQL families

The word NoSQL is not one kind of database. It is several different families that only share one thing: they are “not relational tables”:

Key-valueRedis, DynamoDBcart:42 → {...}session:9 → {...}Read only by keyVery fast and simpleCache, session, cartDocumentMongoDB, Cosmos DB{ name: "X", specs: {...}, lines: [...] }A full doc per recordShapes can differCatalog, contentWide-columnCassandraVery many writesA table per queryEvent logs, time dataGraphNeo4jRelations matter mostFast link traversalSuggestions, friends

In interviews, people ask most about document databases (like MongoDB). The rest of this lesson is about them.

The document model: embed or reference?

In SQL, you first normalize the data and then build each query with JOINs. In MongoDB it is the other way around. First you ask what the app reads together, then you build the document in that shape.

For an order, we have two choices:

  1. Embed. The order lines are inside the order document itself.
  2. Reference. The document is separate, and only its ID is stored, like a foreign key.
Order document{ _id: 1042,customerId: 42,status: "Paid",lines: [{ product: "Phone X", qty: 1 },{ product: "Case", qty: 2 }] }Customer document{ _id: 42,name: "Sara",address: {...} }Reference by IDLines are embeddedAlways read with the order, and limitedCustomer is separate: orders grow without limitSimple rule: what is read and changed together stays together.

When should we embed?

  1. When they are always read together. Order lines have no meaning without the order.
  2. When their number is limited. An order has a few lines, not a million.
  3. When they change together. Changing one document in MongoDB is atomic. So the order and its lines are saved together, without a separate transaction.

When should we reference?

  1. When it grows without end. Do not put all of a customer’s orders inside the customer document. Each document in MongoDB is at most 16 megabytes.
  2. When it is read or changed separately. The customer has their own profile page.
  3. When the data is shared and repeated between several documents.
Copying data on purpose: In MongoDB we sometimes copy data on purpose. For example, we write the name and price of the item into the order line at the moment of purchase. This copy is correct, because the order must keep the price from that moment. But for data that must stay the same everywhere, each copy is one more place that must be updated.

Code with MongoDB.Driver

The product class, with the changing attributes in a Dictionary:

public sealed class Product
{
    public ObjectId Id { get; set; }
    public string Name { get; set; } = "";
    public string Category { get; set; } = "";
    [BsonRepresentation(BsonType.Decimal128)] public decimal Price { get; set; }
    public Dictionary<string, string> Specs { get; set; } = new();
}

Creating the connection, writing and reading:

var client = new MongoClient(builder.Configuration.GetConnectionString("Mongo"));
var products = client.GetDatabase("shop").GetCollection<Product>("products");

await products.InsertOneAsync(new Product
{
    Name = "Phone X",
    Category = "phone",
    Price = 499m,
    Specs = new() { ["ram"] = "8GB", ["screen"] = "6.1 inch" }
}, cancellationToken: ct);

var cheapPhones = await products
    .Find(p => p.Category == "phone" && p.Price < 600m)
    .SortBy(p => p.Price)
    .ToListAsync(ct);

Like SQL, here too MongoDB reads all documents if there is no index. For the query above, create a compound index:

await products.Indexes.CreateOneAsync(
    new CreateIndexModel<Product>(Builders<Product>.IndexKeys
        .Ascending(p => p.Category)
        .Ascending(p => p.Price)),
    cancellationToken: ct);

The MongoClient class is safe for several Threads, and it manages connections by itself. So create it once and register it as a Singleton.

Scale: Sharding

One reason to choose NoSQL is spreading data over several servers. In MongoDB this is called Sharding. Each document goes to one of the servers based on a Shard Key.

Choosing the Shard Key is very important:

  1. The main queries must include the Shard Key. If not, MongoDB must ask all the servers.
  2. The values must spread well. If the Shard Key is the creation date, all new orders go to one server, and that server gets hot.
  3. It is hard to change later. So choose it carefully from the start.

Transactions and consistency

  1. Changing one document is always atomic. If you design the document well, most jobs change only one document.
  2. Multi-document transactions also exist, but they cost more. If you need one for every simple job, maybe your document model is wrong, or the data is really relational.
  3. Reads from copies may be stale. If you read from a Secondary, you may not see the latest write. This is like a Read Replica in SQL.

Choosing between SQL and NoSQL

NoSQL fits

  • The shape of the data differs a lot between records, like a product catalog.
  • The app always reads and writes one “whole thing” together.
  • The data size or write speed is more than one server can handle, and the data must be spread.
  • The queries are known in advance and limited.

SQL is better

  • The data is full of relations and you have many different report queries.
  • Transactions across several records are daily work, like in accounting.
  • The team does not yet know what the queries will look like.
  • Only “no Schema” sounds attractive. In practice, the Schema moves from the database into the code.

Common mistakes

Mistake Result Right way
Copying the SQL table design into MongoDB Many queries, and joining data in the app Design documents based on the queries
Embedding a list that grows without end A big document that hits the document size limit Reference by ID
A query without an index Reading all documents An index for the main queries
A Shard Key with a growing value, like a date All writes go to one server A key that spreads well
A multi-document transaction for every job Slowness and complexity A document model where a job changes one document
Choosing NoSQL only because it has “no Schema” Inconsistent data and bugs in the code A JSON column in SQL, or a well-thought-out document model

Summary in five lines

  1. The word NoSQL covers several families: key-value, document, wide-column and graph.
  2. In MongoDB the design starts from the queries: what is read together stays together.
  3. Embed a limited list. Reference an endless list by ID.
  4. Changing one document is atomic. Choose the indexes and the Shard Key carefully.
  5. When the data is full of relations and transactions, SQL is usually the better choice.