TE
TechEcho
Home24h TopNewestBestAskShowJobs
GitHubTwitter
Home

TechEcho

A tech news platform built with Next.js, providing global tech news and discussions.

GitHubTwitter

Home

HomeNewestBestAskShowJobs

Resources

HackerNews APIOriginal HackerNewsNext.js

© 2025 TechEcho. All rights reserved.

Eradicating N+1s: The Two-Phase Data Load and Render Pattern in Go

64 pointsby gmcabrita12 months ago

15 comments

dfee12 months ago
This seems to be the dataloader pattern. There are implementations in many languages, but the idea is that you have a bunch of threads which declare their I&#x2F;O needs, and then you 1) debounce and merge the requests (uniform access) and 2) cache the results so that later in the graph of calls you don’t need to fetch already loaded data.<p>Here’s one impl: <a href="https:&#x2F;&#x2F;github.com&#x2F;graphql&#x2F;dataloader">https:&#x2F;&#x2F;github.com&#x2F;graphql&#x2F;dataloader</a>
评论 #40506514 未加载
superice12 months ago
I see all these comments stating &#x27;oh ORMs are bad&#x27; and &#x27;just write some SQL&#x27;. Yes, you should probably not be afraid of SQL, and yes, using an ORM for everything is probably not great, and no, ORMs aren&#x27;t a full replacement for writing SQL occasionally, but taking an extremist pro-SQL point-of-view is not doing any favors to this debate.<p>There are very real reasons why writing raw SQL is a pain. You can make arguments about typesafety of queries, maintainability, mapping into application level data structures&#x2F;types yadayada. Imho the primary argument is that you cannot efficiently write a query that doesn&#x27;t suffer from duplication of data. If you have an entity A that has many B&#x27;s, and that same entity A also has many C&#x27;s, then if you join on both you now are loading A * B * C rows. This is going to be slow too, and is difficult to detangle.<p>ORMs in their current form are terrible at tackling this problem, and people are right to call them out for that. &quot;Just&quot; lazy loading or &quot;just&quot; eager loading both do not scale, nor does doing firing event listeners on a per-entity basis. But rawdogging SQL is not a viable solution either, and I wish people advocating that would stop this attitude of &#x27;oh you prissy dev using your puny ORM, not willing to write a line of SQL, look at me with my glorious hair on my chest and superior intellect writing pristine queries&#x27;.
评论 #40508761 未加载
评论 #40508488 未加载
评论 #40509714 未加载
gregwebs12 months ago
Jet can automatically load joined objects into embedded Go structs: <a href="https:&#x2F;&#x2F;github.com&#x2F;go-jet&#x2F;jet&#x2F;wiki&#x2F;Query-Result-Mapping-(QRM)">https:&#x2F;&#x2F;github.com&#x2F;go-jet&#x2F;jet&#x2F;wiki&#x2F;Query-Result-Mapping-(QRM...</a><p>Depending on what you are doing there might be some duplication that you could remove by creating hash lookups as in this post, but I would reach for Jet first.<p>sqlc supports embedding but not embedded slices?
评论 #40507683 未加载
andrscyv12 months ago
So a super complicated work around instead of just doing sql queries or using a query builder ???
评论 #40507540 未加载
评论 #40505742 未加载
评论 #40506699 未加载
评论 #40506731 未加载
Groxx12 months ago
For detecting rather than preventing duplicates, I&#x27;m fond of this pattern in Go:<p>At &quot;entry&quot; to your code, add a mutable &quot;X was called&quot; tracker to your context. Anywhere you want to track duplicates, insert&#x2F;increment something in that tracker in the context. And then when you exit, log the duplicates (at the place where you put the tracker in).<p>It&#x27;s reasonably implicit, works for both tracking and implicitly deduplicating (turn it into a cache rather than a counter and voila, lazy deduplication*), and it&#x27;s the sort of thing that all your middle layers of code don&#x27;t need to know anything about. As long as they forward contexts correctly, which you REALLY want to do all the time anyway, it Just Works™.<p>*: obviously you can go overboard with this, there are read-after-write concerns in many cases, etc which this article&#x27;s &quot;prevent by design&quot; structure generally handles better by making the phases of behavior explicit. but when it works, it&#x27;s quite easy.
peter_l_downs12 months ago
If you&#x27;re using Go, sqlboiler can do this for you in most common cases (e.g., fetch all the Users matching this filter, and for each User, fetch the related Company)<p><a href="https:&#x2F;&#x2F;github.com&#x2F;volatiletech&#x2F;sqlboiler">https:&#x2F;&#x2F;github.com&#x2F;volatiletech&#x2F;sqlboiler</a>
hinkley12 months ago
I&#x27;ve always been sort of fond of 1 + 1. It&#x27;s too often the case that there&#x27;s a popular query that doesn&#x27;t even need the child data to function, and unless you have some elaborate caching mechanism it would be a shame to pay the full cost of the join or however you want to implement it.<p>Making one query that returns the base data and a second that pulls all of the associated data works often enough.<p>Then it&#x27;s only when you need to pull M individual records and the associated data that <i>might</i> put you into M + 1 queries, if you can&#x27;t work out client side grouping for some esoteric reason. But you&#x27;ve reduced the exponent of the fanout by 1, which can hold you for a long time. Years even.
评论 #40507458 未加载
janci12 months ago
To work around N+1 is to write all your database layer functions in form of getFoosByIds(id[]) instead of getFooById(id). This allows you to easily compose the loads when you have subresources. It&#x27;s similar to what the author is doing, but does not tear apart the subresources from the parent object.<p>Pushing the subresource fetching down to the database requires using JOINs and fails badly when you have multiple one-to-many relations in one fetch.<p>Just do a single, separate query per table.
stephen12 months ago
Ah, I look forward to every brandur post! :-)<p>If he can give up Go, we&#x27;ve got a TypeScript ORM that will de-N+1 basically everything* that is not a paginated&#x2F;limit-offset query:<p><a href="https:&#x2F;&#x2F;joist-orm.io&#x2F;docs&#x2F;goals&#x2F;avoiding-n-plus-1s" rel="nofollow">https:&#x2F;&#x2F;joist-orm.io&#x2F;docs&#x2F;goals&#x2F;avoiding-n-plus-1s</a><p>This works even in adhoc loops, i.e. if you have a lifecycle hook** of &quot;after an author changes, do x&#x2F;y&#x2F;z logic&quot;, and you update 100 authors, every SQL operation invoked by those ~100 individual hooks is auto-batched.<p>We&#x27;ve been running this in production for ~4 years at this point, and haven&#x27;t had an N+1 since then (although we didn&#x27;t initially support auto-batch find queries; that came later).<p>Of course kudos to dataloader.<p>*everything --&gt; any queries our &quot;find&quot; API supports, which doesn&#x27;t do aggregates, sums, havings, etc.<p>**lifecycle hooks --&gt; yes, a blessing and a curse; we&#x27;re always attempting to find better&#x2F;higher-level abstractions for declaring the intent of business logic, than raw&#x2F;imperative hooks.
评论 #40507059 未加载
austinpena12 months ago
I&#x27;m very grateful to this post for introducing me to sliceutils to create a map from a slice. I think that&#x27;s a very elegant way to create nested models given a parent and child struct.
marcus_holmes12 months ago
The problem is not in Go&#x27;s endless verbosity. The problem is the basic concept of ORMs - that the model you need for efficiently rendering data is the same model that you need for efficiently storing data. ORMs map them 1:1 and that&#x27;s what results in N+1 queries and all the other problems.<p>Go&#x27;s endless verbosity and lack of dynamic features is a blessing, not a curse. Because you have to write your own data access layer, you can break this blatantly false assumption that what you need in the UI is the same as what you need in the database.<p>To break the N+1 problem, you can do funky stuff like pull back sub-components as array fields in the main component, or pull multiple result sets, or concatenate the sub-component data into a single text field, whatever your UI requires. Because you&#x27;re not forcing the databases query to map 1:1 with whatever structs you&#x27;ve got inside your application, you can get creative with how you structure the database storage, and how you query that storage. You can create funcs in the database that do crazy shit with the data to return exactly what the UI requires in a custom struct. Because it all runs on the database itself it&#x27;s fast and painless. Because you have to manually map all the structs in your application yourself (thanks Go!) then you are not constrained to mirror the database structure in your code. Your UI code does what it needs, your database does what it needs, and you can map between them however you want.<p>ActiveRecord is easy to use, and very concise, which is what it optimises for. Go is not optimising for the same thing. The author is trying to recreate ActiveRecord in Go without realising that ActiveRecord is a straightjacket, and obviously struggling. If you free yourself from those constraints, the world becomes a simpler, better, place.
dventimi12 months ago
I think the N+1 problem is overblown. The number of database calls will scale with the volume of data retrieved, but the volume is data retrieved should always be small.
评论 #40507573 未加载
评论 #40508484 未加载
评论 #40508565 未加载
评论 #40507644 未加载
slotrans12 months ago
ORMs are such a toxic idea, we&#x27;d be better off if we could un-invent them. Look how thoroughly this poor author&#x27;s mind has been poisoned by long-term exposure.<p>Please get out there and write some SQL. I promise it won&#x27;t hurt you.
评论 #40508341 未加载
erikpukinskis12 months ago
As a side note, I think I am adding “render” to my list of words which are meaningless and therefore banned from using in code:<p>- render<p>- container<p>- context<p>- base<p>- model<p>- action<p>- component<p>- bake<p>- build<p>- generate<p>- view<p>- control<p>- process<p>- resource<p>Any I should add&#x2F;remove?
sigmonsays12 months ago
everything about this screams wtf to me<p>engineering around deficiencies sometimes yields interesting results but this isn&#x27;t one of them.<p>I&#x27;d say this code is spaghetti.