Snowflake に Shift_JIS の CSV を取り込む|いったん文字列で受け、日付と金額を直してから型を付ける
Snowflake に Shift_JIS の CSV を COPY INTO で取り込む手順です。ENCODING と引用符をファイル形式に書き、VALIDATION_MODE でエラーを全部出します。すべて文字列のテーブルで受けてから、日付・全角数字・桁区切りの金額を SQL で直して入れます。直せない行は、元の値のまま取り出せます。型を付けたテーブルへ直接入れたときに入らなかった行も、試した結果で書きます。
目次 クリックで開く
取引先から届いた注文の CSV は、Excel で開けば日本語も金額も読めます。ところが Snowflake の COPY INTO では、1行目から文字コードのエラーで止まります。文字コードを指定しても、こんどは日付と金額の列でエラーが並びます。
文字コードと引用符の扱いは読み方の設定(ファイル形式)に書き、取り込む前に VALIDATION_MODE でエラーを全部出します。そのうえで、すべての列を文字列で受けるテーブルに入れ、日付と金額を SQL で変換してから、型を付けたテーブルへ移します。変換できない値の行は文字列のテーブルに残るので、送り手に確かめてもらえます。
当社は、Snowflake へのデータの取り込みを組む仕事をしています。画面と結果は、つまずきやすい値をわざと混ぜた架空の注文の CSV を、トライアルのアカウントで取り込んだときのものです。
1行目で止まる Invalid UTF8 detected は、文字コードの指定漏れ
Snowflake では、取り込む前のファイルを、ステージという置き場所に置きます。Snowflake の中に作るステージが、内部ステージです。ファイルの読み方の設定は、ファイル形式という名前付きのオブジェクトにしておき、SELECT や COPY INTO で指定します。
内部ステージ LANDING に置いた Shift_JIS の CSV を、ENCODING を書かないファイル形式で SELECT しました。試しのファイルは、この節の最後の SQL で先に置いておきます。
-- この記事の試しの SQL は、どれも検証用のデータベース test_csv で実行した
USE SCHEMA test_csv.raw;
CREATE OR REPLACE FILE FORMAT FF_DEFAULT TYPE = CSV FIELD_OPTIONALLY_ENCLOSED_BY = '"';
SELECT METADATA$FILE_ROW_NUMBER AS ROW_NO, $1, $2, $3, $4, $5
FROM @LANDING/orders_cp932.csv (FILE_FORMAT => 'FF_DEFAULT');
Invalid UTF8 detected in string '0x920x8D0x950xB6…' File 'orders_cp932.csv', line 1, character 1 Row 1, column "TRANSIENT_STAGE_TABLE"["$1":1]
引用符の中の、0xB6 より後ろは省きました。全文は写真のとおりです。line 1, character 1 は、ファイルの1行目の1文字目で読めなくなったことを示しています。0x92 0x8D は Shift_JIS で「注」、0x95 0xB6 は「文」で、見出しの「注文番号」の最初の2文字です。
CSV のような区切りのファイルは、文字コードを指定しなければ UTF-8 として読まれます。ファイル形式の ENCODING の既定が UTF8 だからです。「注」のバイトの並びは UTF-8 としては読めないので、最初の文字で止まります。ファイルが壊れているわけではなく、次の節の ENCODING を書いたファイル形式では、すべて読めました。
試しのファイルは、Python のストアドプロシージャで中身を作り、ステージに書き出しました。Snowsight のワークシートからは、ファイルを置く PUT を実行できないためです。これは試すための手段です。実務では、届いたファイルを PUT などの方法でステージに置きます。
中身は、見出しの行と6行の注文で、改行は CRLF です。金額には「3,200」の桁区切りや「12000」の全角数字を、日付には「2026/10/20」の形を入れました。店舗と備考には ㈱ と ① を入れています。どちらも Shift_JIS の規格には無く、cp932(Windows の日本語の文字コード)にある文字です。同じ中身を、BOM 付きの UTF-8 にしたファイルも作りました。
環境は、Snowflake の30日間のトライアル(Enterprise Edition、AWS の東京リージョン)です。ロールは ACCOUNTADMIN を使いました。検証用のデータベース test_csv は、最後に削除しています。
-- 最初の SELECT より前に実行する。試しのファイル2つを作り、LANDING に置く
CREATE DATABASE IF NOT EXISTS test_csv;
CREATE SCHEMA IF NOT EXISTS test_csv.raw;
USE SCHEMA test_csv.raw;
CREATE OR REPLACE STAGE LANDING;
CREATE OR REPLACE PROCEDURE WRITE_TEST_FILES()
RETURNS STRING
LANGUAGE PYTHON
RUNTIME_VERSION = '3.11'
PACKAGES = ('snowflake-snowpark-python')
HANDLER = 'run'
AS
$$
import io
def run(session):
lines = [
'注文番号,注文日,店舗,金額,備考',
'1001,2026/10/20,渋谷店,"3,200",通常',
'1002,2026/10/20,新宿店,12000,全角の数字',
'1003,2026/10/21,池袋店,"1,500","備考に,カンマ"',
'1004,2026/10/21,㈱渋谷,800,①丸数字',
'1005,2026/10/21,新宿店,,金額なし',
'1006,2026/10/22,渋谷店,2000,"改行を\n含む備考"',
]
text = '\r\n'.join(lines) + '\r\n'
session.file.put_stream(io.BytesIO(text.encode('cp932')), '@test_csv.raw.LANDING/orders_cp932.csv', auto_compress=False, overwrite=True)
session.file.put_stream(io.BytesIO(text.encode('utf-8-sig')), '@test_csv.raw.LANDING/orders_utf8_bom.csv', auto_compress=False, overwrite=True)
return 'ok'
$$;
CALL WRITE_TEST_FILES();
LIST @LANDING;
最後の LIST で、2つのファイルが LANDING にあることを確かめました。
ファイル形式で決めるのは、文字コードと引用符
Shift_JIS のファイルには、ファイル形式に ENCODING = 'SHIFTJIS' と書きます。ENCODING に書く値に、アンダースコアは入りません。日本語の値には、ほかに EUCJP と ISO2022JP があり、値の一覧に CP932 はありません。
ENCODING に書いた文字コードのファイルは、取り込むときに中身が UTF-8 へ変えられ、テーブルには UTF-8 で入ります。COPY INTO でファイルへ書き出す(アンロードする)ときの文字コードは UTF-8 だけ、とドキュメントにあります。COPY INTO のアンロードでは、Shift_JIS のファイルは作れません。
値を ” で囲んだ列があるなら、FIELD_OPTIONALLY_ENCLOSED_BY = '"' も書きます。既定は NONE で、” で囲んだ値を1つの値としては読みません。囲んだ値の中のカンマや改行を、区切りと分けて読むために要ります。
USE SCHEMA test_csv.raw;
CREATE OR REPLACE FILE FORMAT FF_SJIS TYPE = CSV ENCODING = 'SHIFTJIS' FIELD_OPTIONALLY_ENCLOSED_BY = '"';
SELECT METADATA$FILE_ROW_NUMBER AS ROW_NO, $1, $2, $3, $4, $5, LENGTH($5) AS LEN5
FROM @LANDING/orders_cp932.csv (FILE_FORMAT => 'FF_SJIS');
見出しを含む7行が、すべて読めました。㈱渋谷と①丸数字も、そのままの文字で出ています。ただし、今回のファイルで確かめた cp932 の文字は、この2文字だけです。
引用符で囲んだ「3,200」と「備考に,カンマ」は、それぞれ1つの値になりました。改行を入れた備考は、改行も含めて1つの値で、LENGTH は8です。行の最後の列の「通常」は LENGTH が2で、行末の CR は値に残っていません。金額が空欄の 1005 は null です。
METADATA$FILE_ROW_NUMBER は1から7で、値の中に改行がある注文も1行に数えています。変換できない行を送り手に伝えるときに、この番号を使います。
UTF-8 で届くファイルには、先頭に BOM という印が付いたものがあります。BOM 付きの UTF-8 は、ENCODING を書かないファイル形式で読めます。SKIP_BYTE_ORDER_MARK の既定が TRUE で、BOM を読み飛ばすからです。
USE SCHEMA test_csv.raw;
CREATE OR REPLACE FILE FORMAT FF_UTF8 TYPE = CSV FIELD_OPTIONALLY_ENCLOSED_BY = '"';
CREATE OR REPLACE FILE FORMAT FF_UTF8_KEEP_BOM TYPE = CSV FIELD_OPTIONALLY_ENCLOSED_BY = '"' SKIP_BYTE_ORDER_MARK = FALSE;
SELECT 'SKIP_BYTE_ORDER_MARK = TRUE' AS SETTING, $1 AS FIRST_VALUE, LENGTH($1) AS LEN, $1 = '注文番号' AS SAME_AS_TEXT FROM @LANDING/orders_utf8_bom.csv (FILE_FORMAT => 'FF_UTF8') WHERE METADATA$FILE_ROW_NUMBER = 1
UNION ALL
SELECT 'SKIP_BYTE_ORDER_MARK = FALSE', $1, LENGTH($1), $1 = '注文番号' FROM @LANDING/orders_utf8_bom.csv (FILE_FORMAT => 'FF_UTF8_KEEP_BOM') WHERE METADATA$FILE_ROW_NUMBER = 1;
既定では、1行目の1列目は「注文番号」の4文字で、'注文番号' と比べると TRUE でした。SKIP_BYTE_ORDER_MARK = FALSE にすると、見た目は同じでも長さは5で、比べると FALSE です。見た目は合っているのに一致しない値は、LENGTH で長さを見ると、BOM が残っていることに気づけます。
入れる前の VALIDATION_MODE で出た9件
ファイルが読めても、型を付けたテーブルにそのまま入るとは限りません。COPY INTO に VALIDATION_MODE を付けると、テーブルには入れずにファイルを確かめ、エラーを返します。RETURN_ALL_ERRORS にすると、指定したファイルのエラーを全部返します。
見出しの行を飛ばす SKIP_HEADER = 1 を、ファイル形式に足しました。ORDERS テーブルは、日付を DATE、金額を NUMBER にして作りました。
USE SCHEMA test_csv.raw;
CREATE OR REPLACE FILE FORMAT FF_SJIS_H TYPE = CSV ENCODING = 'SHIFTJIS' FIELD_OPTIONALLY_ENCLOSED_BY = '"' SKIP_HEADER = 1;
CREATE OR REPLACE TABLE ORDERS (ORDER_NO NUMBER, ORDER_DATE DATE, STORE STRING, AMOUNT NUMBER, NOTE STRING);
COPY INTO ORDERS FROM @LANDING/orders_cp932.csv FILE_FORMAT = (FORMAT_NAME = 'FF_SJIS_H') VALIDATION_MODE = RETURN_ALL_ERRORS;
返ったのは9件です。6件は日付で、Date '2026/10/20' is not recognized のように、6行すべての日付が読めませんでした。CODE は100040です。3件は金額で、'3,200'、'12000'、'1,500' が Numeric value … is not recognized になりました。CODE は100038です。ORDERS には何も入っていません。
LINE はファイルの行で、見出しの行を1行目と数えます。LINE 2 が注文 1001 の行です。金額が空欄の 1005 は、エラーになっていません。空欄は NULL として扱われるためです。
日付が読めなかったのは、日付の書式の既定が AUTO だからです。英語版のドキュメントには、AUTO は「2026/10/20」のような年が先のスラッシュの形を日付と認識しない、と書かれています。同じ箇所には、Snowsight の画面からファイルを読み込むと、この形の列は文字列(VARCHAR)と推定される、ともあります。日付として入れるには、'YYYY/MM/DD' と書式を書いて渡します。
VALIDATION_MODE は、COPY INTO の中に SELECT を書く形には使えません。このあと RAW_ORDERS に入れる COPY INTO は、メタデータの列を足すために SELECT を書く形です。そこで VALIDATION_MODE は、SELECT を書かずに型を付けたテーブルへ入れる COPY INTO に付けて実行します。出たエラーの列が、取り込んだあと SQL で変換する列です。
ここまでに出た値と、どこで直すかを次の表にしました。
すべて文字列の RAW_ORDERS で受け、INSERT で日付と数値にする
型のエラーで行を落とさないよう、取り込みを二段に分けます。一段目は、すべての列を文字列にした RAW_ORDERS に入れます。二段目で、日付と金額を SQL で変換し、型を付けた ORDERS に入れます。
RAW_ORDERS には、ファイルの5列のほかに3つの列を足しました。ファイル名の FILE_NAME、ファイルの中で何行目かの FILE_ROW、取り込んだ時刻の LOADED_AT です。入れるときは、COPY INTO の中の SELECT に、2つのメタデータの列を名前で書きます。METADATA$FILENAME と METADATA$FILE_ROW_NUMBER です。この2つは、SELECT * では出てこない、とドキュメントにあります。
USE SCHEMA test_csv.raw;
CREATE OR REPLACE TABLE RAW_ORDERS (ORDER_NO STRING, ORDER_DATE STRING, STORE STRING, AMOUNT STRING, NOTE STRING, FILE_NAME STRING, FILE_ROW NUMBER, LOADED_AT TIMESTAMP_LTZ);
COPY INTO RAW_ORDERS FROM (SELECT $1, $2, $3, $4, $5, METADATA$FILENAME, METADATA$FILE_ROW_NUMBER, CURRENT_TIMESTAMP() FROM @LANDING/orders_cp932.csv) FILE_FORMAT = (FORMAT_NAME = 'FF_SJIS_H');
SELECT ORDER_NO, ORDER_DATE, STORE, AMOUNT, NOTE, FILE_NAME, FILE_ROW FROM RAW_ORDERS ORDER BY FILE_ROW;
6行が、エラーなしで入りました。すべて文字列なので、「12000」も「3,200」もそのままです。金額が空欄の 1005 は、文字列の列でも空の文字列ではなく null でした。FILE_ROW は1から6で、見出しの行を飛ばしたあとの番号です。
日付と数値への変換は、取り込んだあとの INSERT … SELECT で行います。COPY INTO の中の SELECT では、使いたい関数の一部が使えないためです。ドキュメントでは、COPY INTO の中で使える関数の一覧に TRANSLATE が無く、TRY_TO_DATE の書式もそこでは渡せません。
USE SCHEMA test_csv.raw;
INSERT INTO ORDERS
SELECT TRY_TO_NUMBER(ORDER_NO), TRY_TO_DATE(ORDER_DATE, 'YYYY/MM/DD'), STORE,
TRY_TO_NUMBER(REPLACE(TRANSLATE(AMOUNT, '0123456789', '0123456789'), ',', '')), NOTE
FROM RAW_ORDERS;
SELECT ORDER_NO, ORDER_DATE, STORE, AMOUNT, NOTE FROM ORDERS ORDER BY ORDER_NO;
TRY_TO_DATE に 'YYYY/MM/DD' を渡して、日付を DATE にします。TRANSLATE で全角の0〜9を半角に置き換え、REPLACE で桁区切りのカンマを外してから、TRY_TO_NUMBER で数値にします。TRY_ の付いた関数は、変換できない値でもエラーにならず、NULL を返します。
ORDERS には6行が入りました。金額は 3200、12000、1500、800、2000 で、1005 は null のままです。日付は 2026-10-20 の形の DATE になり、店舗の ㈱渋谷 もそのまま残っています。
変換できない行は、元の値と FILE_ROW で探す
TRY_ の付いた関数では、変換できない値も NULL になります。これでは、元から空欄だった値と区別がつきません。そこで、元の値があるのに、変換すると NULL になる行を、RAW_ORDERS から探します。
今回のファイルには変換できない値が無いので、試すために RAW_ORDERS へ1行を足しました。日付が「令和8年10月23日」、金額が「約3000」の行で、FILE_NAME は test_insert です。ファイルから入れた行ではありません。
-- この INSERT は、試すために RAW_ORDERS へ1行を足す文
USE SCHEMA test_csv.raw;
INSERT INTO RAW_ORDERS VALUES ('1007', '令和8年10月23日', '渋谷店', '約3000', '元号の日付と文字の混ざった金額', 'test_insert', 7, CURRENT_TIMESTAMP());
SELECT FILE_NAME, FILE_ROW, ORDER_NO, ORDER_DATE, AMOUNT,
IFF(ORDER_DATE IS NOT NULL AND TRY_TO_DATE(ORDER_DATE, 'YYYY/MM/DD') IS NULL, 'NG', 'OK') AS DATE_CHECK,
IFF(AMOUNT IS NOT NULL AND TRY_TO_NUMBER(REPLACE(TRANSLATE(AMOUNT, '0123456789', '0123456789'), ',', '')) IS NULL, 'NG', 'OK') AS AMOUNT_CHECK
FROM RAW_ORDERS
WHERE DATE_CHECK = 'NG' OR AMOUNT_CHECK = 'NG';
出たのは、試しに足した 1007 の1行だけです。ファイルから入れた6行は出ませんでした。金額が空欄の 1005 も、元の値が空なので出ません。ドキュメントの日付の書式の要素に、元号を表すものは見当たりません。「令和8年」は、TRY_TO_DATE では変換できません。
変換できない行は、ORDERS には入れません。RAW_ORDERS に元の値のまま残し、送り手に確かめてもらう材料にします。後ろの一式の (5) では、この SELECT と同じ条件で、変換できない行を除いて入れます。直ったファイルが届いたら、ORDERS からそのファイルの行を消して入れ直します。そのために、一式では ORDERS にもファイル名の列を持たせます。
送り手に伝えるときは、FILE_NAME と FILE_ROW を使います。FILE_ROW は、見出しの行を除いて何行目のデータかを表し、値の中の改行は数えません。エディタで開いたときの行番号は、見出しの行の分だけ大きくなります。それより前に改行を含む値があれば、その改行の数だけ、さらにずれます。
一段で入れると、6行のうち3行しか入らない
日付は、ファイル形式に DATE_FORMAT = 'YYYY/MM/DD' を書けば、型を付けたテーブルへ直接入れても読めます。この一段の入れ方も試しました。ON_ERROR = CONTINUE にして、エラーのある行を飛ばして続けます。
USE SCHEMA test_csv.raw;
CREATE OR REPLACE FILE FORMAT FF_SJIS_DATE TYPE = CSV ENCODING = 'SHIFTJIS' FIELD_OPTIONALLY_ENCLOSED_BY = '"' SKIP_HEADER = 1 DATE_FORMAT = 'YYYY/MM/DD';
CREATE OR REPLACE TABLE ORDERS_ONE_STEP (ORDER_NO NUMBER, ORDER_DATE DATE, STORE STRING, AMOUNT NUMBER, NOTE STRING);
COPY INTO ORDERS_ONE_STEP FROM @LANDING/orders_cp932.csv FILE_FORMAT = (FORMAT_NAME = 'FF_SJIS_DATE') ON_ERROR = CONTINUE;
CREATE OR REPLACE TABLE LOAD_ERRORS AS SELECT * FROM TABLE(VALIDATE(ORDERS_ONE_STEP, JOB_ID => '_last'));
SELECT (SELECT COUNT(*) FROM ORDERS_ONE_STEP) AS LOADED_ROWS, ERROR, LINE, CHARACTER, COLUMN_NAME FROM LOAD_ERRORS ORDER BY LINE;
ORDERS_ONE_STEP に入ったのは3行です。日付のエラーは出ませんでした。入らなかったのは 1001・1002・1003 で、金額の「3,200」「12000」「1,500」が数値と認識されなかった行です。
一段で入れたときのエラーは、VALIDATE で全部取り出せます。VALIDATE は、結果をテーブルの形で返す関数です。今回は結果を LOAD_ERRORS というテーブルに保存し、3件の行・列・エラー文が残りました。
ドキュメントによると、ON_ERROR = CONTINUE のとき、COPY INTO の結果に出るエラーは、ファイルごとに最初の1件だけです。入らなかった行の数は、結果の ROWS_PARSED と ROWS_LOADED の差で分かります。ON_ERROR の既定の ABORT_STATEMENT では、エラーが1つでもあれば取り込みが止まり、VALIDATE も何も返しません。
日付の書式が1つにそろい、金額に記号も全角も無いファイルなら、一段で足ります。作るテーブルも SQL も少なく済みます。そろっているか分からないうちは、二段で受けるほうを勧めます。変換できない値のある行も、RAW_ORDERS には元の値のまま残るからです。
届いたファイルを ORDERS へ入れる SQL
本番で使う SQL は、いつ実行するかで3つに分かれます。(1) のファイル形式と3つのテーブルは、最初の1回だけ作ります。(2) の VALIDATION_MODE は、新しい取引先のファイルや、書式が変わったファイルで1回実行します。(3) から (5) は、ファイルが届くたびに実行します。
-- <…> は自社の名前に置き換える。行の末尾に「未実行」とある行は、今回は実行していない
-- (1) 最初の1回だけ。ステージは、ファイルが届く名前付きの内部ステージを先に用意しておく
-- ORDERS_CHECK は (2) の確かめ用で、試したときの ORDERS と同じ5列
USE SCHEMA <データベース>.<スキーマ>;
CREATE OR REPLACE FILE FORMAT FF_SJIS_H TYPE = CSV ENCODING = 'SHIFTJIS' FIELD_OPTIONALLY_ENCLOSED_BY = '"' SKIP_HEADER = 1;
CREATE OR REPLACE TABLE RAW_ORDERS (ORDER_NO STRING, ORDER_DATE STRING, STORE STRING, AMOUNT STRING, NOTE STRING, FILE_NAME STRING, FILE_ROW NUMBER, LOADED_AT TIMESTAMP_LTZ);
CREATE OR REPLACE TABLE ORDERS (ORDER_NO NUMBER, ORDER_DATE DATE, STORE STRING, AMOUNT NUMBER, NOTE STRING, FILE_NAME STRING); -- 未実行
CREATE OR REPLACE TABLE ORDERS_CHECK (ORDER_NO NUMBER, ORDER_DATE DATE, STORE STRING, AMOUNT NUMBER, NOTE STRING); -- 未実行
-- (2) 新しい書式のファイルで1回。COPY の入れ先は5列の ORDERS_CHECK。テーブルには何も入らない
COPY INTO ORDERS_CHECK FROM @<ステージ>/<ファイル名> FILE_FORMAT = (FORMAT_NAME = 'FF_SJIS_H') VALIDATION_MODE = RETURN_ALL_ERRORS; -- 未実行
-- (3) 届くたびに:文字列のまま RAW_ORDERS へ
COPY INTO RAW_ORDERS FROM (SELECT $1, $2, $3, $4, $5, METADATA$FILENAME, METADATA$FILE_ROW_NUMBER, CURRENT_TIMESTAMP() FROM @<ステージ>/<ファイル名>) FILE_FORMAT = (FORMAT_NAME = 'FF_SJIS_H');
-- (4) 届くたびに:変換できない行を出す。出た行は送り手に確かめてもらう
SELECT FILE_NAME, FILE_ROW, ORDER_NO, ORDER_DATE, AMOUNT,
IFF(ORDER_NO IS NOT NULL AND TRY_TO_NUMBER(ORDER_NO) IS NULL, 'NG', 'OK') AS ORDER_NO_CHECK, -- 未実行
IFF(ORDER_DATE IS NOT NULL AND TRY_TO_DATE(ORDER_DATE, 'YYYY/MM/DD') IS NULL, 'NG', 'OK') AS DATE_CHECK,
IFF(AMOUNT IS NOT NULL AND TRY_TO_NUMBER(REPLACE(TRANSLATE(AMOUNT, '0123456789', '0123456789'), ',', '')) IS NULL, 'NG', 'OK') AS AMOUNT_CHECK
FROM RAW_ORDERS
WHERE FILE_NAME = '<ファイル名>' -- 未実行
AND LOADED_AT = (SELECT MAX(LOADED_AT) FROM RAW_ORDERS WHERE FILE_NAME = '<ファイル名>') -- 未実行
AND (ORDER_NO_CHECK = 'NG' OR DATE_CHECK = 'NG' OR AMOUNT_CHECK = 'NG'); -- 未実行
-- (5) 届くたびに:そのファイルの行を消してから、変換できる行だけを ORDERS へ。INSERT でエラーが出たら COMMIT せず ROLLBACK
BEGIN TRANSACTION; -- 未実行
DELETE FROM ORDERS WHERE FILE_NAME = '<ファイル名>'; -- 未実行
INSERT INTO ORDERS
SELECT TRY_TO_NUMBER(ORDER_NO), TRY_TO_DATE(ORDER_DATE, 'YYYY/MM/DD'), STORE,
TRY_TO_NUMBER(REPLACE(TRANSLATE(AMOUNT, '0123456789', '0123456789'), ',', '')), NOTE, FILE_NAME -- 未実行
FROM RAW_ORDERS
WHERE FILE_NAME = '<ファイル名>' -- 未実行
AND LOADED_AT = (SELECT MAX(LOADED_AT) FROM RAW_ORDERS WHERE FILE_NAME = '<ファイル名>') -- 未実行
AND NOT ((ORDER_NO IS NOT NULL AND TRY_TO_NUMBER(ORDER_NO) IS NULL) -- 未実行
OR (ORDER_DATE IS NOT NULL AND TRY_TO_DATE(ORDER_DATE, 'YYYY/MM/DD') IS NULL) -- 未実行
OR (AMOUNT IS NOT NULL AND TRY_TO_NUMBER(REPLACE(TRANSLATE(AMOUNT, '0123456789', '0123456789'), ',', '')) IS NULL)); -- 未実行
COMMIT; -- 未実行
行の末尾に「– 未実行」とある行は、今回は実行していない形です。足したのは、ORDERS の FILE_NAME の列、確かめ用の ORDERS_CHECK、(4)・(5) でファイルを絞る条件です。(5) では、変換できない行を除く条件と、DELETE、BEGIN TRANSACTION、COMMIT も足しました。印の無い行は、試したときに実行した文と同じで、名前だけを <…> にしました。試しのファイルを作るストアドプロシージャと、試しの1行の INSERT は入れていません。
(1) の CREATE OR REPLACE TABLE は、同じ名前のテーブルがあると作り直されて中身が消えます。2回目からは実行しません。ステージは、ファイルが届く名前付きの内部ステージを先に用意しておき、その名前を <ステージ> に書きます。
(2) の COPY の入れ先を、ファイル名の列を足した6列の ORDERS にすると、5列のファイルとは列の数が合いません。ERROR_ON_COLUMN_COUNT_MISMATCH の既定の TRUE では、列の数が違うとエラーになる、とドキュメントにあります。そこで、試したときの ORDERS と同じ5列の ORDERS_CHECK を作り、(2) の COPY の入れ先にします。
(4) は、注文番号・日付・金額のどれかを変換できない行を出します。注文番号の確かめは、試したときの SELECT には無く、足した行です。(5) は、同じ条件の行を除いて ORDERS に入れます。除いた行は RAW_ORDERS に残るので、(4) の結果を送り手に渡して確かめてもらいます。
一度取り込んだファイルと名前も中身も同じファイルは、COPY INTO が読み飛ばします。中身を変えて置き直したファイルは、新しいファイルとして読み直される、とドキュメントにあります。そのとき RAW_ORDERS には、同じ FILE_NAME の行が2回分たまります。
そこで (4) と (5) は、そのファイルの LOADED_AT がいちばん新しい行だけを使います。(5) では、ORDERS からそのファイルの行を先に DELETE してから入れ直すので、送り直しで行が二重になりません。DELETE と INSERT は、BEGIN TRANSACTION と COMMIT で1つのトランザクションにします。INSERT でエラーが出たら、COMMIT は実行せず、ROLLBACK を実行します。ドキュメントでは、失敗した文の変更だけが戻り、トランザクションは続きます。そのまま COMMIT すると、DELETE だけが確定します。ストアドプロシージャの中では、ブロックの BEGIN と紛れないよう BEGIN TRANSACTION と書くことも、ドキュメントが勧めています。
この一式は、同じ名前のファイルを、同じ分の送り直しとして扱います。取引先が毎日 orders.csv のような同じ名前で、その日の分だけを送ってくる場合は、(5) の DELETE が前の日までの注文を消します。そうした取引先のファイルは、ステージに置くときに、その分の日付の入った名前にします。使う日付は、注文日のように、送り直しても変わらないものです。置いた日の日付にすると、送り直しが別の名前になり、DELETE が前の行に当たらず二重に入ります。直ったファイルを入れ直す手順も、直ったファイルが前と同じ名前で届くときだけ成り立ちます。名前を変えられないなら、DELETE を外し、LOADED_AT の回で区切って入れる形が考えられます。その形では、送り直しの日にも、(3) が何も読まなかった日に (5) を実行した日にも、行が二重になります。どれも試していません。
LOADED_AT には、COPY INTO を実行したときの時刻が入ります。CURRENT_TIMESTAMP のような関数は、1つの文の中で1回だけ評価される、とドキュメントにあります。1回の COPY INTO の行は、すべて同じ時刻になると読めます。今回の試しでは、LOADED_AT の値は見ていません。
(3) がエラーで止まった日は、その回の行が RAW_ORDERS に入りません。そのまま (4) に進むと、前の回の行か0行を見ることになり、変換できない行が無い日と見分けがつきません。既定の ON_ERROR では (3) の文そのものがエラーになり、ROWS_LOADED の結果は出ないはずです。(3) がエラーを返さず、ROWS_LOADED が届いた行の数になっていることを確かめてから (4) に進みます。Snowsight のワークシートで続けて実行したときは、最初のエラーで止まる、とドキュメントにあります。
(3) は SELECT を書く形の COPY INTO なので、ファイルの列の数がテーブルと合っているかを確かめません。列の数の違いをエラーにする ERROR_ON_COLUMN_COUNT_MISMATCH は、この形では無視される、とドキュメントにあります。列の数の違いに気づけるのは、(3) ではなく、入れ先が5列の ORDERS_CHECK の (2) のほうのはずです。(2) はテーブルに何も入れないので、届くたびに実行してもかまいません。日付が 2026/10/20 の形なら日付のエラーは毎回出るので、ほかの列にエラーが無いかを見ます。
手元のファイルを名前付きの内部ステージに置く方法として、PUT のページには2つが挙がっています。1つは、Snowflake CLI などから PUT を実行する方法です。もう1つは、Snowsight の Ingestion » Add Data » Load files into a Stage です。PUT は既定で gzip に圧縮するので、名前に .gz が付きます。画面から置けるのは、1ファイル250 MB までです。今回の試しは3つ目の方法で、Snowpark の session.file.put_stream を使い、ストアドプロシージャの中から置きました。どの方法でも、ステージのフォルダの名前に日本語は使えません。
FILE_NAME に入る METADATA$FILENAME は、ステージの中のパスを含めた名前を返す、とドキュメントにあります。<ファイル名> は、ファイルを置いたあとの LIST の name から、先頭のステージの名前を除いて決めます。今回の LIST の name は landing/orders_cp932.csv でした。先頭の landing/ を除いた orders_cp932.csv が <ファイル名> です。(3) のあとに FILE_NAME に入った値も、ステージの直下に置いたので orders_cp932.csv でした。.gz が付くなら、それも含めます。
取り込んだファイルをステージに残しておけば、名前も中身も同じものは、次の (3) で読み飛ばされます。ステージから消す REMOVE などは、今回は試していません。
毎朝決まった時刻に動かすなら、取り込みの自動化の記事のストアドプロシージャを使います。その中で COPY INTO を実行している所を、この (3) から (5) に置き換える形です。そのストアドプロシージャはフォルダごとに COPY INTO しており、ファイルの名前を持つ変数がありません。届いたファイルの名前を取る部分は、自分で足すことになります。ストアドプロシージャの終わりでトランザクションが開いたままだと、Snowflake はそれを取り消す、とドキュメントにあります。その記事のストアドプロシージャは、エラーのときに記録テーブルへ1行書いてから止まるので、この記録も一緒に取り消される見込みです。エラーの処理の中で、先に ROLLBACK してから記録を書く形にします。この組み合わせは試していません。
取引先のファイルごとに変える所
一式のうち、取引先のファイルに合わせて変える所を表にしました。
| 変える所 | 試したときの書き方 | 自社のファイルを見て決めること |
|---|---|---|
<データベース>.<スキーマ> | test_csv.raw | 取り込み先のスキーマ |
<ステージ>・<ファイル名> | LANDING・orders_cp932.csv | 届いたファイルの置き場所と名前。<ファイル名> は (2) から (5) の全部にあり、ステージの名前は付けない |
| ファイルの名前の付け方 | 1回ごとに別の名前 | 同じ名前は送り直しとして扱う。毎回同じ名前で届くなら、注文日のように送り直しても変わらない日付を名前に入れる |
ENCODING | 'SHIFTJIS' | UTF-8 で届くなら書かない。BOM も既定で読み飛ばす。文字コードの見分け方は、当社の BigQuery の記事に書いた |
SKIP_HEADER | 1 | 見出しの行の数。見出しが無ければ0にする |
FIELD_OPTIONALLY_ENCLOSED_BY | '"' | 値を ” で囲むファイルなら残す。今回は、囲んだ値と囲まない値が混ざった行も読めた |
RAW_ORDERS の列と $1〜$5 | 5列 | ファイルの列の数だけ STRING の列を作り、$1 からの並びも同じ数にする |
| ORDERS の列の型 | NUMBER・DATE・STRING | 集計に使う型。先頭に0の付く番号を NUMBER にすると0が落ちるので、そうした列は STRING のままにする |
'YYYY/MM/DD' | 2026/10/20 の形 | 届く日付の形。月と日は1けたでも2けたでも読む、とドキュメントにある。(4) と (5) の両方を変える |
TRANSLATE と REPLACE | 全角数字と桁区切り | 数値にする列ごとに、(4) と (5) の両方に書く |
CREATE OR REPLACE TABLE | 試すたびに作り直した | 最初の1回だけ実行する |
片付けの2行と、確かめていないこと
最後に、データ保持期間を0にしてから test_csv を削除しました。データ保持期間は、削除したあとも戻せる日数のことで、Time Travel の記事に書いています。
ALTER DATABASE test_csv SET DATA_RETENTION_TIME_IN_DAYS = 0;
DROP DATABASE test_csv;
今回の試しで確かめていないことと、自社で確かめるときの手がかりです。
| 確かめていないこと | 自社で確かめるときの手がかり |
|---|---|
| ㈱・① 以外の cp932 の文字。~、-、髙 など | こうした文字を含む行を、SHIFTJIS のファイル形式で SELECT して見る。同じ BigQuery の記事では、Python で文字コードを変えたとき、読み方によって 髙 や ~ が別の文字になった。Snowflake で読んだ結果ではない |
| Shift_JIS で2バイト目が 0x5C になる文字。表・ソなど | 既定では、行の最後の \ のあとの改行が改行として扱われず、次の行とつながる、とドキュメントにある。ESCAPE_UNENCLOSED_FIELD = NONE で避けられる。表・ソで終わる値で同じことが起きるかは確かめていない |
| 元号の日付を西暦にすること | 変換できない行として取り出し、送り手と直し方を決める |
| 全角のカンマやマイナス、▲ を付けた負の数、値の前後の空白 | 実際の値で TRY_TO_NUMBER の結果を見てから、TRANSLATE と REPLACE に足す。TRIM_SPACE の既定は FALSE で、全角スペースを取るかは書かれていない |
| UTF-8 のファイルを SHIFTJIS のファイル形式で読むこと。UTF-16 など、ENCODING のほかの値 | 文字コードごとにファイル形式を分けて作る |
| REPLACE_INVALID_CHARACTERS | TRUE にすると、UTF-8 として読めない文字が置換文字の � になって入る、とドキュメントにある |
| 一式の「未実行」の行 | 検証用のデータベースで、中身を変えたファイルを同じ名前で置き直し、(3) から (5) を2回通して件数を見る |
| かかった料金 | 今回は見ていない |
| ACCOUNTADMIN 以外のロールの権限 | 画面からステージに置くには、データベースとスキーマの USAGE とステージの WRITE が要る、とドキュメントにある |
| PUT と画面からのアップロード、Snowpipe、外部ステージ | 置いたら LIST の name を見て、先頭のステージの名前を除いたものを <ファイル名> に書く |
次に届く CSV で、一段か二段かを決める
次に CSV が届いたら、取り込む前に (2) の VALIDATION_MODE を1回実行します。エラーが日付の列だけなら、DATE_FORMAT を足したファイル形式で、もう一度実行します。試しの FF_SJIS_DATE の形です。そこで0件になれば、5列の型付きのテーブルへ一段で入れても足ります。金額や注文番号にエラーが残るなら、RAW_ORDERS で受ける二段にします。
取引先ごとのファイル形式と変換の SQL は、当社でまとめて組むこともできます。ファイルの取り込みの支援の内容は、Snowflake 導入支援のページに載せています。
データ分析・データ基盤支援
Snowflake・BigQuery・Treasure Dataといったデータ基盤の選定から、基幹・SaaS・広告・POSに散らばったデータの集約、dbtでの指標統一、Lookerなどでのダッシュボード化までを支援します。
