HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>Теги: базы данных, microsoft sql server, управление базами данных, update, ms sql</p>
1 <p>Теги: базы данных, microsoft sql server, управление базами данных, update, ms sql</p>
2 <p>В<strong>SQL Server</strong>есть конструкция<strong>UPDATE .. FROM</strong>которая отличается от стандартной и позволяет вам сразу написать<strong>JOIN</strong>таблицы, которую вы меняете с другими. В результате может получиться так, что на одну строку изменяемой таблицы приходятся две и более строк из результирующего набора. Какая строка в этом случае будет использоваться для UPDATE?</p>
2 <p>В<strong>SQL Server</strong>есть конструкция<strong>UPDATE .. FROM</strong>которая отличается от стандартной и позволяет вам сразу написать<strong>JOIN</strong>таблицы, которую вы меняете с другими. В результате может получиться так, что на одну строку изменяемой таблицы приходятся две и более строк из результирующего набора. Какая строка в этом случае будет использоваться для UPDATE?</p>
3 <p><strong>Случайная</strong>. Такой эффект называется<strong>недетерминированный UPDATE</strong>и, к сожалению, SQL Server<strong>не выводит никаких ошибок</strong>или предупреждений в таком случае.</p>
3 <p><strong>Случайная</strong>. Такой эффект называется<strong>недетерминированный UPDATE</strong>и, к сожалению, SQL Server<strong>не выводит никаких ошибок</strong>или предупреждений в таком случае.</p>
4 <p>Хочу рассказать вам историю про недетерминированный Update. Вспоминая страшные сказки на ночь, программисты вспоминают страшные истории с предыдущих проектов. Одну из таких историй я хочу сейчас вам рассказать.</p>
4 <p>Хочу рассказать вам историю про недетерминированный Update. Вспоминая страшные сказки на ночь, программисты вспоминают страшные истории с предыдущих проектов. Одну из таких историй я хочу сейчас вам рассказать.</p>
5 <p>В чёрном-чёрном городе тёмной-тёмной ночью... а нет, не то.</p>
5 <p>В чёрном-чёрном городе тёмной-тёмной ночью... а нет, не то.</p>
6 <p>Был в проекте программист, который не очень хорошо понимал, что выдаётся в результате SELECT. Ну, или понимал, но, может, был уставшим и не подумал. А правил он процедуру, которая обновляет баланс у клиента. Конечно, баланс можно было рассчитать по транзакциям, но в системе было поле, которое использовалось для отображения клиентам.</p>
6 <p>Был в проекте программист, который не очень хорошо понимал, что выдаётся в результате SELECT. Ну, или понимал, но, может, был уставшим и не подумал. А правил он процедуру, которая обновляет баланс у клиента. Конечно, баланс можно было рассчитать по транзакциям, но в системе было поле, которое использовалось для отображения клиентам.</p>
7 <p>В процедуре использовался не стандартный UPDATE, а существующий только в SQL Server UPDATE From. И написал программист такую конструкцию:</p>
7 <p>В процедуре использовался не стандартный UPDATE, а существующий только в SQL Server UPDATE From. И написал программист такую конструкцию:</p>
8 UPDATE customer SET balance = balance + SUM(transaction_queue.amount) FROM customer JOIN transaction_queue On transaction_queue.CustomerId = customer.CustomerId GROUP BY transaction_queue.CustomerId, transaction_queue.date<p>И, к сожалению, как это часто бывает, в большинстве случаев всё работало, потому что в transaction_queue были только движения за последние 10 минут, и программист даже ошибок никаких не увидел. Ошибка происходила только при смене даты: в конце суток транзакций было не так много, поэтому проблему заметили не сразу. И такой код работал на продакшн около месяца. Пришлось потом срочно править процедуру, пересчитывать баланс по клиентам и долго извиняться и рассказывать клиентам, что произошла ошибка и поэтому у них отображалось неверное количество средств на счету.</p>
8 UPDATE customer SET balance = balance + SUM(transaction_queue.amount) FROM customer JOIN transaction_queue On transaction_queue.CustomerId = customer.CustomerId GROUP BY transaction_queue.CustomerId, transaction_queue.date<p>И, к сожалению, как это часто бывает, в большинстве случаев всё работало, потому что в transaction_queue были только движения за последние 10 минут, и программист даже ошибок никаких не увидел. Ошибка происходила только при смене даты: в конце суток транзакций было не так много, поэтому проблему заметили не сразу. И такой код работал на продакшн около месяца. Пришлось потом срочно править процедуру, пересчитывать баланс по клиентам и долго извиняться и рассказывать клиентам, что произошла ошибка и поэтому у них отображалось неверное количество средств на счету.</p>
9 <h3>Давайте посмотрим, в чём же было дело?</h3>
9 <h3>Давайте посмотрим, в чём же было дело?</h3>
10 <p>Если бы разработчик сначала написал select (а именно это я рекомендую делать, чтобы проверить), то было бы видно, что группировка по двум (!) полям, а join по одному. В данном случае это означает, что на одного клиента придётся несколько полей, если в исходной таблице оказываются данные за несколько дат. А такое всегда бывает на границе суток.</p>
10 <p>Если бы разработчик сначала написал select (а именно это я рекомендую делать, чтобы проверить), то было бы видно, что группировка по двум (!) полям, а join по одному. В данном случае это означает, что на одного клиента придётся несколько полей, если в исходной таблице оказываются данные за несколько дат. А такое всегда бывает на границе суток.</p>
11 SELECT CustomerId, balance as BalanceCurrent, balance + SUM(transaction_queue.amount) AS BalanceNew, SUM(transaction_queue.amount) AS Delta FROM customer JOIN transaction_queue On transaction_queue.CustomerId = customer.CustomerId GROUP BY transaction_queue.CustomerId, transaction_queue.date<p>При использовании такого UPDATE всегда<strong>проверяйте, нет ли у вас недетерминированного изменения таблицы</strong>или используйте конструкцию WITH для изменения данных.</p>
11 SELECT CustomerId, balance as BalanceCurrent, balance + SUM(transaction_queue.amount) AS BalanceNew, SUM(transaction_queue.amount) AS Delta FROM customer JOIN transaction_queue On transaction_queue.CustomerId = customer.CustomerId GROUP BY transaction_queue.CustomerId, transaction_queue.date<p>При использовании такого UPDATE всегда<strong>проверяйте, нет ли у вас недетерминированного изменения таблицы</strong>или используйте конструкцию WITH для изменения данных.</p>
12 <p>Будьте аккуратны с такими Update’ами!</p>
12 <p>Будьте аккуратны с такими Update’ами!</p>
13  
13