PostgreSQL anti-patterns: read-modify-write cycles (2014)
blog.2ndquadrant.com
blog.2ndquadrant.com
Analogous to the example from the article:
create table transaction (user_id integer, amount integer);
insert into transaction values (1, -100);
select sum(amount) as balance from transaction;What are the space concerns for keeping data in this way? For something like a retail bank (audit issues aside which probably make this necessary anyway) does space suddenly become a big factor?
I think the more realistic concern with this approach is that some queries might get complicated when they involve more complex logic based on the balance.
Solved or made worse by caching?
Caching may relieve some stress (on say a dashboard), but if an application is doing calculations around account balances, and uses event sourcing-like structures in the database, then caching isn't going to help as those calculations are going to still need to be done for each new balance changing event.
So caching is never worse (unless you accidentally used the cached value where you needed the live value), but isn't necessarily going to relieve a lot of DB stress.
Double-entry book-keeping is done this way. The data certainly grows over time, but generally at a predictable and manageable rate.
Most people using long-lived event-sourced data structures have some kind of snapshot/checkpoint/archive mechanism, which going back to the accounting example might simply be an opening balance.
Triggers get a lot of hate because of separation from application level version control etc., but these are also long discussions to get into.
edit: This article's contents are good to be included with all the most junior level application design/development training material.
1) you make transactions serialized
BEGIN ISOLATION LEVEL SERIALIZABLE;
select sum(amount) as balance from transaction where user_id = 1;
-- do something with balance
insert into transaction values (1, -100);
COMMIT;
2) or you use row level locking on an additional table (say user, while this conflicts with the system table user, but for the sake of the example we assume this is not the case) to serialize the access. BEGIN;
select user_id from user where user_id = 1 for update;
select sum(amount) as balance from transaction where user_id = 1;
-- do something with balance
insert into transaction values (1, -100);
COMMIT;
Now it may be tempting to change this to BEGIN;
select balance from user where user_id = 1 for update;
-- do something with balance
update user set balance = balance + (-100) where user_id = 1;
insert into transaction values (1, -100);
COMMIT;
Just for clarity: in most cases you should register your transaction event anyway (with a time stamp) for later auditing, but you now will have a risk of introducing inconsistencies between calculated balance and transactions due to the programming errors (that is partially committed transactions).Also you should obviously create proper primary keys and indexes.
Also you should include proper error/exception handling.
Also if you already do locking, then you could also consider different locking strategies described in the article.
Then your sql code can check the balance without doing a sum (which might get expensive, and often the same value until a transaction changes it) just by joining to that view.
i.e. `WHERE total_view.balance > 0`
It made me realize though the incredible safety I'd come to take for granted with a more traditional relational approach. Prior to that I would have had a separate table for the data in that JSON field, with each object within it, represented by its own row. Given my scenario that would have been perfectly safe, because the latest update to a specific object was always the right one.
The original design choice was perceived as "easier".
The JSON functions in postgres are different enough from SQL that I try to avoid them. Writing SQL interspersed with JSON functions becomes clunky, and is not near as powerful as just plain SQL.
There is a "population explorer" in our web application that allows users to search for patients based on the existence (or lack) of any combination of tags, and additionally to filter each tag by the values contained. The JSONB metadata is generated in SQL, the filtering is done on the frontend, and the searching is done by a query generated in the Python backend. It is surprisingly easy to maintain and extend, and even after a year and a half, we haven't run into any insurmountable issues, or even any difficulties of note.
For instance, if a user searched for all patients tagged with a hospitalization since November 1, the following steps would happen (this is vastly different from the actual code, just trying to give a sense of how it functions):
1. the backend generates an SQL query:
select ...
from patient p
where exists (
select 1
from tag t
where t.tag_type_id = :type_id
and t.deleted is false
and t.patient_id = p.id
)
2. the backend iterates through the results, filtering as necessary exclude_list = []
if filters:
for key, patient in results:
for filter in filters:
# each filter is a lambda generated by the
# user-defined parameters (in this case,
# since November 1)
if not filter(patient):
exclude_list.append(key)
for key in set(exclude_list):
del results[key]
3. on the frontend, if the filters for any of the desired tags are changed, a process very similar to the above is run to recompute the display setHowever, on the whole, I tend to agree with you. Really the only other places we use JSON at present are in storing API requests, where the data sent with each HTTP POST are in widely differing formats, and in various intermediate abstraction layers, where data from different sources contains different columns, and we can carry forward the "outlier" columns as a JSON object for later reference.
Use schemaless options, including JSONB in PgSQL or doc DBs like Mongo, for data that really needs it due its unpredictable nature, NOT data that you're too lazy to draw out a schema for.
> In fact, this isolation level works exactly the same as Repeatable Read except that it monitors for conditions which could make execution of a concurrent set of serializable transactions behave in a manner inconsistent with all possible serial (one at a time) executions of those transactions. This monitoring does not introduce any blocking beyond that present in repeatable read, but there is some overhead to the monitoring, and detection of the conditions which could cause a serialization anomaly will trigger a serialization failure.
As far as I can tell, this doesn't make transactions any more serializable; it just fails them if they wouldn't have been serializable anyway. And then clients typically retry. That sounds just like OCC. Like OCC, I'd expect that under any kind of contention, this could lead to very large numbers of failures and retries. It's not quite livelock, since at least one client will always make forward progress, but close to it.
1) Transaction 1 starts and commits before Transaction 2 starts. 2) Transaction 1 starts; Transaction 2 starts; Transaction 1 commits; Transaction 2 attempts to commit. 3) Transaction 2 starts; Transaction 1 starts; Transaction 2 commits; Transaction 1 attempts to commit. 4) Transaction 2 starts and commits before Transaction 1 starts.
1+4 and 2+3 are the same. In 1+4, the transactions are serialized - they are wholly independent of each other, so the second transaction will always see the updated version, so there's no worries about failing on that.
In 2+3, the version number increments when the first transaction commits. For optimistic concurrency control, the version number will have changed, which is detected prior to updating. (In the OP's example, the query is an UPDATE balance WHERE version = X, and since the version is X+1, zero rows get updated.) For the SERIALIZABILITY concern, it will fail because internally the version has changed for the given row ("version" referring to the metadata Postgres keeps to track which rows are current), kicking it back to the user with an error.
I don't think there's a scenario where reordering lets a SERIALIZE'd transaction succeed but optimistic locking fails. They are effectively - as far as the methodology used - the same thing (with some differences that the OP mentions).
EDIT: minor readability things
> Unlike SERIALIZABLE isolation, it works even in autocommit mode or if the statements are in separate transactions. For this reason it’s often a good choice for web applications that might have very long user “think time” pauses or where clients might just vanish mid-session, as it doesn’t need long-running transactions that can cause performance problems.
Second: there's the issue of precision to think about. Neither Postgres SSI nor OCC are perfectly precise; both may incorrectly reject transactions which would not have actually violated serializability. But Postgres SSI is an improvement because it allows certain classes of (provably non-anomalous) write-after-read interleavings to proceed.
The SSI paper[1] is a good read on the subject, and even contains an interesting description of a 3-way serialization conflict involving (requiring(!)) a read-only transaction such that if the read-only transaction were omitted then there would be no conflict among the other two read-write transactions.
That said, the effect is exactly as the author claims. The transaction on the right will ignore the change made by the one on the left.
Surely as soon as one transaction tries to modify the database, it'll lock out all other reads to the affected rows from other transactions, so as to prevent them observing changed data until the transaction's committed? So avoiding the entire problem? Isn't this the entire reason transactions exist?
Edit: I haven't actually used PostgreSQL; I grew up on SQLite. I see from the docs that SQLite transactions are all serializable, which from the linked article seems to be what you get with PostgreSQL's BEGIN ISOLATION LEVEL SERIALIZABLE. So... if by default PostreSQL's transactions aren't like this, what are they? If they're not serializable, then aren't they simply broken, i.e. not enforcing the transactions properly?
I'm not aware of ORMs handling it either (except for my in-house code generator). Is there anything out there that handles it well?
I've never used this approach, but after working a lot with row-level locks (SELECT ... FOR UPDATE) errors in environments with sustained load, I think next time I'm going to evaluate this approach.
Row-level locks need to be carefully implemented in order to avoid deadlocks, which can occur for a number of reasons (eg. missing indexes, other transactions locking rows in different orders, etc.).
Unless I'm misunderstanding, that's absolutely terrible advice, from both a security and proper layering standpoint.
It also depends on what you're trying to achieve. For instance, if you're just trying to help coordinate people updating a wiki page so that two people don't accidentally clobber each other's updates then it's probably not a huge concern.
From a layering perspective, you can probably implement this with checks and triggers in postgres so that it's mostly transparent to the client.
That said, I wouldn't bother myself ;)
Not trying to start a flamewar but... that's Rails in a nutshell. The default patterns are just straight up bad for any kind of large app built by more than one or two people. The gotchas mentioned in this post basically have no built-in rails patterns to mitigate them so you have to know how to write an app without the ORM to effectively use ActiveRecord, even though it's viewed as a great abstraction that you should always use so your code stays readable and you and your future team members won't have to worry about the implementation details in SQL.
For instance there's no built-in way to take advantage of Postgres UPSERT so every codebase I've come across does it wrong with a naive read then create (sometimes wrapped in a .transaction block which doesn't necessarily fix it as the post points out but is used as like a rain dance in many rails codebases whenever something's acting weird and we're not sure why). The built-in Rails increment() is not atomic, and the atomic increment_counter() helper still encourages you to do the wrong thing! (it doesn't use UPDATE .. RETURNING so you see people incrementing and then reading which obviously is no longer atomic). And on and on.
[1]: http://docs.sqlalchemy.org/en/latest/orm/versioning.html
[2]: http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalche...
Am I the only one who would consider a CTE for this, selecting the initial balance and the proposed balance minus amount in the CTE, testing it in the query following the CTE and potentially applying it, and then returning the original and new balance to the application. If both are the same then the update was not applied (not enough balance), and if different the update was applied (enough balance for the withdrawal).
This way it all occurs within a single transaction, even though it is several logical steps, and is simple to reason about.
In postgres, you don't really need to do CTE - UPDATE can return the new row if you add RETURNING * so you can just do the update and look at result.
In Oracle (and what we did in actual bank) was pl/sql procedure that did
- select for update - do the validation - apply the transfer, or return error. - let the application rollback or commit
This is hard to do without procedural language - you can work with CTEs and functions, but SQL is great at dataflow and bad at control flow, and you either drop to procedural language in database, or have the application do the actual logic.
UPDATE balance SET balance = balance - 100
WHERE user_id = 1 and balance >= 100;
Only problem is that you now actually do not know if the query did not update because of insufficient balance or because lack of user in the database.Query 1 (in CTE): Verify all is correct and the current balance (and whether you have enough balance)
Query 2: Perform update and obtain new balance
Then return a summary showing the starting balance and end balance for the transaction (proof of what, if anything, happened).
As a small aside, if anyone is interested in Matrix, Rust, Diesel (the Rust ORM), or the intersection of any of these things, you might be interested in discussing this further on this Ruma issue: https://github.com/ruma/ruma/issues/132
But one side of me wonders why SQL databases do not better adapt to those 'anti-patterns' in software development, when developers for a variety of reasons are more confident in having logic in their app code than their DB code.
RDBMSes are general-purpose tools that need a little bit of extra information to give you better guarantees on how data is manipulated. You can reconfigure the database ensure serializability by default, but if you want it available for specific queries the app developer has to be the one to ensure that constraint.
Oracle Database Transactions and Locking Revealed Authors: Kyte, Thomas, Kuhn, Darl
It's Oracle-specific though, because of one of the authors(Tom Kyte , the Tom behind asktom.oracle.com not so long ago ) I highly recommend it.
At university we learn that transactions should be serializable and that they are meant for these exact cases.
Transactions aren't actually true transactions unless they are serializable.
Most databases don't actually default to serializable transactions, but instead try to get close by implementing snapshot isolation. These aren't true transactions, since snapshot isolation doesn't truly isolate transactions.
The article had a bunch of nice ways to get better isolation than snapshot for a bunch of special cases. These are all very nice, since they can be useful for mainting performance together with integrity.
My professors at university still laugh at commercial databases and calls them toys for still not having implemented more efficient serializable transactions.
In the meantime, hundreds of thousands of businesses are built on REPEATABLE READ.
I'm not really sure how any implementation can be particularly efficient. It seems that either an implementation would have to maintain a huge number of specific row locks or you have to start locking whole tables.
Sure, you pay some price. But if that overhead actually proves to be a problem for you, we'd like to hear about it. There's definitely some room for optimizations, but so far fewer people hit those than some people (i.e. me) anticipated.
> I'm not really sure how any implementation can be particularly efficient. It seems that either an implementation would have to maintain a huge number of specific row locks or you have to start locking whole tables.
You don't need full row locks, and you can summarize up "ranges" of locks if necessary for space reasons. I.e. go from row level to page level, to full table level. That will still be correct, just cause more rollbacks.
For simple CRUD applications SERIALIZABLE is great, because you basically don't have to worry about concurrent updates anymore. Well worth the price imho.
Netezza/IBM had a reasonably efficient implementation. Instead of broadcasting individual locks, they shared ordering relations between transactions as they were discovered.
Essentially, when transaction A reads a record and concurrent transaction B modifies the record, then B serializes after A (even if B's modification occurs before A's read in real time). These ordering relationships form a a graph. If a new ordering relationship between transactions causes the formation of a cycle, then some transaction must be aborted.
What is holding back more efficient serializable transactions becoming more mainstream?
Is it:
A lag between academia and industry? Entrenched software which makes it hard to implement / update to modern technology Performance issues which make the benefits for safer transactions not worth the computation tradeoff
I should note that this is not Postres specific - any MVCC ACID database will behave this way. Some use locks instead of MVCC and behave differently. But it definitely belongs into "What every programmer should know about transactions, ollected works".
Yeah, this definitely belongs in the essentials. Lots of developers treat transactions as a race condition panacea. This article is a nice case study about how expressing relative SQL transformations (e.g. "subtract 100 from current value") using absolute values ("set the value to 200"), even absolute values derived from relative application logic, can be prone to fault.