Things that surprised me while running SQLite in production
joseferben.com
joseferben.com
There are massive performance gains to be had by using transactions. In other RDBMSs transactions are about atomicity/consistency. But in SQLite, transactions are about batching inserts for awesome speed gains. Use them!
This example from the better-sqlite3 docs makes more sense now:
const insert = db.prepare('INSERT INTO cats (name, age) VALUES (@name, @age)');
const insertMany = db.transaction((cats) => { for (const cat of cats) insert.run(cat); });
insertMany([ { name: 'Joey', age: 2 }, { name: 'Sally', age: 4 }, { name: 'Junior', age: 1 }, ]);
SQLite is internally handling the locking for you under most providers[0]. Any sort of external attempts at the same will just make things go slower. The only thing that could really beat the internal SQLite mutex is to put a very high performance MPSC queue in front of your single SQLite connection and take resulting micro batches of logical requests into transactions as noted above, or some more consolidated form of representation.
i would've imagined that most RDBMS would have a pool of connections that get reused, rather than creating a new connection every time.
A bunch of inserts in a single transaction is generally fast and unproblematic. Though if you have weird clustered indexes and a lot of concurrency you might run the risk of frequent deadlocks.
What I have done so far was to collect safe data, that is not sensitive info like passwords and such, and save them in an array (in my case I used PHP language), and at a specific number of elements you know it's enough to commit in your database, you do so only once via transaction; in my case was 25K rows and worked flawlessly without users realizing anything at all.
I have even done the same thing with 100K rows and didn't even break a sweat!
I've told this story before, but the first time I used sqlite, years ago, I needed to store a floating point value, so I read the documentation: https://www.sqlite.org/datatype3.html
>REAL. The value is a floating point value, stored as an 8-byte IEEE floating point number.
I parsed this as "an 8-bit floating point number". Wow, I thought. That's weird as hell. Oh well, I guess it's sql lite, ha ha. I went on to implement storing numeric values as text strings, which worked exactly as well as you'd expect. Only months later did I find out that it's a normal regular IEEE double, 64 bits long.
It's funny that under "maybe it was an example of nobody reading shit all" there is a comment where someone didn't read the nickname and thought he's answering to the anectdote's poster comment :D
An EE only needs to know that you don't stick your finger in a socket. A mechanical engineer only needs to know that you can't push a rope. And a civil enginer only needs to know that sh*t rolls downhill.
If your code allows it you're fucking doomed anyway and will have multiple other type-related problems all over the codebase.
I was taught to think of a RDMS as the last line of defense against data integrity issues. SQLite, on the other hand, is more than happy to let you shoot yourself in the foot.
I imagine it went went ok!
It can be made to work. I would've gone with Binary-Coded Decimal if IEEE 754 standard isn't precise enough.
In principle, you could have some kind of configuration that says "try to commit this for so long, but if you can't commit it by then just abort". But you haven't really solved the manual retry problem, just made it somewhat less likely to occur.
As far as I know, given the costs of a request, most database servers tend to use the latter approach. But in SQLite requests are very much cheaper, and at least part of SQLite's work happens in your thread, so it goes for the former approach. But from an end programmer's perspective it's only a quantitative difference, not a qualitative one.
Also, "the database" is a highly ambiguous term, but in SQLite it can really only refer to a file on disk. There's no database server, it's just a library that you call to manipulate the file. So who even is the database server? Maybe it's the code you're writing. Maybe it's you who should be handling this stuff. And that's SQLite's decision, and fair enough too. SQLite isn't trying to be a database server, it never offered to be a database server, and you shouldn't think it's a database server. You should think of it as a library for executing SQL queries against a file that holds a relational database. If you think that's tantamount to being a database server, then consider what you normally mean by "server". Is a library for manipulating zip files tantamount to an FTP server?
If there are n+1 reads I don't think it would lead to database is locked.
That's a tradeoff you make if you're a user, and it's pretty worthwhile. There's something to play with
Time for some researching :)
There's two classic pieces of wisdom that aren't so much wisdom anymore but pieces of the landscape: The first is, don't write your own datastore, use a database. This is pretty much taken for granted today, but in the year 2000, there weren't a plethora of web frameworks. At the time, the unix filesystem didn't seem like such a bad interface to store things. Storing each user as their own file made sense. Simple enough. One (just one of several) way it runs into problems is when:
The other landscape concept - Run multiple copies of your app. This one is kinda controversial to this day, but it's core to the "Twelve Factor App", which was a somewhat prescient memo for it's day, if looking a little long in the tooth to my eye[1]. It's why people keep using Kubernetes. And it's correct advice - If you need to be running a ton of concurrency, you are better off scaling by process and keeping heavy-lifting logic out of your database. If you keep the database and application on separate hosts, you can scale to one big database server for writes, and many application servers and read-only database shards. This works really well for most crud and render-heavy applications you see on the web today; Social networks in particular absolutely thrive on this model.
This brings up another application architecture anomaly that's mostly disappeared - The "App in Database". There's a mildly successful model whereby almost all application logic lives in the database itself as stored procedures. You can do an entire app update as an atomic DDL commit! The database's permissions handle everything, and users just connect as users. In the old MVC model, the Database tables are M, the stored procedures C, and you write a V on top. I've encountered a few instances of this paradigm, most notably an accounting software that worked pretty great.
Fly suggests throwing all of this out the window. Most applications don't need a lot of write-scaling. You don't even need read concurrency. Run a single shard, make that shard fast, performant, and replicable - When it's shut down, you can make many copies of it, and resume from any of them. When a new one starts up, it becomes the primary, all of the others are marked as stale, and you resume making copies. If you're using Litestream like Fly does, this makes for a drop-dead simple app environment. Alternatives, like I'm using at home, are ZFS replication - SQLite is snapshot safe, so I just snapshot frequently, replicate those snapshots frequently, and reverse direction when necessary.
[1] https://gist.github.com/GauntletWizard/1fd1298304e811529d4b9...
SQLight has connotations of speed and energy and all those marketing keyword association goodies.
In my experience Sqlite is great for everything, with a number of exceptions: https://dev.to/johntellsall/re-more-on-sqlite-vs-mysqlpostgr...
Today I can buy it at much faster speeds and better timings for £0.03/MB. Damn capitalism.
That's 7 orders of magnitude in 35 years.
[1] https://stardot.org.uk/forums/download/file.php?id=29208&sid...
I've had to explain numerous times to our engineers that their laptop is running around 10x the IOPS of our database (~40,000). Table scans are actually more performant on a dev laptop than a replica.
https://blog.turso.tech/sqlite-based-databases-on-the-postgr...
Other than that it's awesome. I've stuck to its native file format though as scanning SQLite is slower.
Perform OLTP actions against sqlite db from within duckdb.
Perform OLAP actions against duckdb db from within duckdb.
Sometimes your OLTP content drives the OLAP query so read from both sqlite db and duckdb db at the same time.
SQLite's locking code looks like:
while lock_is_still_held_by_somebody():
sleep(3) # 3 secondsBut in general, funelling writes into one thread is the way to get around it
I also use SQLite in production and it’s been pretty great so far. Serves about 50 requests per second from a single threaded Node app on a low tier DigitalOcean machine. To be fair, most of those are cache hits but it’s still be nice. It’s saved me money by not needing a separate db server and the operational simplicity is great. Biggest speed up I found was turning on memory mapping (which is different from an in-memory database that the article talks about).
Do they ever call `commit`?
Sticking your data in a self-balancing BST usually gives you log n lookups, but most of us haven't internalised that log n with a small coefficient can be better than constant time with a huge coefficient on expected data sets.
@receiver(connection_created) def configure_sqlite(sender, connection, *kwargs): if connection.vendor == "sqlite": cursor = connection.cursor() cursor.execute("PRAGMA journal_mode = WAL;") cursor.execute("PRAGMA busy_timeout = 5000;") cursor.execute("PRAGMA synchronous = NORMAL;")
Initially, I was worried that Django might screw up migrations since it's a bit awkward to make certain schema changes in SQLite. Also almost no one seemed to use SQLite with Django in production, so I was a bit worried about how battle tested the SQLite part of the ORM was.
So far so good.
Your configuration looks right to me.
> I asked Django Fellow Carlton Gibson what it would take to update that advice for 2022.
Glad to see that SQLite is being presented as a valid option in the current version of the docs.
> Even without WAL mode, bumping the SQLite “timeout” option up to 20s solved most of the errors.
Can you say anything about the tradeoffs when increasing the timeout, maybe in the context of Django?
My hunch is that if you bump it up to 20s while receiving a huge amount of concurrent traffic you may find yourself running out of available Python workers to serve requests.
It seems to me like they removed the warning on SQLite, so I guess they tested it and it works fine. But I didn't research this any further so far.
enabling WAL helps a lot as it allows concurrent reads and a write
Maybe they ran their entire benchmark in a single transaction or something?
Does this mean that my processes need some synchronization primitive of their own to not to concurrent writes?
I only tried for half an hour but I had problems everwhyere, sticking to the php.net docs, and wondered if it was me or the docs or the webhost...
You could theoretically get 128 core CPU and use all of the cores for django process..
That test of memory vs disk I/O performance is ridiculous. Nobody should pay attention to that.