A Case for Upserts
lucumr.pocoo.org
lucumr.pocoo.org
> If Postgres would implement a shitty and
> inefficient version of an upsert statement at
> the very worst it could be as bad as the current
> implementation that people write on their own
Wow. please don't do that.Many people do not understand why an upsert is a hard problem and would blindly use any solution presented without understanding it's limitations. At least with the current situation if a developer finds themselves in need of an upsert they are forced to seek out a solution and in the process understand why it's tricky.
Depending on a "shitty & broken" implementation that goes into postgres which then subtly changes its behaviour after a few years could cause no end of problems.
Having a single, canonical solution, even if it it's imperfect, is usually better than having a myriad of equally imperfect options. At least then documentation and community guidance can coalesce around it. Its caveats can be properly explored and explained, and when you do run into trouble and go looking for help, you're far more likely to find people who have had the same problems in the past.
Saying "feature x is hard, so we'll leave it to developers to figure out" is a disastrous mindset for any piece of software. The whole point of frameworks, runtime libraries, DBs, etc. is to provide developers with standardised, battle-tested infrastructure to do difficult things that require specialist knowledge outside of their area of expertise to get right. The argument that the hardest stuff should be left out, because it encourages developers to try and figure it out themselves is crazy: if the PostgreSQL developers can't work out how to implement upsert perfectly, then non-DB specialists are likely to do even worse.
The culture on the Postgres development side has always taken a very conservative approach, which carries with it much trust that the features that exist work very well.
Introducing a half-baked feature would break this trust. And so I would rather wait.
When you have a huge investment in your data, a conservative approach to database servers is full of merit. Once I began to understand concurrent design my years of MySQL experience were suddenly curdled. I had to switch to a server/community that care about integrity, and do not play fast and loose with new features. It's not a popularity contest.
The real problem is that if the desired behavior is not nailed down entirely in the first implementation then what will happen when the operation is improved is that it will break stuff. That's I think the strongest argument for waiting.
OTOH, the concurrency issues are genuinely hard and there are a large number of applications for whom they don't really apply in the real world. It might seem to be nice to have a canonical syntax.
I guess on balance I agree with you but for very different reasons. Once you have an API, you have a contract.
> implementation that people write on their own
> and then at least, there is an established syntax
> and a way to improve it further.
That was the point.Being aware of the problem doesn't mean you'll actually find a good solution. At least this way, any lingering problems will eventually be fixed instead of draining more of your own resources. Chances are, the Postgres team will be able to figure out the subtleties much better than you.
The more domain knowledge your system requires, the worse off everybody is. Here's a story.
At my previous company, I implemented something close to 'upsert'. After hours of researching, implementing, and testing in as many environment I could think of, I wondered why I wasted so many hours trying to get it right. Sure, I now understand the subtleties involved. (Well, I think so anyway. I still don't know what I don't know.) But am I better off now? Is the company? Not really. What we have is a tangled hack.
And worse, I no longer work with this company. So the next guy will have to figure out what's going on when there's an issue. To be safe, I spent another day re-organizing the code for clarity and documenting the complicated workaround. A scrappy syntax shipped today and fixed tomorrow would have been ideal. That way, the new guy could search for the symptom and see that that the current implementation of UPSERT is the cause. They can then use a recommended workaround, or simply upgrade Postgres to fix it entirely.
The PostgreSQL team already merges features which are not yet perfect (see materialized views in 9.3, JSON in 9.2, and replication in 9.0 for examples), but they will not do that if they think they will have to break the API in a near future.
Once you have a public API you have a contract, and that contract really shouldn't be broken without good reason. If PostgreSQL offered an upsert syntax with the same limitations as writeable CTE's, and promised to solve the concurrency issues in future releases, I wouldn't use it. I would rather have a hand-written implementation which is guaranteed to work in the future with the same caveats than an implementation I have no control over that may pose backwards-compatibility handling issues if I am supporting multiple Pg versions with my app.
Stability in API's is important. The idea that we will just throw something together now, and then fix it later will result in PostgreSQL getting a bad reputation.
People hear, "PostgreSQL is like MySQL except that it is done right." They then go to PostgreSQL and say, "PostgreSQL does not implement my favorite MySQL feature in my favorite MySQL way."
Part of what makes PostgreSQL what it is is that they care so much about getting features right, and making them standards compliant. (Because if you do things differently than the standard, there is a chance that you will find yourself forever maintaining both the way that you did it and the way the standard said to do it.)
I like upserts. I understand the value and the functionality. But please don't add it until you can do it right. And please, please, please make it work like the standard says to. The standard is explicit, flexible, and a standard. Plus if you haven't impressed on the MySQL solution, it really isn't very hard to figure out how to do what you want to do. (I certainly didn't find it hard when I had to do it.)
CREATE RULE Pages_Upsert AS ON INSERT TO Pages
WHERE EXISTS (SELECT 1 from Pages P where NEW.Url = P.Url)
DO INSTEAD
UPDATE Pages SET LastCrawled = NOW(), Html = NEW.Html WHERE Url = NEW.Url;2. IIRC the rule method does not work correctly for multiple-rows inserts including duplicates
Does the SQL standard actually guarantee that a MERGE is atomic? After all the MERGE statement involves a subselect which seems to be about as concurrency safe as a SQLite replace insert which a join.
This rule allows the possibility of raising an exception on INSERT if two Pages with the same Url are "upserted" in parallel.
You would still require a table level lock to ensure you don't get an exception within the statement as a whole.
Also: Rules are easy to get wrong. Try not to reach for them unless absolutely necessary.
Two reasons:
a.) same concurrency problems.
b.) defined globally for the table. Completely useless for the kind of things I need an upsert for.
http://www.postgresql.org/docs/current/static/transaction-is...
"The Serializable isolation level provides the strictest transaction isolation. This level emulates serial transaction execution for all committed transactions; as if transactions had been executed one after another, serially, rather than concurrently."
If you don't get the right behavior with serializable transactions, as you seem to be claiming, it seems to me that serializable transactions should be considered buggy. In this case they do not provide the guarantees they are claimed to provide.
But if I personally had this situation what I would do is (in pseudo code, but think this would be in e. g. Python, or in a stored procedure):
begin transaction
does row with unique key x exist?
if exists: update all relevant fields
else: insert new row
commit
How does this have a different concurrency behavior than e. g. an equivalent Oracle MERGE statement or MySQL ON DUPLICATE KEY?If another process tries to write to this row before the change is committed, it would have to wait (in any case).
If another process tries to read from this row before commit then what happens depends on the transaction isolation level and maybe even on implementation details. But I don't see how this is changed from having a built-in MERGE-like utility.
A: Does row exist? Answer no A: Ok, so insert row. A: ....
B: Does row exist? Answer no (A has not committed yet) B: Insert row (locks, waits to see if A commits or rolls back)
A: Commits B: Unique constraint violation error.
Now here's where it gets really tricky: What is the desired behavior? Do we:
1. Just raise a constraint violation error? That's what we do with LedgerSMB because, frankly, unique constraints are far more likely to be violated by human error than concurrency issues and we can tackle other related errors elsewhere. This is specific to our app. Other apps may differ.
2. Should the upsert lock, wait, and retry if the the upsert criteria is violated by a pending transaction? That's the cleanest option. However, this requires being aware of which unique indexes are affected by the upsert criteria.
A quick and dirty upsert like yours is good enough for most applications based on human input. It is not good enough, IMO, for applications with lots of direct automation coming in from various sources. So those are very different use cases and they have very different criteria.
You're right, since a built in merge utility has more information about the semantical intent of the operation it can essentially repeat the complete operation whereas my naive implementation would not.
A simple Postgres function like the following works for me:
CREATE OR REPLACE FUNCTION upsert_my_table(key text, value text) RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
UPDATE my_table SET value=$2 WHERE key=$1;
IF NOT FOUND THEN
INSERT INTO my_table (key,value) VALUES ($1,$2);
END IF;
-- add PERFORM pg_sleep(10) for concurrency testing
END
$$;
(according to my tests, it has no concurrent UPDATE problems ... duplicate keys are another matter and best handled with restarting transactions / savepoints)You have concurrency updates with deletes as well as inserts. That's literally the worst way to implement upserts currently. Even CTE's are superior to that.
If you have high concurrent updates, you should be dealing with append-only database design and collapsing into a view to query, like Datomic. All the locking required for atomic upserts would wreak havoc on performance anyway no?
When do you not have concurrency?
> you should be dealing with append-only database design and collapsing into a view to query, like Datomic
True, but postgres is very bad at that. So you need to work with the tools that postgres gives you. Yes postgres currently does not give you an upsert but an upsert is much closer to postgres' design than an append only database that denormalizes on inserts.
Highly concurrent updates? By that I understand a workload where you have multiple clients updating the same rows.
In my experience it's either rare or the result of doing something inefficiently (e.g., doing analytics as a "view_count" column). Everytime I've faced that I've remodeled the database, both for performance (you avoid invalidating caches for the entire table) and auditing reasons (if you have a bunch of people replacing each other's changes, you might want to retain the history).
It depends. We use upserts for certain kinds of data in LedgerSMB despite the fact that a lot of data is append-only.
> If you have high concurrent updates, you should be dealing with append-only database design and collapsing into a view to query, like Datomic. All the locking required for atomic upserts would wreak havoc on performance anyway no?
There are many cases beyond high concurrent updates where you want an append-only design. We use a lot of append-only in LedgerSMB but that's for transparency/audit control reasons.
The problem is handling corner cases with upserts. The fact is that it is a useful tool in many areas. However you wouldn't use a chasing hammer to drive nails, would you?
Namely, invalidating any query that comes after the update and asking to redo the transaction. This is done without lock.
In this case, server knows the transaction can pass, so you don't push the error to the client.
The server push errors to the client when:
- it is sure that the transaction can not pass.
- it doesn't want to guess whether the transaction can pass.
- it reached a max retry (because of heavy writes for instance)
Which is why I said multiple times that this needs database support at which point a lock. That would guarantee that the contention at the lock is fair. With a loop around that section the winner is chosen by network latency and retry speed if high contention happens. Also there is not even a guarantee that it will ever finish.
First time read that...
> that this needs database support
Transaction retry is part of mvcc databases
> at which point a lock.
Not necessarily.
> That would guarantee that the contention at the lock is fair.
It's not fair, it's a lock. Locking all the table for one row, is not fair. I don't know what you mean by fair.
> With a loop around that section the winner is chosen by network latency and retry speed if high contention happens.
Yes and? Anyway, this must be in the database, the database knows the transaction can pass, it doesn't need to send it back to the user, except if there is heavy writes on this row, which would mean the application is under attack or not correctly programmed.
> Also there is not even a guarantee that it will ever finish.
There is a MAX_RETRY, it is guaranteed to return.
---
Also:
- You don't state your problem upfront, just say you need this COMMAND. Which is forbidden by netiquette. - You never state that it's a Write/Write transaction problem - What about Read/Write? - There is a solution, use a migration or MYSQL
1=# CREATE TABLE my_table (key text primary key, value text not null);
1=# INSERT INTO my_table VALUES ('key', 'foo');
1=# BEGIN ISOLATION LEVEL SERIALIZABLE;
1=# DELETE FROM my_table WHERE key = 'key';
2=# BEGIN ISOLATION LEVEL SERIALIZABLE;
2=# UPDATE my_table SET value = 'bar';
1=# INSERT INTO my_table (key, value) VALUES ('key', 'value');
1=# COMMIT;
2 ERROR: could not serialize access due to concurrent updateThey fail in different ways (for serializable a in this case more useful way) which should have been pointed out by the author. It does not seem like he actually tested what isolation levels does or he tested it in a database which does not provide serializable isolation.
If this is accounting software then use transactions.
If you're doing a research paper on accounting software then have fun, there are lots of trade offs to play with.
If you are designing for concurrency then drop your constraints and transactions and just make everything an insert. Then when you want to select just look for the first record from the bottom of the table.
No feature, be it upsert or whatever, can ever beat proper design. The best it can ever aspire to do is equal it.
In MySQL, I get the exact behavior that I expect with anything above RC isolation level (which is the default for PostgreSQL):
session 1> create table foo ( k varchar(10) not null, v varchar(10) not null ) ;
session 1> set transaction isolation level read committed;
session 2> set transaction isolation level read committed;
session 1> insert into foo (k,v) values ('a','b');
session 1> begin;
session 2> begin;
session 1> delete from foo where k = 'a';
session 2> delete from foo where k = 'a';
-- transaction hangs here as expected since session 1 has a lock on this record
session 1> insert into foo (k,v) values ('a','c');
session 1> commit;
-- session 2 executes the delete
session 2> insert into foo (k,v) values ('a','e');
session 2> commit;
session 2> select * from foo;
+---+---+
| k | v |
+---+---+
| a | e |
+---+---+
When I do this in PostgreSQL, I end up with 2 rows in the table, which seems fundamentally incorrect and to me implies that PostgreSQL's implementation of read committed is flawed and should be fixed. The transaction in session 2, at RC isolation, should see the most recently committed result, which is the single record from session 1. I'm not CJ Date, though so am very likely missing some subtlety.PostgreSQL after the sequence above:
test=# select * from foo;
k | v
---+---
a | c
a | e
(2 rows)
What am I missing? Why is this considered correct behavior?These transactions aren't serializable but you didn't ask for that.
To get intuitive behavior, a delete followed by an insert with the same primary key in the same transaction would have to be treated as a row update, holding onto a single row lock, rather than as two independent rows with different row locks.
I'd like to give PostgreSQL the benefit of the doubt here, but this behavior seems fundamentally incorrect to me.
Another way i remember doing was having a unique key and inserting if an exception was thrown the update statement would run update.
Both where slow as hell. Du to the back and forwards stuff required.
begin;
delete from my_table where key='key';
insert into my_table (key, value) values ('key', 'value');
commit;
? As far as I understand any outside transaction would either see the old value (assuming one existed) or the new value, not the middle result where the row doesn't exist - am I mistaken and this doesn't work that way in Postgresql?That is interesting - is the current behavior really considered WAD? I.e., shouldn't then the 'correct' solution be in fixing this behavior instead of extending syntax for 'upserts'?
Is that not an upsert?
That is, a proc that just attempts the insert, handles the unique or primary key constraint violation error, and then proceeds to the update as a fallback? Sure you push the concurrency risk to deletion instead of creation, but deletions are generally more rare than creations so a table-lock on deletion is more palatable.
This is why data design is with performance and security as an attribute that you must bake in. You cannot bolt on data design after the fact.
Take the example of a user profile. The problem for me is not typically "the first time the user shows up, I need to create a record, and all other times, I need to overwrite it." In the systems I build, there is almost always a use case for having full access to the history of data. So the better solution is, "I must ALWAYS update whatever record exists AND insert a new record".
That means my UserProfile table also has two extra columns, CreatedOn and InvalidatedOn. Then, creating/updating a user's email address is always the same, two statement operation:
-- If UserName does not yet exist, this does nothing
UPDATE UserProfile
SET InvalidatedOn = NOW()
WHERE UserName = ?
AND NOW() BETWEEN CreatedOn AND InvalidatedOn;
INSERT INTO UserProfile(UserName, Email, <etc.>, CreatedOn, InvalidatedOn)
VALUES(?, ?, <etc.>, NOW(), MAX_DATE());
In practice I would use default value constraints for the CreatedOn and InvalidatedOn columns. Writing it explicitly is just for this example.This becomes really important once you've lived with your database for several months/years, and especially when dealing with customers. For example, say you send an important email to a user, then they change their email address, then you send more important emails referring to the first one, which they now claim they never received in the first place. With an UPSERT, that user's email address for all of time looks like the most current version, and you can't figure out why they didn't receive the first one.
It's better to capture more data and realize down the road that you don't need it than it is to not capture enough and realize you need more. Databases are temporal objects. They grow over time and they change over time.
Another example from my past: a client would send us a dump of their data, every morning. We were performing analysis of their data and were supposed to wipe out our database and re-import it every morning. This meant on day N+1 we were re-importing the first N days again. I implemented this as a temporal database, which caught me some hell when the database grew in size. But, after 6 months, we discovered someone had attempted to alter the historical data. If we had blindly taken the data, they would have gotten away with their crime (and it literally was a crime, we had several government regulations we had to fulfill, the project being used in both a health-care and a financial capacity).
I've never regretted implementing my tables temporally. I've sometimes changed the implementation to a data warehouse setup, after learning that the historical data wasn't necessarily important to day-to-day operation, but I've never regretted having the historical data available, and I have almost always learned to regret NOT having historical data.
It has tended to make me really consider the notion of uniqueness within a database. What does the unique constraint mean? I think that people think of uniqueness to mean "at this point in time", but if you enforce it in the database, it means "for all time." In relational algebra, the unique constraint is really about enforcing a candidate key, a field that could satisfactorily be used as a key, but we've so happened to have chosen a different field as the actual primary key. In that context, given that we would want someone to be able to change their email address, I don't think "unique" email addresses make sense.
So many things change over time and so many seemingly "unique" fields turn out to not be unique in the real world that, for the most part, I've given up on the unique constraint. Full names are neither unique, nor are they final (people get married and change their name). Not even Social Security Numbers are necessarily unique, for a variety of reasons (http://www.computerworld.com/s/article/300161/Not_So_Unique).
CREATE UNIQUE INDEX profile_username_valid_idx_u
ON profile(username)
WHERE invalidated_at IS NULL;
The problem with enforcing this in app-space is it doesn't get around the isolation problem. In both cases, you are assuming that the same profile won't get concurrently updated twice by two different transactions. Since only the database can referee this issue, you must enforce the uniqueness there. I.e. this check must happen on transaction commit and your application has no way of knowing if it did. ALTER TABLE foo ADD CONSTRAINT temporal_pk EXCLUDE USING GIST (
identity_id WITH =,
valid_range WITH &&
);
It'd be nice if temporal foreign keys were supported too, where the existence of contiguous child rows through the valid_range of the parent was enforced, but I've had to use a trigger for that.[1]: http://www.postgresql.org/docs/9.3/static/rangetypes.html [2]: http://www.postgresql.org/docs/9.3/static/ddl-constraints.ht...
create table foo_history;
create view foo as select * from foo_history order by date_added desc limit 1;And I don't see what is so complex about it. It's meaning is explicit and clear. The difference from UPSERT is in the requirements. Any notion of an UPSERT would fail to meet my requirements, requirements I have far more often than I don't.
Also I may be wrong but I believe your view will only ever be filled with a single row. You'd need something along the lines of:
create view foo as
select distinct on (id) *
from foo_history
order by id, date_added desc;There's also a bunch of weird corner cases you need to expect. For example, you can have multiple valid UserProfile objects at once if two transactions insert concurrently. If you LIMIT 1 and sort by CreatedOn that may not be a huge issue, but it will look weird if you try to look at historical data.
Also, even if one considered this "no better" than DELETE+INSERT, it has significant other benefits for other purposes. The history of data is worth it alone.
But you've made me consider the issue more, and I think perhaps a better way to run this would be:
INSERT ... -- same as before
DECLARE @ID int;
SELECT @ID = SCOPE_IDENTITY();
UPDATE the_table
SET InvalidatedOn = CASE WHEN the_table.ID = @ID THEN MAX_DATE() ELSE NOW() END
WHERE surrogate_key = @KEY
AND InvalidatedOn > NOW();
then, even if you intentionally created a race condition, you should always end up with one and only one "definitive" record.