【ITニュース解説】Fixing DuckDB Extension Deadlock: From Query Strings to LIST Types
2025年09月04日に「Dev.to」が公開したITニュース「Fixing DuckDB Extension Deadlock: From Query Strings to LIST Types」について初心者にもわかりやすいように丁寧に解説しています。
ITニュース概要
DuckDBの拡張機能で、関数内でのSQLクエリ実行がデッドロックを引き起こす問題があった。解決策は、SQLクエリ文字列の代わりに、事前に集計済みのLIST型データを渡すことだ。これにより、データベースの安全性を高め、データ集計とグラフ表示の役割を分離し、性能も向上させた。
ITニュース解説
DuckDBは、高速なデータ分析に特化したインメモリデータベースである。このデータベースの大きな特徴の一つが、ユーザーが独自の機能を追加できる「拡張機能」の仕組みを備えている点だ。今回のニュース記事は、このDuckDBの拡張機能開発において遭遇した、デッドロックという深刻な問題とその解決策について解説している。
まず、問題がどのように発生したかを見てみよう。開発者が作成したのは、データ可視化のための拡張機能で、具体的には「スカラー関数」として実装されていた。スカラー関数とは、一つまたは複数の入力値を受け取り、単一の結果値を返す関数のことである。例えば、「合計を計算する関数」や「文字列を大文字にする関数」などがこれにあたる。今回の拡張機能では、このスカラー関数の中で、グラフ化に必要なデータを取得するために、さらに別のSQLクエリを実行していた。つまり、関数が実行されている最中に、データベースに対して「このデータも欲しい」と再び問い合わせをしていたのだ。
この「関数内でのSQLクエリ実行」という設計が、致命的な問題を引き起こした。それが「デッドロック」である。デッドロックとは、複数の処理(タスク)がお互いが必要とするリソースをロック(占有)し合い、それぞれが相手がロックしているリソースの解放を待つ状態になり、結果としてどの処理も進まなくなってしまう現象を指す。データベースシステムでは、データの一貫性を保つために、特定のデータやテーブルにアクセスする際にロックをかけるのが一般的だ。今回のケースでは、おそらく拡張機能が実行されているコンテキスト(文脈)が、データベースの内部ロックと競合したため、関数内のクエリがデッドロックを引き起こしたと考えられる。データベース自身が自分の処理中に、さらに別の処理を要求されると、リソースの管理が複雑になり、このような状況に陥りやすくなるのだ。結果として、データベース全体が応答しなくなり、システムが停止してしまう。
この深刻な問題に対し、開発者は根本的な解決策を見出した。それは、拡張機能の関数内でSQLクエリを実行するのをやめ、代わりに「LIST型」というデータ構造を直接引数として受け取るように変更することだった。LIST型とは、SQLのデータ型の一つで、複数の値をまとめて一つのリスト(配列)として扱うことができる型である。例えば、複数の数値や文字列を一つのカラムに格納するようなイメージだ。
この変更のポイントは、「データの集計」と「データの利用・可視化」という二つの異なる「関心事」を明確に分離した点にある。以前は、拡張機能の中で「データを集計するためのSQLクエリの実行」と「集計されたデータの可視化」の両方を行っていた。しかし、新しいアプローチでは、まず通常のSQLクエリを使って、DuckDBのクエリエンジンがデータを集計し、その結果をLIST型として準備する。そして、拡張機能の関数は、この既に集計されてLIST型にまとめられたデータを直接受け取り、可視化の処理だけを行う。
具体的な変更点としては、拡張機能の関数定義が大きく変わった。以前はSQLクエリ文字列(LogicalType::VARCHAR)を引数として受け取っていたが、変更後はカテゴリ名を格納するLIST(LogicalType::VARCHAR)と、対応する数値を格納するLIST(LogicalType::DOUBLE)といった、LIST型を直接受け取るようにした。関数内部では、受け取ったLIST型データから、要素を一つずつ取り出して処理するロジックに変更された。これにより、拡張機能の関数が実行中にデータベースに対して新たなSQLクエリを発行する必要がなくなり、デッドロックの原因となっていた内部的な競合が解消されたのである。
実際にこの拡張機能を使用する場合、まずDuckDBを起動し、作成した拡張機能をロードする。その後、データをテーブルに挿入する。最も重要なのは、拡張機能を呼び出すSELECT文だ。例えば、売上データから棒グラフを作成する際に、以前であればカテゴリ名と売上金額をそれぞれSQLクエリとして渡していたかもしれないが、新しい方式では「LIST(category ORDER BY amount DESC)」のように、DuckDBのLIST関数を使って、あらかじめカテゴリ名と金額をそれぞれLIST型として集計し、その結果を拡張機能の引数として渡す。
この処理の裏側では、いくつかのステップが実行されている。まず、SQLクエリに含まれるLIST()関数が、DuckDBのクエリエンジンによってデータを集計し、カテゴリと金額のリストを作成する。次に、この集計されたLIST型データが、作成した拡張機能の関数に渡される。拡張機能の関数は、このデータを受け取り、一時的なファイルに書き出す。最後に、このファイルに書かれたデータを元に、Rust言語で書かれた別のプロセス(Iced GUIというフレームワークを使用)が起動され、グラフが表示される。GUIアプリケーションを拡張機能のプロセスとは完全に分離した別のプロセスとして実行することも、重要な設計原則だ。これにより、GUIアプリケーションがデータベースの内部スレッドやリソースと競合するのを防ぎ、システムの安定性を高めることができる。
この解決策は、いくつかの重要な教訓を示している。一つは、データベースの拡張機能内では、決してSQLクエリを「ネスト」して実行すべきではないということだ。二つ目は、データの集合を扱う際には、LIST型のような「集約型」を積極的に利用すべきであるということ。そして、データ集計(SQLの役割)とデータ可視化(拡張機能の役割)のように、「関心事を分離」して設計することの重要性である。さらに、グラフィカルユーザーインターフェース(GUI)のような独立したアプリケーションは、データベースのプロセスとは完全に分離した別のプロセスとして実行するべきだということも示している。
このアプローチは、安全性だけでなく、パフォーマンスの向上にも大きく貢献する。拡張機能の関数内でSQLクエリを解析するオーバーヘッドがなくなるため、処理が高速化される。また、集約されたデータに直接メモリからアクセスできるため、効率が良い。さらに、DuckDBのクエリプランナーが全体のクエリを最適化できるようになり、より効率的な実行計画が立てられるようになる。このように、適切な関数設計とデータ型の選択は、データベース拡張機能の開発において、一般的な落とし穴を回避し、クリーンで効率的なインターフェースをユーザーに提供するための鍵となるのだ。