SQLite or PostgreSQL? It's Complicated
twilio.com
twilio.com
In PostgreSQL you're leaving performance on the table if you store UUIDs as strings instead of as the built in UUID type (128-bit value under the hood)
And on sqlite you might be leaving memory / storage on the table if you use a UUID as PK and don't remove the implicit ROWID.
Also this may be not a very popular opinion, but I prefer to design the database without using database-specific features. Things such as using SQLAlchemy and sticking to basic types. Sure it may affect performance some, but thanks to having done that I was able to port the app from SQLite to PostgreSQL in like an hour.
I prefer to leverage the DB schema to do as much as possible for me. Generally with PostgreSQL that means using a lot of domain types etc.
The problem with that is that SQL as a standard is so patchy and inconsistently implemented that effectively everything has database-specific features. You discovered that yourself when you inadvertently relied on SQLite's "feature" of letting you store UUID strings in an integer column. No matter how portable you imagine your schema is, it will work very, very differently between any two SQL implementations.
I can't see any value whatsoever in voluntarily tying one hand behind your back performance-wise, honestly. It's not like changing "text" to "uuid" would have noticeably increased the time this port-job took you.
As a wise man once said, if you're doing a job manually while the tool designed for that job is sitting idle beside you, the tool is smarter than you are.
Noting that when it comes to such patchiness, sqlite's developers specifically prefer to follow PG's example. When they write new SQL features, their basis for comparison is literally always "do we get the same answer as PG?" Yes, there are differences between the two, but porting between sqlite and PG, provided no fancy PG features are involved, is very likely simpler than porting between any two other arbitrary SQL engines. (That's ignoring any performance factors - i'm only talking about semantic compatibility.)
In what language? Unit testing uses whatever DB client comes with your stack, and those clients have generic interfaces.
For example, they will transparently decode UUIDs into strings when you read them, and then they will write them back as the database's UUID type when writing. As far as your code is concerned, they were always just strings.
The same is true of dates for mainstream, mature stacks.
> Stick to the commonly supported set of SQL data-types.
Postgres is among the most vanilla SQL implementations. UUID/GUID/uniqueidentifier is available in every modern SQL-based RDBMS.
Refusing to use the idioms of your database is a guaranteed way to lose performance for no reason.
Using basic SQL types that have common support across all database vendors simplified development, testing and support. I have seen folks waste weeks (to months!) in data migration/porting and still lose time troubleshooting issues across databases when they used db specific features. Too much tech debt incurred.
Loss of minimal performance weighed against simplicity and maintainability and no vendor lock-in. When you have the same tests and same schema and same DB layer code working against multiple databases, life becomes wonderful and hassle free.
I've seen this type of thing before, but it's not a common use case (and I'm not sure why people still do it).
It makes more sense to just say that the product uses a FOSS RDBMS (like Postgres) and leave it at that. There's no vendor lock-in.
If you to sell a packaged, deployable product to the widest market, yet want to minimise support, you leverage the expertise that the customer already possesses.
This is probably more of a consideration in the business enterprise space than some simple normal end-consumer SaaS, so I am not saying this approach makes sense everywhere.
This is unpopular because it's a bad tradeoff. In the real world, performance matters all day, every day, while switching from Postgres to SQLite is an incredibly unusual scenario (especially because their hosting models are completely different).
Tuning a database for performance (or just taking the hit and scaling the database) is far more expensive than the likely-not-going-to-happen migration from one RDBMS to another. The vast majority of companies will never do it. It makes no sense to optimize for it at great cost elsewhere.
I'm as anti-ORM as the next jaded dev but this is a picture-perfect use case for it: use an ORM to rapidly prototype and get the job done and leave a path for an industrial solution. The adage "you never switch your RDBMS" does not apply here.
I used to be in this camp, having started my dev life in Rails when it ran Sqlite in dev. This turned out to be an anti-pattern for Rails because dev/prod parity was never quite there. Over time I also realized it just wasn't worth the effort to keep things agnostic. Sure a hypothetical migration will be easier later on, but that migration very, very, very (very) rarely occurs in my experience.
SQLite3 has very few types, you cannot create new types, and is duck-typed.
PG has many built-in types, you can create new types, and is (mostly) statically typed.
You really have to know this going in. If you have to support both, you'll probably have to write two versions of your SQL -- or you'll end up using an ORM that supports the two RDBMSes, and then you may get stuck with the ORM.
SQLite3 is amazing, but it's mainly amazing because of how easy it is to embed in apps. If that's what you need, use SQLite3. If you need more power, if you're building a server, then think long and hard about why not just go straight to PG.
EDIT: SQLite3 is amazing for other reasons too.
> though new types might be added in future releases of SQLite
Intriguing! Can't wait for SQLite3 to have `CREATE TYPE`!
EDIT: Without the new strict mode of course, where it is not duck typed but typed by name.
EDIT 2: Actually maybe duck typing is fine to use for dynamic languages too. The affinity of INT keys and stuff is sort of duck typing I guess.
I'm sure there are others but the main language anyone knows static duck-typing for is go. In go at least, the interfaces get checked against implementations based what instances are bound to what interface variables or parameters at compile time.
Blog articles are helpful but most often are a single person's perspective and no one person is perfect. I love HN because I can get some great anecdotes (or sometimes response articles) from many perspectives.
That is, the problem isn't just the ORM but the very different characteristics of the databases behind the ORM.
Any serious production app and I bet no one is touching that config. Especially, if it has been running for months with one db or the other.
And now, you're stuck with an extra database and a ORM.
Nowadays, of course, SQLite3 has JSON support, and PG has JSON and JSONB, and so it should be possible to fix that, but it would be quite the migration, so no one bothers.
This is real life with a multi-RDBMS ORM: the promise of RDBMS neutrality always fails in some way as the differences between the RDBMSes bleed through the ORM's abstractions, and that bleeding occurs the moment you need more expressive power than the ORM provides out of the box for dead-simple apps.
It's the same old story: dev meets ORM, ORM makes things easy (at first), dev needs more power, dev bumps up against ORM's limitations, dev works around those limitations, dev now has a frankenapp, dev wants to switch RDBMSes but can't, or dev wants to ditch the ORM but it's a lot of work.
ORMs are a nice crutch to get started, but that's it.
No, the solution is to always use `text` as your column type, and use a check constraint if you need to enforce a limit. It's much easier and safer to alter a check constraint than it is to change a column type live in production. The `text` type and `varchar(x)` are identical under the hood, `text` takes up no more space on disk.
I know in SQL Server there are two issues with doing varchar(max) for everything and increasing a columns size is metadata only. First indexes have a limit of 900 byte values and will fail at runtime if you index a column with no max length and insert a value larger than 900 bytes. PG seems to have this issue as well but the limit is 2712 bytes.
Second the query planner makes use of the size to determine how much memory to pre-allocate for the query, with unlimited length field it assume something like 4096 bytes and wastes working memory if your values are not actually that size. Not sure if PG has the second issue, having a max value defined is valuable information to a database engine along with other type information such as UUID's only taking 16 bytes instead of 36 bytes.
Note that this is only for bytes indexes. Hash is not so limited.
Tho at this sort of sizes it seems unlikely such simple indexes are of much use. I guess the range matching abilities of btrees could be but that seems unlikely, a 2700 bytes path-like datum is a lot.
ALTER TABLE a ALTER COLUMN b TYPE VARCHAR(n)
This only requires ACCESS EXCLUSIVE for a fraction of a second while the metadata is updated.Whereas updating a CHECK constraint will require a full scan of the table. If you do it naively, this locks the table for the entire scan. If you want to avoid that, it's three steps:
ALTER TABLE a
DROP CONSTRAINT b_c,
ADD CONSTRAINT b_c CHECK (length(b) < n) NOT VALID;
COMMIT;
ALTER TABLE a
VALIDATE CONSTRAINT b_c;
So as long as you're only increasing the length (which in my own experience is a safe assumption), VARCHAR is much easier to work with.I trust myself to do this, I don't trust my coworkers and future devs writing applications for the same database to follow the same constraints in their code though. Constraints at the DB level keep your data clean(er) even if some other devs tries to shit it up.
In practice, declaratively creating data constraints at schema-creation time has a much much higher success rate than trying to imperatively enforce data constraints at write time, in my experience. The missed cases and bugs always come back to haunt you.
Constraints generally should be enforced both application- and database-side wherever possible. Application constraints can be more robust and have better UX, but only database constraints can offer you guarantees about what is actually in your database.
The workaround we came up with was to add check constraints for instant partition attachment, lock the table, detach all the partions, update the type on each partition, update the parent table type, then reattach all the partitions.
In our case, we didn't already have those instant-attach check conditions, so we incurred table scans anyway to validate those, but if you were to make multiple updates, you only have to do that once.
The reduction in latency brought on by in-process databases, combined with modern NVMe storage, means that SQLite is a substantially faster approach than any other solution which requires a trip through the network stack.
I've got services in production for several financial institutions right now that are resolving most SQL queries within 100-1000 micro seconds by simply using SQLite on reasonable hardware.
How many more users could you support if the amount of time you had to await IO for each was reduced by ~2 orders of magnitude?
Obvious caveats here being the resilience angle. We addressed this with application-level protocols and additional instances.
None - or practically not many, when using async (or sort of green threading) backend stacks. The waiting connections / users are just waiting and don't block any new connections.
If the network round trip to Postgres (in my experience around 3-10 ms, but of course highly depending on your server infra) was a concern Postgres could be placed on the same server as the backend, though I would not recommend it. But this relatively small IO overhead usually is not a concern for many apps.
Unix sockets are faster, but they still require some system calls that an in-process database can do without.
Can you share more about the context? How big/gnarly are the tables? How frequently/concurrently are they written to? Based on your experience here, when wouldn't you want to use this approach?
Could be able to squeeze more perf out of both SQLite and PostgreSQL.
Also, another important point I tried to make is that this benchmark is useful for this particular deployment. I don't really want to convince you the numbers that I've got will also apply to you, my proposal is that you should test your application and make decisions based on your own results, which can certainly be different than mine.
Indexes don’t work the same between SQLite and PostgreSQL so a 1:1 transition between the two databases on indexes probably isn’t going to yield the best results.
For example UUIDs were stored incorrectly in PostgreSQL.
SQLite does not enforce varchar lengths, and typically in PostgreSQL you just use text type and not impose any length restrictions unless absolutely necessary.
PostgreSQL can use multiple indexes in a single query which may help with the 2gb ram limits.
PostgreSQL supports partitioning which could help with the range queries.
All I’m saying is that I believe the performance you get from SQLite imo could be better, and the performance from PostgreSQL a lot better.
There was a horror story posted on here a while back about some old API causing an outage because there happened to be a random text field that a user was effectively using as an S3 bucket. Their conclusion was to always set a sane length constraint (even if very large, say 1MiB for the smallest fields) on field sizes.
Hyrum's Law. Yes user input validation, blah blah, but that is infinitely more surface area than having a sane (or absurdly large) limit by default.
That sounds more like a code review issue -- `blob` would've been an appropriate storage type in that case.
No, it was supposed to be a short text field. Not an infinite storage endpoint!
So, this is what Data Warehousing is all about: reporting related optimizations (typically what monthly totals and such are aimed at). The idea being that you trade disk for speed., so that kind of optimization is actually pretty normal if you've identified it as a bottleneck. There's no need necessarily to separate data warehousing constructs into another DB, though it's often good practice because sooner or later you're going to start running into problems on your important system related to storage inflation and non-indexed sorts/filtered.
If you might want to use multiple app instances connected to a shared DB, I would say it's probably easier to just use a local postgres container for dev and a managed cloud DB. Really not that much effort and you get automatic backups.
If you plan to never use a shared DB, SQLite is great though.
I think this is what it comes down to for me too. Yes there might be some use cases that really benefit from Postgres features like GIS and the native JSON type, but ultimately, for most use-cases, it's going to hinge on whether or not you ever expect to need to scale any part of the system horizontally, whether that's the frontends or the DB itself.
Or a proper UUID type, materialized views, enforced data-constraints, lateral joins, queries that consider multiple indexes, etc, etc.
It's not like there's just a couple of niches where PostgreSQL's features distinguish it from SQLite. SQLite is appropriate for very simple DB use-cases where you aren't too bothered by silent data inconsistency/corruption, but not a whole lot beyond that. It's a good little piece of software, but it hardly relegates PostgreSQL to "Just GIS things" or "Just JSON use-cases".
SQLite is also appropriate for use cases where you are bothered by silent data inconsistency/corruption, it's just that often the hardware running PostgreSQL (or any "serious" DBMS, relational or not) is usually less prone to random inconsistency/corruption (non-interruptible redundant power sources, well cooled environment, running lower but more stable clock speeds on everything, ECC RAM, raid disk array, corruption-resistant FS).
If you run PostgreSQL on the cheapest consumer hardware, expect random corruption of the database when power runs out several times in a row.
Recent SQLite versions have STRICT mode where they forbid that.
https://www.sqlite.org/stricttables.html
> or too-long data in a too-short column
You can use a constraint for that:
CREATE TABLE test (
name TEXT NOT NULL CHECK(length(name) <= 20)
) STRICT;For most business applications used by more than one user there are usually some expectations that data will be retained and the application will be available.
Still, PGSQL requires some level of setup/maintenance: create user, permissions, database, service with config/security effort. If there is a way to run PGSQL as lib and say to it: just store your data in this dir without other steps, I am very interested to learn.
- [1] https://aws.amazon.com/rds/
- [2] https://azure.microsoft.com/en-us/services/postgresql/#overv...
yeah, premium is very large if I want to do a lot of data crunching (lots of iops, cores and ram is needed), plus latency/throughput to those services will be much higher than to my local disk through PCIE bus.
Steps will be:
- sudo apt install postgres (more steps are needed if you are not satisfied with OS default version)
- sudo su postgres
- psql -> create db, user, grant permissions
- modify hba.conf file to change connection permissions
- modify postgresql.conf to change settings to your liking because defaults are very out of touch with modern hardware
Another options would be to build your docker image, but I am not good with this.
- sudo apt install sqlite3 # you're going to want the client
- sudo chmod myapp db.sqlite3
- sudo chmod myapp . # make sure you have directory write permissions for WAL/journal
- sudo -u myapp sqlite3 db.sqlite3 'PRAGMA journal_mode=WAL;'
Nothing is ever single-command easy in all cases.
I am familiar with Java and H2, you just say in config somewhere: store db in this directory, and you are all set.
I think there is no such route currently for PGSQL.
Which isn't bad. This is a pretty low bar to clear.
(All that said, I am of the opinion that separating data storage from application logic is better for operations, for security, and for disaster recovery unless you desperately need to prioritize avoiding network latency above everything else, so this is all pretty moot to me.)
I think usual way to do it is say having shared library file like sqlite.so in some directory or installed through OS package manager, and thin API binding for specific runtime in form of runtime package (e.g. npm).
> you desperately need to prioritize avoiding network latency above everything else, so this is all pretty moot to me
Goal here is easy distribution of embedded DB. Many apps are doing this with SQLite, for example Dropbox client. Many other examples: https://www.sqlite.org/famous.html
> you could conceivably run postgres on your app server
So what you've been saying has been taken in the context of packing sqlite or Postgres next to your web application, which has pretty limited use cases, and reads really weirdly in that light.
But there is another approach: one self contained binary/artifact which can include embedded DB too, and be launched by one command.
That seems pretty... odd on their part. I know that Ruby gems in particular can and do include C sources, so there's no reason it couldn't ship sqlite3.c (or even the various .c files that get combined into sqlite3.c); if you're able to install Nokogiri (i.e. you have a usable C toolchain), then you should be able to compile sqlite3.c no problem.
Meanwhile, Python ships with an "sqlite3" module in its standard library (last I checked), so if you have Python installed at all then you almost certainly already have SQLite's library installed.
I think the thinking is that packing The Entire Library rather than your necessary bindings is overkill/duplication/etc etc. - it's the static versus dynamic argument in a sense, even though you may be dynamically linking while you pack your own library. Duplication versus OS-controlled security patching? Admittedly when most deployables end up being Docker containers (for good or for ill) that's less important, but most of these tools are from a distinctly earlier era of deployment tools.
It does, but it still compiles bindings for it. My point is that if you're able to compile C code at all (including Nokogiri's bindings to libxml2), then strictly requiring some external libsqlite3.so is unnecessary - because it's pretty dang simple to just compile sqlite3.c alongside whatever other C code you're compiling.
> And I think--don't quote me, Python isn't my thing and I only looked briefly--that Python on Ubuntu requires the `python-sqlite` APT package, which depends on both `python` and `libsqlite0`
Right, and my point there is that if you've installed Python, then in doing so you've already installed SQLite (because your package manager did so automatically, because it's a dependency), so you don't need to run an additional command to install SQLite.
I don't want stand-alone client for app which customer downloaded from my site.
Also, docker adds complexity itself.
docker run -it -e POSTGRES_PASSWORD -p 5432:5432 postgres:12
And bam, you have a server
Like:
FROM postgres:14.3
COPY pg/pg-config.sql /docker-entrypoint-initdb.d/000-pg-config.sql
COPY pg/schemas.sql /docker-entrypoint-initdb.d/010-schemas.sql
COPY pg/meddra-tables.sql /docker-entrypoint-initdb.d/020-meddra-tables.sql
# ...
RUN chmod a+r /docker-entrypoint-initdb.d/*
ENV POSTGRES_USER=myapp
ENV POSTGRES_PASSWORD=xxx
ENV POSTGRES_DB=mydb
docker exec postgres psql -h localhost -p 5432 -U ${DBADMINUSER} -c "some SQL statement"
Also, I usually change hba.conf file too, to disallow outside connections.
If you need all this customization then did SQLite really fit the bill in the first place?
I need to change config for prod pgsql instance, because default config performance wise is not necessary suitable for serious prod machines.
> If you need all this customization then did SQLite really fit the bill in the first place?
I am not familiar with SQLite, but somehow familiar with Java H2 DB, which has similar idea. You can embed it and all configs into self contained java jar file together with your app and no moves in host OS is needed.
It seems to me (or at least, I'm hoping this is happening) that the pendulum is swinging back to simple deployments and away from AWS/Cloud/K8s for every tiny little app.
One thing I'd love to see is that the 'default' MVP deployment is your code + SQLite running on a single VM. That will scale well into "traction" for the vast majority of applications people are building.
It runs faster than 90% of webapps on the internet.
Good resume fodder though I guess.
I have zero experience with this but I am very curious how people do it in sqlite.
What does matter however, is enforcing parametrized queries everywhere. Unless all the db handles you pass to the client handling code are read-only, chaos will ensure from the DDL permissions.
Why is it superior to put all of the (bespoke) access control logic in the server side bridge rather than use what's available in the database (accessed by the bridge, not the client)?
I have been watching like a hawk for 6 months but I haven't stumbled upon a clear reason why this is done, except for "it helps source code db portability".
For a multiorg/multiuser application this seems like the crucial distinction between sqlite and postgresql.
Again I have no experience here, talk to me like I'm stupid (I really am!).
Within a single org, multiuser approach, there are 2 big problems that I remember with attempting to shoehorn DB auth into application auth:
* assuming you use a connection pool, you might run out of TCP connections/ports if you need to handle too much stuff;
say for example that your load balancer need 3 application nodes behind it - you will need 2 (connections per user) x 3 (application nodes) connections just to handle a user - 6 connections/user. That will eat your database connection limit very fast, for no good reason.
* assuming you don't use a connection pool, you now have horrible latency on every connection (bad) and need to handle plain text passwords (assuming you use scram-sha-256), or md5 non-replay-resistant hashes of user passwords in, either sent in every client request, or in a shared session system. No matter what you pick, you have a security disaster in the making (very bad).
sqlite looks like great technology to me (as is postgresql) but I am a bit of a fanatic for keeping the overall system as understandable as possible, so these questions are important (for me, I'm stupid).
Preemptive scaling never really works and most projects never scale enough to warrant more than one server (unless you write very inefficient code).
We once did an app ages ago where the database was used to create a materialized view of code (PHP) + web pages for everyone and everything. We then rsynced that to 6 machines. This is ancient times, but this thing FLEW -> click click click click you could fly through the pages. It was a read heavy workload, but still, even the front-end (just hand coded) was somehow faster than all the latest and greatest going on these days.
Plus it seems to be a readonly workload, something for which posgres is not ultra relevant (not that it doesn't work, but...).
> RDBMSs are there to enforce constraints on data.
If you did not ask SQLite to enforce type constraints on relations (with STRICT), it will work in the relaxed, backwards-compatible expected behavior of previous versions of SQLite.
That being said, if you want actual validation, you probably need more complex CHECK expressions anyways with your business rules, and those work by default on any database.
https://www.sqlite.org/stricttables.html
* unless you're a madman that runs things with "PRAGMA ignore_check_constraints = false;" enabled or equivalent; in that case, no DB can help you.That does not mean it is applicable to all databases use-cases, but production-grade it undoubtedly is.
By your example, a bank should use SQLite because it is deployed widely.
You have to look where they are deployed and the use cases. They also come with dramatically different tooling which make sa huge difference. More tooling does not mean that a full RDMS is a better choice:
It is just about what you need, what use case you need to cover and what industry you are in.
Widely-used constraints like UNIQUE are production-quality in SQLite.
If you’re writing something to run in an embedded/client app environment, then yeah why would you use Postgres for your one machine? You could, but it’ll add a lot of moving parts you’ll never need and probably don’t want (like remotely accessible network ports you’ll need to secure)
A migration is all but expected. Might as well make things easy on yourself for the first year and start with SQLite.
Postgres and SQLite are very different in several aspects, e.g.
- types and enforcing type safety
- handling concurrency and transactions (available isolation levels)
- functions / procedures
- array support
Postgres can be much more valuable than just using it for "relational data storage". Its features can be a good reason for choosing it from the beginning.
Otherwise, the post is a nice reminder for SQLite users that foresee a database switch to use the newly-introduced strict mode.
Also this is apparently benchmarking Postgres and SQLite _with Python on top_. Some of the overhead here might not have much to do with either SQLite or Postgres at all. Just because one uses an ORM doesn't mean the same code is running and especially considering how sluggish Python is, I'm not convinced by any of this either way outside of this being an improvement for the very specific use-case presented. But we don't know _why_ that is.
Also note that I'm not saying Postgres is faster the SQLite in absolute terms. That happened to be the case for this particular application and how it was deployed. If you perform a similar test with a different app or stack you will likely get different results. A main point I'm trying to make in the article is that people should do their own benchmarking instead of making decisions on stuff you read online.
I thought I'd have another go at it today (sqlite 3.34) and I'm mildly surprised that the things I didn't like are still there:
sqlite> select '1 apple' + '2 oranges';
3
sqlite> select count(*) where 0.9;
1
sqlite> select count(*) where 1.1;
1
sqlite> select count(*) where not 0.9;
1
sqlite> select count(*) where not 1.1;
0The fact that this is debatable at all means that there are arguments and counter-arguments that serve both sides well, which further implies that there are use cases where some pros outweigh the cons on each side.
(Of course, this entirely neglects the point that it's usually fun and even educational to have a technical argument.)
And if they want to say '3' + 5 = 8, I could imagine why that might be fine.
But I don't think that's a good reason to have behaviour like the above.
I think it's more likely that they wouldn't choose to do those things again today: I think this is what fell out of the initial TCL-based implementation and they feel they're stuck with keeping it backwards-compatible.
That's because SQLite doesn't ship changes that break existing applications.
> Some developers appreciate the freedom that SQLite's flexible typing rules provide and use that freedom to advantage. But other developers are aghast at SQLite's flagrant rule-breaking and prefer the traditional rigid type system found in all other SQL database engines and in the SQL standard. For this latter group, SQLite supports a strict typing mode, as of version 3.37.0 (2021-11-27), that is enabled separately for each table.
[1]: https://www.sqlite.org/stricttables.html
Related past discussions:
Strict Tables – Column type constraints in SQLite - Draft | 2021-08-21 | 331 points, 108 comments | https://news.ycombinator.com/item?id=28259104
SQLite Release 3.37.0 | 2021-11-27 | 333 points, 112 comments | https://news.ycombinator.com/item?id=29363054
"I want an extensible versitile database platform for use as a component in an infrastructure that is part of a larger system" - postgres.
"I want an embeddable lightweight SQL storage backend for a narrow scope, single-purpose, probably single-user, probably single-system application" - sqlite
Postgres should perform well in any role in the hands of an expert - it's a question of whether or not that is overkill. Sqlite can also perform well in a subset of those roles.
Best takeaways here:
- SQLite kills it. It appears untuned PostgreSQL is only 2 seconds (???) faster than untuned SQLite; always try to kickoff a project with SQLite
- PostgreSQL has a performance cliff when query responses leave memory. This is extra true when an index no longer fits in memory. worker_mem is critical for pushing back this performance cliff
Side note: Tuning postgres can be really painful, but it's also a really dope tried-and-true db.
Assuming 6.5M rows/day*1.5yr=~3.5B rows, how large is the dataset in both SQLite and pg? With indexes, is it fitting in RAM?
Linode 2GB plan is shared vcpu, so results will vary depending on who is sharing it with your service.
This article is about one of them only, how I designed a test system to evaluate the two databases head to head.
Query planning seems like it should be the topic of the article. SQLite and PostgreSQL do their query planning very differently - not to mention that SQLAlchemy is probably generating different queries. I can't really fathom how one can meaningfully "evaluate the two databases head to head" while seeming to actively avoid how they plan/optimize/execute queries.
Meanwhile, if all you have to offer in response to such a suggestion is "hurr durr do it yourself", then why bother commenting at all?
Good advice, contextually.
Wouldn't moving to a static site generator been a better direction in that case?
If you're maxing out at four people reading your blog, I can't imagine more infrastructure is better.
Of course, SQLite might be the best choice for embedded or local usage, they even have this lovely page about using it as an application file format: https://sqlite.org/appfileformat.html
And PostgreSQL might scale better for multiple writers and situations where you need it on a separate VM/server/container that might be shared between different apps through a network connection of some sort.
Though it's also curious, because you never hear a lot about MySQL or MariaDB (comparatively, in my subjective opinion) on HN or many other places for that matter. And yet, it's still a decent option, has good tooling (MySQL Workbench is great), is widely supported and backs a significant amount of production systems out there.
Do people just view it as old/legacy, or is there the belief that for many use cases PostgreSQL might simply be the better option, e.g. also because of projects like PostGIS and support for more complex setups, like custom data types, more advanced scripting language for in-database processing and whatnot?
It’s great when you just want simple relational storage without a lot of features or ceremony, but need a bit more than SQLite. It’s fast, predictable, and gets the job done.
Postgres is great too and offers more, but sometimes that can tempt you to try to do too much in the DB layer.
https://www.sqlite.org/lang_vacuum.html#vacuum_with_an_into_...
SQLite would be best for development, testing, and small sites. MySQL would be better for production . And SQLite would be be great for archiving old sites.
Internally, Movable Type had a really nice object persistence / ORM abstraction, with support for MySQL, Postgres, SQLite, and Oracle. Meanwhile, iirc around this time WordPress internally had raw queries strewn about its codebase, which certainly would have been difficult to support multiple DBs. (I haven't looked at WP since then so I don't know if this is still the case.)
Faced with shrinking market share and being out-executed on features by WordPress/Automattic, Movable Type actually removed Postgres and SQLite support in response! Then again, these DBs weren't nearly as popular back then as they are today.
The implementation is (to my mind) simply astonishing: they run regular expressions against the SQL to convert it from MySQL dialect to SQLite! https://github.com/aaemnnosttv/wp-sqlite-db/blob/9a5604cce13...
I'm amazed that this works, but apparently it works really well. I would expect it to break the moment it ran into a WordPress plugin that executed custom SQL in a way they hadn't anticipated though.
SQLite really shines when compared to running a database in a separate server from the app. This is when the network latency starts being noticeable. Latency between you and the app is usually constant. The latency between the app and the database is what matters. If in this case, Postgres can run in the same host as the main app, I think it's a good thing to do. But very often that's not the case.
Even though not surprising, it's great to see how SQLite handles concurrency. From my experience, SQLite would be able to handle much more read concurrency. It would have been great to see more benchmarks (like 1000 concurrent users, even if the app would never see such traffic).
I was surprised that SQLite didn't beat Postgres on simpler queries. Without seeing the queries, if they contain multiple joins, that could be another reason why SQLite is slower since it can only do "nested loop" JOINs. The workloads also seem mostly analytical, we'll see where DuckDB goes in this space (looking promising for now).
One thing not mentioned in the blog post is "easy to maintain and update". If the app is deployed with Docker, adding Postgres means you go from worrying about:
1 docker container + 1 docker volume
to: 2 docker containers (docker-compose?) + 1 docker volume + internal docker networking
The fact that SQLite is so simple to operate also has some advantages. You can usually go back to an SQLite-based app 1 year later and understand everything. With other databases, you may need to review users, roles, permissions, connection string, etc.Another cool thing about SQLite is that it scales mostly linearly as you add more CPU cores. It can scale as long as you can also scale the number of processes in your app. In other databases, scaling means adding connection pools and other tools in the middle.
Lastly, and related to the latency topic. The low latency in SQLite means that you can sometimes convert a big query into 5 separate, small and reusable queries without any performance hit. It's really remarkable home much cleaner your code can become with this.
In any case, I love working both with Postgres and SQLite, and we need more blog posts like this one showing real-world cases.
- porting the application, making all these benchmarks and writing a blog post
vs
- simply getting a VM with more RAM and more CPUs.In the conclusion the author goes on to talk about the options "if I get the budget" vs "without throwing more money at the problem". I'd guess that the cost of the day of a Principal Engineer would far surpass the cost of years running a 4CPU server.
My understanding of the situation is sqlite's solution is a bit more low level than postgresql but should be workable.
There are lower-level approaches built-in to SQLite, such as the Backup API, but there is at least one 3rd party project, Litestream, which does streaming WAL backups too.
Of course you still need backup for other incidents.
Depends on the failure. What do you have in mind?
> when more than one process need to acces the DB
Easy, you use `journal_mode` set to `WAL` when you connect to the sqlite database.
> when you need RBAC
If you need RBAC, you aren't going to be using sqlite. In theory you could have your application handle the RBAC part since sqlite is embedded in your application.
Another solution is to leverage unix users and groups and set permissions appropriately for the database file and the application running.
> Easy, you use `journal_mode` set to `WAL` when you connect to the sqlite database.
and set isolation level and check_same_thread and PRAGMA synchronous and then put all queries in retry loops because the DB is maybe locked https://sqlite.org/wal.html#sometimes_queries_return_sqlite_...
I wasted so much time trying to get multiple sqlite queries running at the same time
You were trying to write at the same time right?
If the discussion is around failures for the use case of the article (dashboard for blogs which is read-only), my best guess at 'high availability' would be just to have a separate SQLite DB file on multiple app server nodes and call it done. The author mentions that the DB is updated once per day (probably during the night) by a cron job. Just as easy to then scp a few copies to other app server nodes.
If the discussion is around a 24x7 read-write workload with high business costs for performance, availability, and scalability then it's an entirely different problem.
Same thing you have for failure when your pg instance is running on a single node?
I've implemented the pattern where writes sit in an in-memory queue and are applied to a dedicated write connection in series. I've not run into any problems with this so far, but I've also not tried to handle thousands of writes per second with it yet.
I have a lot of worker processes writing new data.
With SQLite, I was getting a lot of "Database locked" errors and ended up having to use pg.
DuckDB performance is astounding for doing group by, sum/count/avg with filters and sorts.
I think too many people use ORMs and rely on them to do everything, then blame the database when things are slow. :(
Then their apps grow complex enough that the ORMs get in the way.
Then the devs get sad.
Well, you're using query parameters, but I think I understand what you mean.
What I would do is write a stored procedure that takes whatever parameters, then I'd write PG SQL to generate bindings to that procedure for whatever language you're using (Golang here). I might write the bindings by hand if it's not a lot of work, but definitely would write a little compiler if it's a tone of work. That way you can get type-safety.
So, you'd do something like:
CREATE VIEW IF NOT EXISTS things AS ...;
CREATE TRIGGER IF NOT EXISTS create_thing
INSTEAD OF INSERT ON things
FOR EACH ROW
BEGIN
...
END;
and to invoke such a "stored procedure" you'd INSERT INTO things (...)
VALUES (...), ...;
or `INSERT .. SELECT ..;`.Now, with SQLite3 you don't get reflection via SQL, like you do in PG via the `pg_catalog` schema, but you do get reflection via the API. Also, because it's SQLite3, you don't get strong typing, and you get very few types, so even with reflection via the API, generating type-safe stub functions to invoke the "stored procedure" is not exactly easy or possible.
FYI, SQLite3 is Turing-complete. You can loop via recursive triggers. And you can do conditionals via `WHERE` clauses on the statements in your trigger bodies (and also via `OR IGNORE` and such).
EDIT: I made a post of this: https://news.ycombinator.com/item?id=31913062
I don't think you are doing anything wrong from a noobies opinion.
I don't know if Go's SQLite support in turn supports named parameters, but if it does, then that would likely help considerably with readability.
If it doesn't work with an real life implementation e.g. docker test container, then I don't consider it a robust test.
- Type-safe DSL generated from your schema that looks like SQL
- (If you can write the SQL, you can write it in jOOQ)
- SQL-injection protection
- Strong multi-dialect and vendor-specific support
- Custom-type mappers
- Composable queries
- Compile-time, schema-aware query safety
- Fine-grained transaction control
- And so much more...
https://jawj.github.io/zapatos/
Using an ORM doesn't mean you have to rely on behaviour that generates slow (e.g. n+1) query issues.