0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

パス文字列でもバイナリでもない。DatabricksのFILE型を試す

0
Posted at

はじめに

Databricksに FILE という新しいデータ型がベータ版で追加されました。非構造化ファイルへの「ガバナンスされた参照」をテーブルの列として持てるようにするものです。

PDFや画像をテーブルで管理したい、という要望は昔からありました。これまでは列にパスの文字列を入れるか、バイナリを丸ごと突っ込むかの二択で、どちらにも決定的な弱点があります。FILE 型はその二択を置き換えるものとして設計されています。

何が新しくて、これまでの方法と何が違うのか。実際に手元のワークスペースで動かして確かめたので、期待通りだった点と、そうでなかった点も含めて書いていきます。

FILE型とは何か

FILE 型の値は、ファイルの実体ではなく、ファイルへの参照とそのメタデータを保持します。中身は5つのフィールドです。

フィールド 内容
uri STRING ファイルのURI。nullにはならない
offset BIGINT ファイル内のオフセット (バイト)
size BIGINT ファイルサイズ (バイト)
content_type STRING 分かる場合のMIMEタイプ
checksum STRING <アルゴリズム>:<ダイジェスト> 形式の整合性トークン


Databricksドキュメント「FILE タイプと非構造化データ」より。clip列に入っているのは { path, size } だけで、1.8GBの動画本体は別のストレージにある

ポイントは、この5つがテーブル側に保持されているので、メタデータを見るだけならファイル本体を一切読まなくていいという点です。「1GBの動画が入った列に対してサイズの合計を出す」ようなクエリで、1GBを読みに行く必要がなくなります。

サポートされるのはDeltaテーブルのみ、Databricks SQLまたはDatabricks Runtime 18 LTS以上です。

これまでのやり方の何が困っていたのか

FILE 型のありがたみは、従来の2つのやり方の欠点を知らないと分かりにくいので、そこから整理します。

STRINGでパスを持つ場合

一番よくやるのがこれです。/Volumes/.../report.pdf のような文字列を列に入れておく。

軽くて分かりやすいのですが、その文字列は単なる文字列であって、ファイルの存在を何も保証しません。別のワークロードがファイルを消しても、テーブルには古いパスが残り続けます。逆にテーブルの行を消しても、ファイルはストレージに居座ります。サイズもバージョンも分からないので、メタデータが欲しければ都度ストレージに問い合わせる必要があります。

テーブルとファイルの間にガバナンス上のつながりが一切ない、というのが本質的な問題です。

BINARYで実体を持つ場合

もう一方の極が BINARY 列に生バイトを入れる方法です。テーブルとファイルが一体になるので、整合性の問題は消えます。

ただしこちらには別の問題があります。バイト列がテーブルのデータファイルの中に物理的に格納されるという点です。今回の検証で使ったファイルは合計455,246バイトでしたが、これを BINARY で持てばDeltaテーブル自体が約455KB分太ります。マルチギガバイトの動画なら、テーブルがそのままギガバイト級になります。

そしてファイルサイズを知りたいだけのクエリでも、こうなります。

SELECT length(content) FROM clips;

length() を計算するには中のバイト列を読み出す必要があります。5MBのPDFが100件あれば、サイズを数えるためだけに500MBをディスクから読んでメモリに展開することになる。これがマテリアライズ (実体化) と呼ばれる状態です。メタデータしか要らないのに、毎回ファイルの中身そのものが展開されてしまいます。

Databricksのドキュメントでも BINARY の用途はデフォルトで64KBまでの小さなオブジェクトと位置づけられていて、例に挙がっているのはサムネイルを行データとインラインで持つケースです。ドキュメント本体を入れる想定ではありません。

FILE型は参照とメタデータだけを持つ

3つの方式で「テーブルの1セルに実際に何が入っているか」を並べると、違いがはっきりします。

方式 セルの中身
STRING /Volumes/.../orders_sample.csv
BINARY [1747バイトの生データ]
FILE {"uri":"dbfs:/Volumes/.../orders_sample.csv", "size":1747, "content_type":"text/csv", "checksum":"ETAG:\"54fcd27a...\""}

FILE のセルは数百バイトです。ファイル本体はボリュームに置いたまま。だからサイズの集計も種別によるフィルタも、ボリュームに一切アクセスせずテーブルだけで完結します。

SELECT sum(file.size) FROM documents WHERE file.content_type = 'image/png';

BINARY で同じことをやるなら、sizecontent_type を得るために1747バイトを読み出す必要があります。ファイル本体が読まれるのは、AI関数やUDFが実際に処理するときだけ。この「必要になるまで読まない」という性質が、大量の非構造化ファイルを扱ううえで効いてきます。

で、STRINGでパスを持つのと何が違うのか

ここが自分でも一番引っかかったところです。「参照を持つ」だけならSTRINGでパスを入れるのと同じでは、と。

違いは4つあります。

メタデータが一緒に入っている。 STRINGは文字列だけなので、サイズや種別が欲しければ都度ストレージに問い合わせるか、自分でメタデータ列を用意して同期し続ける必要があります。FILE は最初から持っています。

書き込み時点で存在が検証される。 STRINGはタイプミスした存在しないパスでも平然と入ります。FILE は違います。

SELECT to_file('/Volumes/.../ghost.csv');
-- TO_FILE_ERROR.FILE_NOT_EXISTS

存在しないファイルへの参照はそもそも作れないので、テーブルに入っている時点で「作成時には確実にあった」ことが保証されます。エラーを避けたい場合は try_to_file を使うとnullが返ります。

AI関数やUDFにそのまま渡せる。 実用上はこれが一番効きます。ai_parse_document はパス文字列を受け取りません。バイト列か FILE 値です。STRINGでやるなら read_files(..., format => 'binaryFile') でバイトを読み込む経路を挟むことになり、そこで結局全体をマテリアライズします。

FILE MANAGED ならライフサイクルが連動する。 STRINGにはこれが絶対にありません。行を消してもファイルは孤児として残り続けます。

整理するとこうなります。FILE を1列にまとめず、EXTERNALとMANAGEDを分けたほうが実態に近いです。

STRINGパス BINARY FILE EXTERNAL FILE MANAGED
テーブルに入るもの パス文字列 生バイト 参照 + メタデータ 参照 + メタデータ
テーブルのサイズ ほぼ増えない ファイル総量ぶん膨らむ ほぼ増えない ほぼ増えない
メタデータ参照 都度問い合わせ 全体をマテリアライズ テーブルから即座に テーブルから即座に
作成時の存在検証 なし (実体を持つ) あり あり
AI関数に直接渡せる 不可
checksum なし なし null (list_files 経由) ETAGが入る
ライフサイクル連動 なし テーブルと一体 なし あり
想定サイズ 制限なし 小さいもの (64KB程度) 制限なし 制限なし

この表で言いたいのは、STRINGの問題を全部解決するのは FILE MANAGED のほうだということです。FILE EXTERNAL はライフサイクルが連動しないので、実態としては「メタデータ付きで、AI関数に渡せて、作成時に存在検証されるSTRING」に近い。

もうひとつ FILE EXTERNAL で押さえておくべきなのは、メタデータは書き込み時点のスナップショットだという点です。ボリュームの実ファイルが後から差し替えられても、テーブルの size は古いままです。checksumがあれば比較で検知できますが、後述するように list_files 経由だとそのcheckumがnullなので、実質検知できません。

Databricksがファイルレベルの権限とコンプライアンスを重視するワークロードに FILE MANAGED を推奨しているのは、この差があるからだと思われます。

2つのモードの使い分け

FILE 型を列として宣言するときは、FILE MANAGEDFILE EXTERNAL のどちらかを必ず指定します。FILE 単体では列として宣言できません。

FILE EXTERNAL はUnity Catalogボリューム上の既存ファイルを参照するだけで、ファイルはコピーされず元のパスに留まります。同じファイルを別のツールが読んでいる、といった状況を壊したくないときに使います。

FILE MANAGED はファイルを FileSpace と呼ばれるマネージドストレージにコピーします。MLトレーニングやRAGのように、テーブル経由でしかファイルを触らないワークロード向けです。

権限のかかり方も違います。FILE EXTERNAL はボリュームの READ VOLUME 権限だけで制御されますが、FILE MANAGED はテーブルの SELECT とボリュームの READ VOLUME の両方が必要になります。ファイル単位のアクセス制御をテーブルの権限に寄せられる、というのが FILE MANAGED の売りです。

なお、クエリを書く側から見れば両者に違いはありません。ドット記法もAI関数への引き渡しも同じです。

環境の準備でひとつ注意

FILE 型はベータ機能です。ワークスペース管理者がプレビューページからアクセスを制御しているので、まずここで有効化されているか確認してください。有効になっていないと、そもそも FILE MANAGED の列を宣言できません。

Screenshot 2026-08-12 at 11.25.38.png

そのうえで、FILE 型はサーバーレスノートブックではサポートされていません。ここは最初のつまずきポイントになりえます。

一方で、サーバーレスSQLウェアハウスにアタッチしたノートブックではサポートされます。SQLエディタから叩くのが一番手っ取り早く、PySparkと組み合わせたい場合はDBR 18 LTS以上のクラシックコンピュートを使う、という使い分けになります。

「サーバーレス全般がダメ」ではないので、そこは誤解しないようにしてください。パイプライン化するときのサーバーレスLakeflowパイプラインはむしろ推奨側です。

FILE EXTERNALで既存ボリュームを参照する

ここから実際に触っていきます。ボリュームにはdocx 1つ、PNG 3枚、CSV 1つを置いてあります。

Screenshot 2026-08-12 at 11.29.26.png

list_files テーブル値関数でボリュームを走査して、そのままテーブルに投入します。

CREATE OR REPLACE TABLE takaakiyayoi_catalog.filetype.documents AS
SELECT
  monotonically_increasing_id() AS id,
  regexp_extract(path, '([^/]+)$', 1) AS file_name,
  path AS source_path,
  file
FROM list_files('/Volumes/takaakiyayoi_catalog/filetype/source_files/');

list_filespath / size / modification_time / file の4列を返し、このうち fileFILE 値です。ファイル名を別カラムに切り出しているのは後で効いてきます (理由は後述)。

メタデータはドット記法で読めます。

SELECT id, file_name, file.uri, file.size, file.content_type, file.checksum
FROM takaakiyayoi_catalog.filetype.documents
ORDER BY id;

結果がこちらです。

id file_name size content_type checksum
0 20260430_Databricks_LLM_Governance_Guide.docx 212598 application/vnd.openxmlformats-...wordprocessingml.document null
1 Screenshot 2026-08-06 at 15.41.49.png 204221 image/png null
2 Screenshot 2026-08-06 at 15.43.14.png 19265 image/png null
3 Screenshot 2026-08-06 at 15.44.19.png 17415 image/png null
4 orders_sample.csv 1747 text/csv null

content_type が拡張子ではなく中身から正しく判定されています。docxがZIPとして扱われずOOXMLのMIMEタイプになっているのは地味に嬉しいところです。

一方で checksum が全部nullです。これは仕様通りで、list_filesread_files はチェックサムを埋めません。埋まるのは to_filecreate_filecopy_file を使い、かつオブジェクトストアが ETAG を返したときだけです。

もうひとつ、返ってきた uri には dbfs: プレフィックスが付きます。

dbfs:/Volumes/takaakiyayoi_catalog/filetype/source_files/orders_sample.csv

入力は /Volumes/... で渡しているので、uriを文字列比較したりパースする処理を書くときには気をつけてください。

FILE MANAGEDに取り込む

FileSpaceはソースと別のボリュームにする

ここが最大のハマりどころでした。FileSpace として指定するボリュームを、元ファイルが入っているボリュームと同じにすると、INSERTが以下のエラーで落ちます。

Cannot get file metadata under managed storage

エラーメッセージからは原因が読み取りにくいのですが、マネージドストレージのロケーションは外部テーブルや外部ボリュームと重複できない、というUnity Catalogの原則に引っかかっています。取り込み先が取り込み元と同じ場所を指しているので、外部ファイルとしてのメタデータ取得が拒否される形です。

FileSpace専用のボリュームを別途作るのが正解です。

CREATE VOLUME IF NOT EXISTS takaakiyayoi_catalog.filetype.filespace;

CREATE TABLE takaakiyayoi_catalog.filetype.reports (
  id BIGINT,
  file_name STRING,
  attachment FILE MANAGED
)
TBLPROPERTIES ('databricks.filespace-preview' = '/Volumes/takaakiyayoi_catalog/filetype/filespace/');

INSERT INTO takaakiyayoi_catalog.filetype.reports
SELECT id, file_name, file FROM takaakiyayoi_catalog.filetype.documents;

これで5件が取り込まれます。FILE EXTERNAL 列の値をそのまま FILE MANAGED 列に書き込むと、Databricksがマネージドストレージへ取り込んでくれます。

なお既存テーブルに FILE MANAGED 列を後付けする場合は、テーブルプロパティを設定してから列を追加します。順番を逆にすると失敗します。

取り込むとチェックサムが埋まる

EXTERNAL側とMANAGED側を並べて比較してみます。同じファイルなので、単純に考えれば同じメタデータが返ってくるはずです。

SELECT
  d.file_name,
  d.file.checksum          AS external_checksum,
  r.attachment.checksum    AS managed_checksum,
  d.file.content_type      AS external_ctype,
  r.attachment.content_type AS managed_ctype
FROM takaakiyayoi_catalog.filetype.documents d
JOIN takaakiyayoi_catalog.filetype.reports r ON d.id = r.id
ORDER BY d.id;

ところが結果は違いました。

file_name external_checksum managed_checksum
20260430_Databricks_LLM_Governance_Guide.docx null ETAG:"146eb7d10a6b960f78cb601f723be2c6"
Screenshot 2026-08-06 at 15.41.49.png null ETAG:"d853e7d28ca02f3205c56cbd19cbdce0"
Screenshot 2026-08-06 at 15.43.14.png null ETAG:"ab3f7581fc37efca326123b440eef5bb"
Screenshot 2026-08-06 at 15.44.19.png null ETAG:"d8596307812876f2d0151772372b3f05"
orders_sample.csv null ETAG:"54fcd27a10cee6bd410ee85a7ff93fdc"

取り込みの過程でチェックサムが埋まります。 FileSpaceへコピーする際にオブジェクトストアがETAGを返すため、EXTERNALではnullだったフィールドがMANAGEDでは値を持つわけです。content_type のほうは両方とも保持されていました。

「チェックサムでファイルの同一性を検証する」という使い方を想定しているなら、list_files から作った FILE EXTERNAL 列では実現できません。ここは設計時に効いてきます。

なお FILE 列は結合キーに使えないので、上のクエリでは id で結合しています。順序が保証されない型なので、パーティション列・クラスタリング列・MAPのキー・グループ化式にも使えません。ファイル単位で集計したいときは uri フィールドでグループ化します。

ファイル名はUUIDに置き換わる

取り込み後のuriを見ると、こうなっています。

dbfs:/Volumes/takaakiyayoi_catalog/filetype/filespace/ebe20fb3-19b8-40c7-bf88-c7f0195589d6

カタログエクスプローラでFileSpaceのボリュームを開くと、この違いが一目で分かります。

Screenshot 2026-08-12 at 11.35.22.png

元のファイル名も拡張子も残りません。 20260430_Databricks_LLM_Governance_Guide.docx はUUIDになります。サイズと content_type は保持されるので同一ファイルだと追跡はできますが、ファイル名で何かを判断する処理を書いているなら壊れます。

最初のテーブル作成で file_name を別カラムに切り出しておいたのはこのためです。公式ドキュメントの取り込み例がわざわざ _metadata.file_name を別カラムに入れているのも、同じ理由だと思われます。ファイル名が必要なら、取り込み前に別カラムへ退避しておく。これは覚えておいた方がいいポイントです。

FILE値を作る関数

FILE 値はテーブルから取り出す以外に、関数からも作れます。

create_file はコンテンツをアップロードして FILE を返します。

SELECT create_file(
  content => 'FILE型の検証用テキストです。\n2026-08-12',
  destination_path => '/Volumes/takaakiyayoi_catalog/filetype/source_files/note.txt',
  if_file_exists => 'overwrite'
) AS f;

返ってきた値がこちらです。

{"uri":"dbfs:/Volumes/takaakiyayoi_catalog/filetype/source_files/note.txt",
 "size":51,
 "content_type":"application/octet-stream",
 "checksum":"ETAG:\"7f46c67bae19591e1fd6927be892de64\""}

チェックサムは期待通り埋まりました。ただし content_typeapplication/octet-stream です。.txt に書いたUTF-8のテキストなのに、テキストとは判定されませんでした。

create_filecontent_type を省略すると入力バイトから推論しますが、推論の材料はバイト列であって、保存先パスの拡張子ではありません。日本語テキストは判定に失敗するようです。content_type で後段の処理を分岐させる設計なら、明示的に指定した方が安全です。

SELECT create_file(
  content => my_binary_col,
  destination_path => '/Volumes/.../image.png',
  content_type => 'image/png'
) FROM raw_uploads;

既存パスから参照を作るのが to_file、失敗時にnullを返すのが try_to_file です。

SELECT to_file('/Volumes/takaakiyayoi_catalog/filetype/source_files/note.txt').checksum;
-- ETAG:"7f46c67bae19591e1fd6927be892de64"

SELECT try_to_file('/Volumes/takaakiyayoi_catalog/filetype/source_files/ghost.csv');
-- null

to_file で読み直したチェックサムは create_file の返り値と完全に一致しました。存在しないパスを to_file に渡すと TO_FILE_ERROR.FILE_NOT_EXISTS になるので、パイプラインの中では try_to_file を使うことになると思います。

このほかに copy_file / try_copy_file もあります。

ai_parse_documentにFILEを渡す

FILE 型のうまみが一番出るのがここです。列をそのままAI関数に渡せます。

SELECT
  file_name,
  file.content_type,
  ai_parse_document(file) AS parsed
FROM takaakiyayoi_catalog.filetype.documents
WHERE file_name LIKE '%.docx' OR file_name LIKE '%.png';

docx 1件とPNG 3件、計4件すべてが成功しました。ドキュメントのサンプルがPDFと画像中心だったのでdocxは弾かれるかと思っていましたが、通ります。

docxの結果を抜粋します。

{"confidence":1, "content":"LLM Governance and Best Practices on Databricks", "type":"title"}
{"confidence":1, "content":"1. Routing Multiple LLMs", "type":"section_header"}

typetitle / section_header / text / figure / page_footer が分類され、それぞれに bbox (座標) と confidence が付きます。バージョンは2.0が使われていました。

面白かったのは信頼度の差です。docxは構造がそのまま取れるのでconfidenceが1で揃うのに対し、スクリーンショットのOCRは0.28から0.99までばらつきます。ロゴのアスキーアート部分は type: figure として扱われ、description に「A grayscale image displays a horizontal arrangement of blocky, pixelated letters.」という説明文が入っていました。画像として認識したうえで言語化までしています。

content_type を条件にして処理を分岐させれば、ファイル種別ごとに違うAI関数を当てるパイプラインが素直に書けます。ここが FILE 型の実用上の主戦場になりそうです。

ガベージコレクションはまだ手動

FILE MANAGED の売りは「行を削除すればファイルもガベージコレクションの対象になる」ことですが、ベータ版では自動ガベージコレクションがサポートされていません

実際に確かめてみます。

SELECT count(*) AS files_before
FROM list_files('/Volumes/takaakiyayoi_catalog/filetype/filespace/');

DELETE FROM takaakiyayoi_catalog.filetype.reports WHERE id = 0;

SELECT count(*) AS files_after
FROM list_files('/Volumes/takaakiyayoi_catalog/filetype/filespace/');

結果は files_beforefiles_after も同じ。行は4件に減りましたが、ファイルは1つも消えていません。

さらに分かりやすいのが、ノートブックを流し直したときの挙動です。この検証では途中で DROP TABLE してテーブルを作り直しているのですが、実行のたびにFileSpaceのファイルが5件ずつ増えていきます。テーブルは常に5行なのに、ボリュームの中身だけが積み上がっていく。

つまり行を削除してもテーブルごと削除しても、ファイルは残ります。「行の削除でガベージコレクションの対象になる」というのは、あくまで対象としてマークされるだけで、ベータ版では実際の回収が走らないということです。

試行錯誤しながら検証しているとFileSpaceが静かに肥大化していくので、list_files でファイル数を時々確認する習慣をつけたほうがいいです。公式ドキュメントにクリーンアップ用のノートブックが用意されているので、ベータ期間中はこれを併用することになります。

まとめ

FILE 型を触ってみて分かったことをまとめます。

  • FILE 型は参照 + メタデータを持つ型。STRINGの「存在を保証しない」問題と、BINARYの「読むたびに全体をマテリアライズする」問題の両方を回避する
  • 列として宣言するときは FILE MANAGEDFILE EXTERNAL の指定が必須。Databricksはファイルレベルの権限を重視するなら FILE MANAGED を推奨している
  • サーバーレスノートブックでは使えない。 サーバーレスSQLウェアハウスにアタッチしたノートブックやSQLエディタなら動く
  • FileSpaceをソースと同じボリュームにすると Cannot get file metadata under managed storage で落ちる。 専用ボリュームを分ける
  • list_files から作った FILE EXTERNAL 列はチェックサムがnull。FILE MANAGED に取り込むとETAGが埋まる
  • FILE MANAGED に取り込むとファイル名がUUIDになる。 ファイル名が必要なら取り込み前に別カラムへ退避する
  • create_filecontent_type 推論はバイト列を見ており、保存先の拡張子は見ない。日本語テキストは application/octet-stream になった
  • ai_parse_document はdocxもPNGも処理できる。typebboxconfidence 付きの構造化結果が返る
  • ベータ版では自動ガベージコレクションが動かない。行を消してもテーブルを消してもFileSpaceのファイルは残り、検証を流し直すたびに積み上がっていく

一番の収穫は、FILE EXTERNALFILE MANAGED が「ファイルをコピーするかしないか」だけの違いではなかったことでした。チェックサムの有無もファイル名の扱いも変わります。クエリを書く側からは同じに見えるので、どちらを選ぶかは取り込み時点の設計判断になります。RAGやMLトレーニングのようにテーブル経由でしかファイルを触らないなら FILE MANAGED、既存のツールがボリュームを直接読んでいるなら FILE EXTERNAL。この線引きを最初に決めておくと、後から作り直さずに済みそうです。

参考リンク

はじめてのDatabricks

はじめてのDatabricks

Databricks無料トライアル

Databricks無料トライアル

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?