SQLite: Wal2 Mode
sqlite.org
sqlite.org
Looks so logical that I don't understand why WAL mode was not implemented like this from the get go. Probably an optimization wrongly dismissed as premature?
Anyways, looking forward to this mode reaching general availability.
You don't have that with sqlite, so I don't see an obvious advantage for this, except if they now spawn a process or thread to do this concurrently.
Edit: so I read the doc (shame on me) and it has nothing to do with speed. Its purpose is to prevent a wal file from growing too large.
Other databases do do similar to what you suggest, though obviously the trade-offs will differ because of other different internals and product priorities, so it would have been thought about. For instance MS SQL Server has multiple “virtual logs” in its log files, for at least some overlapping reasons.
Switching to WAL already makes handling Sqlite databases much less convenient, since you now have three files instead of one, and need a filesystem snapshotting mechanism to reliably back them up (so you don't have one state in the database and another in the wal). Making the filenames and number of files less predictable would make that mode not worth it for many use cases
Besides, even if the database is single-file it's still necessary to use filesystem snapshotting for live backup, or it's likely to get an inconsistent copy.
Never saw it myself, so I have no idea what the cause was.
VACUUM INTO?
Instead, sqlite provides an online backup api specifically for creating backups. This also takes wal mode into account.
Probably because of this.
> but it does mean that the wal file may grow indefinitely if the checkpointer never gets a chance to finish without a writer appending to the wal file. There are also circumstances in which long-running readers may prevent a checkpointer from checkpointing the entire wal file - also causing the wal file to grow indefinitely in a busy system.
> Wal2 mode does not have this problem. In wal2 mode, wal files do not grow indefinitely even if the checkpointer never has a chance to finish uninterrupted.
I don't get how wal2 fixes the long-running reader problem though. Maybe they were just referring to the former problem?
Because with a single wal file you can't checkpoint it during a read since said file may change out from under you.
With two wal files, the one you are actively appending to can be treated like in wal1 mode but the one that isn't being appended to is immutable for the time being just like the main database.
This means you can treat the actual db file and the immutable wal file together as one immutable database file with some underlying abstraction. That abstraction then allows you to perform the checkpoint operation since the abstraction can keep all that immutable data accessible in some form or another while reworking the data structure of the db file.
Then once the checkpoint is complete, the abstraction can clear the now redundant immutable wal file, become transparent, and just present the underling single DB file.
And now once the wal file you are actively appending to reaches a sufficient size, you "lock" that one, rendering it immutable, and switch over to appending to the cleared wal file you were previously checkpointing. With this you can now checkpoint again without blocking reads or writes.
so there might eventually be wal3 and wal4 files and so on?
Checkpointing can be considered "lock free" since the operation will always eventually complete. How long it takes will depend on the wal file being checkpointed into the db but it'll eventually complete in some finite amount of time.
Because you know that any given checkpointing operation has to eventually complete, you can simply keep appending to the current "append" wal file and then tackle those changes when you finish the current checkpoint op (at which point the wal file you just finished checkpointing is free to take the appends).
When a reader is reading, it puts a shared lock on the specific data it is reading in the shm file. The checkpointer respects that lock and may (potentially) continue working elsewhere in the db file, slowly updating the indices for checkpointed data in the shm file.
The checkpointer won't change the underlying data that the reader has a lock on but they may have created a new location for it. When the reader is finally done reading, the checkpointer can quickly grab an exclusive lock and update the header in the shm for that data to point to the new destination (and then release said lock). Since the checkpointer never holds this lock for very long, the reader can either block when trying to get a shared lock or it can retry the lock a few moments later. Now that the header in the shm only points to the new location, the checkpointer can safely do whatever it needs to with the data in the old location.
Slowly rinse repeat this until the checkpointer has gotten through the entire write ahead log. At that point there should be no remaining references in the shm to data within the wal file.
Now the wal file can be "unlocked" and if the other wal file is large enough, it can be locked, writes switch over to the other wal, and the cycle repeats anew.
Edit: Importantly, this requires that all readers be on a snapshot that includes at least one commit from the "new" wal file. So compared with wal1, wal2 allows you to have long running readers as long as they start past the last commit of the "previous" wal file.
So while you can make multiple back to back reads that use the same snapshot, I believe there's no guarantee that the snapshot will still exist when the next read is opened unless the previous read is also still open (in which case an error is returned).
That seems to set an upper bound on how long a reader can block a checkpoint (unless the reader is intentionally staging reads to block the checkpoint).
Theoretically you could implement checkpoints that flatten everything between snapshots into single commits but the complexity and overhead probably isn't worth it given that the only real blocker for wal2 is an edge case that is nigh impossible to encounter unless you intentionally try to trigger it.
BEGIN a, read x0 BEGIN b, write x1, END b BEGIN c, read will return x1 Back to a transaction, read again, return x0 still.
> In wal mode, a checkpoint may be attempted at any time. In wal2 mode, the checkpointer has to wait until writers have switched to the "other" wal file before a checkpoint can take place.
While it has advantages, it is also more code so more possible places to hide, and other disadvantages hence it doesn't completely deprecate the other WAL mode.
Also the advantages might not have been as commonly cared about in sqlite in earlier times, but it is being used in more & more places and sometimes at larger scales or with more significant concurrency needs, and the core has been pretty darn stable for quite some time, all of which factors change the dynamics of what is worth committing the dev/testing time to in terms of usefulness to the end users.
Bedrock is the more interesting branch.
It’s WAL2 + CONCURRENT
It’s also the branch Expensify uses to scale to 4M QPS, on a single node (6-years ago)
https://sqlite.org/src/timeline?r=bedrock
https://use.expensify.com/blog/scaling-sqlite-to-4m-qps-on-a...
The primary use case of this branch is to make SQLite into a more "client/server" like architecture, which deviates from the predominate target use of SQLite (embedded).
Though I too would love a client/server version of SQLite.
a. SQLite, by default, doesn’t allow multiple writers.
b. There’s also real challenges to writing to a non-local (network) filesystems
a. SQLite, by default, does allow multiple writers to connect, but only one can write at a time.
b. NFS was never suggested to be used -- embedding it in an app that exposes a network api works fine.
c. The behavior being discussed (multiple concurrent writers) already kind of exists (multiple writers) and this would just make them more performant.
https://sqlite.org/src/doc/754ad35c/README-server-edition.ht...
I assume you could use the latter without the former.
Heh, I wonder how many people will delete the "wal" file thinking that, since they switched to wal2, the wal file must be a leftover.
I've been bitten badly by that issue once. I just mounted the .db file into a docker container and didn't realize that sqlite creates wal files. On an non-graceful shutdown of the application the wal file was not merged into the db and the container deleted. And around a day of changes were lost.
Conclusion: Sqlite databases should be placed into their own folder, so it's obvious that it's not always just one file.
When using WAL, if you’re copying or backing up the database it’s possible to force a checkpoint, then you can copy the .db file alone knowing exactly up to when it contains data.
IMHO this (having a variable number of files containing the data, depending on your configuration) is the only real design quirks of this technology.
If you're using it as a standalone file format you presumably shouldn't leave .sqlite files with associated wal files lying around in places where users are going to get confused by them, either by sticking to the rollback journal mode or by using some other method
Also, a scheduled backup process might come along at any moment and non-atomically copy the database file and any -journal or -wal files.
Ideally, user visible files should survive copying at random points in time without corruption and without losing too much recent data.
Having read "How To Corrupt An SQLite Database File"[1], I'm still not quite sure how to achieve this.
I know reading the manual isn't very common and people are lazy, but getting burned can be a useful and necessary lesson.
It happens.
(I'm really, really hoping the "previous" person wasn't me.)
You make mistakes. Do you want them to be as painful as possible?
https://docs.rs/left-right/latest/left_right/
My understanding is that this technique is older than the linked implementation (though independently rediscovered), but notably, this implementation was written to support a different high concurrency SQL database (for some definition of that) called Noria.
[1] https://learn.microsoft.com/en-us/sql/relational-databases/s...
This WAL2 feature is a perfect example of a new kind of concern I have. SQLite has a really competent facility for handling write-ahead today, but it has these edge cases where it may fail under adverse (but totally plausible) scenarios. I haven't yet had a completely corrupted SQLite database, but I have had one incident on a QA server where I had to delete the WAL/SHM files to get the database to work again.
It's been off-trunk since its inception in Oct. 2017 and there's been no discussion within the project of merging it into trunk (why that is i cannot speculate). It is actively maintained for use with the bedrock branch, as can be seen in the project's timeline:
I'm not an expert on this, but i think the idea is to separate durability from db corruption. (When synchronous = normal instead of full) you can potentially lose (comitted) data in WAL mode if a power failure happens at just the right moment, however your database won't be corrupt. No data will be half written. Each transaction will either be fully there or fully missing.
Since SQLite is single writer I'm not sure if it does this. But this (batch yet block) is how I understood Postgres works.
Of course you can turn off the blocking too by setting postgres fsync configuration to an interval rather than synchronous.
The buffering analogy doesn’t really work tho, because all three sources (db file, wal being flushed, and wal being written to) are read sources.
With double-buffering (2d/3d graphics) you are literally writing the final pixel-level data to the back buffer.[1]
In a database WAL scenario, to further analogize, it's more like you are writing the 2d/3d graphics commands to the buffer and executing them later. Because that is part of the point of the WAL -- it results in reduced disk writes because only the log file needs to be flushed to disk to guarantee a transaction is committed, rather than every data file/byte(/pixel) changed by the transaction.[2][3] (The WAL content is loosely a bit more like 3D (or 2D) vertex buffer objects/display lists [4] if you are familiar with those.)
Swapping the two WAL files though and alternating writing to each is yes like double buffering.
A third similar design pattern (to WALs) is used in operating systems' journaling filesystems[5] and actually was a contribution from OSes adopting database WAL techniques back in the 1990s.
Apologies if you know all this.
[1] https://en.wikipedia.org/wiki/Multiple_buffering#Double_buff...
[2] https://www.postgresql.org/docs/15/wal-intro.html
[3] https://en.wikipedia.org/wiki/Write-ahead_logging
https://sqlite.org/hctree/doc/hctree/doc/hctree/threadtest.w...
This project contains no code stable enough to deploy. The database backend works well enough to run some test cases, but is still quite incomplete.
Link to shortcomings, some of which are significanthttps://sqlite.org/hctree/doc/hctree/doc/hctree/index.html#s...
This doesn't happen.
The logfile is a sequentially-written file on storage. When the memtable fills up, it is flushed to a sstfile on storage and the corresponding logfile can be safely deleted.
[...]
Background compaction threads are also used to flush memtable contents to a file on storage. If all background compaction threads are busy doing long-running compactions, then a sudden burst of writes can fill up the memtable(s) quickly, thus stalling new writes. This situation can be avoided by configuring RocksDB to keep a small set of threads explicitly reserved for the sole purpose of flushing memtable to storage.
The WAL is ONLY read after crashing, to fill a new memtable.
Your comment looked like "WAL is sorted and converted to sstable":
> If a WAL file gets too long then a new one is created and the old one is sorted and written to an sstable asynchronously.
- data in WAL segments has to be checkpointed
- no replication slot, physical or logical, may require the WAL file (see the pg_replication_slots view)
- archiving, if configured, has to have archived the file (see pg_stat_archiver)
It used to be more complicated, for historical reasons we used to keep two checkpoints worth of WAL around, but I don't think any supported versions of postgres still have that behavior.
Edit:
What's more mysterious is whether WAL files are removed when not necessary, or whether they're recycled (renamed to be reused). That's indeed a bit hard to get insight to.