Upsert anti-pattern in SQL Server
sqlperformance.com
sqlperformance.com
I believe the best option in PostgreSQL is still INSERT ... ON CONFLICT ...
In general, this is one of those spots where every single RDBMS behaves differently, so knowledge from one platform doesn't necessarily transfer well.
(Which, in turn, is one of the reasons why it's not necessarily a good idea to try to be database-agnostic.)
It's very ergonomic and intuitive. I understand why SQL Server needs to retain older forms, but why can't they also adopt these new forms that are more pleasant for the end user? I suppose the people buying licenses to SQL Server usually aren't the ones actually using it.
Everyone else uses different, DBMS-specific, non-portable syntaxes. A lot of them (e.g., PostgreSQL and Sqlite) have some similarities, but are still mutually incompatible.
I think that Microsoft arguably made the right decision by choosing to conform to the standard. It's just kind of unfortunate that the ISO standard syntax is (in my personal opinion) not very good.
The problem with TMTOWTDI is that it means there's also more than one way to have to understand and reason about. Which ultimately makes my job harder.
It's been a while, but as far as I remember, the main reason was that the behaviour of MERGE mandated by the SQL standard, wasn't clear enough and doesn't deal with race conditions when multiple transactions try the same insert (or update) (not so much the syntax itself).
MERGE simply doesn't provide the guarantees that most users want -- not in theory and not in practice. For example, the Oracle docs introduce MERGE by saying: "This statement is a convenient way to combine multiple operations. It lets you avoid multiple INSERT, UPDATE, and DELETE DML statements."
It's likely impossible to make the concurrency guarantees work sensibly in READ COMMITTED mode while supporting MERGE's very general semantics. There are subtle but very real problems with making it behave like ON CONFLICT in one context but not in others. In my view MERGE is a perfectly reasonable feature, but it just isn't what people want when they say they want to upsert.
The SQL standard deliberately underspecifies anything related to transaction isolation or concurrency. I cannot see the standard providing any guidance on upsert for this reason.
One unobvious (to me) but possibly necessary side effect of the INSERT...ON CONFLICT pattern is that it reserves a primary id for every row ahead of time, presumably because it does not know what will be updated and what will be inserted. We have a heavily updated table that started failing inserts one day because the primary id sequence hit the max int (2.1b), despite the table only reaching 100mm rows. The immediate fix was to reset the sequence to -1 and have it start going down...
I'm sympathetic, but unfortunately I cannot think of any tractable way of avoiding the problem at the implementation level. Maybe it would be possible to do something with an identity column (as opposed to a SERIAL column or a raw sequence) -- we have access to sufficient context there. Maybe we could skip consuming a sequence in the common case where there is a clear conflict at the start, and the sequence isn't accessed through the EXCLUDED pseudo table. We wouldn't actually promise anything more, but in practice we wouldn't burn through sequences at the same rate in cases where updates are common.
This is quite a significant project, but maybe it'll happen some day.
It really feels like it was a half-assed feature.
Great that they followed the standard, not so great that they made it nearly unusable with such a poor implementation.
I was super excited when we got SQL server 2008 at work (since upserts are a relatively common issue) and was devastated to learn all the issues with MERGE. I didn't care that the syntax was funky, I just wanted to avoid needing to manage transactions/locks/etc.
[0] https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-cond...
merge is a single, logically-atomic statement in sql server (with or without hints)
it doesn't have the same physical locking characteristics as a single insert or update statement, how could it?
INSERT INTO students(id, firstname, lastname, gender, d_o_b, email)
VALUES (1516, 'Gerard', 'Woodka', 'M', 'January 27 1995', 'gerardo_woodka@gmail.com')
ON CONFLICT (id) DO UPDATE SET
firstname = EXCLUDED.firstname,
lastname = EXCLUDED.lastname,
gender = EXCLUDED.gender,
d_o_b = EXCLUDED.d_o_b,
email = EXCLUDED.email;
At least it doesn't force you to repeat the values like many other servers. But still the repetition is bug prone and hard to maintain as the table changes. And it's just annoying to type.not everyone is stuck on a 15-year-old version of SQL Server. odds are you can go ahead and use modern language features. I can understand why a DBA consultant that frequently encounters production issues with old bugs would feel this way though.
https://www.mssqltips.com/sqlservertip/3074/use-caution-with...
In my own experience of working with MS SQL Server since 1998 (6.5), Microsoft are fairly conservative about changes to the core database engine, despite seeing new releases every year or so. New features tend to be stuff built on top of the engine.
In real life you're far more likely to encounter older versions of SQL running mission critical apps because upgrades are that one time where you're likely to screw up the crown jewels (your data), and so therefore many organisations tend to be conservative about that too. Now it's nice to get to work on a greenfield application where you can kick off on a shiny new version of SQL, but there's a good chance those bugs and behaviours are still likely to be present because the core engine is pretty much the same.
We've had multiple issues with SQL Server caused by the usage of MERGE statements. It's been bad enough that we've ended up banning them as an organization in favor of the block of "semantically weird shit". Why? Because that block has become common, clear, and most importantly, doesn't tend to cause weird deadlocks, race conditions, etc.
that depends on so much else going on with your database - fine policy but when I said "test" I didn't mean whatever you mean by "simple testing".
> caused by the usage of MERGE statements
"exacerbated by" is probably more accurate - just because replacing a merge statement with something else alleviated an issue doesn't mean that the root cause was some bug in SQL Server.
> MERGE, as implemented within other major systems and as described by the SQL standard does not require an atomic insert-or-update for even the simplest possible case of OLTP style insert-or-update.
> SQL MERGE as implemented within other systems doesn't require that a unique index be defined on the column or columns of the MERGE statement's outer join. I am not enthusiastic about the idea of falling back to table-level locking when an appropriate index isn't available. That could be a foot-gun for the large majority of users that just want to solve their basic insert-or-update problem.
A MERGE patch was submitted for pg11, it got bumped multiple time and returned with comments early 2019 with no visible activity since.
[0] https://www.postgresql.org/message-id/CAM3SWZRP0c3g6+aJ=YYDG...
Much easier to write.
DELETE FROM t WHERE id IN (SELECT id FROM new_data); INSERT INTO t SELECT id, other_field FROM new_data;
It seems so natural to me that I do not have to repeat field list.
Queries were mostly in context of ETL, so never considered performance issues. Never wrote such code for OLTP.
Quite a good pattern actually.
By the same author (who I highly respect):
https://www.mssqltips.com/sqlservertip/3074/use-caution-with...
- To much hidden complexity which can leaf to bugs if you don't know about the hidden complexity.
- To messy and/or misunderstood concurrency.
- Many People having fundamental misunderstandings about how to do things and how far transactions are useful for given (often implicit default) options.
One thing I had to learn the hard way is that many programmers which supposedly "know" about RDMS/SQL don't even understand isolation levels properly.
Also is it just me or is this way easier in postgres (with on conflict ...).
None of this stuff is hard to learn, and I'm not exactly Mr Ace Developer. But time and again I've been asked to debug/fix data inconsistencies that were caused simply because the developer didn't know what a transaction was, whilst running multiple inserts and updates, and one of them failed along the way.
But I use the shit out of transactions. I rather have contention issues than non-atomic operations done to corrupt our data. When I get to Facebook scale I'll let you know what I think about eventually-consistent and NoSQL, etc. ;)
No need to know what ACID is an acronym for (I forget it all the time) or how the isolation levels are called, but you need to know what to look out for (mainly data dependencies between read an writes in the same transaction having race conditions you need to "team" and multiple read inconsistencies)
Sure, but if you're mentoring a junior developer then I'd like to think you'd pass this knowledge on and make them understand its importance in a formal way, rather than some partial hand-wavey "this kinda means this". Get them to go and learn these fundamentals and test them on it, which is good for you because it keeps it fresh in your mind to just roll off the tongue at parties :)
I did know all the important thinks it stands for but not the exact definition. But I think the he/she meant that people didn't understand ACID and not that they didn't know the exact definition.
EDIT:
I would never expect someone to know the exact definition of ACID or SOLID or similar. But if they have a lot of experience with db they should always know the ideas behind it and even if they are not an expert (but will have to write SQL) I would expect them to know what problems rough to look out for and when they should make sure they are currently not running into such an problem by e.g. checking the postgres documentation. It's good.
ACID wasn't new to me, I've been around databases since the 80's in their various forms. But I learned the solids about ACID through, believe it or not, being sent on a jolly to learn me some Oracle back in '98 and then the concept being hammered home whilst taking the SQL Server module when acquiring my MCSD (that was a jolly-free self study to earn more bucks - which paid off).
Now I know a bunch of people poo-poo corporate certifications e.g. the MCP and MCSD etc, but I've yet to encounter in any modern code-school/bootcamp type educational material that covers these important fundamentals. And that's a shame because as developers it makes you think more carefully about decisions you make when interacting with a database product such as MS SQL, MySQL, Postgres etc, especially via ORM's. i.e. proper training material that sets up the knowledge fundamentals before being let loose.
I'm not having a go at the competency of folks that aren't aware of ACID, but it's a fundamental concept you need to be aware of when dealing with databases such as Oracle, SQL Server, MySQL, Postgres etc. Knowing this stuff allows you to build reliable and consistent scalable databases by balancing trade-offs around the ACID concept.
This can easily lead to an situation where people (wrongly) come to the conclusion that RDBs don't work anyway and they should just go to NoSQL DBs as it's "just easier" to not have to handle a scheme.
Which yes is completely flawed as a argument as doing NoSQL without any implicitly schema can bit you very hard and you still want to do transactions but now your DBMS might not support the transactions you need (instead of you just doing them incorrectly) so you "somehow" bolt them on top of it and if it fails now it's "clearly your fault". Except there is a good chance it isn't as some NoSQL DBMS are much more tricky to handle then they seem (like Mongo not necessary keeping to the Durability part in ACID |EDIT: In the past I don't know about now).
I have seen this fallacy or parts of it more then one time both in "real live" and comments on Hacker News.
Yesterday there was a item on a piece by rachelbythebay about checking for and dealing with unexpected conditions in your code as a practice that separated professionals from ... er, idiots.
This would appear to be a prime example of that antipattern -- in general, not just in SQL Server. If a tuple snuck into your relation while you weren't looking, don't you need to rollback and find out the current state of things?
edit: Rachel's piece: https://rachelbythebay.com/w/2020/09/02/check/
In OLTP, this practice (UPSERT) would cause all sorts of mayhem.
... because that requires a redundant query, its associate lookup cost, and in order to be sure that tuple wasn't added or removed between the time your check and the insert/update executes, either a write lock on the table or on the row itself (if it exists).
"Blind" inserts are the right way to go IMO.
Because you've got a unique constraint on some sort of natural key and the service this data is fed from doesn't have any guarantee it'll only deliver the information once. Or the feed can be replayed in the normal course of operation (e.g. system imports / synchronisations).
Handrolling your upsert creates extra failure modes aplenty and is very tricky to get right, which is why built-in support was ultimately introduced.
> In OLTP, this practice (UPSERT) would cause all sorts of mayhem.
So?
Whatever your answer is, UPSERT just combines that into a single more efficient statement. There's no new logic, just a better expression of it.
If you want examples: 1) analytics with `key(id,timestamp)` to avoid manually creating new rows as time goes on. 2) OLTP updates where we only want the latest data. 3) new data from multiple sources concurrently and we want the last write. 4) duplicate data and we want a single copy for each key.
With ON CONFLICT .. DO, it's in the database, which has advantages sometimes: it's typical to have multiple clients, and one database.
Even better, you can treat conflict as an expected condition, and specify what to DO when it arises.
An example in SQLite which I happened to be working on this week: I have a table with a (foreign key, ordinal) tuple as the primary key of the table. Bad things happen to the client code if there are duplicate ordinals, but the row itself is mutable, so: on conflict, update.
There's nothing left to check, this is simply a compact way for me to specify the correct behavior. The client can simply write new rows, without having to delete the old ones.
Our users have integrations which can update the catalogs "common" data. Descriptions can change, classification codes can change etc. A few users have several million articles, and they simply hand us a dump of them each night.
I tried initially handling this client side, load the articles, check if each article already existed, if so update else insert. If no changes don't do anything. I quickly discovered it used 4-5 hours each night when one of these big catalog changes started rolling in.
Then I rewrote it to issuing "insert ... on conflict" and just overwrote everything. Now it uses 15-20 minutes tops.