Best database for a web app: how to choose · DpkDB
Best database for a web app: how to choose
How to choose between PostgreSQL, MySQL, SQLite and MongoDB for a web app, where Redis and a search engine fit, a decision table by app type, and when the PostgreSQL default is wrong.
For most web applications the best database is PostgreSQL. It handles relational data, JSON documents, full-text search and geospatial queries in one system, it is available as a managed service on every major cloud, and its permissive license carries no vendor risk. The default is wrong in a few specific cases: your team already runs MySQL well, the app is small enough to live on one server with SQLite, the framework you are using requires MySQL, or your data really is a set of self-contained documents that suits MongoDB. Redis and a search engine are additions to a primary database, not replacements for it.
The rest of this article explains how to tell which case you are in.
The candidates and what each is for
Database
Kind
Runs as
Strong at
Weak at
PostgreSQL
Relational
Server
Mixed workloads, complex queries, data integrity, extensions
Needs a connection pooler and vacuum awareness at scale
MySQL / MariaDB
Relational
Server
Simple high-volume reads and writes, mature replication, wide hosting support
Fewer index and data types, DDL not transactional
SQLite
Relational
Library inside your application process
Zero administration, single-server apps, edge and mobile
One writer at a time, no network access, no built-in replication
Joins across collections, integrity across documents
Redis
In-memory key-value and data structures
Server
Cache, sessions, rate limits, queues, pub/sub
Dataset limited by memory, not a system of record for most apps
Elasticsearch / OpenSearch
Search engine
Cluster
Relevance-ranked text search, faceting, log search
No multi-record transactions, must be fed from a primary store
MySQL and MariaDB share an origin and most syntax. MariaDB is a separately developed fork and the two have diverged in features, so pick one and test against that one.
The old UUID performance problem came from UUIDv4's randomness, not UUIDs themselves. How UUIDv7 changes the trade-off, what the extra 8 bytes still cost and when to pick each.
Most web application data is relational whether or not you planned it that way. Users have orders, orders have line items, line items refer to products, products belong to categories. You will want to query it from more than one direction: all orders for a user, all users who bought a product, revenue by category by month. A relational database answers all of these from the same normalised tables with joins. That flexibility is the reason it is the default.
A document database fits when the unit you read and write is a self-contained aggregate that is always loaded whole and rarely queried across: a CMS page with nested blocks, a product catalogue where each category has different attributes, an event payload. MongoDB stores each as one document, and an operation on a single document is atomic. The trade-off shows when you need a different access path. Joining collections is possible with $lookup, but the data model was not designed around it, and a single document cannot exceed 16 mebibytes. MongoDB schema design: embed or reference? covers that decision.
You do not have to choose one model for the whole application. PostgreSQL's jsonb and MySQL's JSON type let you keep the relational core and put the variable part in a document column:
-- PostgreSQLCREATE TABLE products (
id bigint GENERATED ALWAYS ASIDENTITYPRIMARY KEY,
category_id bigintNOT NULLREFERENCES categories (id),
name text NOT NULL,
price_cents integerNOT NULLCHECK (price_cents >=0),
attributes jsonb NOT NULLDEFAULT'{}'
);
CREATE INDEX products_attributes_idx ON products USING GIN (attributes);
SELECT id, name
FROM products
WHERE attributes @>'{"color": "red", "size": "M"}';
This covers most "our data is semi-structured" cases without giving up foreign keys and transactions for the rest of the schema.
2. Consistency requirements
If a mistake costs money or trust, you need multi-statement transactions and constraints enforced by the database: inventory that cannot go negative, payments recorded once, a username that is unique under concurrent sign-ups. PostgreSQL, MySQL with InnoDB and SQLite all provide ACID transactions, foreign keys, unique constraints and CHECK constraints.
MongoDB supports multi-document transactions, and its documentation notes that a well-designed document model makes them unnecessary for many use cases. That is true when the aggregate boundaries are right. It is harder when the business rules span documents, because there are no foreign keys, and schema validation is opt-in per collection.
Redis is not designed for this role. Its persistence options, point-in-time snapshots (RDB) and an append-only file (AOF), are configurable, and common configurations can lose the most recent writes on a crash. That is acceptable for a cache and unacceptable for orders.
3. What your team knows
A team that has run MySQL in production for years knows how to back it up, read its slow query log, add an index to a large table and recover from a failed replica. That knowledge prevents outages. Switching to PostgreSQL for a better feature list replaces it with a learning period during which the mistakes are made on live data. Unless a specific feature is required, the database your team can operate is the right one.
The same applies to the framework. Rails, Django, Laravel, Spring and the Node.js ORMs all support PostgreSQL and MySQL well, and most support SQLite. WordPress requires MySQL or MariaDB. Follow the ecosystem's most used path, because that is where the documentation, plugins and hosting guides are.
4. Hosting and operations
A database you do not have to patch, back up and fail over yourself is worth a lot to a small team. PostgreSQL and MySQL are both offered as managed services by AWS, Google Cloud and Azure, and by many smaller providers. MongoDB has a managed service from its vendor. SQLite has no server to manage at all, but backups and replication become your responsibility, usually through a tool such as Litestream that streams changes to object storage.
Check two things before committing: that point-in-time recovery is included, and that the provider supports the extensions or version you need.
What does not matter at small scale
Benchmark results. On a dataset that fits in memory with correct indexes, PostgreSQL, MySQL and SQLite all answer typical queries in well under the time your application spends rendering the response. Slow pages at this stage come from missing indexes and from an ORM issuing a query per row, not from the engine. The mechanics are in Postgres vs MySQL performance: which is faster, and when?.
Horizontal scalability. Choosing a database for its sharding story before you have enough data to fill one server means paying real costs now (weaker joins, weaker transactions, more operations work) for a problem you may never have. A single PostgreSQL or MySQL primary with read replicas carries most applications for their whole life.
"SQL does not scale." Relational databases run some of the largest sites in the world. The scaling path is well documented: indexes, connection pooling, read replicas, caching, partitioning, then sharding.
Schemaless speed. Skipping migrations feels faster in the first weeks. The schema still exists, in application code and in whatever old documents were written before the last change. A relational migration is that same work done once and enforced.
Decision table by application type
Application
Primary database
Add when needed
Reasoning
CRUD SaaS (accounts, teams, billing)
PostgreSQL
Redis for sessions and background jobs
Relational data, strict integrity, row-level security available for multi-tenancy
Content site or blog
PostgreSQL or MySQL; SQLite if single server
CDN and page cache first
Read-heavy and cacheable. Use what your CMS expects
E-commerce
PostgreSQL or MySQL
Search engine for catalogue search, Redis for carts and caching
Orders and inventory need transactions. Product attributes fit a JSON column
Real-time chat or collaboration
PostgreSQL or MySQL for history
Redis pub/sub or streams for fan-out
Durable message history is relational. Delivery is a messaging problem
Analytics-heavy product
PostgreSQL for application data
A columnar warehouse for large aggregations
Row stores handle dashboards over modest data. Large scans belong elsewhere
Mobile, edge or embedded
SQLite on the device
A server database to sync with
Works offline, no server process
Prototype or internal tool
SQLite, or PostgreSQL if you will deploy it
Nothing
Lowest setup cost. Move later only if needed
Some notes on the less obvious rows.
Real-time. Redis pub/sub has at-most-once delivery: a subscriber that is disconnected when a message is published never receives it. It is the transport for "notify connected clients now", and the message itself still needs to be written to the primary database so that clients can fetch what they missed. PostgreSQL LISTEN/NOTIFY does a similar job without an extra system for small deployments.
Search. Start with the full-text search in your database. PostgreSQL has tsvector with GIN indexes, MySQL has FULLTEXT indexes and SQLite has the FTS5 extension. Move to Elasticsearch or OpenSearch when you need typo tolerance, tuned relevance ranking, faceted navigation or search across very large text. The search index is then a derived copy: the primary database remains the source of truth and the index can be rebuilt from it.
SQLite. It is a serious production choice for a single-server application, not only a development convenience. In WAL mode readers and the writer do not block each other, though there is still only one writer at a time. The SQLite project's own guidance is that it suits most low to medium traffic websites and does not suit many machines writing to the same database over a network. A minimal production setup:
journal_mode is stored in the database file. The other three apply to the connection that runs them, so your application must set them each time it opens a connection. Details are in SQLite in production: WAL mode and knowing when it's enough.
The default, and when it is wrong
Use PostgreSQL unless one of the conditions below applies. The reasons:
It covers relational data, documents (jsonb), full-text search, geospatial queries (PostGIS) and vector similarity search (pgvector), which delays or removes the need for extra systems.
Constraints are thorough: foreign keys, CHECK, exclusion constraints, partial unique indexes and deferrable constraints.
Schema changes are transactional, so a failed migration rolls back cleanly.
It is released under a permissive license and is not controlled by a single company.
Managed offerings exist on every major cloud and from several specialised providers.
Use MySQL or MariaDB instead when:
your team has production experience with it and none with PostgreSQL;
your framework, CMS or e-commerce platform is built around it (WordPress and much of the PHP ecosystem);
the workload is overwhelmingly primary-key reads and writes and you want the operational tooling built around MySQL at very large scale.
Keep transactions short. Never hold one open across an external API call.
Checklist
Write down your main entities and how they relate. If you can draw it as tables with foreign keys, you want a relational database.
Identify the operations where incorrect data costs money. Those need transactions and constraints.
Ask what the team has operated in production. Give that heavy weight.
Check what your framework or platform expects.
Default to PostgreSQL. Switch to MySQL, SQLite or MongoDB only if a condition in the section above applies.
Start with one database. Add Redis when a measured problem needs a cache or queue, and a search engine when database full-text search is no longer enough.
Set up backups, migrations and connection pooling before launch.
Explains by mechanism where InnoDB or PostgreSQL does less work for lookups, updates, joins, connections and replication, then shows how to benchmark your own workload with pgbench and sysbench.
A reference comparison of PostgreSQL and MySQL: storage and MVCC, isolation defaults, DDL, data types, indexes, replication, licensing and tooling, with a decision summary.