Live data from Hacker News

12 requests per second: A realistic look at Python web frameworks

suade.org

211–220 of 239 posts

Re: 12 requests per second: A realistic look at Python web frameworks

#211
post #91

Earlier quoted context omitted.

Another great option in Java is jOOQ, which lets you write type-safe and potentially composable queries such as: context .update(User.USER) .set(User.USER.NAME, userName) .where(User.USER.ID.eq(userId)) .execute()

jOOQ and its DSL is good, however IMO it's more readable using raw SQL (by using `context.fetchInto` and its variants) than to using DSL when deal with complex query.

Why not just create views and query those with jOOQ, then?

Re: 12 requests per second: A realistic look at Python web frameworks

#212
post #5

Don't forget that you're paying a huge price using the sqlalchemy orm - https://docs.sqlalchemy.org/en/13/faq/performance.html If I know an endpoint is going to be hit hard, I forgo trying to use the ORM (except to maybe get the table name from the model obj so some soul can trace it's usage here in the future) and directly do an engine.execute( ). Makes a huge difference. Next optimization I do is create stored proc…

To be clear, this is FUD. If you know how to make SQA emit the right SQL, the performance is basically the same as psycopg2 + your custom code, usually better. I've written many high volume SQA services and never once saw SQA per se as the bottleneck.

Re: 12 requests per second: A realistic look at Python web frameworks

#213
post #112

Earlier quoted context omitted.

There is lots of truth to this. Some ORMs like Django perform joins in very unsuspecting ways. A simple example is, say, foreign keys. Trying to access the foreign key of an object by doing `book.user.id` does an additional query for the user table to get the ID. It's less known that the id is immediately available by just doing `book.user_id` instead. I've spent time optimising things like text searches down from 20…

I encountered this a few times and started adding tests that assert each handler only executes the expected number of queries (and no more). If the application code is modified such that this N+1 query pattern occurs the test will immediately fail and you go optimise the query, problem solved. https://docs.djangoproject.com/en/dev/topics/testing/tools/#...

Or the person who changed the code then disables the test or sets N to 100,000,000 or something equally pleasurable to debug.

Re: 12 requests per second: A realistic look at Python web frameworks

#214
post #122

Earlier quoted context omitted.

The programmer also already had the value in book.user_id but still chose to ask the ORM to fetch all of .user so they could get .id from there instead. And they might then afterward call .name on it as well, and there would be no further queries, because the ORM has already been asked to fetch all fields of .user - so it might in fact have been sensible to fetch all of .user if so. The query builder cannot know whet…

Perhaps i'm a bit odd, but when I'm going to lean on an ORM to do things I expect it to actually do them. I expect that foo.user_id does not exist, because that representation has been transformed into an object. foo.user.id should be the only viable reference to the id. foo.user.id should return the value it already knows, any other property access i would expect will do the equiv of `select * from ...` if the objec…

> Now perhaps some ORM's prefer to be thinner, to provide more footguns via a leaky abstraction that mixes implementation details with the object mapping. I don't think those are good implementaations.

To me, it seems like the (hypothetical?) implementation you're talking about is much more leaky and footgun-y than the more straightforward ("thinner", in your words) version. In order for foo.user.id to not execute a new query, foo.user would have to return some sort of proxy object that only fetched the user row when you tried to access a field that hasn't been loaded. That's way more magic than the more obvious solution—which is to load the row when you access the related object—and could easily cause more problems than it solves in the long run when you need to debug very specific queries.

Furthermore, how is going out of your way to hide a field that exists in the database (user_id) not the definition of a leaky abstraction? What purpose does it serve to direct you through an unnecessary layer if all you need is the ID?

Re: 12 requests per second: A realistic look at Python web frameworks

#215
post #115

Earlier quoted context omitted.

`QuerySet.select_related()` and `QuerySet.prefetch_related()` are the bread and butter of Django query optimisation. I think most of the time that I've noticed a performance issue in our code, it's been easily fixed with one of those.

Django's ORM gets a lot of flak, but I don't remember the last time I had complex queries that I could not do with it. You still need to understand a minimum of SQL and databases, and usually those that complain about the ORM are the ones that expect it to be a "sufficiently advanced compiler", but it has matured so much that nowadays the developers consider a *bug* every time the answer to How do I do this query X?…

This is true, though to be fair to the critics, the syntax through which you express these complex queries is often clunky and unintuitive. For example, I need to re-read the documentation every time I use the annotation API because it's generally not obvious how to use it, and I've run into a few edge cases where you need extra code/syntax just to deal with its nuances and ambiguities.

Even though Django has come a long way, I greatly prefer ORMs like SQLAlchemy and Ecto that map more closely to the SQL query I'm trying to write.

Re: 12 requests per second: A realistic look at Python web frameworks

#217

Earlier quoted context omitted.

I just wanted to know how it would compare with Sanic, because I never used it.

The biggest difference is that Falcon is synchronous while Sanic is asynchronous. With Sanic, you would explicitly specify async/await for asynchronous operations and use asynchronous libraries for I/O. Switching to Sanic could also affect how you deploy to production. Both are plenty fast. FastAPI [1] is also worth considering if you’re looking into asynchronous API frameworks. It comes with nice features for specif…

I prefer async big time. We once had to implement async routines while using flask, where the server would return a 200 but keep processing the request, and the actual result would be sent by email. It was hellish and inefficient to make it. In hindsight would have been better to use a queue service and a consumer and decouple the whole process, even if it meant increased infrastructure and maintenance complexity.

Re: 12 requests per second: A realistic look at Python web frameworks

#218

Earlier quoted context omitted.

I agree with your general take on developer productivity, but I don't feel that modern JS is significantly messier than Python, at least not to a level where it significantly impacts productivity (I'd rather avoid a debate on the abyssal depths of the language, eg, type coercion) I feel about the same amount of grievances with both. For instance I dislike Python's async and functional semantics ( list(map(lambda n...…

> list(map(lambda n... List comprehensions are much better for this. Functional doesn't mean you have to use a function call. If you can use the paradigm with literal syntax, just do so.

They don't really compose nicely. (At least from my point of view, as I prefer fluent interfaces, eg. those you usually find in Rust/Scala/Java.)

Re: 12 requests per second: A realistic look at Python web frameworks

#219

Earlier quoted context omitted.

My guess is accessing a related field within a loop causing a database request per iteration, e.g. ``` [book.author.name for book in Book.objects.all()] ```

Maybe I spent too much time with Django already, but if I see anyone doing anything but Book.objects.values_list('author__name', flat=True) for this type of expression, I would mark it as a 3x WTF? in the code review.

As written, it's obvious you should be doing something else like `values()` or `values_list()`. You're much more likely to fall victim to this anti-pattern if it's done within a standard for-loop that has a bunch of other stuff going on. I just wrote it as a list comprehension to avoid having to muck about with formatting on my phone.

Re: 12 requests per second: A realistic look at Python web frameworks

#220

Related to ORMs/queries/performance, I have found the following combination really good: * aiosql[0] to write raw SQL queries and having them available as python functions (discussed in [1]) * asyncpg[2] if you are using Postgres * Map asyncpg/aiosql results to Pydantic[3] models * FastAPI[4] Pydantic models become the "source of truth" inside the app, they are designed as a copy of the DB schema, then functions rece…

Do you need to define your models more than once with these? I'm looking for a single source solution and haven't quite found it yet.

I only define them once, but I define all the database schema by hand (with an SQL script). I’d love to have something that translates Pydantic to an SQL schema definition.

Each asyncpg result has a dictionary-like interface, so I can convert it to a Pydantic model easily.

Post reply on HN