日付データには、イベントストリームの Unix タイムスタンプ、レガシーデータベースのエクスポートに見られる独特な数値形式の日付、API から返される ISO 8601 文字列など、実にさまざまな形式が存在します。幸いなことに、ClickHouse にはこれらすべてを処理できる豊富な関数が用意されています。本ブログ記事では、それらの関数の使い方を詳しく見ていきます。
まずは、最も明示的なアプローチから始めます。fromUnixTimestamp による Unix タイムスタンプの変換、YYYYMMDDToDate によるパックされた数値日付のパース、そして parseDateTime による既知のフォーマット文字列のパースです。次に、フォーマットが不明な場合や混在している場合に役立つ parseDateTimeBestEffort ファミリーを紹介します。
最後に、ユースケースによっては明示的な関数呼び出しよりも、cast_string_to_date_time_mode 設定と組み合わせた日付のキャストが適している理由を解説します。
Unix タイムスタンプ
まずは Unix タイムスタンプです。Unix タイムスタンプは 1970年1月1日 からの経過秒数を表します。変換には fromUnixTimestamp 関数を使用します:
SELECT
fromUnixTimestamp(1704067295) AS val1, toTypeName(val1);これは DateTime 型を返します。1970年1月1日 からのミリ秒単位の値がある場合は別の関数 fromUnixTimestamp64Milli があり、戻り値の型はミリ秒までの精度を表す 3 を指定した DateTime64(3) になります。
SELECT
fromUnixTimestamp64Milli(1704067295123) AS val2, toTypeName(val2);マイクロ秒の場合は、fromUnixTimestamp64Micro が DateTime64(6) を返します:
SELECT
fromUnixTimestamp64Micro(1704067295123456) AS val3, toTypeName(val3);数値形式の日付フォーマット
日付が区切り文字やフォーマットなしで、年・月・日をエンコードした単純な数値として表されることがあります。これはレガシーデータベースのエクスポートやメインフレームのフラットファイルでよく見られます。この処理には YYYYMMDDToDate 関数を使います:
SELECT
YYYYMMDDToDate(20240115) AS val1, toTypeName(val1);数値に時刻情報も含まれている場合は、YYYYMMDDhhmmssToDateTime で処理できます:
SELECT
YYYYMMDDhhmmssToDateTime(20240115143022) AS val2, toTypeName(val2);既知のフォーマット文字列
API は多くの場合、日付を文字列として返します。フォーマットがわかっている場合は、MySQL 形式の日付フォーマット文字列を指定して parseDateTime を使用できます:
SELECT
parseDateTime('15/01/2024 14:30:22', '%d/%m/%Y %H:%i:%s') AS val1,
toTypeName(val1);これはタイムゾーンを含む DateTime を返します。
Joda 形式の日付フォーマット文字列を使いたい場合は parseDateTimeInJodaSyntax も用意されており、同じ出力が得られます:
SELECT
parseDateTimeInJodaSyntax('15/01/2024 14:30:22', 'dd/MM/yyyy HH:mm:ss') AS val2,
toTypeName(val2);DateTime のベストエフォートパース
ここまでの 3 つのアプローチは、すべて正確な日付フォーマットが判明していることを前提としていました。しかし、不明な場合はどうすればよいでしょうか。そこで役立つのが parseDateTimeBestEffort ファミリーの関数です。さまざまなフォーマットが混在する日付を想定してみましょう:
WITH dates AS (
SELECT '2024-01-15T14:30:22.000Z' AS raw
UNION ALL
SELECT '2024-01-15' AS raw
UNION ALL
SELECT '1704067295' AS raw
)
SELECT raw, parseDateTimeBestEffort(raw) AS val, toTypeName(val)
FROM dates;前述の関数と同様に、parseDateTimeBestEffort64 を使用して DateTime64 に変換することもできます:
WITH dates AS (
SELECT '2024-01-15T14:30:22.000Z' AS raw
UNION ALL
SELECT '2024-01-15' AS raw
UNION ALL
SELECT '1704067295' AS raw
)
SELECT raw, parseDateTime64BestEffort(raw) AS val, toTypeName(val)
FROM dates;まったく無効な日付が含まれている場合はどうなるでしょうか。
WITH dates AS (
SELECT '2024-01-15T14:30:22.000Z' AS raw
UNION ALL
SELECT '2024-01-15' AS raw
UNION ALL
SELECT '1704067295' AS raw
UNION ALL
SELECT 'not a date' AS raw
)
SELECT raw, parseDateTime64BestEffort(raw) AS val, toTypeName(val)
FROM dates;ClickHouse は例外をスローします。
これは、エラーの代わりに NULL を返すバリアントの parseDateTimeBestEffort64OrNull で回避できます:
WITH dates AS (
SELECT '2024-01-15T14:30:22.000Z' AS raw
UNION ALL
SELECT '2024-01-15' AS raw
UNION ALL
SELECT '1704067295' AS raw
UNION ALL
SELECT 'not a date' AS raw
)
SELECT raw, parseDateTime64BestEffortOrNull(raw) AS val, toTypeName(val)
FROM dates;あるいは実際の日時値を取得したい場合は、parseDateTimeBestEffort64OrZero を使用すると 1970年1月1日 午前 0 時にフォールバックします:
WITH dates AS (
SELECT '2024-01-15T14:30:22.000Z' AS raw
UNION ALL
SELECT '2024-01-15' AS raw
UNION ALL
SELECT '1704067295' AS raw
UNION ALL
SELECT 'not a date' AS raw
)
SELECT raw, parseDateTime64BestEffortOrZero(raw) AS val, toTypeName(val)
FROM dates;キャスト
クエリ全体で明示的なパース関数を毎回呼び出さずに済ませたい場合は、::DateTime を使用して文字列値を日付型に直接キャストできます。ただし、注意すべき重要な設定として cast_string_to_date_time_mode があります。
デフォルトでは basic に設定されており、YYYY-MM-DD や YYYY-MM-DD HH:MM:SS などの標準的なフォーマットを処理できますが、それ以外の形式は失敗します。より幅広いフォーマットに対応させるには、これを best_effort に変更します。なお、この設定でも完全に無効な日付に対しては例外がスローされます。
この設定は、クエリごとに行内で渡すことができます:
WITH dates AS (
SELECT '2024-01-15T14:30:22.000Z' AS raw
UNION ALL
SELECT '2024-01-15' AS raw
UNION ALL
SELECT '1704067295' AS raw
)
SELECT raw, raw::DateTime AS val, toTypeName(val)
FROM dates
SETTINGS cast_string_to_date_time_mode = 'best_effort';またはセッションレベルで設定しておけば、クエリごとに指定する必要がなくなります:
SET cast_string_to_date_time_mode = 'best_effort';そうすれば、SETTINGS 句なしでも同じクエリが動作します:
WITH dates AS (
SELECT '2024-01-15T14:30:22.000Z' AS raw
UNION ALL
SELECT '2024-01-15' AS raw
UNION ALL
SELECT '1704067295' AS raw
)
SELECT raw, raw::DateTime AS val, toTypeName(val)
FROM dates;最後に、さまざまな日付が含まれる次のようなファイルを想定してみましょう:
dates.csv
raw
2024-01-15T14:30:22.000Z
2024-01-15
1704067295同じアプローチを使って、そのファイル内の日付をパースできます:
SELECT raw, raw::DateTime AS val, toTypeName(val)
FROM file('dates.csv', CSVWithNames);┌─raw──────────────────────┬─────────────────val─┬─toTypeName(val)─┐
│ 2024-01-15T14:30:22.000Z │ 2024-01-15 14:30:22 │ DateTime │
│ 2024-01-15 │ 2024-01-15 00:00:00 │ DateTime │
│ 1704067295 │ 2024-01-01 00:01:35 │ DateTime │
└──────────────────────────┴─────────────────────┴─────────────────┘


