SQLiteで現在時刻を取得・保存する方法|日時の扱い完全ガイド

データベース・SQL

SQLiteは軽量で使いやすいデータベースとして人気ですが、
「現在時刻の取得方法がわからない」
「日時をどう保存すればよいかわからない」
といった疑問を持つ方も多いのではないでしょうか?

特に初心者の方は、
「なぜ時刻がずれるの?」
「どの形式で保存すればいいの?」
「アプリケーションで使うときの注意点は?」
といった悩みがあったりします。

この記事では、SQLiteで現在時刻を取得する方法とその使いどころ、保存形式の違いや注意点まで、実際のコード例を交えながらわかりやすく解説します。

スポンサーリンク
  1. SQLiteの日時機能の基本を理解しよう
    1. SQLiteの日時の特徴
    2. なぜこのような仕組みなの?
  2. SQLiteで現在時刻を取得する方法
    1. 基本的な取得関数
    2. 時刻のみ、日付のみを取得する
    3. カスタムフォーマットで取得する
    4. 主要なフォーマット指定子
    5. 実際の使用例
  3. 現在時刻をテーブルに保存する方法
    1. 基本的なテーブル設計
    2. 方法1:DEFAULT句を使った自動設定
    3. 方法2:INSERT文で明示的に指定
    4. 方法3:トリガーを使った自動更新
    5. 実践的な使用例
  4. 日時の保存形式:どれを選ぶべき?
    1. 保存形式の比較
    2. TEXT型(ISO 8601形式)- 最も推奨
    3. INTEGER型(Unixタイムスタンプ)- 高速処理が必要な場合
    4. REAL型(Julian日付)- 高精度が必要な場合
    5. 混在する場合の対処法
    6. どの形式を選ぶべきか?判断基準
  5. タイムゾーンの扱いと注意点
    1. SQLiteのタイムゾーン処理の特徴
    2. UTCとローカル時間の使い分け
    3. タイムゾーン変換の実践例
    4. よくある問題と対処法
    5. 推奨されるベストプラクティス
  6. よくあるトラブルと解決方法
    1. トラブル1:時刻が9時間ずれる(日本の場合)
    2. トラブル2:文字列として保存した日時で正しく検索できない
    3. トラブル3:Unixタイムスタンプの精度問題
    4. トラブル4:異なるタイムゾーンのデータが混在
    5. トラブル5:日時の計算で予期しない結果
    6. トラブル6:パフォーマンスの問題
  7. 実践的な使用例とベストプラクティス
    1. 使用例1:ログ管理システム
    2. 使用例2:予約管理システム
    3. 使用例3:勤怠管理システム
    4. ベストプラクティスのまとめ
  8. まとめ:SQLiteでの日時処理をマスターしよう
    1. 核心的なポイントの復習
    2. よくある質問への回答
    3. 次のステップ

SQLiteの日時機能の基本を理解しよう

SQLiteは、他のデータベースシステムとは少し異なる日時の扱いをします。

まずは基本的な特徴を押さえましょう。

SQLiteの日時の特徴

専用の日時型がない SQLiteには、MySQLのDATETIME型やPostgreSQLのTIMESTAMP型のような、専用の日時データ型がありません。代わりに、以下の方法で日時を扱います:

  • TEXT型:文字列として保存
  • INTEGER型:数値として保存
  • REAL型:浮動小数点数として保存

組み込み関数が豊富 専用型がない代わりに、日時を扱うための関数が充実しています。こ
れらの関数を使って、現在時刻の取得や日時の計算を行います。

UTCベースの処理 SQLiteの日時関数は、基本的にUTC(協定世界時)で動作します。
ローカル時間が必要な場合は、明示的に指定する必要があります。

なぜこのような仕組みなの?

SQLiteが軽量で高速なのは、複雑な型システムを持たないことも理由の一つです。

シンプルな型システムと豊富な関数の組み合わせで、柔軟で効率的な日時処理を実現しています。

SQLiteで現在時刻を取得する方法

SQLiteには、現在の日時や時間を取得するための組み込み関数があります。用途に応じて使い分けましょう。

基本的な取得関数

CURRENT_TIMESTAMP 最もシンプルな現在時刻の取得方法です。

SELECT CURRENT_TIMESTAMP;
-- 結果例: 2025-06-17 12:34:56

datetime(‘now’) CURRENT_TIMESTAMPとほぼ同じ結果を返します。

SELECT datetime('now');
-- 結果例: 2025-06-17 12:34:56

datetime(‘now’, ‘localtime’) ローカル時間(システムのタイムゾーン)で現在時刻を取得します。

SELECT datetime('now', 'localtime');
-- 結果例: 2025-06-17 21:34:56 (JST の場合)

時刻のみ、日付のみを取得する

time(‘now’):時刻のみ

SELECT time('now');
-- 結果例: 12:34:56

SELECT time('now', 'localtime');
-- 結果例: 21:34:56 (JST の場合)

date(‘now’):日付のみ

SELECT date('now');
-- 結果例: 2025-06-17

SELECT date('now', 'localtime');
-- 結果例: 2025-06-17

カスタムフォーマットで取得する

strftime()関数の活用 strftime()関数を使えば、好きな形式で日時を取得できます。

-- 年月日を斜線区切りで
SELECT strftime('%Y/%m/%d', 'now', 'localtime');
-- 結果例: 2025/06/17

-- 時刻を12時間表記で
SELECT strftime('%I:%M %p', 'now', 'localtime');
-- 結果例: 09:34 PM

-- 曜日を含む日時
SELECT strftime('%Y年%m月%d日 %H:%M:%S', 'now', 'localtime');
-- 結果例: 2025年06月17日 21:34:56

-- Unixタイムスタンプ
SELECT strftime('%s', 'now');
-- 結果例: 1718624096

主要なフォーマット指定子

指定子意味
%Y4桁の年2025
%m月(01-12)06
%d日(01-31)17
%H時(00-23)21
%M分(00-59)34
%S秒(00-59)56
%w曜日(0=日曜)2
%sUnixタイムスタンプ1718624096

実際の使用例

アクセスログの記録

-- アクセス時刻をローカル時間で記録
INSERT INTO access_log (user_id, access_time) 
VALUES (123, datetime('now', 'localtime'));

システムの現在状態を確認

-- システム時刻とローカル時刻の確認
SELECT 
    'UTC' as timezone, 
    datetime('now') as current_time
UNION ALL
SELECT 
    'LOCAL' as timezone, 
    datetime('now', 'localtime') as current_time;

これらの関数を使い分けることで、アプリケーションの要件に応じた適切な時刻取得ができます。

現在時刻をテーブルに保存する方法

データベースに現在時刻を保存する場面は非常に多くあります。

ユーザーの登録時刻、商品の注文時刻、ログの記録時刻など、様々な用途で活用されます。

基本的なテーブル設計

シンプルなログテーブルの例

CREATE TABLE access_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    action TEXT NOT NULL,
    created_at TEXT NOT NULL
);

このテーブルに現在時刻を保存する方法をいくつか紹介します。

方法1:DEFAULT句を使った自動設定

テーブル作成時にデフォルト値を設定

CREATE TABLE logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    message TEXT NOT NULL,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);

使用例

-- created_at は自動で現在時刻が設定される
INSERT INTO logs (message) VALUES ('ユーザーがログインしました');

-- 結果を確認
SELECT * FROM logs;
-- id | message | created_at | updated_at
-- 1  | ユーザーがログインしました | 2025-06-17 12:34:56 | 2025-06-17 12:34:56

ローカル時間をデフォルトにする場合

CREATE TABLE logs_local (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    message TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now', 'localtime'))
);

方法2:INSERT文で明示的に指定

UTC時刻で保存

INSERT INTO logs (message, created_at) 
VALUES ('データを更新しました', CURRENT_TIMESTAMP);

-- または
INSERT INTO logs (message, created_at) 
VALUES ('データを更新しました', datetime('now'));

ローカル時刻で保存

INSERT INTO logs (message, created_at) 
VALUES ('ファイルをアップロードしました', datetime('now', 'localtime'));

カスタムフォーマットで保存

INSERT INTO logs (message, created_at) 
VALUES (
    'システムを再起動しました', 
    strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime')
);

方法3:トリガーを使った自動更新

更新時刻を自動で設定するトリガー

-- テーブル作成
CREATE TABLE products (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    price INTEGER NOT NULL,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);

-- 更新時にupdated_atを自動更新するトリガー
CREATE TRIGGER update_products_updated_at 
    AFTER UPDATE ON products
BEGIN
    UPDATE products 
    SET updated_at = datetime('now', 'localtime') 
    WHERE id = NEW.id;
END;

使用例

-- 商品を挿入
INSERT INTO products (name, price) VALUES ('ノートPC', 80000);

-- 価格を更新(updated_at が自動で更新される)
UPDATE products SET price = 75000 WHERE id = 1;

-- 結果確認
SELECT * FROM products;
-- id | name | price | created_at | updated_at
-- 1  | ノートPC | 75000 | 2025-06-17 12:34:56 | 2025-06-17 12:45:30

実践的な使用例

ユーザー管理テーブル

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT UNIQUE NOT NULL,
    email TEXT UNIQUE NOT NULL,
    password_hash TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now', 'localtime')),
    last_login_at TEXT,
    is_active INTEGER DEFAULT 1
);

-- ユーザー登録
INSERT INTO users (username, email, password_hash) 
VALUES ('yamada_taro', 'yamada@example.com', 'hashed_password');

-- ログイン時刻を更新
UPDATE users 
SET last_login_at = datetime('now', 'localtime') 
WHERE username = 'yamada_taro';

注文履歴テーブル

CREATE TABLE orders (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    total_amount INTEGER NOT NULL,
    status TEXT DEFAULT 'pending',
    ordered_at TEXT DEFAULT (datetime('now', 'localtime')),
    shipped_at TEXT,
    delivered_at TEXT,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- 注文作成
INSERT INTO orders (user_id, total_amount) VALUES (1, 15000);

-- 発送時刻を記録
UPDATE orders 
SET status = 'shipped', shipped_at = datetime('now', 'localtime') 
WHERE id = 1;

-- 配達完了時刻を記録
UPDATE orders 
SET status = 'delivered', delivered_at = datetime('now', 'localtime') 
WHERE id = 1;

これらの方法を使い分けることで、アプリケーションの要件に応じた適切な時刻管理ができます。

日時の保存形式:どれを選ぶべき?

SQLiteでは複数の形式で日時を保存できます。それぞれにメリット・デメリットがあるため、用途に応じて選択しましょう。

保存形式の比較

形式データ型メリットデメリット
ISO 8601TEXT‘2025-06-17 12:34:56’人間が読みやすい、SQLで扱いやすい文字列なので若干メモリを使う
UnixタイムスタンプINTEGER1718624096計算が高速、メモリ効率が良い人間には読みにくい
Julian日付REAL2451545.5精度が高い、計算に適している一般的でない、理解しにくい

TEXT型(ISO 8601形式)- 最も推奨

特徴

  • 人間が読みやすい
  • SQLの日時関数と相性が良い
  • 他のデータベースとの互換性が高い

使用例

CREATE TABLE events (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    start_time TEXT NOT NULL,  -- '2025-06-17 14:30:00'
    end_time TEXT NOT NULL
);

-- データ挿入
INSERT INTO events (title, start_time, end_time) VALUES 
('会議', '2025-06-17 14:30:00', '2025-06-17 15:30:00');

-- 日時での検索
SELECT * FROM events 
WHERE start_time >= '2025-06-17 14:00:00' 
  AND start_time < '2025-06-17 16:00:00';

-- 期間の計算
SELECT 
    title,
    start_time,
    end_time,
    (julianday(end_time) - julianday(start_time)) * 24 AS duration_hours
FROM events;

INTEGER型(Unixタイムスタンプ)- 高速処理が必要な場合

特徴

  • 数値計算が高速
  • メモリ効率が良い
  • プログラミング言語との連携が簡単

使用例

CREATE TABLE sensor_data (
    id INTEGER PRIMARY KEY,
    sensor_id INTEGER NOT NULL,
    value REAL NOT NULL,
    timestamp INTEGER NOT NULL  -- Unixタイムスタンプ
);

-- 現在時刻のUnixタイムスタンプで挿入
INSERT INTO sensor_data (sensor_id, value, timestamp) VALUES 
(1, 23.5, strftime('%s', 'now'));

-- 時刻での検索(過去1時間のデータ)
SELECT 
    sensor_id,
    value,
    datetime(timestamp, 'unixepoch', 'localtime') AS readable_time
FROM sensor_data 
WHERE timestamp >= strftime('%s', 'now', '-1 hour');

-- 時系列データの集計
SELECT 
    strftime('%Y-%m-%d %H:00:00', timestamp, 'unixepoch', 'localtime') AS hour,
    AVG(value) AS avg_value,
    COUNT(*) AS data_count
FROM sensor_data 
GROUP BY strftime('%Y-%m-%d %H', timestamp, 'unixepoch', 'localtime')
ORDER BY hour;

REAL型(Julian日付)- 高精度が必要な場合

特徴

  • 非常に高精度
  • 天文学的計算に適している
  • SQLiteの内部的な日時表現

使用例

CREATE TABLE astronomical_events (
    id INTEGER PRIMARY KEY,
    event_name TEXT NOT NULL,
    julian_date REAL NOT NULL
);

-- Julian日付で挿入
INSERT INTO astronomical_events (event_name, julian_date) VALUES 
('日食', julianday('2025-08-12 10:30:00'));

-- Julian日付から人間が読める形式に変換
SELECT 
    event_name,
    julian_date,
    datetime(julian_date) AS readable_date
FROM astronomical_events;

混在する場合の対処法

実際のプロジェクトでは、複数の形式が混在することがあります。そんな時の変換方法を紹介します。

異なる形式間の変換

-- サンプルデータ
CREATE TABLE mixed_dates (
    id INTEGER PRIMARY KEY,
    iso_date TEXT,
    unix_timestamp INTEGER,
    julian_date REAL
);

INSERT INTO mixed_dates VALUES 
(1, '2025-06-17 12:34:56', 1718624096, 2460479.52423611);

-- 全て人間が読める形式に統一
SELECT 
    id,
    iso_date,
    datetime(unix_timestamp, 'unixepoch') AS unix_to_iso,
    datetime(julian_date) AS julian_to_iso
FROM mixed_dates;

-- 全てUnixタイムスタンプに統一
SELECT 
    id,
    strftime('%s', iso_date) AS iso_to_unix,
    unix_timestamp,
    strftime('%s', datetime(julian_date)) AS julian_to_unix
FROM mixed_dates;

どの形式を選ぶべきか?判断基準

TEXT型(ISO 8601)を選ぶべき場合

  • 管理画面でデータを直接見る機会が多い
  • 日時での検索や集計が多い
  • 他のデータベースとの連携がある
  • 一般的なWebアプリケーション

INTEGER型(Unix)を選ぶべき場合

  • 大量のログデータを扱う
  • 高速な時系列処理が必要
  • IoTセンサーデータなど高頻度更新
  • モバイルアプリとの連携

REAL型(Julian)を選ぶべき場合

  • 天文学や科学計算のアプリケーション
  • 非常に高精度な時刻が必要
  • 複雑な日時計算を頻繁に行う

一般的なWebアプリケーションでは、**TEXT型(ISO 8601形式)**を推奨します。

タイムゾーンの扱いと注意点

SQLiteでの日時処理で最も注意すべきは、タイムゾーンの扱いです。

適切に理解していないと、思わぬトラブルの原因となります。

SQLiteのタイムゾーン処理の特徴

基本的にタイムゾーン情報を持たない
SQLiteは、保存された日時文字列にタイムゾーン情報を付与しません。
'2025-06-17 12:34:56'という文字列だけでは、これがUTCなのかローカル時間なのか判断できません。

関数はUTCベース datetime('now')CURRENT_TIMESTAMPは、デフォルトでUTC時刻を返します。

ローカル時間は明示的に指定 ローカル時間が必要な場合は、'localtime'修飾子を明示的に指定する必要があります。

UTCとローカル時間の使い分け

UTC時刻で保存するメリット

-- UTC で統一して保存
CREATE TABLE global_events (
    id INTEGER PRIMARY KEY,
    event_name TEXT NOT NULL,
    event_time_utc TEXT NOT NULL  -- UTC で統一
);

-- 世界各地からのイベント登録(全てUTCで保存)
INSERT INTO global_events (event_name, event_time_utc) VALUES 
('東京オフィス会議', datetime('now')),
('ニューヨーク会議', datetime('now')),
('ロンドン会議', datetime('now'));

-- 表示時にローカル時間に変換
SELECT 
    event_name,
    event_time_utc AS utc_time,
    datetime(event_time_utc, 'localtime') AS local_time
FROM global_events;

ローカル時間で保存する場合

-- ローカル時間で保存(単一タイムゾーンのアプリケーション)
CREATE TABLE local_events (
    id INTEGER PRIMARY KEY,
    event_name TEXT NOT NULL,
    event_time_local TEXT NOT NULL  -- ローカル時間
);

-- ローカル時間で保存
INSERT INTO local_events (event_name, event_time_local) VALUES 
('店舗営業開始', datetime('now', 'localtime'));

タイムゾーン変換の実践例

世界時計の実装

-- タイムゾーン変換のサンプル
WITH timezone_offsets AS (
    SELECT 'UTC' as timezone, '+0 hours' as offset
    UNION ALL SELECT 'JST', '+9 hours'
    UNION ALL SELECT 'EST', '-5 hours'
    UNION ALL SELECT 'PST', '-8 hours'
)
SELECT 
    timezone,
    datetime('now', offset) as current_time
FROM timezone_offsets;

ユーザー別タイムゾーン設定

-- ユーザーテーブル
CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    username TEXT NOT NULL,
    timezone_offset TEXT DEFAULT '+0 hours'  -- UTCからのオフセット
);

-- イベントテーブル(UTC で保存)
CREATE TABLE user_events (
    id INTEGER PRIMARY KEY,
    user_id INTEGER NOT NULL,
    title TEXT NOT NULL,
    event_time_utc TEXT NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- ユーザーのタイムゾーンでイベント表示
SELECT 
    e.title,
    e.event_time_utc,
    datetime(e.event_time_utc, u.timezone_offset) as user_local_time
FROM user_events e
JOIN users u ON e.user_id = u.id
WHERE u.id = 1;

よくある問題と対処法

問題1:時刻がずれる

-- 問題のあるコード(UTC とローカル時間が混在)
INSERT INTO logs (message, created_at) VALUES 
('ログ1', datetime('now')),           -- UTC
('ログ2', datetime('now', 'localtime')); -- ローカル時間

-- 解決案:統一したルールで保存
-- 保存は常にUTC、表示時にローカル変換
INSERT INTO logs (message, created_at) VALUES 
('ログ1', datetime('now')),
('ログ2', datetime('now'));

-- 表示時
SELECT 
    message,
    created_at as utc_time,
    datetime(created_at, 'localtime') as local_time
FROM logs;

問題2:夏時間の考慮不足

-- SQLite では夏時間を自動処理しないため、
-- アプリケーション側で対応が必要

-- 夏時間を考慮した時間変換(アプリケーション側での処理例)
CREATE TABLE timezone_rules (
    timezone_name TEXT PRIMARY KEY,
    standard_offset TEXT,
    dst_offset TEXT,
    dst_start TEXT,
    dst_end TEXT
);

-- 基本的な解決策:UTCで統一して保存し、
-- 表示時にアプリケーション側でタイムゾーン変換

問題3:異なるシステム間での時刻同期

-- システム間でのデータ連携時の注意点

-- 送信側:必ずUTCで送信
SELECT 
    id,
    title,
    datetime(created_at_local, '-9 hours') as created_at_utc  -- JSTをUTCに変換
FROM events_local;

-- 受信側:UTCで受信してローカル保存
INSERT INTO events (title, created_at_utc, created_at_local) VALUES 
('イベント', ?, datetime(?, 'localtime'));

推奨されるベストプラクティス

データベース設計時のルール

-- 推奨:UTCで保存、カラム名で明示
CREATE TABLE best_practice_events (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    created_at_utc TEXT DEFAULT CURRENT_TIMESTAMP,
    updated_at_utc TEXT DEFAULT CURRENT_TIMESTAMP,
    -- 必要に応じてローカル時間も保存
    display_time_local TEXT
);

-- トリガーでローカル時間を自動計算
CREATE TRIGGER update_local_time 
    AFTER INSERT ON best_practice_events
BEGIN
    UPDATE best_practice_events 
    SET display_time_local = datetime(NEW.created_at_utc, 'localtime')
    WHERE id = NEW.id;
END;

適切なタイムゾーン処理により、グローバルなアプリケーションでも安定した時刻管理が実現できます。

よくあるトラブルと解決方法

SQLiteの日時処理では、初心者が陥りやすい罠がいくつかあります。

実際のトラブル例と解決方法を紹介します。

トラブル1:時刻が9時間ずれる(日本の場合)

症状

-- データを挿入
INSERT INTO events (title, event_time) 
VALUES ('会議', datetime('now'));

-- 確認すると時刻が9時間早い
SELECT * FROM events;
-- title | event_time
-- 会議  | 2025-06-17 12:34:56  (実際は21:34のはずなのに...)

原因 datetime('now')はUTC時刻を返すため、日本時間(UTC+9)とは9時間の差が生じます。

解決方法

-- 方法1:挿入時にローカル時間を指定
INSERT INTO events (title, event_time) 
VALUES ('会議', datetime('now', 'localtime'));

-- 方法2:表示時にローカル時間に変換
SELECT 
    title,
    datetime(event_time, 'localtime') as local_event_time
FROM events;

-- 方法3:テーブル設計時にデフォルト値をローカル時間に
CREATE TABLE events_local (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    event_time TEXT DEFAULT (datetime('now', 'localtime'))
);

トラブル2:文字列として保存した日時で正しく検索できない

症状

-- 日時を文字列として保存
CREATE TABLE logs (
    id INTEGER PRIMARY KEY,
    message TEXT,
    log_time TEXT  -- '2025/06/17 21:34:56' 形式で保存
);

-- このような形式で保存されている
INSERT INTO logs VALUES (1, 'エラー', '2025/6/17 9:34:56');
INSERT INTO logs VALUES (2, '警告', '2025/06/17 21:34:56');

-- 時刻での範囲検索がうまくいかない
SELECT * FROM logs 
WHERE log_time >= '2025/06/17 10:00:00';
-- 期待通りの結果が得られない

原因 文字列の辞書順比較と日時の大小比較が異なるため。また、フォーマットが統一されていない。

解決方法

-- 方法1:正規化して比較
SELECT * FROM logs 
WHERE datetime(log_time) >= datetime('2025-06-17 10:00:00');

-- 方法2:データを正しい形式で保存し直す
UPDATE logs 
SET log_time = strftime('%Y-%m-%d %H:%M:%S', 
    datetime(substr(log_time, 1, 4) || '-' || 
             substr(log_time, 6, 2) || '-' || 
             substr(log_time, 9, 2) || ' ' || 
             substr(log_time, 12, 8)));

-- 方法3:新しいテーブルで正しい形式を使用
CREATE TABLE logs_fixed (
    id INTEGER PRIMARY KEY,
    message TEXT,
    log_time TEXT  -- 'YYYY-MM-DD HH:MM:SS' 形式で統一
);

トラブル3:Unixタイムスタンプの精度問題

症状

-- ミリ秒を含むUnixタイムスタンプで保存
CREATE TABLE high_precision_logs (
    id INTEGER PRIMARY KEY,
    event_type TEXT,
    timestamp_ms INTEGER  -- ミリ秒単位のタイムスタンプ
);

INSERT INTO high_precision_logs VALUES 
(1, 'click', 1718624096123);  -- ミリ秒込み

-- 変換時に正しく表示されない
SELECT datetime(timestamp_ms, 'unixepoch') FROM high_precision_logs;
-- 結果が正しくない

原因 SQLiteのunixepochは秒単位なので、ミリ秒単位のタイムスタンプをそのまま使用できません。

解決方法

-- ミリ秒を秒に変換してから処理
SELECT 
    event_type,
    timestamp_ms,
    datetime(timestamp_ms / 1000, 'unixepoch') as readable_time,
    datetime(timestamp_ms / 1000, 'unixepoch', 'localtime') as local_time
FROM high_precision_logs;

-- または、ミリ秒の精度を保持したい場合
SELECT 
    event_type,
    timestamp_ms,
    datetime(timestamp_ms / 1000, 'unixepoch') || 
    '.' || (timestamp_ms % 1000) as precise_time
FROM high_precision_logs;

トラブル4:異なるタイムゾーンのデータが混在

症状

-- 複数の拠点からデータが登録される
CREATE TABLE multi_location_events (
    id INTEGER PRIMARY KEY,
    location TEXT,
    event_name TEXT,
    event_time TEXT
);

-- 各拠点からローカル時間で登録
INSERT INTO multi_location_events VALUES 
(1, '東京', '朝会', '2025-06-17 09:00:00'),
(2, 'ニューヨーク', '朝会', '2025-06-17 09:00:00'),
(3, 'ロンドン', '朝会', '2025-06-17 09:00:00');

-- 実際は異なる時刻なのに同じ時刻として扱われる

解決方法

-- 方法1:UTC変換テーブルを作成
CREATE TABLE timezone_offsets (
    location TEXT PRIMARY KEY,
    utc_offset_hours INTEGER
);

INSERT INTO timezone_offsets VALUES 
('東京', 9),
('ニューヨーク', -5),
('ロンドン', 0);

-- UTC時刻を計算して表示
SELECT 
    e.location,
    e.event_name,
    e.event_time as local_time,
    datetime(e.event_time, '-' || t.utc_offset_hours || ' hours') as utc_time
FROM multi_location_events e
JOIN timezone_offsets t ON e.location = t.location;

-- 方法2:最初からUTCで保存
CREATE TABLE events_utc (
    id INTEGER PRIMARY KEY,
    location TEXT,
    event_name TEXT,
    event_time_utc TEXT,
    event_time_local TEXT
);

トラブル5:日時の計算で予期しない結果

症状

-- 期間の計算が思った通りにならない
SELECT 
    '2025-06-17 10:30:00' as start_time,
    '2025-06-17 15:45:00' as end_time,
    ('2025-06-17 15:45:00' - '2025-06-17 10:30:00') as duration;
-- 結果: 0 (文字列の引き算になってしまう)

解決方法

-- Julian日付を使った正しい計算
SELECT 
    '2025-06-17 10:30:00' as start_time,
    '2025-06-17 15:45:00' as end_time,
    (julianday('2025-06-17 15:45:00') - julianday('2025-06-17 10:30:00')) * 24 as hours,
    (julianday('2025-06-17 15:45:00') - julianday('2025-06-17 10:30:00')) * 1440 as minutes;

-- より実用的な関数として
CREATE VIEW duration_calculator AS
SELECT 
    start_time,
    end_time,
    ROUND((julianday(end_time) - julianday(start_time)) * 24, 2) as duration_hours,
    ROUND((julianday(end_time) - julianday(start_time)) * 1440, 0) as duration_minutes
FROM (
    SELECT 
        '2025-06-17 10:30:00' as start_time,
        '2025-06-17 15:45:00' as end_time
);

トラブル6:パフォーマンスの問題

症状

-- 大量のデータで日時検索が遅い
CREATE TABLE large_logs (
    id INTEGER PRIMARY KEY,
    message TEXT,
    created_at TEXT
);

-- 100万件のデータがある状態で
SELECT * FROM large_logs 
WHERE datetime(created_at) >= datetime('2025-06-01');
-- 非常に遅い

解決方法

-- 方法1:インデックスを作成
CREATE INDEX idx_logs_created_at ON large_logs(created_at);

-- 方法2:関数を使わずに文字列比較
-- ISO 8601形式なら文字列比較でも正しく動作
SELECT * FROM large_logs 
WHERE created_at >= '2025-06-01 00:00:00';

-- 方法3:Unixタイムスタンプを使用
CREATE TABLE large_logs_optimized (
    id INTEGER PRIMARY KEY,
    message TEXT,
    created_at_unix INTEGER,
    created_at_readable TEXT
);

CREATE INDEX idx_logs_unix ON large_logs_optimized(created_at_unix);

-- 高速な検索
SELECT * FROM large_logs_optimized 
WHERE created_at_unix >= strftime('%s', '2025-06-01');

実践的な使用例とベストプラクティス

ここまで学んだ知識を活用して、実際のアプリケーションでよく使われる日時処理パターンを紹介します。

使用例1:ログ管理システム

要件

  • アクセスログを記録
  • ログレベル別の検索
  • 時間範囲での絞り込み
  • 日別の集計

実装例

-- ログテーブルの設計
CREATE TABLE application_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    level TEXT NOT NULL CHECK(level IN ('DEBUG', 'INFO', 'WARN', 'ERROR')),
    message TEXT NOT NULL,
    module TEXT,
    user_id INTEGER,
    ip_address TEXT,
    created_at_utc TEXT DEFAULT CURRENT_TIMESTAMP,
    created_at_local TEXT DEFAULT (datetime('now', 'localtime'))
);

-- インデックスの作成
CREATE INDEX idx_logs_level ON application_logs(level);
CREATE INDEX idx_logs_created_at ON application_logs(created_at_utc);
CREATE INDEX idx_logs_user_id ON application_logs(user_id);

-- ログ挿入のサンプル
INSERT INTO application_logs (level, message, module, user_id, ip_address) VALUES 
('INFO', 'ユーザーログイン', 'auth', 123, '192.168.1.100'),
('ERROR', 'データベース接続エラー', 'database', NULL, '192.168.1.101'),
('WARN', '不正なアクセス試行', 'security', NULL, '192.168.1.102');

-- 実用的なクエリ例

-- 1. 過去24時間のエラーログ
SELECT 
    level,
    message,
    module,
    datetime(created_at_utc, 'localtime') as local_time
FROM application_logs 
WHERE level = 'ERROR' 
  AND created_at_utc >= datetime('now', '-1 day')
ORDER BY created_at_utc DESC;

-- 2. 日別エラー件数の集計
SELECT 
    date(created_at_utc, 'localtime') as log_date,
    level,
    COUNT(*) as count
FROM application_logs 
WHERE created_at_utc >= datetime('now', '-7 days')
GROUP BY date(created_at_utc, 'localtime'), level
ORDER BY log_date DESC, level;

-- 3. 時間別アクセス数(ヒートマップ用)
SELECT 
    strftime('%H', created_at_local) as hour,
    COUNT(*) as access_count
FROM application_logs 
WHERE date(created_at_local) = date('now', 'localtime')
GROUP BY strftime('%H', created_at_local)
ORDER BY hour;

使用例2:予約管理システム

要件

  • 会議室の予約管理
  • 重複チェック
  • 今後の予約一覧
  • 使用率の計算

実装例

-- 会議室テーブル
CREATE TABLE meeting_rooms (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    capacity INTEGER NOT NULL
);

-- 予約テーブル
CREATE TABLE reservations (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    room_id INTEGER NOT NULL,
    user_id INTEGER NOT NULL,
    title TEXT NOT NULL,
    start_time TEXT NOT NULL,
    end_time TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now', 'localtime')),
    FOREIGN KEY (room_id) REFERENCES meeting_rooms(id)
);

-- 重複予約を防ぐためのインデックス
CREATE INDEX idx_reservations_time ON reservations(room_id, start_time, end_time);

-- サンプルデータ
INSERT INTO meeting_rooms VALUES (1, '会議室A', 10), (2, '会議室B', 6);

-- 予約の挿入(重複チェック付き)
-- 新しい予約前に重複をチェック
WITH new_reservation AS (
    SELECT 1 as room_id, 
           '2025-06-17 14:00:00' as start_time,
           '2025-06-17 15:00:00' as end_time
),
conflicts AS (
    SELECT COUNT(*) as conflict_count
    FROM reservations r, new_reservation n
    WHERE r.room_id = n.room_id
      AND NOT (r.end_time <= n.start_time OR r.start_time >= n.end_time)
)
SELECT 
    CASE 
        WHEN conflict_count = 0 THEN '予約可能'
        ELSE '時間が重複しています'
    END as status
FROM conflicts;

-- 予約が可能な場合の挿入
INSERT INTO reservations (room_id, user_id, title, start_time, end_time) 
SELECT 1, 101, '企画会議', '2025-06-17 14:00:00', '2025-06-17 15:00:00'
WHERE NOT EXISTS (
    SELECT 1 FROM reservations 
    WHERE room_id = 1 
      AND NOT (end_time <= '2025-06-17 14:00:00' OR start_time >= '2025-06-17 15:00:00')
);

-- 実用的なクエリ例

-- 1. 今日の予約一覧
SELECT 
    mr.name as room_name,
    r.title,
    r.start_time,
    r.end_time,
    (julianday(r.end_time) - julianday(r.start_time)) * 24 as duration_hours
FROM reservations r
JOIN meeting_rooms mr ON r.room_id = mr.id
WHERE date(r.start_time) = date('now', 'localtime')
ORDER BY r.start_time;

-- 2. 空き時間の検索
WITH time_slots AS (
    SELECT 
        datetime('2025-06-17 09:00:00', '+' || (value * 1) || ' hours') as slot_start,
        datetime('2025-06-17 09:00:00', '+' || ((value + 1) * 1) || ' hours') as slot_end
    FROM generate_series(0, 8) -- 9:00-18:00 の9時間
),
available_slots AS (
    SELECT 
        1 as room_id,
        ts.slot_start,
        ts.slot_end
    FROM time_slots ts
    WHERE NOT EXISTS (
        SELECT 1 FROM reservations r
        WHERE r.room_id = 1
          AND NOT (r.end_time <= ts.slot_start OR r.start_time >= ts.slot_end)
    )
)
SELECT 
    '会議室A' as room_name,
    slot_start as available_from,
    slot_end as available_to
FROM available_slots;

-- 3. 月別使用率の計算
SELECT 
    mr.name as room_name,
    strftime('%Y-%m', r.start_time) as month,
    SUM((julianday(r.end_time) - julianday(r.start_time)) * 24) as used_hours,
    COUNT(DISTINCT date(r.start_time)) as used_days,
    ROUND(SUM((julianday(r.end_time) - julianday(r.start_time)) * 24) / 
          (COUNT(DISTINCT date(r.start_time)) * 9) * 100, 1) as utilization_rate
FROM reservations r
JOIN meeting_rooms mr ON r.room_id = mr.id
WHERE r.start_time >= date('now', 'start of month', '-2 months')
GROUP BY mr.id, strftime('%Y-%m', r.start_time)
ORDER BY month DESC, mr.name;

使用例3:勤怠管理システム

要件

  • 出退勤時刻の記録
  • 労働時間の計算
  • 残業時間の算出
  • 月次集計

実装例

-- 従業員テーブル
CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    department TEXT NOT NULL
);

-- 勤怠記録テーブル
CREATE TABLE attendance (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    employee_id INTEGER NOT NULL,
    work_date TEXT NOT NULL,  -- YYYY-MM-DD 形式
    clock_in_time TEXT,       -- 出勤時刻
    clock_out_time TEXT,      -- 退勤時刻
    break_minutes INTEGER DEFAULT 60,  -- 休憩時間(分)
    created_at TEXT DEFAULT (datetime('now', 'localtime')),
    FOREIGN KEY (employee_id) REFERENCES employees(id)
);

-- 一意制約(1日1レコード)
CREATE UNIQUE INDEX idx_attendance_unique ON attendance(employee_id, work_date);

-- 出勤打刻
INSERT OR REPLACE INTO attendance (employee_id, work_date, clock_in_time) VALUES 
(1, date('now', 'localtime'), time('now', 'localtime'));

-- 退勤打刻
UPDATE attendance 
SET clock_out_time = time('now', 'localtime')
WHERE employee_id = 1 AND work_date = date('now', 'localtime');

-- 実用的なクエリ例

-- 1. 日次労働時間の計算
SELECT 
    e.name,
    a.work_date,
    a.clock_in_time,
    a.clock_out_time,
    CASE 
        WHEN a.clock_out_time IS NOT NULL THEN
            ROUND(
                (julianday(a.work_date || ' ' || a.clock_out_time) - 
                 julianday(a.work_date || ' ' || a.clock_in_time)) * 24 * 60 - 
                a.break_minutes, 0
            )
        ELSE NULL
    END as work_minutes,
    CASE 
        WHEN a.clock_out_time IS NOT NULL THEN
            ROUND(
                (julianday(a.work_date || ' ' || a.clock_out_time) - 
                 julianday(a.work_date || ' ' || a.clock_in_time)) * 24, 2
            ) - (a.break_minutes / 60.0)
        ELSE NULL
    END as work_hours
FROM attendance a
JOIN employees e ON a.employee_id = e.id
WHERE a.work_date = date('now', 'localtime');

-- 2. 残業時間の計算(所定労働時間8時間として)
WITH daily_work AS (
    SELECT 
        employee_id,
        work_date,
        CASE 
            WHEN clock_out_time IS NOT NULL THEN
                (julianday(work_date || ' ' || clock_out_time) - 
                 julianday(work_date || ' ' || clock_in_time)) * 24 - 
                (break_minutes / 60.0)
            ELSE 0
        END as work_hours
    FROM attendance
    WHERE work_date >= date('now', 'start of month', 'localtime')
      AND clock_out_time IS NOT NULL
)
SELECT 
    e.name,
    dw.work_date,
    ROUND(dw.work_hours, 2) as work_hours,
    CASE 
        WHEN dw.work_hours > 8 THEN ROUND(dw.work_hours - 8, 2)
        ELSE 0
    END as overtime_hours
FROM daily_work dw
JOIN employees e ON dw.employee_id = e.id
ORDER BY dw.work_date DESC, e.name;

-- 3. 月次集計
SELECT 
    e.name,
    e.department,
    strftime('%Y-%m', a.work_date) as month,
    COUNT(*) as work_days,
    ROUND(SUM(
        CASE 
            WHEN a.clock_out_time IS NOT NULL THEN
                (julianday(a.work_date || ' ' || a.clock_out_time) - 
                 julianday(a.work_date || ' ' || a.clock_in_time)) * 24 - 
                (a.break_minutes / 60.0)
            ELSE 0
        END
    ), 1) as total_work_hours,
    ROUND(SUM(
        CASE 
            WHEN a.clock_out_time IS NOT NULL AND
                 (julianday(a.work_date || ' ' || a.clock_out_time) - 
                  julianday(a.work_date || ' ' || a.clock_in_time)) * 24 - 
                 (a.break_minutes / 60.0) > 8 THEN
                (julianday(a.work_date || ' ' || a.clock_out_time) - 
                 julianday(a.work_date || ' ' || a.clock_in_time)) * 24 - 
                (a.break_minutes / 60.0) - 8
            ELSE 0
        END
    ), 1) as total_overtime_hours
FROM attendance a
JOIN employees e ON a.employee_id = e.id
WHERE a.work_date >= date('now', 'start of month', '-1 month', 'localtime')
  AND a.work_date < date('now', 'start of month', 'localtime')
GROUP BY e.id, strftime('%Y-%m', a.work_date)
ORDER BY month DESC, e.name;

ベストプラクティスのまとめ

1. データ設計の原則

  • UTC で保存、表示時にローカル変換
  • カラム名でタイムゾーンを明示(_utc, _local
  • 必要に応じてインデックスを作成

2. パフォーマンス対策

  • ISO 8601 形式なら文字列比較を活用
  • 頻繁な検索にはUnixタイムスタンプを併用
  • 適切なインデックス設計

3. 保守性の向上

  • 一貫した日時フォーマット
  • コメントでタイムゾーンの取り扱いを明記
  • ビューを活用した共通処理の抽象化

これらの実例を参考に、あなたのアプリケーションに適した日時処理を実装してください。

まとめ:SQLiteでの日時処理をマスターしよう

ここまでSQLiteでの現在時刻取得と日時処理について詳しく解説してきました。最後に重要なポイントをまとめます。

核心的なポイントの復習

現在時刻の取得方法

  • CURRENT_TIMESTAMP:シンプルで最も使いやすい
  • datetime('now'):UTC時刻、関数の組み合わせに便利
  • datetime('now', 'localtime'):ローカル時間、ユーザー向け表示に最適

保存形式の選び方

  • TEXT型(ISO 8601):一般的なアプリケーションに最適
  • INTEGER型(Unix):高速処理や大量データに有効
  • REAL型(Julian):科学計算や高精度が必要な場合

タイムゾーンの扱い

  • 保存はUTC、表示時にローカル変換が基本
  • カラム名でタイムゾーンを明示
  • システム間連携では必ずUTCを使用

よくある質問への回答

Q: いつもUTC で保存すべきですか? A: グローバルなアプリケーションや複数システム連携がある場合はUTC推奨。単一地域のみなら、ローカル時間でも問題ありません。

Q: パフォーマンスが心配です A: ISO 8601形式なら文字列比較で十分高速。大量データの場合はUnixタイムスタンプとの併用を検討しましょう。

Q: 夏時間はどう対応すれば? A: SQLite単体では自動対応できません。アプリケーション側でタイムゾーンライブラリを使用することを推奨します。

次のステップ

SQLiteの日時処理をマスターしたら、以下の技術も学んでみましょう:

関連技術

  • トリガーの活用:自動的な時刻更新処理
  • ビューの作成:複雑な日時計算の抽象化
  • プログラミング言語との連携:Python、JavaScript等での活用

発展的なトピック

  • 時系列データベースの概念
  • データ分析での日時処理
  • マイクロサービスでの時刻同期

コメント

タイトルとURLをコピーしました