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');
Snowsight の結果の欄。0 rows と出て、Invalid UTF8 detected in string で始まるエラー文が1つ表示されている。エラー文には File orders_cp932.csv、line 1、character 1 Row 1 とある
Snowsight の結果の欄。0 rows で、1行も返らない
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');
SHIFTJIS で読んだ7行の結果。ROW_NO 1 が見出しの行で、注文番号・注文日・店舗・金額・備考。5行目に 1004、㈱渋谷、800、①丸数字がそのまま表示されている。金額の列には 3,200、全角の 12000、1,500、null がある。7行目の備考は「改行を 含む備考」で、LEN5 は 8
SHIFTJIS で読んだ7行。枠は ㈱渋谷・①丸数字の行と、改行を入れた備考の長さ

見出しを含む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;
BOM 付き UTF-8 の1行目の1列目を比べた結果。SKIP_BYTE_ORDER_MARK = TRUE の行は FIRST_VALUE が注文番号、LEN が 4、SAME_AS_TEXT が TRUE。SKIP_BYTE_ORDER_MARK = FALSE の行は FIRST_VALUE が同じく注文番号に見え、LEN が 5、SAME_AS_TEXT が FALSE
1行目の1列目の長さ。既定では4、SKIP_BYTE_ORDER_MARK = FALSE では5

既定では、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;
VALIDATION_MODE の結果の9行。1〜6行目は Date 2026/10/20 is not recognized などの日付のエラーで、LINE は 2〜7、CODE は 100040。7〜9行目は Numeric value 3,200、全角の 12000、1,500 is not recognized の金額のエラーで、LINE は 2・3・4、CODE は 100038。CATEGORY はすべて conversion
VALIDATION_MODE の9件。上の6件が日付、枠の3件が金額

返ったのは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 で変換する列です。

ここまでに出た値と、どこで直すかを次の表にしました。

日本語の CSV でつまずく値と、どこで直すかの表。ファイル形式で決めるもの:見出しの注文番号は、ENCODING の既定の UTF8 では Invalid UTF8 detected で1文字目から読めず、ENCODING = SHIFTJIS で読めた。㈱渋谷・①丸数字は、ENCODING の値に CP932 が無いが、今回の2文字は読めた。引用符で囲んだ 3,200 は FIELD_OPTIONALLY_ENCLOSED_BY で1つの値になった。引用符の中の改行は1つの値に残り、長さ8。行末の CRLF の CR は残らなかった。空欄は文字列の列でも NULL。BOM 付きの UTF-8 は既定で読み飛ばされ、見出しの長さ4、FALSE にすると5。取り込んだあと SQL で直すもの:2026/10/20 は AUTO では日付と認識されず、TRY_TO_DATE に YYYY/MM/DD を渡して6行とも DATE になった。全角の 12000 は TRANSLATE で半角にして 12000。数値の列の 3,200 は REPLACE でカンマを外して 3200。令和8年10月23日と約3000は、日付の書式の要素に元号のものが見当たらず、変換できない行として取り出した日本語の CSV でつまずく値と、どこで直すかの表。ファイル形式で決めるもの:見出しの注文番号は、ENCODING の既定の UTF8 では Invalid UTF8 detected で1文字目から読めず、ENCODING = SHIFTJIS で読めた。㈱渋谷・①丸数字は、ENCODING の値に CP932 が無いが、今回の2文字は読めた。引用符で囲んだ 3,200 は FIELD_OPTIONALLY_ENCLOSED_BY で1つの値になった。引用符の中の改行は1つの値に残り、長さ8。行末の CRLF の CR は残らなかった。空欄は文字列の列でも NULL。BOM 付きの UTF-8 は既定で読み飛ばされ、見出しの長さ4、FALSE にすると5。取り込んだあと SQL で直すもの:2026/10/20 は AUTO では日付と認識されず、TRY_TO_DATE に YYYY/MM/DD を渡して6行とも DATE になった。全角の 12000 は TRANSLATE で半角にして 12000。数値の列の 3,200 は REPLACE でカンマを外して 3200。令和8年10月23日と約3000は、日付の書式の要素に元号のものが見当たらず、変換できない行として取り出した
● は今回の試しで出たエラー、○ は Snowflake のドキュメントの記述

すべて文字列の RAW_ORDERS で受け、INSERT で日付と数値にする

型のエラーで行を落とさないよう、取り込みを二段に分けます。一段目は、すべての列を文字列にした RAW_ORDERS に入れます。二段目で、日付と金額を SQL で変換し、型を付けた ORDERS に入れます。

二段で入れる流れ。届くファイルは Shift_JIS の CSV で、注文が6行。ファイル形式で ENCODING = SHIFTJIS と引用符を決め、SKIP_HEADER を書かずに読むと見出しを含めて7行が読めた。入れる前に COPY INTO ORDERS に VALIDATION_MODE = RETURN_ALL_ERRORS を付けて、エラーが9件。一段目の RAW_ORDERS は、SKIP_HEADER = 1 で見出しを飛ばし、すべての列が STRING で FILE_NAME と FILE_ROW つきの6行。二段目の ORDERS には INSERT … SELECT で TRY_TO_DATE・TRANSLATE・REPLACE・TRY_TO_NUMBER を使って6行。INSERT の前に、変換できない行を SELECT で探す。ファイルの6行は0行で、試しに足した1行が出た。本番ではこの行を ORDERS に入れず、RAW_ORDERS に残す二段で入れる流れ。届くファイルは Shift_JIS の CSV で、注文が6行。ファイル形式で ENCODING = SHIFTJIS と引用符を決め、SKIP_HEADER を書かずに読むと見出しを含めて7行が読めた。入れる前に COPY INTO ORDERS に VALIDATION_MODE = RETURN_ALL_ERRORS を付けて、エラーが9件。一段目の RAW_ORDERS は、SKIP_HEADER = 1 で見出しを飛ばし、すべての列が STRING で FILE_NAME と FILE_ROW つきの6行。二段目の ORDERS には INSERT … SELECT で TRY_TO_DATE・TRANSLATE・REPLACE・TRY_TO_NUMBER を使って6行。INSERT の前に、変換できない行を SELECT で探す。ファイルの6行は0行で、試しに足した1行が出た。本番ではこの行を ORDERS に入れず、RAW_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;
RAW_ORDERS の6行。ORDER_NO は 1001〜1006、ORDER_DATE は 2026/10/20 の形の文字列。AMOUNT は 3,200、全角の 12000、1,500、800、null、2000 のまま。FILE_NAME はすべて orders_cp932.csv、FILE_ROW は 1〜6
RAW_ORDERS の6行。枠の金額は、ファイルの文字のまま入っている

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行。ORDER_DATE は 2026-10-20 の形の日付。AMOUNT は 3200、12000、1500、800、null、2000 の数値。STORE の ㈱渋谷 もそのまま
INSERT のあとの ORDERS の6行。枠の金額は数値になった

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';
変換できない行の SELECT の結果は1行。FILE_NAME は test_insert、FILE_ROW は 7、ORDER_NO は 1007、ORDER_DATE は令和8年10月23日、AMOUNT は約3000、DATE_CHECK と AMOUNT_CHECK はどちらも NG
変換できない行の SELECT。出たのは試しに足した1行で、日付も金額も 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;
一段で入れたあとの結果の3行。LOADED_ROWS はどの行も 3。ERROR は Numeric value 3,200、全角の 12000、1,500 is not recognized で、LINE は 2・3・4、COLUMN_NAME はどれも ORDERS_ONE_STEP の AMOUNT
一段で入れたあとの件数と、VALIDATE で残したエラー。LOADED_ROWS は3

ORDERS_ONE_STEP に入ったのは3行です。日付のエラーは出ませんでした。入らなかったのは 1001・1002・1003 で、金額の「3,200」「12000」「1,500」が数値と認識されなかった行です。

一段と二段で ORDERS に入った行の比べ。一段で入れると、1001・1002・1003 が入らず、1004・1005・1006 の3行だけが入った。入らなかった理由は金額が数値と認識されないことで、VALIDATE の結果を LOAD_ERRORS に保存して3件が残った。日付は DATE_FORMAT で読めた。二段で入れると、1001〜1006 の6行が入った。変換できない値の行は RAW_ORDERS に元のまま残り、ファイル名とファイルの行番号つきで SELECT で出せる一段と二段で ORDERS に入った行の比べ。一段で入れると、1001・1002・1003 が入らず、1004・1005・1006 の3行だけが入った。入らなかった理由は金額が数値と認識されないことで、VALIDATE の結果を LOAD_ERRORS に保存して3件が残った。日付は DATE_FORMAT で読めた。二段で入れると、1001〜1006 の6行が入った。変換できない値の行は RAW_ORDERS に元のまま残り、ファイル名とファイルの行番号つきで SELECT で出せる
一段と二段で、ORDERS に入った行。どちらも同じファイルを取り込んだ

一段で入れたときのエラーは、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_HEADER1見出しの行の数。見出しが無ければ0にする
FIELD_OPTIONALLY_ENCLOSED_BY'"'値を ” で囲むファイルなら残す。今回は、囲んだ値と囲まない値が混ざった行も読めた
RAW_ORDERS の列と $1〜$55列ファイルの列の数だけ 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_CHARACTERSTRUE にすると、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などでのダッシュボード化までを支援します。

AT
aurant technologies 編集

上場企業からスタートアップまで、数多くのデータ分析基盤構築・AI導入プロジェクトを主導。単なる技術提供にとどまらず、MA/CRM(Salesforce, Hubspot, kintone, LINE)導入によるマーケティング最適化やバックオフィス業務の自動化など、常に「事業数値(売上・利益)」に直結する改善実績多数。

この記事が役に立ったらシェア:

データ基盤・データ分析の資料

「BIツール選定チェックリスト&4社比較」

データ分析・データ基盤支援の内容を見る