Your database skills are not 'good to have' (2023)
renegadeotter.com
renegadeotter.com
Check in on your databases periodically, and refresh your memory. There's a very real chance they've picked up features that'll make your life a lot easier.
Joining the tables in the database should perform better than joining them on the application layer. If it doesn't, there's something very odd.
This holds for any production ready DBMS. Even the ones that don't care about performance or preserving your data.
But wrong is almost always odd, so the normal expectation is that you understand the oddity on your system. Otherwise you are blind to problems.
Anyway, it's also not common for an application to have "reduce total CPU usage" as a goal. Available CPU on the database is way more valuable than on the application server, and so it makes sense to trade them up.
Admittedly I'm mostly using MSSQL, but joining multiple tables seems a pretty natural thing to do (assuming you are joining on well indexed tables).
This is specific to the default MyISAM engine, where it locks all tables used in a query for the duration of the query. So two queries that touch the same table can't run concurrently.
You may never hit this as a problem if your queries are fast enough, but it is something to be aware of.
InnoDB uses row-level locks, so it's extremely unlikely to cause such issues.
It seems excessive at first but the performance is so dramatically better that it makes a lot of sense. Especially if the queries are part of your hot path. I always thought of it like having core business logic tables, then utility tables which are essentially derived from those core tables.
The only issue is joining with aggregations and sorting, that's where things get ugly with MySQL.
The explains can be hairy at first but once you've started systematically analysing a query and know some of the quirks of the MySQL query planner you'll manage to identify missing indices and squeeze out very, very good performance.
That was a database where we had warm tables bigger than fifty million rows. I personally migrated it from single instance MySQL 5 to a MySQL 8 clustered rig and optimised hundreds of queries written by people learning basics on the job, going from horrifying to very reasonable latencies as seen from the web clients.
You could break the joins into three parts:
1. searching/filtering -- which tables you need to join on to match the selected queries;
2. sorting -- determining which column(s) of data to sort the results by; and
3. displaying -- which tables you need to join on to retrieve the information needed to display the information to the user.
These may be different or have overlaps.
I feel like a lot of code you see in articles or modern companies if often just following the popular architecture/paradigm du jour. As opposed to what this article highlights in terms of making backends return all the data you need in the minimal number of queries. I feel like I understand what's being described here, but I've never actually had the chance to work on anything non-trivial that does it like this, most places I've worked have gone no-SQL or used an orm or were not more than very thin wrappers around some CRUD queries.
The solution is to know how a database works, instead of thinking that Product.objects.filter(color="red") and [product for product in Product.objects.all() if product.color = "red"] are exactly the same thing.
For anyone else, I also remember PostgresSQL manual/docs being very high quality, so that might be worth reading just to better grok SQL/relational DB topics.
https://github.com/getsentry/sentry
Here's all the database models:
https://github.com/getsentry/sentry/tree/master/src/sentry/m...
Here's an example of hacks in the form of massive caching to improve performance:
https://github.com/getsentry/sentry/blob/master/src/sentry/d...
There's a ton of DB passion at Sentry but most importantly for anyone this deep in the thread, Sentry also detects and helps fix common DB mistakes whether it's you or the ORM: https://docs.sentry.io/product/issues/issue-details/performa...
If you're at the “why it’s slow” runbook, Sentry turns the first 2 steps into 1 click: https://docs.sentry.io/product/performance/queries/
Good on you for correctly determining the difference, though!
The one exception to the obvious "results are accessed" intuition is slices (such as [:3] to mean first 3 elements), which thanks to how python implemented them lets the ORM use them to further modify the query by adding LIMIT instead of executing it.
This is the efficient one, filtering results upstream, via SQL, before they are returned to the client.
The table naming convention from the model name is a bit different, but impossible to know from just that code snippet. Also Django likes to explicitly list every field instead of *
Tbf this is a good pattern. It’s rare that you _need_ SELECT *, and if you do, you may not in six months after DDL has added more columns to the table.
https://til.hashrocket.com/posts/l4t5xok8hg-activerecord-str...
One of the biggest perks with the association loads is that it avoids joins by default, which can become a scaling barrier as you grow. Instead, it will capture the associated ids, find all of the related records in a single query and then transparently handle everything for you behind the scenes.
Without any optimizations, you'll get essentially 1 query per loaded association rather than the N+1 "one per association per row".
You can always tune things to utilize joins, subqueries and optimize specific cases later but this is a sensible setting that will avoid the vast majority of issues.
Databases, pretty much all of them, are relatively simple creatures. If you can understand how a Tree structure and pointers work, you basically have most of the fundamental knowledge underlying the "black art" concepts of databases.
The primary key/clustered index is how the data is physically stored and sorted, in a btree. Additional secondary indexes are generally quiet literally just secondary tables clustered on the secondary index key with primary key pointers. The job of the optimizer is to take when you have "Select foo from bar where baz=2" and ask questions like "Is there an index on baz. Does that index contain foo?" and then executing based on that. If there is an index on baz, it traverses the baz index looking for 2. If that index covers foo, then the query is done and it just pulls foo from the baz index. If it doesn't, then it queries the foo table with each of the primary keys in the baz index. If no index exists, then the db has to do a linear search through the table looking for "baz=2".
When databases are slow, it's because you are forcing them to do O(n). When they are fast, it's because you use an index to turn that into a O(log n) search. (or in some cases, O(1).)
The black black arts of databases is when you start getting into things like CTEs and windowed functions and their interactions with the indexes/optimizers. That's when it can be almost trivial to introduce O(n^2) performance if you aren't careful.
NoSQL databases generally speaking, are implemented similar to how SQL databases are (just trees) but with less features and less guarantees.
This is only true for RDBMS with a clustered index, like MySQL (assuming InnoDB) and MSSQL. Postgres stores tuples in a heap. The indices are generally B+trees, yes, but there is an extra level of indirection via the visibility map.
There are a million gotchas, is the problem. You declared a partial index on foo as WHERE foo = true, but you’re querying for WHERE foo IS NOT false? I have bad news about the secret third state, WHERE foo IS NULL (not to mention the subtle differences between equality and IS).
You made a UUIDv4 as the PK, thinking that since Postgres doesn’t cluster on it, it won’t suffer the same performance issues? Visibility map just wrecked your IOPS, enjoy the 7x hit to pages.
You have an index and it’s not being used? Could be incorrect equality check (see first example), or inadequate auto-analyze on a write-heavy table leading to incorrect statistics, or any other number of things.
RDBMS are much easier to understand if you know what a B+tree looks like, yes, and I highly recommend any backend dev take the time to do so. But there is so, so much more that can go wrong.
I agree, but that stuff that goes wrong isn't (in my experience) super common. At least, not until your tables reach fairly large (as in 10s of GBs of data) sizes. Which is why I'd suggest you still employ DBAs :).
You wouldn't, for example, think about pulling out a filter index if you weren't dealing with fairly large tables where a full index might have an overly large negative impact.
On the flip side, I've seen more than a few tables where a fundamental understanding of how the data is structured would have prevented a multitude of issues like bad or missing indexes. (My favorite that is depressingly common `Create Index blah ON foo(id, thingToIndex)`)
As a DBRE who oversees tables in the 100s of GB, some in TB range, these things happen entirely too often. But then, it’s my job. And yes, I whole-heartedly agree that past a certain scale, you need DB folks.
Or did you mean O(1)? Some RDBMs have Hash indexes which given you (effectively) O(1) searches.
For example: https://www.postgresql.org/docs/current/indexes-types.html#I...
The next project will be like this: I will name the endpoints PagenameFunctionality. E.g. /IndexGetMyProfile There will be global scope functions or endpoints but many scoped endpoints.
And there I will do the proper queries just for this use case, and so on for each new use case.
In the context of SPA/PWA/TWA I think this is a good approach.
Graphql is too complex on the client and RESTful too vague.
It's been a while since I've used it at scale, but at my previous job half of our incidents were caused by Postgres randomly deciding to change the query plan for a call that was working just fine. Then I'd go in there and re-write the SQL a few times until it figured out what to do, rinse and repeat every few months.
Prior to that, we tried various indexing strategies, de-normalizing some of the data, but ultimately i found that weird plan inconsistency to be solved by the vacuum job
Most of the time, query flips are due to inaccurate statistics over time. Try changing the default statistics target for the affected column.
Postgres doesnt just randomly do things and will always produce the same result given the same circumstances.
Either you werent running routine maintenance and your statistics are messed up, or the statistics arent properly configured or your data is just poorly stored.
> A user will land on the search page and have the ability to “drill down” and narrow the results based on those properties. To make things much harder, all of these properties would come with exact counts.
Click on "Types of Clothing" - unfortunately the link isn't archived but you can see the URL: http://nymag.com/search/fashion-search.cgi?nymbreadcrumb_pus...
Judging from the query params, it looks like you can only filter one item at a time - seems like static pages would work.
So, you could go "Dolce & Gabbana -> Bag -> Red". Then you could remove the middle one, for example, and end up with "Dolce & Gabbana -> Red"
This gives you unlimited permutations of results, so static pages were a no go.
> I don’t even want to know.
I feel this. I'm working on a team where I have significantly more tenure at the company than the other engineers on the team, and they'll often ask me questions starting with "why".
Normally, I love to encourage this -- curiosity is an amazing quality to have in coworkers. There are still some times when I just need to look at them like I'm dead inside and warn them that it's not worth their time on this earth to find out.
Understand and optimize relational databases (MySQL, PG) rather than introducing a new type of database (e.g., DynamoDB, Kafka, Redis) whenever issues arise. Common mistakes:
- Lack of indexes
- Using ORM (generating too many SQL queries or inefficient queries that return all columns)
Some suggestions:
- Use relational databases where possible, instead of NoSQL
- Use read-only replicas instead of Redis when applicable
- Keep relational databases clean and data size small
The author's message is: Hey, folks, if you don't have the ability to use relational databases properly, then your usage of Kafka and DynamoDB will certainly be a mess as well.
I’ve never seen it in the wild, so I get the blanket ban. In theory though you COULD contain ORM usage behind an intentional data access interface.
I can only really speak for Rails from my own experience, but it most certainly tries exactly 0% to get you to care about this problem. It encourages deeply nested, at all layers of abstraction, through class inheritance and all that garbage, unrestricted access to the database through a very powerful ORM.
You end up tuning performance of queries that end up changing in a month because of this. It’s wild.
There are exceptions, like jooq, which try not to really be an ORM and instead are just a language extension to help write sql (funnily, being an even more leaky abstraction is preferable here :D)
Sure ORM's are not going to write the most optimized code by default, and they're not going to build indexes for you, etc. But they are a huge productivity enhancer and work well enough for a lot of the queries you're going to need. Most ORM's these days have advanced options too which enable the more fine-tuned use cases without having to drop into raw SQL.
Am I missing something?
And it's not one or the other. There's always an SQL escape hatch when you need one for complex queries.
[1] https://michaelscodingspot.com/npgsql-dapper-efcore-performa...
What an ORM gives you is a tunable level of abstraction. I know Ruby on Rails best so I'll use it as an example. ActiveRecord has a lot of tuning you can do, like partial selects and snippets of sql where you need it. If that isn't enough, there is find_by_sql as an escape hatch. If that is still not fast enough, you can call ActiveRecord::Base.connection and skip the ORM entirely.
This will keep complexity limited to places where it needs to be complex.
In my experience, every level of optimization you do comes with decreasing maintainability.
Not really. Without ORM, your entire application is now tied to sets of tuples (i.e. relations) instead of rich graph structures (i.e. objects), but the relation need not be equal to the relation returned by the query. You could still have a transformation from one relation to another.
Not since the PHP days of embedding MySQL calls right in the middle of HTML have I seen anyone carry relations from end-to-end, though. The idea that anyone is not doing ORM these days seems like a straw man.
And, in some edge-cases, the relation is more useful than the object graph.
Also, they don't have to be slow. When benchmarking my hobby golang ORM, I found the performance cost vs hand writing optimal SQL to be in the microseconds per returned row. Small price to pay for everything it gives you: schema migrations, query builder, object mapping, validation, etc. And if you happen to have a particularly hot endpoint that needs to return tens of thousands of rows at once during a HTTP lifetime, you can always dip back down to the driver level.
It pains me that every time the topic of ORMs come up on HN that the comments are full of people warning not to use such a powerful category of tools. Maybe the tools they are using aren't very mature, maybe the underlying technologies are bit dated, maybe the documentation doesn't emphasize performance enough. I'm not really sure what's going on. Hopefully we can improve things so fewer people have issues and instead see performance improvements from using ORMs.
With all this: is there an ORM with documentation that’s good at explaining what would I gain over plain old SQL?
So write the initial system with an ORM and then optimize as needs be. That might be obvious from the start, or might become evident later.
I used to use them all the time. When I started doing pure FP I just realized I just didn’t really need my old ORM. When I came back to OOP languages (out of necessity, not pleasure), I realized I didn’t really need my old ORM there either.
It’s not a strong opinion and it’s not a religious thing, it’s just a different perspective whereby you see that something isn’t really necessary.
Benefits or not, you do pay quite a heavy price for the ORM, and I think most people don’t see this price because it has been normalized so much.
ORM benefits mostly from navigation + caching.
Usually, though, I get better performance and less fragility out of leaning on carefully-crafted SQL+indexes to do the heavy lifting.
Some ORM toolkits bundle query builders as a secondary feature, but they are still logically distinct features.
What ORMs have you seen that don't do that?
There are ORM toolkits that help produce the code to perform that mapping to save you from doing it by hand (although you can do it by hand, if you so wish!), but not even they could shove query building in the middle. Query execution has to happen before ORM can take place. You cannot perform ORM until you have the relation already in hand. (Mutation cases are in reverse, but I know you are capable of understanding that without it spelt out in detail)
What would it even mean for ORM to include query building?
If I'm reading you right, you seem to be saying that database helper libraries often include both query building and ORM toolkit features, usually along with other features like database migration management. Which is very much true. But why would you call one feature by the name of another? If you call query building ORM, is ORM best called database migration?
"(Mutation cases are in reverse, but I know you are capable of understanding that without it spelt out in detail)"
Guess you were't capable after all. How do tech people manage to be so out to lunch all the time?
> Guess you were't capable after all.
Stop and think. If you actually did somehow mistakenly believe it was a one-on-one with another person, why would you but your head into the conversation? That would be completely illogical. There is a curious contraction here.
The first half of your comment does not logically precede that, though. There is no such implication.
Do you have any examples of open source ORM libraries that stick to the terminology you are advocating here? I've not seen one myself.
Maybe this is partly an ecosystem thing. What ecosystems do you mainly work in? I'm primarily Python with a bit of JavaScript, so I don't have much exposure to ORM terminology in Java or C#.
1. A feature that prepares a query to send to the database engine.
2. A feature that transforms data structures from one representation to another.
And we agree that these are independent? You can prepare a query without data transformation, and you can perform data transformation without preparing a query? I think we can prove that, if you are still unconvinced.
Okay, so that just leaves naming. What should we call these distinct features?
Here are my suggestions:
1. Query building. Building a query is the operation being performed.
2. Object-relational mapping; ORM for short. Mapping objects to relations (and vice-versa) is the operation being performed.
These descriptive names seem well suited to the task in my opinion, but I am open to better ideas. What have you got?
Now, there is something else in the wild that we haven't really talked about yet, but may be that which that you are alluding to. Another abstraction, or pattern if you will, that rests above (to use my suggested terms, bear with me) both query building and objet-relational mapping to unify them into some kind of cohesive system that actively manages records. This is where it starts to become sensible to consider (again, using my suggested terms for lack of anything better) query building and ORM intertwined.
I would suggest we call that active record. In large part because that's what we already call it. In fact, what is probably the most popular and well known active record library is literally named ActiveRecord, so-named because it was designed after the active record pattern. But, again, open to better ideas.
> What ecosystems do you mainly work in?
Python, Javascript (well, Typescript, if you want to go there).
Take Django for example:
Entry.objects.filter(created__year__gte=2024).exclude(tags__tag="python")
That's an object-oriented API that builds a query that looks something like this: SELECT
"blog_entry"."id",
"blog_entry"."created",
"blog_entry"."slug",
"blog_entry"."metadata",
"blog_entry"."search_document",
"blog_entry"."import_ref",
"blog_entry"."card_image",
"blog_entry"."series_id",
"blog_entry"."title",
"blog_entry"."body",
"blog_entry"."tweet_html",
"blog_entry"."extra_head_html",
"blog_entry"."custom_template"
FROM
"blog_entry"
WHERE
(
"blog_entry"."created" >= "2021-01-01 00:00:00"
AND NOT (
EXISTS(
SELECT
1 AS "a"
FROM
"blog_entry_tags" U1
INNER JOIN "blog_tag" U2 ON (U1."tag_id" = U2."id")
WHERE
(
U2."tag" = 'python'
AND U1."entry_id" = ("blog_entry"."id")
)
LIMIT
1
)
)
)
ORDER BY
"blog_entry"."created" DESC
After executing the query it uses the returned data to populate multiple Entry objects (sometimes with clever tricks to fill in related objects so they don't have to be fetched separately, see select_related() and prefetch_related().But the bit that builds the SQL SELECT query and the bit that populates the returned objects is pretty tightly coupled.
But, I do care about being able to precisely communicate with other people. What are we going to call the other things?
> But they are a huge productivity enhancer and work well enough
I disagree with the implication that using an ORM is the default that 'smart developers' always choose.
Good developers choose the right tool for the job, so the tool selected depends on the job.
Also, what the term ORM means (and its effects on the design) vary based on language ecosystem and library. For instance, if the application doesn't need to dynamically navigate relations, and instead can get by with just treating the database as a set of structs, an ORM like Hibernate would probably introduce more complexity than it gives benefit. In such cases, a library like Jooq or JDBI could make the overall design simpler, perform better, and make it easier to reason about the behavior of the system. I would argue those are not ORMs, yet they can yield a better product, depending on the situation.
What DBMS do you use? I've used a few where that is true, and really, instead of using an ORM for that, you should stick to Postgres.
Copilot may be your little helper in writing code, but it is not going to consistently write the best code that YOU can write.