DEV Community

Manohari Jayachandran
Manohari Jayachandran

Posted on • Edited on

Azure Table Storage: The Middle Ground Between a SQL Table and a Document Database

I published a comparison of Azure SQL, Cosmos DB, and Blob Storage a little while back. Looking back at it, Azure Table Storage was a real gap - it sits in a genuinely useful middle position between a relational table and a full document database, and it deserves its own proper explanation rather than a passing mention. This post fills that in, with real C# code and a few lessons from using it in production alongside Azure Service Bus at Blue Yonder.

What Azure Table Storage Actually Is

Azure Table Storage is a NoSQL key-value store for large amounts of structured, non-relational data. It occupies an interesting middle position - simpler and considerably cheaper than Cosmos DB, while still being schema-flexible in a way Azure SQL fundamentally is not. Despite the name, it has nothing to do with SQL tables in the relational sense - "table" here is closer to a giant, partitioned dictionary than a database table with fixed columns.

Azure SQL is a filing cabinet with identical forms in every folder.
Cosmos DB is a box of index cards where every card can say something different.
Table Storage sits somewhere in between - a long row of labeled shoeboxes on a shelf, where each shoebox holds many index cards, and every card in a shoebox can still carry different fields, but things get found quickly specifically because you know which shoebox to check first.

Where Table Storage Actually Sits

Against Azure SQL, Table Storage trades real relational power and joins for dramatically lower cost and a flexible schema per entity.
Against Cosmos DB, it trades rich query capability and global distribution for an even simpler pricing model and operational footprint. It is genuinely the cheapest of the three options, and the one with the most limited query language by a meaningful margin.

Core Concepts

A Table is a collection of entities - schema-flexible in spirit, similar to a Cosmos DB container, but considerably simpler in practice.
An Entity is a single row-like record, identified by a PartitionKey and a RowKey, plus any number of additional properties beyond those two required fields.
The PartitionKey groups related entities together for fast lookup and horizontal scalability - exactly the same kind of design decision as a partition key in Cosmos DB, and just as consequential if chosen poorly. The RowKey uniquely identifies an entity within its specific partition.

Together, PartitionKey and RowKey form an entity's complete unique identifier within the table.

A practical example makes the flexibility clear: two log entities sharing the same partition, representing the same month, can still carry entirely different fields if a logging format changes over time - no schema migration required, the same flexibility spirit as Cosmos DB, at a fraction of the operating cost.

Reading and Writing From C

After installing the Azure.Data.Tables NuGet package, working with Table Storage from C# follows a straightforward pattern: construct a TableClient pointed at a specific table, then read and write TableEntity objects using their PartitionKey and RowKey.

A point read - fetching one specific entity by its exact PartitionKey and RowKey - is the fastest possible operation the service offers, conceptually identical to a point read in Cosmos DB. Querying within a single partition using a filter expression remains fast and well-supported. Updates and deletes follow the same PartitionKey-plus-RowKey addressing scheme throughout.

The Query Language, and Its Real Limitation

This is where Table Storage is honest about being the cheapest, simplest option of the three. There is no SQL-like query language the way Cosmos DB offers one. Filtering uses OData-style string expressions - reasonably readable for simple conditions, but with no joins, no aggregates, and no server-side grouping available at all.

A filter checking for entities in a specific partition with a specific status level reads naturally enough as an OData expression,and produces broadly similar results to the equivalent Azure SQL or Cosmos DB query for that simple case. The difference becomes obvious the moment a query needs a join, an aggregate like a count or an average, or needs to span many partitions efficiently. Table Storage simply does not offer that capability - querying happens within a partition, or by scanning across all partitions, with nothing in between.

Why The Limitation Is Actually The Point

Table Storage deliberately trades query power for extreme low cost and high throughput on simple lookups. It is the right tool specifically when the access pattern is almost always "give me everything in partition X" or "give me the one entity at this exact PartitionKey and RowKey combination" - when joins, aggregates, and complex filtering are simply not needed, when data volume runs into the millions of records but the logic per record stays simple, and when cost matters more than query flexibility.

Real-world scenarios that fit this profile well include application logging and audit trails, IoT telemetry keyed by device identifier and timestamp, session state keyed by session identifier, simple lookup caches, and a message or event audit trail running alongside a service like Azure Service Bus - logging every message processed, keyed by date and message identifier, without needing the query power or cost of a full database to answer a simple question like "did this message get processed, and when." At Blue Yonder, Table Storage and Service Bus worked together in exactly this way, with Service Bus handling actual message delivery while Table Storage held a lightweight, low-cost record of what had moved through the pipeline.

A Genuine Naming Trap Worth Knowing

Confusingly, Cosmos DB also offers its own "Table API" that uses the exact same entity model - PartitionKey and RowKey - and is largely code-compatible with standalone Table Storage. Azure Table Storage, the storage-account-based service covered in this post, remains the cheapest option, with no throughput SLA beyond standard storage account limits and the simplest possible pricing model. Cosmos DB's Table API uses the identical programming model but runs on Cosmos DB's underlying infrastructure - global distribution, guaranteed throughput, multi-region replication - at Cosmos DB pricing.

The practical migration path this enables is genuinely useful: start on plain Table Storage for cost efficiency, and if global distribution or guaranteed low-latency service levels become necessary later, migrate to Cosmos DB's Table API with minimal code changes, since the entity model is shared between them.

No native recovery - know this before you rely on it

Table Storage has no built-in soft-delete or point-in-time restore for individual entities - once deleted, an entity is gone immediately. The common production workaround is to never actually delete: add an IsDeleted flag and update it instead, or pair the table with a scheduled export for backup. This is a real limitation, and it is part of why Table Storage fits append-heavy, rarely-deleted data (logs, telemetry, audit trails) far better than it fits data where deletion and recovery are routine operations.

An Honest Three-Way Comparison

Choose Azure SQL when data is relational, real joins and aggregates are genuinely needed, and the data shape stays consistent across records.
Choose Table Storage when data volume is high and the access pattern is simple - point lookups or partition scans - and cost matters more than
query flexibility.
Choose Cosmos DB when document shape varies meaningfully between records, real query power over JSON structure is needed, or global distribution and guaranteed low-latency reads are genuine requirements.

Key Lessons

  • Table Storage and Cosmos DB share an entity model but are priced and operated in completely different ways - know precisely which one is actually being provisioned before committing to either.

  • The OData filter syntax is genuinely limited. If a query needs a join or an aggregate, Table Storage is the wrong tool for that job regardless of how attractive its cost looks.

  • PartitionKey design matters here exactly as much as it does in Cosmos DB get it wrong and partition scans become expensive as data grows.

  • Table Storage is frequently the right default first choice for logging and telemetry data specifically because that access pattern is almost always simple key-based lookups rather than anything more complex.

  • Pairing it with a messaging service like Service Bus for a lightweight audit trail is a genuinely useful pattern worth knowing, not just a theoretical one.

Summary

Azure Table Storage fills the real gap between a relational table and a full document database - schema-flexible in the same spirit as Cosmos DB, but priced and queried far more simply, with genuine limitations on what it can be asked to do. It earns its place specifically when the access pattern is simple and the volume is high. It was a gap worth closing properly, with real production context behind it rather than just a theoretical comparison.


Originally published at TechStack Blog:
https://www.techstackblog.com/post.html?slug=azure-table-storage-explained

Related reading on TechStack Blog:
https://www.techstackblog.com/post.html?slug=azure-cosmos-db-blob-storage

More from TechStack Blog, by category:
Azure: https://www.techstackblog.com/category.html?cat=azure
Database: https://www.techstackblog.com/category.html?cat=database

Follow for weekly posts on Azure, C#, and cloud engineering.

Top comments (0)