
2026/05/13 2:56
「SQL:本来間違えやすい言語」とします。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Summary:
標準的な SQL データベースは、深刻なデータ破損を引き起こす危険な競合制御のバグを頻繁に抱え込んでおり、これらはしばしば経済的損失や最悪の場合には致命的な医療事故へと繋がります。根本的な問題は、開発者が複雑なトランザクションラッパーと明示的なロックを標準 TSQL 内で手動で管理する必要があり、コードのサイズを約 50% 増大させつつ可読性を著しく低下させることにあります。一般的な失敗例には、トランザクションが失敗した際に資金が消失する原子性問題、過引き落としを可能にするチェック時点でのエラー、循環的なロック依存関係により発生するデッドロックが含まれます。これらの問題は、医療システムのような高リスク環境において、コードが可読であることが正しい動作を保証するとは限らないことを示しており、医薬品のレコードを見逃すだけでも命に関わる可能性があります。これらを解決するために、著者はエラーの多い SQL から脱却し、Rust の「無恐れの競合制御」哲学に触発された代替システムへの移行を提案します。この新アプローチでは、原子性トランザクションをデフォルト動作とし、ロック管理を簡素化するとともに、静的解析を実行前に潜在的なデッドロックを検出します。最終的に、重要なデータを扱う業界は、これらの安全なパラダイムを採用するか、脆弱な手動による補正に依存し、論理に過剰でリスクの高いデータベース手順から離脱する方向へと移行せざるを得なくなります。
本文
SQL およびリレーショナルデータベースシステムの実装は、重大な並行処理バグを誤って導入しやすくしています。以下に、T-SQL で示した典型的な資金移転手続きがあります:アリスがボブに 10 ドルを送金したい場合、アリスが口座を引き越す(過剰出資する)のを防ぐため、最初に彼女の残高が十分かどうかを確認します。このコードは一見すると完全に合理的に見えますが、実はいくつか致命的なバグを含んでいます。これらのバグは何か気づきましたか?
DECLARE @balance INT; SET @balance = ( SELECT balance FROM accounts WHERE owner = 'alice' ); IF @balance >= 10 BEGIN UPDATE accounts SET balance = balance - 10 WHERE owner = 'alice'; UPDATE accounts SET balance = balance + 10 WHERE owner = 'bob'; END
原子性(Atomicity)
まず、この手続きが途中で中止されてしまった場合、アリスの口座から資金を引き出しながらボブへの振込を完了させなかったという状況になり得ます。アリスはそれを喜んでくれるでしょうか?加えて、その過程で資金が消失しています。すべての転送が成功するか、あるいは何も起こらないのか——どちらか一方でなければならないはずです。解決策は、この手続きをトランザクションで囲むことです:
BEGIN TRANSACTION; DECLARE @balance INT; SET @balance = ( SELECT balance FROM accounts WHERE owner = 'alice' ); IF @balance >= 10 BEGIN UPDATE accounts SET balance = balance - 10 WHERE owner = 'alice'; UPDATE accounts SET balance = balance + 10 WHERE owner = 'bob'; END COMMIT TRANSACTION;
TOCTOU(チェックと使用間の時間差)バグ
これで完了でしょうか?いえ、まだです。 suppose アリスがボブへの 2 つの並行する転送命令 T1 と T2 を同時に発行したと想定してみましょう。起こることを見てみましょう:
- T1: アリスの口座残高を確認
- T2: アリスの口座残高を確認
- T1: アリスの口座から 10 ドルを引き出す
- T2: アリスの口座から 10 ドルを引き出す
- T1: ボブの口座に 10 ドルを振込む
- T2: ボブの口座に 10 ドルを振込む
注目してください:T2 は、T1 がアリスの口座から任意の資金を引き出す前に残高をチェックしています。したがって、T2 が実際に引き出しを実行した時点で、口座が過剰出資状態(マイナス残高)になり得ます。これは「チェック時点と使用時点」(Time-of-Check to Time-of-Use, TOCTOU)バグです:プリコンディション(事前条件)を確認した時点と、それに基づいて動作を実行する時点の間に状態が変わってしまっていることを意味します。
解決策は、トランザクションが完了するまでアリスの口座をロックすることです。隔離レベルを変更してロックを自動的に取得させたり、明示的にアカウント行を手動でロックしたりできます:
BEGIN TRANSACTION; DECLARE @balance INT; SET @balance = ( SELECT balance -- これは概ね SELECT FOR UPDATE に相当します FROM accounts WITH (UPDLOCK) WHERE owner = 'alice' ); IF @balance >= 10 BEGIN UPDATE accounts SET balance = balance - 10 WHERE owner = 'alice'; UPDATE accounts SET balance = balance + 10 WHERE owner = 'bob'; END COMMIT TRANSACTION;
UPDLOCK ヒントは、SELECT が実行された時点でアリスの口座に対して行レベルのロックを取得します。他のトランザクションがアリスの口座を変更しようとしても、そのロックが解放されるまでブロックされます。
デッドロック
次に、アリスとボブが同時に互いにお金を送金しようとした場合どうなるでしょうか?もう一度トランザクションの流れを見てみましょう:
- T1: アリスの口座にロックを取得
- T2: ボブの口座にロックを取得
- T1: アリスの口座残高を確認
- T2: ボブの口座残高を確認
- T1: アリスの口座から 10 ドルを引き出す
- T2: ボブの口座から 10 ドルを引き出す
- T1: ボブの口座を更新しようとしたが、T2 によってロックされていたためできない
- T2: アリスの口座を更新しようとしたが、T1 によってロックされていたためできない
T1 はボブの口座に対する T2 のロックを待ち、逆に T2 もアリスの口座に対する T1 のロックを待っています——つまりデッドロックに陥ってしまいました。解決策は、全てのロックを事前に取得することです:
BEGIN TRANSACTION; DECLARE @balance INT; SELECT owner FROM accounts WITH (UPDLOCK) WHERE owner IN ('alice', 'bob'); SET @balance = ( SELECT balance FROM accounts WHERE owner = 'alice' ); IF @balance >= 10 BEGIN UPDATE accounts SET balance = balance - 10 WHERE owner = 'alice'; UPDATE accounts SET balance = balance + 10 WHERE owner = 'bob'; END COMMIT TRANSACTION;
結論
元のコードに含まれていた並行処理のバグは修正できましたが、その過程でコード量は約 50% 増え、可読性が低下してしまいました。確かに、より習慣的な方法(idiomatic approaches)でこの問題を解決する他の手段があることは認めることができますが、ポイントは依然として変わりがありません:一見すると完全に合理的に見える SQL プログラムにも、重大なバグがちりばめられている可能性があるということです。
もしソーシャルメディアサイトを構築しているなら、ユーザーが投稿を二回でも「いいね!」付けてしまったとしても世界が終わるわけではありません。しかし、医療システムにおいて患者が投薬を受け取ったことを記録し忘れた場合などは、致命的な結果をもたらす可能性があります。正解性が求められるシステムにおいては、より優れたツールが必要なのです。
提案される解決策
私は、SQL の代替手段として、「Rust の無畏な並行処理アプローチ」を採用するシステムを望んでいます—that is to say、正しい振る舞いをデフォルトとし、必要に応じて「unsafe」なエスケープハッチを提供するものです。具体的な提言としては:
- トランザクションをデフォルトで原子性とする;ユーザーが中間の「チェックポイント」状態を保存したい場合は、明示的に指示すること。
- ユーザー自身がロック管理を行い、データベースオブジェクトの変更前に正しいロックが取得されていることを保証する。
- 静的解析を用いて潜在的なデッドロックを検出する(これは困難な問題であり、現在も研究が続けられている分野です)。決定論的データベースシステムも一つの解決策となり得ます。
このシステムには他のトレードオフも伴います;例えば、現代の SQL システムよりも低いスループットになる可能性があります。しかし、それは問題ありません——正解性が重要でないユースケースには依然として SQL を使用できます。