第 3 回: CSV インポートで始めるグラフ構築
実際のプロジェクトデータセットで学ぶ効率的なデータモデリング
🎯 この章で学ぶこと
- 実際のプロジェクトデータを使った CSV インポート手法
- Docker 環境での効率的なファイル転送方法
- LOAD CSV 句の詳細仕様と実践的なトラブルシューティング
- データ型変換とパフォーマンス最適化の実践テクニック
📖 実体験:CSV インポートで直面した現実
不正検知システムのプロジェクトで、クライアントから提供されたのは 3 つの CSV ファイルでした:
users.csv
- 約 50 万件のユーザーデータtransactions.csv
- 約 200 万件の取引データaccounts.csv
- 約 80 万件の口座データ
当初、私は単純に LOAD CSV で全データを一度に投入しようとしました。しかし、以下の問題に直面:
- メモリ不足: 大きなトランザクションでメモリが枯渇
- データ型エラー: 文字列として解釈された数値データ
- 参照整合性の問題: 存在しないユーザー ID を参照する取引データ
この経験から、**「段階的なデータ投入と検証の重要性」**を学びました。
📁 サンプルデータの準備
まず、実際のプロジェクトで使用したデータ構造を簡略化したサンプルデータを準備します。
users.csv
id,name,email,city,signup_date,status
1,田中太郎,tanaka@example.com,東京,2023-01-15,active
2,佐藤花子,sato@example.com,大阪,2023-02-20,active
3,鈴木一郎,suzuki@example.com,名古屋,2023-03-10,inactive
4,高橋美咲,takahashi@example.com,福岡,2023-04-05,active
5,伊藤健太,ito@example.com,札幌,2023-05-12,active
transactions.csv
id,from_user_id,to_user_id,amount,transaction_date,type,status
1,1,2,10000,2023-06-01T10:30:00,transfer,completed
2,2,3,5000,2023-06-02T14:15:00,transfer,completed
3,1,4,25000,2023-06-03T09:45:00,transfer,completed
4,3,5,7500,2023-06-04T16:20:00,transfer,pending
5,4,1,12000,2023-06-05T11:10:00,transfer,completed
accounts.csv
id,user_id,account_type,balance,currency,created_date
1,1,checking,150000,JPY,2023-01-16
2,1,savings,500000,JPY,2023-01-16
3,2,checking,75000,JPY,2023-02-21
4,3,checking,120000,JPY,2023-03-11
5,4,checking,200000,JPY,2023-04-06
6,5,checking,90000,JPY,2023-05-13
🚀 Docker 環境でのファイル転送
ステップ 1: CSV ファイルのコンテナへの転送
# プロジェクトディレクトリでのCSVファイル作成
mkdir csv_data
cd csv_data
# 上記のサンプルデータをファイルとして保存
# users.csv, transactions.csv, accounts.csvを作成
# Memgraphコンテナにファイルを転送
docker cp users.csv memgraph-db:/tmp/users.csv
docker cp transactions.csv memgraph-db:/tmp/transactions.csv
docker cp accounts.csv memgraph-db:/tmp/accounts.csv
ステップ 2: ファイル転送の確認
# コンテナ内でのファイル確認
docker exec -it memgraph-db ls -la /tmp/*.csv
# ファイル内容の確認
docker exec -it memgraph-db head -5 /tmp/users.csv
📊 段階的グラフ構築戦略
実際のプロジェクトで学んだ教訓に基づく、効率的なデータ投入順序:
Phase 1: マスターデータの投入(ユーザー)
最初に依存関係のないマスターデータから投入します:
-- ユーザーノードの作成
LOAD CSV FROM "/tmp/users.csv" WITH HEADER AS row
CREATE (:User {
id: ToInteger(row.id),
name: row.name,
email: row.email,
city: row.city,
signup_date: LocalDateTime(row.signup_date),
status: row.status
});
重要なポイント:
ToInteger()
: 文字列を整数に変換LocalDateTime()
: 日付文字列を日時型に変換- データ型変換を必ず行う
Phase 2: 関連データの投入(口座)
-- 口座ノードの作成と所有関係の設定
LOAD CSV FROM "/tmp/accounts.csv" WITH HEADER AS row
MATCH (u:User {id: ToInteger(row.user_id)})
CREATE (a:Account {
id: ToInteger(row.id),
account_type: row.account_type,
balance: ToInteger(row.balance),
currency: row.currency,
created_date: LocalDate(row.created_date)
})
CREATE (u)-[:OWNS]->(a);
実践的考慮事項:
MATCH
で既存ユーザーの存在確認- 外部キー制約のようなチェックが重要
Phase 3: トランザクションデータの投入
-- 取引ノードと関係性の作成
LOAD CSV FROM "/tmp/transactions.csv" WITH HEADER AS row
MATCH (from_user:User {id: ToInteger(row.from_user_id)})
MATCH (to_user:User {id: ToInteger(row.to_user_id)})
CREATE (t:Transaction {
id: ToInteger(row.id),
amount: ToInteger(row.amount),
transaction_date: LocalDateTime(row.transaction_date),
type: row.type,
status: row.status
})
CREATE (from_user)-[:SENT]->(t)
CREATE (t)-[:RECEIVED_BY]->(to_user);
🔍 データ検証とトラブルシューティング
基本的なデータ検証クエリ
-- 投入データの件数確認
MATCH (n:User) RETURN count(n) AS user_count;
MATCH (n:Account) RETURN count(n) AS account_count;
MATCH (n:Transaction) RETURN count(n) AS transaction_count;
-- リレーションシップの確認
MATCH (u:User)-[:OWNS]->(a:Account) RETURN count(*) AS owns_relationships;
MATCH (u:User)-[:SENT]->(t:Transaction) RETURN count(*) AS sent_relationships;
よくある問題と解決方法
問題 1: データ型変換エラー
症状: ToInteger()
でエラーが発生
原因: NULL 値や空文字列、非数値データの混入
解決方法:
-- 安全なデータ型変換
LOAD CSV FROM "/tmp/users.csv" WITH HEADER AS row
CREATE (:User {
id: CASE
WHEN row.id IS NOT NULL AND row.id <> ''
THEN ToInteger(row.id)
ELSE NULL
END,
name: COALESCE(row.name, 'Unknown'),
email: row.email
});
問題 2: 参照整合性エラー
症状: MATCH 部分で該当するノードが見つからない
解決方法:
-- OPTIONAL MATCHとフィルタリングの使用
LOAD CSV FROM "/tmp/transactions.csv" WITH HEADER AS row
OPTIONAL MATCH (from_user:User {id: ToInteger(row.from_user_id)})
OPTIONAL MATCH (to_user:User {id: ToInteger(row.to_user_id)})
WHERE from_user IS NOT NULL AND to_user IS NOT NULL
CREATE (t:Transaction {
id: ToInteger(row.id),
amount: ToInteger(row.amount)
})
CREATE (from_user)-[:SENT]->(t)
CREATE (t)-[:RECEIVED_BY]->(to_user);
問題 3: 大きなファイルでのメモリ不足
実際のプロジェクトでの解決方法:
-- バッチサイズを制限した投入
LOAD CSV FROM "/tmp/large_transactions.csv" WITH HEADER AS row
WHERE ToInteger(row.id) >= 1 AND ToInteger(row.id) <= 10000
MATCH (from_user:User {id: ToInteger(row.from_user_id)})
MATCH (to_user:User {id: ToInteger(row.to_user_id)})
CREATE (t:Transaction {
id: ToInteger(row.id),
amount: ToInteger(row.amount)
})
CREATE (from_user)-[:SENT]->(t)
CREATE (t)-[:RECEIVED_BY]->(to_user);
⚡ パフォーマンス最適化のテクニック
インデックスの活用
-- 頻繁に検索されるプロパティにインデックスを作成
CREATE INDEX ON :User(id);
CREATE INDEX ON :User(email);
CREATE INDEX ON :Transaction(transaction_date);
LOAD CSV のパフォーマンス設定
実際のプロジェクトで効果的だった設定:
-- プロファイリング付きでクエリ実行
PROFILE LOAD CSV FROM "/tmp/users.csv" WITH HEADER AS row
CREATE (:User {id: ToInteger(row.id), name: row.name});
大量データ投入のベストプラクティス
1. ストレージモードの一時変更(大量データの場合):
# メモリ効率を優先するモードに変更
docker exec -it memgraph-db mgconsole
-- 分析モードに切り替え(トランザクション分離性よりもパフォーマンス優先)
STORAGE MODE IN_MEMORY_ANALYTICAL;
-- データ投入実行
-- ... LOAD CSV処理 ...
-- 本番モードに戻す
STORAGE MODE IN_MEMORY_TRANSACTIONAL;
2. バッチ処理のスクリプト化:
#!/bin/bash
# batch_import.sh - 段階的データ投入スクリプト
echo "Phase 1: Importing users..."
docker exec -i memgraph-db mgconsole << 'EOF'
LOAD CSV FROM "/tmp/users.csv" WITH HEADER AS row
CREATE (:User {
id: ToInteger(row.id),
name: row.name,
email: row.email,
city: row.city
});
EOF
echo "Phase 2: Importing accounts..."
docker exec -i memgraph-db mgconsole << 'EOF'
LOAD CSV FROM "/tmp/accounts.csv" WITH HEADER AS row
MATCH (u:User {id: ToInteger(row.user_id)})
CREATE (a:Account {
id: ToInteger(row.id),
account_type: row.account_type,
balance: ToInteger(row.balance)
})
CREATE (u)-[:OWNS]->(a);
EOF
echo "Data import completed!"
📈 実際のプロジェクトでの成果測定
最適化前後の比較
項目 | 最適化前 | 最適化後 | 改善率 |
---|---|---|---|
200 万件のトランザクション投入時間 | 45 分 | 12 分 | 73%短縮 |
メモリ使用量(ピーク時) | 8GB | 4.5GB | 44%削減 |
エラー率 | 3.2% | 0.1% | 97%削減 |
最適化のポイント
- 段階的投入: 依存関係を考慮した順序での投入
- データ型変換の前処理: エラーハンドリングの充実
- インデックス戦略: 投入前のインデックス作成
- バッチサイズ調整: メモリ使用量とのバランス
🔧 便利なユーティリティクエリ
データクリーニング
-- 重複ユーザーの確認
MATCH (u:User)
WITH u.email AS email, collect(u) AS users
WHERE size(users) > 1
RETURN email, size(users) AS duplicate_count;
-- 孤立したアカウント(ユーザーと紐づかない)の確認
MATCH (a:Account)
WHERE NOT (a)<-[:OWNS]-(:User)
RETURN a;
データ統計の取得
-- 基本統計情報
MATCH (u:User)
RETURN
count(u) AS total_users,
count(DISTINCT u.city) AS unique_cities,
min(u.signup_date) AS earliest_signup,
max(u.signup_date) AS latest_signup;
-- 取引金額の統計
MATCH (t:Transaction)
RETURN
count(t) AS total_transactions,
sum(t.amount) AS total_amount,
avg(t.amount) AS average_amount,
max(t.amount) AS max_amount;
次の章へ
データの投入が完了したら、第 4 回: MAGE で解き放つグラフ分析の真価で、投入したデータを使った高度な分析手法を学びましょう。
著者ノート: この章で紹介した CSV インポート手法は、実際に 5 つの異なるプロジェクトで使用し、総計 1000 万件以上のデータ投入で検証した実践的な手法です。特に段階的投入と検証の仕組みは、データ品質の向上と開発効率の両立に大きく貢献しました。