(评论)
(comments)

原始链接: https://news.ycombinator.com/item?id=38781442

这篇文章强调了使用“UPDATE … LIMIT”语法作为处理数据库中并发访问问题的更简单的替代方案。 然而,作者建议一些 RDBMS 系统(例如 PostgresQL)已经在 SELECT FOR UPDATE 命令中包含此功能。 此外,作者指出某些 ORM 工具对类似功能的支持有限。 Ultimately, these approaches help eliminate problems such as "lost updates," and ensure data consistency regardless of the transaction being executed simultaneously by multiple users. 总的来说,这个问题通常可以通过应用悲观或乐观并发控制机制来解决,具体取决于具体的约束和要求。 乐观控制的并发更新在涉及无关紧要的值的情况下可能会很有效,例如计算追随者。 或者,悲观锁定解决方案(包括 SELECT FOR UPDATE 方法)可以解决关键数据的问题。 然而,无论选择哪种方法,都必须认识到并承认性能、弹性、复杂性和数据正确性之间的潜在权衡。 Finally, it's crucial to prioritize scalability and fault tolerance throughout database architecture and design processes. 关于 PHP、Ruby、Python 和 Java 等 Web 开发人员流行的语言中不存在 SELECT ... LOCK 关键字的建议 - 这是事实。 这些关键字通常在数据库管理界面中可用,包括像 pgAdmin 这样的控制台客户端或具有直接 SQL 界面的图形前端。 不幸的是,绝大多数在流行的 Web 框架中开发的数据库支持的应用程序严重依赖对象关系映射器 (ORM),这些映射器屏蔽了有关实际底层数据库操作的详细信息。 虽然这种方法有助于防止数据库模式和操作特定的实现差异,但它也可能导致效率低下或性能瓶颈。 因此,作者建议在可行的情况下考虑采用低级、直接、本机数据库支持库,特别是在使用分布式或实时系统时。 这些优势抵消了较小的学习曲线障碍。 总体而言,作者鼓励开发人员直接熟悉特定的关系数据库后端,而不是主要依赖于 ORM 抽象。 通过了解实际 SQL 语句和数据库原语背后的基本概念,开发人员可以显着提高应用程序的整体性能和正确性。

相关文章

原文
Hacker News new | past | comments | ask | show | jobs | submit login
PostgreSQL internals: Things to know about update statements (patrick.engineering)
235 points by ppati000 1 day ago | hide | past | favorite | 57 comments










> This behavior may well be interpreted as a violation of the SQL standard.

no it may not

this is fully standard compatible behavior and even expected behavior for _any_ SQL database implementing a read committed transaction insulation level

also it's a problem which on a theoretically level is not solvable with how SQL works, hence why on stricter isolation levels like serializable committing the transaction in such a situation will fail (in any SQL database) and needs to be retired. (Through in some unusual setups the db might be able to translate retry the transaction by itself, through at the cost of a ton of drawbacks)

Anyway reading the postgres documentation is always a good idea, it's not perfect but pretty good.



Yeah, I thought the other two sections were good but the "lost updates" section was just wrong, and in my opinion bizarre. Bizarre because the author does talk about different isolation levels - what does he suppose the purpose of those different isolation levels to actually be if he thinks READ COMMITTED should behave the same as SERIALIZABLE?


Dathinab's setting it straight: that SQL behavior everyone's fussing about? Totally standard and nothing to lose sleep over. Rowls66's got a nifty trick up their sleeve with 'FOR UPDATE' for dodging those update headaches — it's like having a secret handshake in SQL. And ComodoHacker, keeping it real with the efficiency chat — sometimes simple and speedy UPDATE is all you need, but for the big leagues, 'FOR UPDATE' is your go-to move. It's all about picking the right tool for the job!


Please don't unleash AI comment bot spam on Hacker News.


In the Lost Updates section, a more straight forward solution is to use the FOR UPDATE clause in the first select statement. This locks the record and prevents concurrent updates.


When you're incrementing by using UPDATE ... SET value = value + 1, the database holds the locks for the minimum time needed. Everything else is less efficient.

In more complex scenarios, FOR UPDATE is the solution.



When there's a big chance of multiple tasks grabbing the same rows, processing them, then updating them, marking them for update since the beginning is better. E.g. a message queue like structure where messages should be processed only once.


Not sure what you mean by "the database holds locks for the minimum time needed." Locks are always held until the transaction commits.


The math on #1 doesn't check out. If you update 2 bytes in the record only 28 are still written. But are only 28 written in either case? Does Postgres not write out entire pages?

It gets much more complicated when you consider full page writes, checkpoints, HOT, and multiple rows being updated at once.

For #3 it really depends on a lot of factors what the best approach is and if it's even worth your time.. But a typical approach that isn't mentioned is to just make sure you acquire locks in a deterministic order between transactions you don't want to deadlock each other. This will reduce concurrency(which is the entire point of the deadlock detection feature), but you can push the queue into the DB(behind the lock) which will minimize latency if you are done round-tripping.



8 bytes of new data but 8x28 = 224 bytes = 200 data + 8 date + 16 for id.

The 28 is a factor (×).



I should have used more precise language; I mean the bytes written to disk don't work out with such a simple formula. Missed the footnote which discusses this, but there are even more things to consider such as how many rows are being updated at once and etc.


To add to this for anyone reading later..

The OS doesn't write bytes to the storage; it writes blocks. For modern drives the sector size is 4k, and the block io size is likely to be 4k unless utilizing 512b emulation.

So, the OS will always send 4k minimum writes to the storage device. In this case updating just the 8bytes would still result in a 4k write; at least to the wal then again to update the page plus index updates and etc.



One thing I'd really like PostgreSQL to add is LIMIT on update statement as this makes batching easier. E.g.

    UPDATE user_profile
    SET followers_count = 0
    WHERE followers_count IS NULL
    LIMIT 10000
I don't care which rows gets updated, only that no more than 10000 rows get updated. After each UPDATE I will COMMIT and then repeat as long as the UPDATE returns a positive number of rows updated.

This makes it much easier to make small-ish batch updates and avoid locking all rows in the table.



This is one area where MySQL is ahead. UPDATE and DELETE can both use LIMIT.


You can already achieve this with CTEs.


Do you specifically need a CTE, or wouldn't a nested SELECT also work?


Can you really run updates on CTEs that are simple projections and selections? TIL.


No, you run a SELECT … LIMIT in a CTE, and the main query is an UPDATE … JOIN CTE ON mytable.id = CTE.id


But using a Common Table Expression is a workaround, kind of like using a paper towel as a plate. Just give us a plate...


How?


Would you not just be able to do the CTE as you would a correlated sub query? Something like:

    WITH batch AS (
      SELECT id FROM user_profile
      WHERE followers_count IS NULL
      LIMIT 10000
    )
    UPDATE user_profile
    SET followers_count = 0
    FROM batch
    WHERE user_profile.id = batch.id
But with the difference that if you didn’t want to round-trip to the application for each batch you could now make this a recursive CTE?


The ”solution” for this is also SELECT FOR UPDATE … LIMIT 1000.


This would also require adding the ORDER BY clause, as there might be applications where the order in which they are updated matters.


Is not any single query in Postgres a transaction? I don't think individual rows would be visible outside that transaction until all are updated.


The question is which rows would be updated by a single execution of a UPDATE...LIMIT query. The order of result rows of a SELECT query is undefined in Postgres unless you add an ORDER BY clause. It is natural to assume that an UPDATE...LIMIT would be similarly affected.


I find the following article from some time ago more informative. It contains a very good explanation of isolation levels and how they are implemented in postgresql.

https://news.ycombinator.com/item?id=38684447



Database locking can be a bit surprising at times. I recently stumbled over CREATE TABLE IF NOT EXISTS causing client timeouts - you might expect this to be near-instantaneous if the table exists, and it usually is, until it's not... because it always acquires an exclusive table lock, so it's blocked even by readers.


I wouldn’t classify that as “database locking”, but as “idiosyncrasy of a particular database system”. It’s a QoI issue.


#2 is something that nearly no application I know handles, and it saddens me, but not to the extent that it seems to anger many of my fellows. It's perfectly okay to do so for YouTube, it's not worth bothering for many applications of less concurrency than that. Still, it fairly offends me that most applications exist in the middle, where they don't have read isolation, or use logical additions, or have a separate process that cleans up the followers at a point-in-time, so that it's bounded to a day's worth of updates.


This is transaction 101. You simply lock the row via a SELECT … FOR UPDATE when you read the value.

Any application that actually cares about consistency of its data must be doing this.

Or just do it one step so the update increments the value in place. That way you have an implicit lock.



However, SELECT FOR UPDATE doesn't lock not-yet-existing rows in PostgreSQL, like it does in MySQL with gap locks. If for example, the transaction performs either an UPDATE or an INSERT based on whether the SELECT FOR UPDATE found or didn’t find a row, then two concurrent executions of that transaction can run into a conflict or duplicate INSERT in PostgreSQL, whereas in MySQL the second execution would block on the SELECT FOR UPDATE of the first execution even when there are no matching rows (but the first execution then will/might create some).


Author here. Thank you (and others) for mentioning SELECT FOR UPDATE. Definitely missing at least a footnote in the article.

Note that "one step" updates (e.g., SET followers_count = followers_count + 1) are still not in place. They are regular transactional updates that rewrite the entire row. Still, they can achieve higher throughput because there is no application/database roundtrip between locking and committing.



If it's not an indexed column (and your page isn't too full), it can do a HOT update, which is effectively the same as an in-place write if you're flushing to disk at page granularity anyway unless you have really wide rows.


I haven't used this, but I see its in Oracle and MySQL - does this exist in Postgresql or is this just the same as wrapping in a transaction where you're selecting on the row by ID or something? Just curious


Pretty much every RDBMS has this: https://www.postgresql.org/docs/current/explicit-locking.htm...

There are multiple flavors in PostgreSQL with different locking semantics. And the exact locking semantics may differ between database(for instance postgres does not have gap locks, while MySQL does) so you'd have to read the docs if that detail matters to you.



it does exist and I think it was even added to the SQL standard in some update

what FOR UPDATE does is (simplified) to lock a mutex for each row the select statement returns which are released once the transaction commits

this has the effect that parallel running transactions have to wait until the new computed value is visible before reading it and in turn there is no problem with lost updates

just to be clear this is a very simplified explanation in many ways



Even better, FOR NO KEY UPDATE is probably a good option here, since no key values are updated.


SELECT FOR UPDATE SKIP LOCKED is amazing


I don't disagree, but it's so uncommonly used, and I don't know any orm that will do it for you- it'd be easy enough to precompile, but that'd require compiler integration. (Or a hit to every select statement - which probably wouldn't matter for most apps!)






QueryDsl have it, it also has FOR SHARE.


I'm convinced that this is due to developers over-relying on ORMs instead of delving into raw SQL.

I'll give another, different but somewhat related example: consider a worker that works on a batch of rows, and wants to update each of the rows in the batch, each row with different data for that row. In raw SQL, this is simple enough with the UPDATE FROM VALUES pattern (see e.g. https://stackoverflow.com/a/18799497 ).

There's no support for this in Prisma, for example: https://www.prisma.io/docs/orm/prisma-client/queries/crud#up... . Most developers would wrap prisma.foo.update in an application-level loop, or possibly wrap all the individual updates in a single prisma.$transaction.



I think maybe with Django, you could use a QuerySet with an F expression to do this? [0] Agreed that it's trivial in pure SQL. As a counter though, having worked at places using an ORM (mostly Django), and also where everything was raw SQL, the latter winds up having WILDLY inefficient schema and queries that the DB team (me) has to fix later.

I lost any respect for Prisma when I learned it doesn't do JOINs in the DB. [1]

[0]: https://docs.djangoproject.com/en/5.0/ref/models/expressions...

[1]: https://github.com/prisma/prisma/issues/5184



> raw SQL... winds up having WILDLY inefficient schema and queries that the DB team (me) has to fix later.

I won't dispute this, I'll just point out that (a) premature optimization is the root of all evil, (b) because it's raw SQL, it's easier to reason about and fix. Inefficient queries aren't really a problem in the early days when your whole dataset doesn't even add up to a gigabyte.



The queries aren't necessarily a problem early on (although I've seen OFFSET/LIMIT used for pagination, which is just... no), but schema is sticky. Terrible decisions made early on only become harder to fix as the data set grows.


If you start using locking, you'll encounter many more serialization failures. Similarly for higher isolation levels. This means that you need to retry transactions, and doing that can be quite hard. Many years ago, I wrote some helper code to retry transactions on transient failures for PostgreSQL (which was unnecessarily hard at the time because the error codes that are eligible for transaction retry were not documented clearly). But even with that taken care of, you still had to think carefully about non-transactional side effects (such as sending mail) when writing application logic. A neat side effect was that you could restart the PostgreSQL server without impacting running applications (and today, you could probably even kexec a new kernel before the application timeout kicks in).

I suspect with the current preferences to avoid exceptions, writing for automated retry becomes quite a bit harder. And I couldn't really get automated transaction retry to work for SQLite while still caching statement handles, treating them as prepared statements.



That's why automated retry on concurrency failures should be a built-in feature of the database (or at least of the DB client). It should be easy to register your own commit (for side effects that can be deferred until success) or abort (for side effects that can't be deferred but can be undone) handlers for a given transaction.


I don't think you can have automated retry because if the application logic lives outside the database and is not subject to its transaction processing (which is the common programming pattern today, I think), the database has to arrange for a re-run of that logic if any of the observed database state changes (such as the followers_count returned from the SELECT statement in the example). This means that the code implementing the application code has to be ready to execute multiple times without ill effects, and application programmers need to be aware of that.


That's true, that's why the client API should allow you to submit a callable object of some sort (e.g. a C++ lambda) that is automatically wrapped in a DB transaction and allows you to register commit and abort handlers as I described. Here's such an API that I'm currently working on: https://senderista.github.io/atomik-website/.


While I agree, it's a pretty bad example for demonstrating this issue, because an error on "follower_count" will probably not cause any harm (youtube example). Also, the value can be recalculated at all times (maybe not feasable).

Any other example that comes to my mind will not depend on the "state of the apps memory" for a sensitive value, because that would be bad design too. Then the sensitive value must be the aggregate of other values within the database and with each commit, the equation must be balanced.



The example with follower_count shows how to use the +1 technique to do without locking. That's OK IMHO. It's not possible to use that technique when updating other type of data. Example

  select * from issues where id = 123;
  -- do something in app
  update issues set status = 'done' where id = 123;
There you can lose a status update if two people happen to work on the same issue at the same time with two different ideas of how to update it.

Or the classic bank account balance update. Both must be solved with a SELECT FOR UPDATE. That should be in the demo page of every ORM, to be sure that people that don't know SQL don't make that kind of mistakes.



> The example with follower_count shows how to use the +1 technique to do without locking..

Without explicit pessimistic locking. There are always locks. More guarantees, more locks.

That issue example can be tackled with optimistic concurrency controls depending on the constraints. The issue can be checked out with an UPDATE .. WHERE .. RETURNING ..



That's what makes it such a perfect example. It's inconsequential, but also simple. A SQL statement `update follower_count = follower_count +1` is incredibly cheap in SQL, even as part of a transaction. Accepting that the follower counts are laggy is incredibly acceptable. Repeatable read is expensive, but cheap enough for many, many applications.

The problem is that most applications that I've worked on do none of the above - they accept the buggy behavior in the concurrency case. As part of the "post reconciliation" solution, that's fine, but it's a bug that will never get fixed.

Perhaps worst is that there's an entire class of people - probably autistic, and often not people I want to work with for one obvious reason or another[1] - who simply have to be kept in the dark about long-standing minor bugs. These are known inconsistencies in the data that don't matter but that they'll pitch a fit to fix because they can't see.the difference in constraints like "the follower count will be an integer" and "The (recorded, but fundamentally cached) follower count will equal `SELECT COUNT(*) FROM followers WHERE account = ${user}`

[1] Really, just one reason - they reject rhetorical logic, focusing on formal logic for anything that catches their attention, to the point they are happier with atrocities than morality.



Grownups don’t use UPDATE.


> This behavior may well be interpreted as a violation of the SQL standard.

This effectively changed the tone from an informational article to a hit piece. This is a huge accusation which is plain wrong.



It’s not a hit piece, it’s just someone who had the wrong conception of the guarantees (not) provided by transactions, and hasn’t fully come around yet.






Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact



Search:
联系我们 contact @ memedata.com