- https://sqlite.org/whentouse.html
The blog lists ways to optimize sqlite for production, for those who have already made that choice. But these comment links are about choosing sqlite vs others and differences, which is completely unrelated and unnecessary for those who have already made that choice.
I'm seeing a worrying trend on HN. Nearly for all articles, there is one unquantified, unproven comment at the top saying it's 100% AI — no proof, just baseless emotion of what fits the commenter's writing style. This is the new witch hunt, or virtue trolling.
I read the original article and to be honest, this off topic comment definitely looks more like an AI troll bot wrote it than the article. The troll bot is instructed to do a google search and put 3 source links — no matter how irrelevant they are.
So what is it now — one emotion vs another? If an article has sections and blocks, it's AI. What about infinite poor humans like me who have been writing like this since childhood?
Since LLMs were trained on high quality blog posts, we can no longer have posts that fit the pre-AI definition of high quality? The content must be dumbed down with typis and scatter unconventional words, to prove AI trollers that it's not AI? Or a timelapse video of typing the article?
Instead how about: if you don't want to read the article because it doesn't fit your personal style expectation — just don't bother commenting?
I hope HN would ban AI trolling comments, and discuss substance.
The site has had that forever though. You missed out on the last witch hunt of going after straight white men as the root of all evil.
I feel like "this is AI" needs to join the ranks "it's not that deep" and "this is a Wendy's" as an annoying thought-terminating cliche.
1 - https://www.ilfoglio.it/il-foglio-ai/2025/03/22/news/a-first...
- Column definitions aren't able to be changed with something like `alter column` after creation. To change a column definition you have to manually update the underlying schema using the `writable_schema` pragma. If you mess this up you can be left with a corrupt database.
- Column types are pretty limited. This isn't too much of an issue in practice since you can handle this somewhat in application code, but it can still be a bit annoying at times.
- You have limited options for dealing with schema migrations. You basically either copy the migrations to the server and run it there (manually or with something like Ansible), or you run the migrations in your application on startup. Ideally you'd perform your schema migrations separately from your application, and having to somehow copy/get the migrations to your server to then run the migration is a bit clunky.
All 3 of these are handled in a more powerful (and not local-only) database, and so I don't get why someone would choose SQLite except for prototyping (or places like the browser or phone apps) where performance concerns aren't really relevant.
You can define constraints on a column that ensure it is text that's valid JSON for example:
CREATE TABLE documents (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL
CHECK (
json_valid(data)
json_type(data) = 'object'
)
);
Or to ensure specific keys: CREATE TABLE documents (
id INTEGER PRIMARY KEY,
data TEXT NOT NULL CHECK (
json_valid(data)
AND json_type(data) = 'object'
AND json_type(data, '$.name') = 'text'
AND json_type(data, '$.age') = 'integer'
)
);
You can even use this for things like enforcing a valid YYYY-MM-DD date, though that gets a bit convoluted: CREATE TABLE events (
id INTEGER PRIMARY KEY,
occurred_on TEXT NOT NULL CHECK (
length(occurred_on) = 10
AND occurred_on GLOB
'[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
AND date(occurred_on, '+0 days') = occurred_on
)
);For alter table it offers a "transform" command which implements the pattern of creating a new table with your desired scheme, copying data to it from the old table, then renaming the tables (all in a transaction): https://sqlite-utils.datasette.io/en/stable/cli.html#transfo...
sqlite-utils transform fixtures.db roadside_attractions \
--rename pk id \
--default name Untitled \
--column-order id \
--column-order longitude \
--column-order latitude \
--drop address
And for migrations there's a new-in-v4 "migrate" command which lets you create and execute an ordered sequence of migrations: https://sqlite-utils.datasette.io/en/stable/cli.html#running... sqlite-utils migrate creatures.db path/to/migrations.py
Migrations files look like this: https://sqlite-utils.datasette.io/en/stable/migrations.html#... from sqlite_utils import Migrations
migrations = Migrations("creatures")
@migrations()
def create_table(db):
db["creatures"].create(
{"id": int, "name": str, "species": str},
pk="id",
)
@migrations()
def add_weight(db):
db["creatures"].add_column("weight", float)Why is that the ideal? With SQLite your database is 1:1 connected to your application (meaning there is no other application using that database), it doesn't make sense to move the app to a new version but not the database or vice versa. Running migrations on startup of the app is ideal.
Migrations are a bit more difficult to write for SQLite than they need to be (DROP column only being added recently...), though. I usually iterate a few times to get the column definitions just right so that I don't have to change them later.
As you say column types are limited (and enforcement lax) but in practice it's a non-issue because you convert the data to application-specific types when reading from db (and enforce by writing only right data types) anyway.
I don't think this solves the issue though. To be fair, I was a bit loose with my wording and the principle is actually "don't make backwards breaking changes to your database schema" rather than "do your migrations separately", but if you do them separately it is a good way to enforce it. The issue you want to prevent is your application having bugs/issues in production necessitating a rollback, and your now rolled back application doing things that are incompatible with the current database version (or in a concurrent setting, that some applications may not be updated).
There's still the issue where you're copying over all of the migrations to your server too when you do it in the application, which is in my opinion something you are ideally able to avoid, but it's not a problem in practice until you have 1000s of migrations.
For the rare case when you do rollback the safest thing to do is stop the app, downgrade the db (by running some sql if necessary) and app and rerun it. Not that different in postgres no?
In rollback mode, only the migration thread can be active because it's a write transaction, but I'm guessing in WAL mode, readers can continue to read during the migration, as with any other write transaction. I don't use WAL mode much because for my application (HashBackup), I don't need db concurrency.
I see the migration argument come up a lot. But, in practice with sqlite you'll be using projections where you have a source of truth database (event log) and project off it into disposable/expendable sqlite databases. So schema changes are often just delete and rebuild the projection.
I don’t think that’s really a positive or negative.
And the point about migrations ideally being separate is really just your own opinion. I prefer having the database definition in the same source tree as the application, ideally just a .sql file in the project.
After that you won't be able to change column to NOT NULL. You would need migration to create new table with not null column, copy everything, drop old table and rename the new one.
Edit: unless the table is empty.
This is again a scenario I’ve never run into 20ish years of SQL.
https://www.sqlite.org/lang_altertable.html
On Go you can embed your migrations in your binary:
https://oscarforner.com/blog/2023-10-10-go-embed-for-migrati...
No you don’t [0]. It is less convenient than being able to directly alter columns, but you do not need to mess around with writable schemas or risk corruption.
Or at least that's how I view it. Whenever I think about using SQLite, I make sure I read these documents to see if I am fine with the limitations.
"SQLite is not directly comparable to client/server SQL database engines such as MySQL, Oracle, PostgreSQL, or SQL Server since SQLite is trying to solve a different problem.
Client/server SQL database engines strive to implement a shared repository of enterprise data. They emphasize scalability, concurrency, centralization, and control. SQLite strives to provide local data storage for individual applications and devices. SQLite emphasizes economy, efficiency, reliability, independence, and simplicity.
SQLite does not compete with client/server databases. SQLite competes with fopen()."
If you're saying "do this, get that", you should be able explain how to measure and reproduce that result.
The answer to why someone might choose SQLite in production could be latency but if it is then prove it's worth the trade-offs.
That's the biggest pain of dealign with SQLite.
Meanwhile, you might be surprised how often “single-tenant edge deployment” comes up in embedded contexts; a Pi with an SD card is a common configuration for an embedded database, and it's right at the intersection of these problems.
> In NORMAL mode, the database engine syncs to disk only at critical moments (e.g., during checkpoints) rather than at every single transaction commit. In WAL mode, this is completely safe from database corruption; even if the server crashes, only the uncommitted transactions in the WAL are lost, but the database integrity remains intact.
No, this is not safe. You can lose the latest committed transaction with this pragma.
I'm mainly referring to this quote.
So, you can lose committed transactions too.
Another downside is that it will take time to make it look nice.
https://dbeaver.com/docs/dbeaver/Database-driver-SQLite/#rem...
- https://powersync.com/blog/sqlite-optimizations-for-ultra-hi...
hence we ended up writing our own WAL for a Postgres compatible engine, and what surprised me was how much of the work is in the fsync ordering rather than the log format.
Relaxing that is a real choice to lose the last few transactions on a crash. Fine for some workloads but it should be a decision someone made on purpose.
The answer to that being running it on top of LiteFS or LiteStream seems like starts to make the setup a lot less simple and lot less battle tested, which kind of starts to negate the advantages over just running Postgres.
If for no other reason, dealing with these at any scale often distracts from running your actual business.
> To ensure write operations don't suffer from disk synchronization bottlenecks, pair WAL mode with the following pragma
Only do this if you are prepared to sacrifice durability (i.e can afford to lose transactions).
That being said it can be done and it's pretty normal for mobile apps, desktop apps etc. You just have to make sure the migrations are run when the tenant connects/unlocks/runs the app - and make sure that you minimise the risk of it going wrong!
I don't think it scales when you have unbounded amounts of tenants.
It's a bit of a 1990s setup (as the sibling comment says, it's more or less the same model as a widely-deployed desktop app), but it does scale and can be useful in situations where strict isolation between tenants is beneficial.