Distributed SQLite: Paradigm shift or hype?
kerkour.com
kerkour.com
Rather than a paradigm shift or hype, I see distributed SQLite as an extension of a path that devs can go down. With Litestream, the most common complaint I got was that devs were worried that they couldn't horizontally scale with SQLite and they'd be stuck. While you probably won't hit vertical scaling limits of SQLite on most projects, it still caused concern. So LiteFS became a "next step" that a dev could take if they ever got to that point. It doesn't need to be your starting point.
As for the "hacky" solution of txid, I'm not sure why that's hacky. Your application isn't required to use it or the optional built-in proxy but it's available if it fits your application's needs. It also works for plugging legacy applications into distributed SQLite without retrofitting the code. The proposed solution of caching seems orthogonal to the discussion of distributed application data. I don't think any database provider would suggest to avoid caching when it's appropriate but there's plenty of downsides of caching. Hell, it's one of the two hardest problems in computer science.
I think this goes underappreciated, or rather the opposite is overstated.
Sure there are some edge cases that don't work the same, but most apps won't hit those.
My _biggest_ gripe with SQLite so far is the lack of column reordering like other DBs. And my simplistic understanding is that the others do it exactly the same way as you'd do it manually with SQLite - table gets _replaced_ with an identical table with the data correctly ordered and the data is shoved into the new table.
"SQLite does not pad or align columns within a row. Everything is tightly packed together using minimal space."
"Missing values at the end of the record are filled in using the default value for the corresponding columns defined in the table schema."
If you have a table with 5 columns and you only insert the first 3 columns (based on create table column order) because the last 2 values are null or default, SQLite will only insert 3 type bytes in the header. However, if the first column (in create table order) is the one you omit, SQLite has to include its type byte, even if the value is null.
That really depends on your modelling style. If you like things like types, SQL-side processing (eg using functions), or covering indexes, then you’ll hit issues every five minutes in sqlite.
SQLite really wants the logic (including consistency logic) in the application, just compare the list of aggregate functions in postgres versus sqlite, or consider that you have to enable FKs on a per-connection basis.
Which I guess is why ORMs help a lot: they are generally based on application-side logic and LCD database.
I checked to be sure I had not missed it, and didn’t find anything. You have expressions and conditions, but no covering. Obviously you can kinda emulate it by adding the columns you want to cover to the key, but…
> though if you want to enforce your own rules for things like dates you're still on your own
That’s what I was talking about, having richer types, and the ability to create more (especially domains).
Strict tables provides table stakes of actually enforcing the all-of-5-types sqlite has built-in. Afaik a strict mode is something that’s still being discussed if it ever becomes reality.
Right, that seems like a good solution to me.
My impression is that this mechanism is less general than what one finds in full-fat client-server SQLite databases.
I guess it's less of an issue in sqlite than in databases with richer datatypes in the sense that all datatypes are ordered and thus indexable.
CREATE INDEX tab_x_y
ON tab(x) INCLUDE (y);- it's not constrained (e.g. to be orderable)
- it does not affect the behaviour of the index, so you can have covering data in a UNIQUE index, or in a PK constraint (although for the latter one might argue a clustered index is superior)
- it only takes space in leaf nodes, not interior nodes, so you can have better occupancy of interior node pages, less pages to traverse during lookup, and they have better cache residency
- and finally the intent is clearer, when you put everything in the key it does not tell the reader what's what and why it there, and thus makes it harder to evaluate changes
Of course, this will differ a lot between projects.
sqlite-utils transform data.db mytable \
-o id -o title -o description
That will change the order of the columns in the specified table such that id, title and description come first.The same command can handle many other operations such as changing column types, renaming columns or assigning a new primary key.
https://sqlite-utils.datasette.io/en/stable/cli.html#transfo...
if you are using Zig (and like to live on the bleeding edge), you can also just use my library which includes similar script and also a simple query builder https://github.com/cztomsik/fridge?tab=readme-ov-file#migrat...
It stores them as strings, so to do something like extract just the year from a date, you have to do 'CAST(substr(game_date,0,5) AS INTEGER).'
Hackish and error prone.
CAST(strftime(“%Y”, game_date)) as INTEGER
Which is somewhat higher level and less easily mistyped
Still, having that all over a query looks ugly. SQL is can be unreadable enough as it is without all the joins/table renaming.
I just want something more readable like EXTRACT(year from date), like you can in Postgres et al.
Would also be nice if there was a native timestamp like there is in, pretty much every other database.
I'm sensitive to "feature creep" but this doesn't seem like too big of an ask.
for instance `select date(-50000000000, "unixepoch");` returns `0385-07-25`
Interestingly %Y doesn't seem to handle negative dates either if you need to handle BC, so I guess that is one downside for both. This is one reason I sometimes prefer to use low level code even when it is less obviously correct with a cursory glance, because abstractions may not mean what you think they mean, or even worse, may be lying to you. At least with low level code I can reason about how it would behave under certain edge cases I might care about.
With dynamic partial replication you can synchronise a subset of your database to a SQLite db on your users device, eliminating the network from the ui interaction loop. Then with the emerging eventually constant syncing systems, many using CRDTs, it's possible to have conflict free eventual consistency. Just read and write to a local database and it will sync with your central server or other clients in the background, both in realtime or after working offline.
I work on one such system at ElectricSQL, but there are many people building variants of this such as Evolu, SQLsync, CR-SQLite, and PowerSync.
That's all not to say SQLite on the edge isn't really damn cool!
I don't think it's a dev problem, but a business one. No consumer is demanding local-first, and no company wants to give up that profitable data. We're doing it because we're purposely not interested in users' data and resiliency is part of our value—but we're still holding some of it for consumer convenience.
WRT Pouch/Couch, and this take is probably annoying to a user of it like you, but it’s an ecosystem that you need to have a reason to get into - IOW it ain’t your grandmas SQL :)
I'm generally a Postgres user not because I love SQL but because I hate drama. That said, my Pouch/Couch use case is fairly simple and probably well aligned with the intent so far and so the paradigm clicks for me.
I’ve been using liteFS in production for a couple months.
Your web app is able to resolve db queries instantly.
You don’t need loading states if you’re using complex charts and other frontend JS that waits for data.
All the data is resolved so fast and you can just return all your data like more traditional apps, and the load times are insane.
If you’re multi region you can deploy one app instance there. Instead of 2-3. Postgres read replica, maybe something like redis.
It really replaces both of those, assuming you have a read heavy app it works great.
> LiteFS’ use of FUSE limits the write throughput to about 100 transactions per second so write-heavy applications may not be a good fit.
https://fly.io/docs/litefs/faq/#what-are-the-tradeoffs-of-us...
See https://www.sqlite.org/np1queryprob.html
I've implemented GraphQL on top of SQLite and found it to be an amazingly good match, because the biggest weakness of GraphQL is that it makes it easy to accidentally trigger 100s of queries in one request and with SQLite that really doesn't matter.
i think this discussion is confusing the use of sqlite in local-first apps, where there's no loading states because the database is in the browser. you can use sqlite on your server, but you still need a "loading state".
even with postgres, if your data and server are colocated, the time between the 2 is already almost 0
now maybe the argument is your servers are deployed globally each with an sqlite db. that's not all that different from global postgres read replicas
Could you give a pointer to the repository, or is this part of Datasette?
Or a slow anything else, e.g., SQLite queries. This thread has focused on the network aspects, and they do stand out since there can be such a large gap between a network call vs a local SSD read. But we're still talking about a database, which could be huge (presumably it's the main, single DB for the whole app). And there is still all of the actual SQLite work that needs to happen to execute a query, plus opportunities for really bad performance b/c of bad queries, lack of indexes, all the usual suspects. Not to mention the load on the owning process itself. So, I'm agreeing that a loading state is needed.
All data for one customer that is important enough to load easily fits within less than 5MB. That is of course not counting logs and such, but it's all "important" user-specific data. It's not -that- dissimilar from a small to medium-sized redux store in complexity. Lots of toggles, forms, raw text and some relations.
Of course this architecture doesn't scale to the enterprise level, or to other certain heavily data-driven applications (like imagine running the entirety of your sentry database in-browser?), but that's what architecture is -for-! Pick one that synergizes well with your use-case!
SQLite, on the other hand, skips all of that with its simple in-process model.
Ceph, with which I have much experience, is a very solid and quite bulletproof storage solution that offers S3 protocol and FS. However, maintaining it in the long run is really challenging. You better become a Ceph expert.
SeaweedFS struggles with managing large data groups. It's inspired by an outdated Facebook study (Haystack) and is intended for storing and sharing large images. However, I think it's only average—it has poor documentation, underwhelming performance, and a confusing set of components to install. Its design allows each server process to use one big file for storage, bypassing slow file metadata operations. It offers various access points through gateways.
MinIO has evolved a lot recently, making it hard to evaluate. MinIO relies on many small databases. Currently, it's phasing out some features, like the gateway, and mainly consists of two parts: a command line interface (CLI) and a server. While MinIO's setup is complex, SeaweedFS's setup is much simpler. MinIO also seems to be moving from an open-source model towards a more commercial one, but I have not closely followed this transition.
All of these solutions are not simple enough to be the base for a distributed database application. What we really need would be something like an Ext4 successor, let's call it Ext5, with native distributed storage capabilities in the most dead-simple way. ZFS is another good candidate. ZFS has already solved the problem of how to distribute storage across multiple hard drives within one server very well, but it still lacks a good solution on how to distribute storage across different hard drives on different servers connected via a network.
Yes, I know there is the CAP theorem, so it is really a hard challenge to solve, but I think we can do better in terms of self-hosted solutions.
Are you sure you are not talking in reverse?
I find Minio single binary deployment very easy, and you also complained about SeaweedFS's complexity in the previous paragraph.
Yes, but S3 is basically a standardized protocol at this point. There are many both open and commercial alternatives, like Cloudflare R2 (no egress). So depending on the reason for self-hosting (such as preventing lock-in), S3 might be the least important thing to actually move away from. It’s way more difficult to migrate away from eg a proprietary db, sometimes by design.
[1] https://www.tigrisdata.com/ [2] https://github.com/tigrisdata-archive/tigris
The idea of LiteFS is:
* Most apps are read-heavy, and some of them are overwhelmingly read-heavy.
* If you're read-heavy and can thus get away with it, there's a pretty significant performance win in replacing the networked n-tier architecture with SQLite, because Postgres round trips add up over the lifecycle of any given request.
* In fact, that's so much the case that --- as Richard Hipp has been pointing out for over a decade --- you can blow off the N+1 problem and just write natural queries.
I think Ben Johnson would be the first to tell you that LiteFS isn't a perfect fit for every application. We have systems that use both Postgres and LiteFS.
Meanwhile, you have to dig to find it on our web page! It's an open source project that works everywhere Linux does. Dial back the cynicism a bit! :)
A plug here for:
https://kerkour.com/sqlite-for-servers
Same author, and one of the best SQLite articles ever.
Can’t wait for the day that projects like litefs are just a default that nobody knows about, lol.
Go from “technology nobody knows about” to “technology nobody knows about, but runs the world.”
I think "a few milliseconds" vastly understates this: if you want to run your application closer to users, even just across the US, each query is (at least) 70ms just to get over the network and back again.
"Application code spills into your database" was a bad thing when you wrote one language (say, Java, or PHP) and another language (PSQL/TSQL/etc) for your "stored procedures", but that's not what most modern databases are advocating for.
Instead, and not unlike something like React Server Components (RSC), you can choose whether to run code close to the user or closer to the DB (for transactions) in the same language as your application, because it's still part of your application code. This is the model that Durable Objects[1], our coordinated storage service, uses.
Disclaimer: I work on D1 & Durable Objects at Cloudflare, so I'm likely to be called biased here, but it's not like we haven't a) thought about this deeply and b) actually use D1 and Durable Objects to build distributed systems at Cloudflare.
[1]: https://blog.cloudflare.com/durable-objects-easy-fast-correc...
Notable SQLite use cases: https://www.sqlite.org/famous.html
Postgres does not have a similar page: https://www.postgresql.org/about/press/faq/
> While SQLite is a really amazing database, most teams will benefit from avoiding it and going the PostgreSQL way instead.
> Bazillions of engineering hours have been spent to make Postgres the best backend database and choosing SQLite will inevitably force you to reinvent what Postgres already had for many years, in a fragile and buggy way.
Could the same not also be said for MS SQL Server, Oracle, Sybase, MySQL, or MariaDB? The author offers no supporting evidence for this statement.Rewrite that: "Bazillions of engineering hours have been spent to make XYZ the best backend database..."
What you're saying (and what the author is saying) however is clashing with the reality of so many developers using sqlite and being happy with it.
I'd suggest to rewrite it another way:
> Bazillion of developers think they'll need a full-fledged database for their new project while sqlite will cover most of their needs.
Agree. The confusion probably comes from the fact, the SQLite programming language interfaces still have the "connection" abstraction.
Also, it would be great if there was a simple way to simply "load this entire database into memory". Its not too difficult to manually copy tables, but its much slower than it could be. Even a smallish ~250 MB database was taking like 30 seconds to copy row-by-row.
I would say that if one needs to query across more SQLite files than that, it's definitely time for a different data policy.
Here's a package in golang I wrote to help with that process:
https://pkg.go.dev/gitlab.com/martyros/sqlutil@v0.0.0-202312...
I'm doing this in a project I'm developing for language learning, except that you have both shared databases for content, and individual databases for view logs, preferences, and so on. What I actually do is open a :memory: database, ATTACH all the appropriate databases. Transactions work just fine, but because the shared database is basically read-only, then there's no write contention because each user is just writing to their own database. Overall it makes queries easier, because you don't even need to include the user (or the language). (Of course, the flip side is that getting stats on all the users and languages is more difficult.)
Currently it's just single server, but it should be possible to read-replicate the content, and actually move the write replica of the study database to a local server. It should also make it straightforward to let people download their own information: just hand them the actual SQLite file.
If I ever grow large enough that I need multiple servers in different geos, I'll write up my experience and post it here.
I can see that the organization running the application needs a global view of all data. But regional users perhaps don't. Often they just need to know their own data.
Write first to edge then copy to central database rather than write first to central database then trickle down to the site from where the write originated in. Just wondering what portion of applications could use this alternative design.
Say you flag a write as "local". Later, some other place in your app starts relying on this write in another locality. If you don't update your write spec from "local" to "primary", you don't have a consistent database anymore, but you will make decisions thinking that you do.
Now consider a team of 10. Or 20...
Mayhem can spiral very quickly from there.
> We’re actively working on global read replication and realizing the above proposal (share feedback In the #d1 channel on our Developer Discord).
Perhaps it will be out by the time the book is finished.
[1] https://blog.cloudflare.com/building-d1-a-global-database
Last year's major Cloudflare outage really was the final straw for me with regards to D1. I don't mean that as a knock at Cloudflare at all, the situation sounded horrible and I appreciate how the entire team responded to it. I just worry that internal responses to fundamental infrastructure issues will leave newer projects like D1 on ice for a year or two.
My concerns, and this is very much my own concerns with no context of what has or is happening internally st Cloudflare, is that the outage exposed some serious issues that will take time to fix safely. The fact that D1 still doesn't support replication is an indication to me that it has been deprioritized, likely with other newer and less used products, while the infrastructure updates are dealt with.
My guess is that they wanted it for 1.0 but the release slipped. It happens.
D1 is definitely not deprioritized. We're heads down on replication, and it's important for us to get it right. Takes time!
...to pretty much eliminate write contention. I know, it sucks these aren't built in yet.
Also if we got some sort of router + map reduce helper (Vitess, Citus -like) it'd make massively distributed SQLite a lot more viable. Setups that don't hammer a single master would make all the difference.
Postgres' main disadvantage at scale is all the additional machinery required (backups, failover, proxy, no DDL replication with built-in logical replication .... UGH!!), even when you're using Citus. Feels like it forces you into k8s with the amount of orchestration you need to run.
Would you mind elaborating
It's really nice to have all of that power available in one piece of infrastructure.
e.g handling of dates is a big one. In SQLite they are just strings(kludgy IMO), where as Postgres has the timestamp data type.
Otherwise, keeping a very close eye on it.
I'm looking at Marmot for high availability. Not necessarily horizontal scaling, but instead having a backup server or two that have a constant up-to-date copy of the live db but also can take over if the main server dies, and the main server can then sync the data back when it comes online.
Then TFA mentions caching without talking at all about how that is a solution, let alone a simpler solution.
Should this be available, numerous lightweight web applications could operate without having to set up a separate PostgreSQL or MySQL database.
(I'm planning a background job system based around SQLite at the moment)
If you are running a workers loop together with your http serving loop, running on the same process is awkward: you would need to stop serving your webapp each time you want to deploy new workers. Also you would need to wait until all workers are done before you could redeploy the app. If one of the workers does something unexpected, it could take down your webapp together with any other workers running, etc.
If you used multiple processes you would need to perform sync through some IPC or something like redis to perform writes sequentially, but using a DB that already ships as a daemon would fit the problem better.
My design should be OK - I'm planning on having the workers retrieve jobs and send back their results via an HTTP API to a single process that wraps the SQLite database (Datasette with a custom plugin).
In case it helps, I've been investigating bg jobs too and saw a bunch of resources that can be helpful. One is a hn post about a job queue on top of pg [0] that has some cool pointers. Someone mentioned the "transactional outbox" pattern [1]. Separately, I found this video about implementing a work queue with Nats JS [2].
I suspect you could implement the outbox pattern in Datasette and provide a way to offload the jobs to any external queue, but Nats/JS seems nice since it provides all the building blocks to implement "exactly once" delivery, dead letter queue, hearbeats to ensure the workers completes the work, etc, and it is very easy to run. I think it could save you a lot of the tricky work of implementing all these features with SQL(ite).
The overall design would be something like:
def trigger():
"""Enqueue a job transactionally: either fully succeeds or fully fails.
job_id = transaction {
job_data = ...
enque job_data into outbox
}
# This can fail but no biggie, you will still need to poll in case of failure,
# so no jobs will be created without their backing data, and no jobs will be dropped.
transaction { remove job_id and send to nats }
def background_poll():
"""Poll the outbox in case we succeeded in inserting into the outbox but somehow failed to deliver to the queue."""
try periodically { transaction { remove from outbox and send to nats } }
... then in another process or processes, the workers would talk to nats/js to perform the work.Elsewhere someone described a job system that only relied on a database without support of events (that is, not pg), and required creating a sessions table to ensure workers complete the jobs, etc [3]. This is the kind of thing that I think could be simplified by using nats/js or another external job queue.
--
0: https://news.ycombinator.com/item?id=38349716
1: https://microservices.io/patterns/data/transactional-outbox....
2: https://www.youtube.com/watch?v=7Jp3tyCGMZs
3: https://forum.cockroachlabs.com/t/how-to-implement-a-work-qu...
SQLite has pros and cons, like anything else. When it comes to web frameworks and app platforms, to fundamental problem is that many committed fully to server less/edge and ignored the los of persistent storage. You just can't use a local database when using serverless, or any type of distributed compute/rendering for that matter.
Databases are centralized by design, you can dodge some of that complexity with clever synchronization protocols but you are still limited to having a single primary DB at the end of the day.
For read-heavy use cases, tools like Turso can be invaluable. If database writes are more common, you'll always be limited by network latency. More importantly for most modern web apps, whether you render HTML in the browser or a server you can't avoid loading states. IMO you might as well lean on the platform and use server rendering whenever persistent state is involved.
RIP website.
edit: it's back
Sqlite only really works as a throwaway database. For that it's pretty great. Although a lot of the current use cases for sqlite could have just been a .ini file.
Version 3.26.0 from 2018 is 2286469 bytes.
That's a 20% increase in 6 years, but it's still just 2.6MB of compressed C.