SQLite or PostgreSQL: which database to choose
How SQLite and PostgreSQL differ: fourteen criteria, which database for which project, signs that SQLite is no longer enough, portable SQL, the move step by step and common mistakes.
In short
SQLite is a database in one file next to the application: nothing to install or administer, very fast reads, and enough for most sites, blogs, catalogues and internal tools on one server. PostgreSQL is a separate database server: many simultaneous writers, access over the network from several servers, users and rights, strict types and extensions such as PostGIS and pgvector. Choose SQLite when the project lives on one server and writes are moderate; choose PostgreSQL when there are many writers, several servers or the data needs its own rules. Starting on SQLite and moving later is normal if the SQL is portable from day one.
In short: which one to choose
The question is not which database is better but where the data lives and who writes to it. If the application runs on one server and most requests read — pages, a catalogue, articles, settings — SQLite gives the same result with less to run: no server, no connections, a backup is a copy of a file.
PostgreSQL is needed when the file stops being enough: many users write at the same moment, the application runs on several servers, analysts connect over the network, different people need different rights, or the project relies on extensions — maps, vector search, time series.
- One server, mostly reads — SQLite
- Many writers or servers — PostgreSQL
- Not sure — SQLite with portable SQL
SQLite and PostgreSQL: a detailed comparison
Fourteen criteria side by side — from how the data is stored to backups and hosting.
| Criterion | SQLite | PostgreSQL |
|---|---|---|
| Where the data lives | one file next to the code | a separate database server |
| Setup | none — a library in the language | a server, users, settings |
| Reads | very fast, no network | fast, through a connection |
| Simultaneous writes | one writer at a time | many, locks on rows |
| Several app servers | no, one machine | yes, over the network |
| Users and rights | file permissions only | roles, rights down to rows |
| Types | lax by default, strict with STRICT | strict always |
| Changing the schema | limited ALTER TABLE | full, inside a transaction |
| JSON | functions and the ->> operator | jsonb with indexes |
| Full-text search | FTS5, no morphology | built in, with language dictionaries |
| Extensions | a few | PostGIS, pgvector, TimescaleDB and hundreds more |
| Replication | external tools: Litestream, LiteFS | built in |
| Backups | a copy of a file via .backup | pg_dump, restoring to any moment |
| Hosting | anywhere the code runs | VPS or managed service |
Which database for which project
Twelve typical projects with a recommendation and the reason.
| Project | Take | Why |
|---|---|---|
| Corporate site or blog | SQLite | almost only reads |
| Catalogue or reference site | SQLite | fast reads, a backup is one file |
| Internal tool for a team | SQLite | few writers, nothing to administer |
| Prototype or MVP | SQLite | start today, move when it grows |
| Online shop with orders | PostgreSQL | orders and stock change at the same moment |
| SaaS with many clients | PostgreSQL | many writers, rights down to rows |
| CRM or accounting | PostgreSQL | strict types and constant writes |
| App on several servers | PostgreSQL | one file cannot be shared over the network |
| Maps and geodata | PostgreSQL | PostGIS |
| Search by meaning, RAG | PostgreSQL | pgvector next to the data |
| Browser extension or desktop app | SQLite | a database inside the app, no server |
| Analytics over the network | PostgreSQL | analysts connect with their own tools |
Signs that SQLite is no longer enough
SQLite rarely fails suddenly — it sends signals. Any one of them is a reason to plan the move.
-
01
“database is locked” in the logs
It keeps appearing even with WAL and
busy_timeout— writers queue up for too long. -
02
A second server
The application needs to run on two machines, and both must write.
-
03
Access over the network
Analysts, reports or another service need to read the data directly.
-
04
Different rights
Someone must see only their own rows, and the code alone should not be the only guard.
-
05
An extension is needed
Maps, vector search or time series — PostGIS, pgvector, TimescaleDB.
-
06
Search with morphology
Users search by word forms, and FTS5 finds only exact ones.
The difference in SQL: 3 examples
What works the same in both, where they differ, and how to move. Every example was run in SQLite 3.40 and PostgreSQL 16.
Portable SQL
A table and an upsert with RETURNING that run unchanged in both — the basis for an easy move.
-- One file, two databases: runs unchanged in SQLite 3.35+ and PostgreSQL
CREATE TABLE products (
sku TEXT PRIMARY KEY,
title TEXT NOT NULL,
price INTEGER NOT NULL CHECK (price >= 0),
stock INTEGER NOT NULL DEFAULT 0
);
-- Add a product or, if the SKU exists, update the price and add to the stock
INSERT INTO products (sku, title, price, stock)
VALUES ('A-100', 'Oak table', 24000, 3)
ON CONFLICT (sku) DO UPDATE
SET price = excluded.price,
stock = products.stock + excluded.stock
RETURNING sku, price, stock;
-- first run: A-100 | 24000 | 3
-- second run: A-100 | 24000 | 6
Types: the main difference
One INSERT, three results. An ordinary SQLite table keeps text in a number column; STRICT and PostgreSQL refuse.
-- The same INSERT with a word instead of a number
INSERT INTO products (sku, title, price)
VALUES ('C-300', 'Birch chair', 'free');
-- SQLite, ordinary table: the row is saved, price = 'free'
-- (CHECK passes too: in SQLite any text is "greater" than any number)
-- SQLite, table declared STRICT:
-- cannot store TEXT value in INTEGER column products.price
-- PostgreSQL:
-- invalid input syntax for type integer: "free"
Moving a table
Export to CSV, load with \copy and move the id counter — the step most often forgotten.
# Move a table from SQLite to PostgreSQL. The table in PostgreSQL is created first:
# CREATE TABLE products (id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
# sku text UNIQUE NOT NULL, title text NOT NULL, price integer NOT NULL);
# 1. Export from SQLite to CSV
sqlite3 -header -csv app.db "SELECT id, sku, title, price FROM products" > products.csv
# 2. Load into PostgreSQL, keeping the same ids
psql "$DATABASE_URL" -c "\copy products (id, sku, title, price) FROM 'products.csv' CSV HEADER"
# 3. Move the id counter past the loaded rows,
# or the next INSERT fails with "duplicate key value violates unique constraint"
psql "$DATABASE_URL" -c "SELECT setval(pg_get_serial_sequence('products', 'id'), max(id)) FROM products"
Moving from SQLite to PostgreSQL: what to keep in mind
The data moves easily; the surprises come from what SQLite forgave and PostgreSQL does not.
-
01
Check the types first
Find text in number columns with
typeof()and clean it before the move, or the load stops on the first bad row. -
02
pgloader for large databases
It reads the SQLite file directly and moves the schema and data in one command.
-
03
Id counters
After loading with explicit ids, move each counter with
setval. -
04
Booleans and dates
SQLite keeps them as 0/1 and text — in PostgreSQL they become
booleanandtimestamptz. -
05
LIKE and case
In SQLite
LIKEignores case for Latin letters, in PostgreSQL it does not — searches needILIKE. -
06
Double quotes
SQLite often accepts a string in double quotes; PostgreSQL reads it as a column name and fails.
Common mistakes when choosing between them
-
PostgreSQL “just in case”
A separate server, updates and backups for a site that only reads.
-
SQLite on a network drive
Locks over the network are unreliable — the file gets damaged.
-
Blaming SQLite without WAL
Most “SQLite is slow” complaints disappear after WAL and
busy_timeout. -
Lax types for years
Without STRICT, dirty data accumulates and surfaces on the day of the move.
-
SQL tied to one database
Specific functions everywhere turn a simple move into a rewrite.
-
Moving because of the name
If the slowness is in queries and indexes, PostgreSQL will be slow too.
Questions about SQLite and PostgreSQL
Is SQLite suitable for production?
Yes, for a project on one server with moderate writes — with WAL, busy_timeout and regular backups.
Which is faster?
SQLite on reads in one application — there is no network; PostgreSQL when many write at the same time.
How much traffic can SQLite handle?
Reads are rarely the limit; the limit is how many writes arrive at the same moment.
Can I start on SQLite and move later?
Yes, if the SQL is portable and the tables are STRICT — then the move takes hours, not weeks.
Does SQLite support JSON?
Yes, functions and the ->> operator; PostgreSQL adds jsonb with indexes on fields.
How do I back up each one?
SQLite — .backup or Litestream, not a plain copy of a live file; PostgreSQL — pg_dump and WAL archiving.
What about MySQL?
It is a server database like PostgreSQL; how they differ is in a separate comparison.
Online form
Choosing
the database
I work with both: SQLite for projects on one server, PostgreSQL when the load, the team or the data need more. Tell me about the project — I answer within one working day.