Why SQLite is so great for the edge
blog.turso.tech
blog.turso.tech
Also, I think the idea of “edge” doesn’t make a ton of sense. What we really need is code and data that move around as needed for the best performance. See: https://blog.cloudflare.com/announcing-workers-smart-placeme...
What people call “edge” is a single optimization bringing code near the user. I think we should go far beyond that. Sure, use something like Cloudflare Workers for your “edge” needs (ie bringing your React app close to the end user/doing server side rendering). But don’t stop there because where data resides, what APIs you call are all going to matter.
That's the vision of something I called the "Supercloud": https://blog.cloudflare.com/welcome-to-the-supercloud-and-de...
See also: https://archive.is/e7u9x / https://www.the-paper-trail.org/post/2020-04-06-physalia
But yes, I think the next step is then some sort of sharding. Sharding by user would be an obvious approach for many apps. I think we should build a framework to help manage this, so apps would only need to provide some callbacks e.g. to compute shard key for a particular query.
Alternatively, apps that want full control will be able to use Durable Objects directly. We'll soon (in a few months, probably?) enable the new storage engine for all Durable Objects which means every object will have a private SQLite database.
(I'm the tech lead for Workers in general, and currently focused on this project in particular.)
Much like R2 has an S3 compat API as well as a way to retrieve files outside of workers (IIRC) will this be true of all forms of storage on the platform?
Sometimes it'd be nice to prime the cache with KV for example, without having to invoke a worker
For D1, yes — on the roadmap is a native HTTP API. KV has a HTTP API as well, so you can write directly.
You can't. D1 won't let you create databases at runtime. At least that was the case last time I tried it, and it's what killed it for me.
wrangler d1 create customer28 --experimental-backend
wrangler d1 execute customer28 --file=schema.sqlTherefore, creating a database per user as part of the user signup flow is not possible.
I suppose you could make something to write the bindings to the toml and trigger a redeploy, but that's definitely not pretty.
The nice part is the decoupling. User not using their DB for 5 minutes? Time to idle them and use that compute for another customer.
And it's also really nice that storage becomes way way way easier. Storage is the biggest hurdle when your database count scales with your user count so you can instead build out the compute system once, fix a few issues along the way and from then on just babysit a single big storage cluster.
And if that becomes too big at some point, replicate it, shard the tenants by region or whatever and you "only" have to manage storage a dozen times or something. It's obviously always annoying, but at least the setup is similar enough that you can park on FTE per Storage cluster and serve enough customers to make it worth it
Server side rendering or “single optimization” as the “edge” is kind of an abuse of terms.
Prior to SQLite I tried CSV files and raw binary formats. None of them could match the flexibility, ease of use, and throughput of SQLite. My setup has been in use for several weeks now and has processed numerous rows of data. As the SQLite project outlines on its website:
> SQLite does not compete with client/server databases. SQLite competes with fopen().
> The system is for a Formula SAE (FSAE) Electric style race car. FSAE is a collegiate competition, and I'm part of a US Pacific Northwest team. Our car uses a 550 V battery and is 4WD. Most of our electronics and firmware/software are in-house besides motors and inverters. I lead the telemetry project and work with 1-2 other members on the system.
> The system consists of an on-vehicle computer (Raspberry Pi or NXP i.MX) and a ground station (Rockchip RK3588S). We use Python heavily and the real-time dashboard part is done with InfluxDB and Grafana. Both computers and any user devices are on a Wi-Fi network made with a long-range WISP access point.
> Our team is unfortunately very light on public tech blogs and how I wished I can change that... There is plenty of information on FSAE electric cars, vehicle telemetry, and both on the internet and they were a source of inspiration as I designed my own solution.
> An SQLite database is highly resistant to corruption. If an application crash, or an operating-system crash, or even a power failure occurs in the middle of a transaction...
Quite often after a run the entire car is turned off and, on next power up, the databases are left as .db and .db-journal files. The code has no problem processing or even continuing on logging with DBs in this state.
SQLite writes are "atomic" transactions. After writing new data, it goes back to the index and registers that new data has been written using a single instruction. That's why interrupting it in the middle of a write doesn't result in partial data or a corrupted index.
A bit unrelated, but curious as to why you wrote to three separate databases only later to merge them.
[1]: https://www.keysight.com/blogs/tech/sim-des/2021/06/10/autom...
Curious to see how far off I am :)
But at 4000 samples per second? You'd think twenty would be plenty.
NyQuist: 4000 samples per second means we are looking to get information about an up to 1800 Hz signal. A6 on the piano keyboard (excluding harmonics).
Sorry, I mean Nyquist. I was confused by NyQuil, the night time flu medicine.
Doesn't seem unreasonable to want to sample at that rate, especially for tweaking suspension and such.
The propagation speed can be misleading.
All that matters is whether there are oscillations in the suspension that go up to 1800 Hz, not how fast the car is going forward. If there are, those would have to be harmonics. You'd think would be beyond the frequency response of the suspension. Suspensions are heavy, bulky components and are heavily dampened (which is a kind of low-pass filter).
If the suspension moves anywhere near 17 mm per sample (250 km/h vertically), that car is in serious trouble.
Surely you care about more than just oscillations, for example how exactly the suspension compresses during breaking. If you've seen slow-mo shots from high-speed race cars riding a curb, it can be quite violent with lots of movement.
In this article[1] about Formula 1, they state they sample vibration data at 200kHz. This is then filtered to a lower rate for logging, but getting say 5kHz out of 200kHz raw sensor data doesn't seem unreasonable to me.
They also mention they collect about 30MB per lap of sensor data from more than 250 sensor, and laps are typically around 1.5-2 minutes long. If one assumes a 2 minute lap and 250 sensors, that's 1kB/s per sensor on average.
[1]: https://www.racecar-engineering.com/articles/how-data-works-...
The entire graph of suspension vs time should be well below the frequency range. It doesn't matter whether we are looking for frequency domain features or time domain features.
(Like someone said upthread, it's probably 4000 events per second as an aggregate from numerous sensors, mutiplexed into one sqlite. Or maybe even a total across the three sqlites.)
Maybe not quite 4kSps is needed but for vibration and such it'd make sense to me.
But sure, could very well be an aggregate rate.
[1]: https://www.speedwaymotors.com/the-toolbox/general-sprint-ca...
The sample rate you need to reconstruct the motion of shocks is entirely determined by Nyquist, just like sampling audio or any other signal.
You need some 2.2x the highest frequency you want to capture, and make sure you filter out anything above that (if it exists).
If there is nothing above 20 Hz in the suspension's movement, or nothing you're interested in, then you need a 44 Hz sample rate. (More if you implement oversampling, but not 10 times more let alone 100.)
Lots of folks put work into a car and they all have metrics of interest to tell how their systems perform. An ideal telemetry system for us captures all this information and makes it available both in real time and analysis.
This setup is easy to put together but might not be optimal. I don't have a lot of experience with SQLite performance tuning, and I wonder if it will be faster to have worker threads pass everything thru IPC to a writer thread, which batches rows and writes to a centralized database.
It is however possible to have multiple applications access the same sqlite database on the same filesystem (i.e. locally, not via network access, not via remounts or symlinks or other forms of synonyms) and it will sort out the locking for you. Depending on what you need to do, there's I think three possible strategies you can use so it can probably handle your concurrency needs faster than Postgres via the network can. For instance, this particular task is write-heavy, and it would benefit from a different setting than a read-heavy application.
There are certainly occasions when Postgres or MySQL or MS SQL or Oracle is a better choice than SQLite, and sometimes concurrency contributes to that decision. But it's unlikely to be the case that you had an SQLite solution that worked really well with one process reading and writing from the database, and now all of a sudden you need a second or third application to use the database concurrently with the first, and you find you need to switch to a client-server model And even if sometimes you need to take specific steps (like adopting a different locking model), you also have to take specific steps to run Postgres (e.g. using a pool to speed up connections and limit the number of concurrent connections because each connection is a separate process). It's a matter of knowing your tools.
* SQLite is 35% Faster Than The Filesystem [1]
* Writing to files safely is hard [2], and SQLite goes to great details to ensure consistency in case of application, OS, or computer crash [3]
[1]: https://sqlite.org/fasterthanfs.html
I was really pleased to add it to my toolkit.
SQLite's reputation and adoption makes me comfortable to assume its operational correctness so I can focus on data itself, not reader/writer logics for it. Even though a SQLite database's schema may change, I'm glad knowing that the database file can still be easily opened and read many years into the future, something that custom formats may not guarantee.
On CSV: Syntax and library differences aside, SQLite almost works like a drop-in replacement for CSV since a single table database can be essentially treated as a glorified spreadsheet/CSV if wished. My post processing data pipeline is built with pandas so it is virtually the same for me to read a SQLite database vs. a CSV file.
The system consists of an on-vehicle computer (Raspberry Pi or NXP i.MX) and a ground station (Rockchip RK3588S). We use Python heavily and the real-time dashboard part is done with InfluxDB and Grafana. Both computers and any user devices are on a Wi-Fi network made with a long-range WISP access point.
Our team is unfortunately very light on public tech blogs and how I wished I can change that... There is plenty of information on FSAE electric cars, vehicle telemetry, and both on the internet and they were a source of inspiration as I designed my own solution.
[0]: https://code.kx.com/dashboards/
[1]: https://www.aemelectronics.com/products/software-aemnet/aemd...
It sounds unbelievable that you couldn't append some CSV or binary record to file via an open file descriptor more efficiently than inserting into SQLite.
(It's clear that to have the data already in SQLite form saves post-race steps.)
SQLite is the only solution for me so far that provides a structured data store for mixed data types, is power loss safe (important to an embedded system,) and is a high-quality and portable industry standard. Let along the SQL language itself, which has proved to be hugely useful for first-pass post processing given the amount of data that I ingest.
Perhaps there are other technology that will work just as well or better: maybe Protocol Buffers and alike? Yet SQLite currently ticks all my boxes and I'm quite happy with it.
SQLite could scale to even quite large customers with this.
Another really compelling architecture is a DB per user, with partial/selective sync between the nodes. If you then couple this with a "local first" design, the "edge" just becomes an other local deployment that the users db can sync against. Collaborative apps, where the users have their own documents but can also share/fork them would align well with this.
I believe starting with "local first" and using the edge for sync and "online only" modes is going to become the default for a significant number of apps moving forward. SQLite with CRDT based syncing and merge conflict resolution is the way to do this. There are a couple of exciting projects working on this:
Electric is super cool. Can't wait until it is a bit more mature.
If it's a case of 1 node per customer (node being vm, lambda, cloudflare worker, whatever) then there should be no limit.
Maybe you're thinking of a more "traditional" approach with a server process managing 1 sqlite db per customer, which might make sense in order to keep cloud costs down. Even in that case it should be trivially easy to distribute the load.
It actually makes a few things simpler, say progressively rolling out visiting upgrades. You need to write schema migrations anyway, this lets you upgrade one customer at a time.
You can parallelize it, of course, but that's a challenge in of itself.
Not a challenge at all really.
Postgres can run locally, communicating via a Unix socket. You should try benchmarking this before stating that it's so much slower than SQLite.
1. create the ramdisk
2. mount the ramdisk, configure systemd to automount it
3. modify the systemd unit file for psql to depend on the ramdisk
4. initialize all the psql data structures in the ramdisk mount folder
kernel (filesystem) -> app with SQLite library
kernel (filesystem) -> Postgres -> kernel -> app with Postgres client
If Postgres reads data from own cache (not from disk) the chain will be one step shorter: Postgres -> kernel -> client, but the same true for SQLite own cache.If mmap is used (make sense for small data sets cached in RAM) then reading data by SQLite can be zero-copy [1].
Benchmark result highly depends on a use case - neither is better for all use cases but at least for some use cases SQLite is faster.
Which is already IPC, which already makes it slower than having the data never crossing process boundaries. No benchmark required.
No clue what Postgres would manage, but I suspect it would be about an order of magnitude higher latency in the happy case.
Unless you’re talking 1 versus 10 microseconds (or less), I don’t think Postgres will have an order of magnitude higher latency. And if we are talking this range, why would it matter for a web app where the client’s latency is almost certainly >1 millisecond?
With SQLite, it's often practical to issue several queries in situations where that would be too slow for a traditional client-server database.
$ pgbench -h localhost -p 5432 -b select-only -T 60 -c 10 -j 2 bench
Password:
pgbench (15.3 (Ubuntu 15.3-1.pgdg22.04+1))
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 10
number of threads: 2
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 10370147
number of failed transactions: 0 (0.000%)
latency average = 0.058 ms
initial connection time = 54.881 ms
tps = 172993.008029 (without initial connection time)
$ pgbench -h /var/run/postgresql -p 5432 -b select-only -T 60 -c 10 -j 2 bench
pgbench (15.3 (Ubuntu 15.3-1.pgdg22.04+1))
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 10
number of threads: 2
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 16890415
number of failed transactions: 0 (0.000%)
latency average = 0.036 ms
initial connection time = 7.186 ms
tps = 281540.260418 (without initial connection time)
YMMV depending on your workload, but Unix sockets should always be significantly faster.I would say "low computing power" but a raspberry pi is 4Ghz these days.
Personally I'm not yet convinced that the normal SQL representation for what a 'date' object is matches real world use cases that well, or at least doesn't cover all of them.
As a programmer, what I find I want is a 'moment' which retains the input specification. Possibly in a sanitized binary format that's not the literal text, but also isn't a single numeric value either. E.G. Timezone Specifier / City (not just one per TZ!) + Human timespec (5 PM Tuesday the Whatever day of Month in Year) AND a pair of evaluation rules version and 'effective UTC||TAI time' for comparisons. The evaluation version thing needn't be a full version it might even just be a couple bits at the top or bottom of a long range 64 or 128 etc time number of some units. Something that can be used to determine if a value still needs to be updated with the latest rules set rather than using the cached representation.
It’s more that there’s no such thing as a date type, but that date and time functions can work with text or numbers, in a few different interpretations.
Documentation: https://www.sqlite.org/lang_datefunc.html.
It's an option for those who want easier handling.
"When the DSN Option "JDConv" (Julian Day conversion) is enabled the SQLite 3 driver translates floating point column data interpreted as Julian Day to/from SQL_DATE, SQL_TIME, and SQL_TIMESTAMP data types (supported since May 2013)."
That's a bit like saying that a family car is not suited for heavy goods transport.
One writer. Any session that issues BEGIN TRANSACTION and then hangs halts all dml.
WAL mode confusion. WAL cannot safely be used on network filesystems, and it breaks ACID on ATTACHed databases, among other problems.
Date and time types don't really exist. There are functions to assemble your own, but it does require some thought. The ODBC driver for SQLite does have options to "emulate" this.
Length specifications on a column are ignored. CHAR(2) will allow the insertion of a blob. I think that check constraints could be used to enforce this.
Type affinity means that any data type can be inserted into columns declared as any other data type. Rigid type enforcement can be done, but it is not the default.
Those are the major eye-openers.
(I wrote a little more about this, with examples, at https://news.ycombinator.com/item?id=33282830.)
I agree that it would be nice if some of these things were built in but at least sqlite is reliable when configured and used in particular ways.
The context of this thread's article is about "edge computing" which means resource-constrained endpoints like IoT, mobile devices, cloud "workers" on CDN, etc.
Those scenarios will inherently favor a lightweight database like SQLite over a full-blown heavyweight RDBMS like Postgresql/MySQL. RDBMS engines have extra code and cpu/RAM requirements for handling multi-user concurrency, locking, etc. You don't need all that machinery for edge computing.
Another example of the above tradeoffs is smartphones like iPhone and Android. Their default persistence API framework in iOS/Android use embedded SQLite as the local backing database. It doesn't make sense for a battery-powered device to waste cpu and RAM on a multi-user Postgresql/MySQL engine when there's only a single user of a smartphone.
With SQLite, the data is stored very local to where it’s being accessed.
Either move it local to the user (SQLite with WASM/OPFS does this — this is mentioned at the end of the article, but that’s not the edge), or centralize it — for SQLite this means centralizing the execution of the code accessing the data too, since it’s designed for high granularity, low latency access, which, again, isn’t the edge.
SQLite is so good and convenient that you might still use it for some cases at the edge (e.g. some read-only or precomputed, predistributed chunk of data you want to access in a flexible way) though it would never be the only option for that kind of thing.
It works by consuming data out of upstream APIs and then publishes a new, faster version of that API in edge locations. The underlying data there is stored in SQLite (plus some custom bits), but that’s just an implementation detail.
I am looking at moving us towards 1 gigantic SQL Server Hyperscale DB for most things. Force our clients into a PaaS solution over time, enforce one standardized product model, etc.
Where you probably don't want to be is somewhere in the middle where half your data lives in some centralized place and the other half is in your edge/client nodes. Synchronizing across these domains can rapidly become a nightmare.
If you start having trouble "beyond customer #5", you have a problem of labor-intensive manual changes and insufficient automation, not a task that a different DBMS could do better.
[0] https://www.splitgraph.com/blog/deploying-serverless-seafowl
Wasm Labs group in VMware was involved with this.
See: "SQLite builds for WASI since 3.41.0" discussed at https://news.ycombinator.com/item?id=36054521
It consists of server functions to add to your Cloudflare Worker and a CLI to download all Durable Object state to a local SQLite database.
Take a look: https://github.com/emadda/durafetch-server
My personal opinion, is that the relational model is the best option to model data, any attempt to avoid it, replace it, mimic it adds more complexity than it solve
Linked Tables (relations) is the ultimate data modeling tool
10+ years ago there were 2 big names in open source embedded databases, Berkley DB, and SQLite.
Then the NoSQL fad hit, and a bunch of new key value stores emerged. Now developers are remembering what made relational databases great.
Even when you don't need relational features, SQLite is battle tested, and a great choice for storing key, value pairs.
This means you'd be paying per writes, reads and storage.
Last time I set up MySql it took me around 10 minutes. I don't what this person is smoking.
Most of these features are better served by things like polars/parquet/arrow. Maybe not the feature packed side yet