azure-cosmosdb について
Azure Cosmos DB の NoSQL API を使う開発者向けに、データ設計、SDK 操作、運用方法をまとめたスキルです。パーティションキー、アクセスパターン、一貫性の選択が性能とコストに与える影響を扱います。
主な機能
- 01パーティションキーとデータ分散の設計
- 02TypeScript と Python SDK の操作
- 03一貫性レベルの選択
- 04バッチ操作と変更フィード処理
- 05インデックス、スループット、TTL の管理
- 06RU コストとスロットリングの最適化
活用シーン
- 01マルチテナントコンテナーのパーティション設計
- 02アイテムの CRUD 操作の実装
- 03コンテナーの変更フィード処理
- 04クエリコストとスループットの最適化
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ alinaqi/ maggy --skill azure-cosmosdbインストール後、Claude Code で/azure-cosmosdbを入力して読み込まれたか確認します。
詳細
核心原則
パーティションキーを慎重に選び、アクセスパターンに合わせて設計し、一貫性のトレードオフを理解しましょう。
Cosmos DB はデータをパーティション間で分散します。パーティションキーの選択によって、スケーラビリティ、パフォーマンス、コストが決まります。データが均等に分散され、クエリが効率的になるように設計しましょう。
Cosmos DB API
| API | 用途 |
|---|---|
| NoSQL (Core) | ドキュメントデータベース。最も柔軟 |
| MongoDB | MongoDB ワイヤープロトコルに対応 |
| PostgreSQL | 分散 PostgreSQL(Citus) |
| Apache Cassandra | ワイドカラムストア |
| Apache Gremlin | グラフデータベース |
| Table | キーと値(Azure Table Storage に対応) |
このスキルでは、最も一般的な選択肢である NoSQL (Core) API に焦点を当てます。
主要な概念
| 概念 | 説明 |
|---|---|
| コンテナー | アイテムのコレクション(テーブルのようなもの) |
| アイテム | 単一のドキュメント/レコード(JSON) |
| パーティションキー | データの分散方法を決定します |
| 論理パーティション | 同じパーティションキーを持つアイテム |
| 物理パーティション | ストレージ単位(最大 50GB、10K RU/s) |
| RU(リクエストユニット) | スループットの単位 |
パーティションキーの設計
適切なパーティションキー
// High cardinality, even distribution, used in queries
// E-commerce: userId for user data
{ "id": "order-123", "userId": "user-456", ... } // PK: /userId
// Multi-tenant: tenantId
{ "id": "doc-1", "tenantId": "tenant-abc", ... } // PK: /tenantId
// IoT: deviceId for telemetry
{ "id": "reading-1", "deviceId": "device-789", ... } // PK: /deviceId
// Logs: synthetic key (date + category)
{ "id": "log-1", "partitionKey": "2024-01-15_errors", ... } // PK: /partitionKey
階層型パーティションキー
// For multi-level distribution (e.g., tenant → user)
// Container created with: /tenantId, /userId
{
"id": "order-123",
"tenantId": "acme-corp",
"userId": "user-456",
"items": [...]
}
// Query within tenant and user efficiently
不適切なパーティションキー
// Avoid:
// - Low cardinality (status, type, boolean)
// - Monotonically increasing (timestamp, auto-increment)
// - Frequently updated fields
// - Fields not used in queries
// Bad: Only 3 values → hot partitions
{ "status": "pending" | "completed" | "cancelled" }
// Bad: All writes go to latest partition
{ "timestamp": "2024-01-15T10:30:00Z" }
SDK のセットアップ(TypeScript)
インストール
npm install @azure/cosmos
クライアントの初期化
// lib/cosmosdb.ts
import { CosmosClient, Database, Container } from '@azure/cosmos';
const endpoint = process.env.COSMOS_ENDPOINT!;
const key = process.env.COSMOS_KEY!;
const databaseId = process.env.COSMOS_DATABASE!;
const client = new CosmosClient({ endpoint, key });
// Or with connection string
// const client = new CosmosClient(process.env.COSMOS_CONNECTION_STRING!);
export const database: Database = client.database(databaseId);
export function getContainer(containerId: string): Container {
return database.container(containerId);
}
型定義
// types/cosmos.ts
export interface BaseItem {
id: string;
_ts?: number; // Auto-generated timestamp
_etag?: string; // For optimistic concurrency
}
export interface User extends BaseItem {
userId: string; // Partition key
email: string;
name: string;
createdAt: string;
updatedAt: string;
}
export interface Order extends BaseItem {
userId: string; // Partition key
orderId: string;
items: OrderItem[];
total: number;
status: 'pending' | 'paid' | 'shipped' | 'delivered';
createdAt: string;
}
export interface OrderItem {
productId: string;
name: string;
quantity: number;
price: number;
}
CRUD 操作
アイテムの作成
import { getContainer } from './cosmosdb';
import { User } from './types';
const usersContainer = getContainer('users');
async function createUser(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
const now = new Date().toISOString();
const user: User = {
id: crypto.randomUUID(),
...data,
createdAt: now,
updatedAt: now
};
const { resource } = await usersContainer.items.create(user);
return resource as User;
}
アイテムの読み取り(ポイント読み取り)
// Most efficient read - requires id AND partition key
async function getUser(userId: string, id: string): Promise<User | null> {
try {
const { resource } = await usersContainer.item(id, userId).read<User>();
return resource || null;
} catch (error: any) {
if (error.code === 404) return null;
throw error;
}
}
// If id equals partition key value
async function getUserById(userId: string): Promise<User | null> {
try {
const { resource } = await usersContainer.item(userId, userId).read<User>();
return resource || null;
} catch (error: any) {
if (error.code === 404) return null;
throw error;
}
}
アイテムのクエリ
// Query within partition (efficient)
async function getUserOrders(userId: string): Promise<Order[]> {
const ordersContainer = getContainer('orders');
const { resources } = await ordersContainer.items
.query<Order>({
query: 'SELECT * FROM c WHERE c.userId = @userId ORDER BY c.createdAt DESC',
parameters: [{ name: '@userId', value: userId }]
})
.fetchAll();
return resources;
}
// Cross-partition query (use sparingly)
async function getOrdersByStatus(status: string): Promise<Order[]> {
const ordersContainer = getContainer('orders');
const { resources } = await ordersContainer.items
.query<Order>({
query: 'SELECT * FROM c WHERE c.status = @status',
parameters: [{ name: '@status', value: status }]
})
.fetchAll();
return resources;
}
// Paginated query
async function getOrdersPaginated(
userId: string,
pageSize: number = 10,
continuationToken?: string
): Promise<{ items: Order[]; continuationToken?: string }> {
const ordersContainer = getContainer('orders');
const queryIterator = ordersContainer.items.query<Order>(
{
query: 'SELECT * FROM c WHERE c.userId = @userId ORDER BY c.createdAt DESC',
parameters: [{ name: '@userId', value: userId }]
},
{
maxItemCount: pageSize,
continuationToken
}
);
const { resources, continuationToken: nextToken } = await queryIterator.fetchNext();
return {
items: resources,
continuationToken: nextToken
};
}
アイテムの更新
// Replace entire item
async function updateUser(userId: string, id: string, updates: Partial<User>): Promise<User> {
const existing = await getUser(userId, id);
if (!existing) throw new Error('User not found');
const updated: User = {
...existing,
...updates,
updatedAt: new Date().toISOString()
};
const { resource } = await usersContainer.item(id, userId).replace(updated);
return resource as User;
}
// Partial update (patch operations)
async function patchUser(userId: string, id: string, operations: any[]): Promise<User> {
const { resource } = await usersContainer.item(id, userId).patch(operations);
return resource as User;
}
// Usage:
await patchUser('user-123', 'user-123', [
{ op: 'set', path: '/name', value: 'New Name' },
{ op: 'set', path: '/updatedAt', value: new Date().toISOString() },
{ op: 'incr', path: '/loginCount', value: 1 }
]);
アイテムの削除
async function deleteUser(userId: string, id: string): Promise<void> {
await usersContainer.item(id, userId).delete();
}
楽観的同時実行制御(ETag)
async function updateUserWithETag(
userId: string,
id: string,
updates: Partial<User>,
etag: string
): Promise<User> {
const existing = await getUser(userId, id);
if (!existing) throw new Error('User not found');
const updated: User = {
...existing,
...updates,
updatedAt: new Date().toISOString()
};
try {
const { resource } = await usersContainer.item(id, userId).replace(updated, {
accessCondition: { type: 'IfMatch', condition: etag }
});
return resource as User;
} catch (error: any) {
if (error.code === 412) {
throw new Error('Document was modified by another process');
}
throw error;
}
}
一貫性レベル
| レベル | 保証内容 | レイテンシ | 用途 |
|---|---|---|---|
| Strong | 線形化可能な読み取り | 最も高い | 金融、在庫管理 |
| Bounded Staleness | 定められた範囲内で一貫性を維持 | 高い | リーダーボード、カウンター |
| Session | 自分の書き込みを読み取れる | 中程度 | ユーザーセッション(既定) |
| Consistent Prefix | 順序どおりの読み取り | 低い | ソーシャルフィード |
| Eventual | 順序の保証なし | 最も低い | 分析、ログ |
リクエストごとに一貫性を設定
// Override default consistency
const { resource } = await usersContainer.item(id, userId).read<User>({
consistencyLevel: 'Strong'
});
// For queries
const { resources } = await container.items.query(
{ query: 'SELECT * FROM c' },
{ consistencyLevel: 'BoundedStaleness' }
).fetchAll();
バッチ操作
トランザクションバッチ(同じパーティション)
async function createOrderWithItems(userId: string, order: Order, items: any[]): Promise<void> {
const ordersContainer = getContainer('orders');
const operations = [
{ operationType: 'Create' as const, resourceBody: order },
...items.map(item => ({
operationType: 'Create' as const,
resourceBody: { ...item, userId, orderId: order.orderId }
}))
];
const { result } = await ordersContainer.items.batch(operations, userId);
// Check if any operation failed
if (result.some(r => r.statusCode >= 400)) {
throw new Error('Batch operation failed');
}
}
一括操作
// For large-scale imports (not transactional)
async function bulkImportUsers(users: User[]): Promise<void> {
const operations = users.map(user => ({
operationType: 'Create' as const,
resourceBody: user,
partitionKey: user.userId
}));
// Process in chunks
const chunkSize = 100;
for (let i = 0; i < operations.length; i += chunkSize) {
const chunk = operations.slice(i, i + chunkSize);
await usersContainer.items.bulk(chunk);
}
}
変更フィード
変更の処理
import { ChangeFeedStartFrom } from '@azure/cosmos';
async function processChangeFeed(): Promise<void> {
const container = getContainer('orders');
const changeFeedIterator = container.items.changeFeed({
changeFeedStartFrom: ChangeFeedStartFrom.Beginning()
});
while (changeFeedIterator.hasMoreResults) {
const { result: items, statusCode } = await changeFeedIterator.fetchNext();
if (statusCode === 304) {
// No new changes
await sleep(1000);
continue;
}
for (const item of items) {
console.log('Changed item:', item);
// Process the change...
}
}
}
// For production, use Change Feed Processor with lease container
変更フィードプロセッサーパターン
async function startChangeFeedProcessor(): Promise<void> {
const sourceContainer = getContainer('orders');
const leaseContainer = getContainer('leases');
const changeFeedProcessor = sourceContainer.items.changeFeed
.for(item => {
// Process each change
console.log('Processing:', item);
})
.withLeaseContainer(leaseContainer)
.build();
await changeFeedProcessor.start();
}
Python SDK
インストール
pip install azure-cosmos
セットアップと操作
# cosmos_db.py
import os
from azure.cosmos import CosmosClient, PartitionKey
from azure.cosmos.exceptions import CosmosResourceNotFoundError
from typing import Optional, List
from datetime import datetime
import uuid
# Initialize client
endpoint = os.environ['COSMOS_ENDPOINT']
key = os.environ['COSMOS_KEY']
database_name = os.environ['COSMOS_DATABASE']
client = CosmosClient(endpoint, key)
database = client.get_database_client(database_name)
def get_container(container_name: str):
return database.get_container_client(container_name)
# CRUD Operations
users_container = get_container('users')
def create_user(email: str, name: str, user_id: str = None) -> dict:
user_id = user_id or str(uuid.uuid4())
now = datetime.utcnow().isoformat()
user = {
'id': user_id,
'userId': user_id, # Partition key
'email': email,
'name': name,
'createdAt': now,
'updatedAt': now
}
return users_container.create_item(user)
def get_user(user_id: str) -> Optional[dict]:
try:
return users_container.read_item(item=user_id, partition_key=user_id)
except CosmosResourceNotFoundError:
return None
def query_users(email_domain: str) -> List[dict]:
query = "SELECT * FROM c WHERE CONTAINS(c.email, @domain)"
parameters = [{'name': '@domain', 'value': email_domain}]
return list(users_container.query_items(
query=query,
parameters=parameters,
enable_cross_partition_query=True
))
def update_user(user_id: str, **updates) -> dict:
user = get_user(user_id)
if not user:
raise ValueError('User not found')
user.update(updates)
user['updatedAt'] = datetime.utcnow().isoformat()
return users_container.replace_item(item=user_id, body=user)
def delete_user(user_id: str) -> None:
users_container.delete_item(item=user_id, partition_key=user_id)
# Paginated query
def get_users_paginated(page_size: int = 10, continuation_token: str = None):
query = "SELECT * FROM c ORDER BY c.createdAt DESC"
items = users_container.query_items(
query=query,
enable_cross_partition_query=True,
max_item_count=page_size,
continuation_token=continuation_token
)
page = items.by_page()
results = list(next(page))
return {
'items': results,
'continuation_token': page.continuation_token
}
インデックス作成
カスタムインデックスポリシー
{
"indexingMode": "consistent",
"automatic": true,
"includedPaths": [
{ "path": "/userId/?" },
{ "path": "/status/?" },
{ "path": "/createdAt/?" }
],
"excludedPaths": [
{ "path": "/content/*" },
{ "path": "/_etag/?" }
],
"compositeIndexes": [
[
{ "path": "/userId", "order": "ascending" },
{ "path": "/createdAt", "order": "descending" }
]
]
}
インデックスを指定してコンテナーを作成
await database.containers.createIfNotExists({
id: 'orders',
partitionKey: { paths: ['/userId'] },
indexingPolicy: {
indexingMode: 'consistent',
includedPaths: [
{ path: '/userId/?' },
{ path: '/status/?' },
{ path: '/createdAt/?' }
],
excludedPaths: [
{ path: '/*' } // Exclude all by default
]
}
});
スループットの管理
プロビジョニング済みスループット
// Container level
await database.containers.createIfNotExists({
id: 'orders',
partitionKey: { paths: ['/userId'] },
throughput: 1000 // RU/s
});
// Scale throughput
const container = database.container('orders');
await container.throughput.replace(2000);
オートスケール
await database.containers.createIfNotExists({
id: 'orders',
partitionKey: { paths: ['/userId'] },
maxThroughput: 10000 // Auto-scales 10% to 100%
});
サーバーレス
// No throughput configuration needed
// Pay per request (good for dev/test, intermittent workloads)
await database.containers.createIfNotExists({
id: 'orders',
partitionKey: { paths: ['/userId'] }
// No throughput = serverless
});
CLI クイックリファレンス
# Azure CLI
az cosmosdb create --name myaccount --resource-group mygroup
az cosmosdb sql database create --account-name myaccount --name mydb --resource-group mygroup
az cosmosdb sql container create \
--account-name myaccount \
--database-name mydb \
--name orders \
--partition-key-path /userId \
--throughput 400
# Query
az cosmosdb sql query --account-name myaccount --database-name mydb \
--container-name orders --query "SELECT * FROM c"
# Keys
az cosmosdb keys list --name myaccount --resource-group mygroup
az cosmosdb keys list --name myaccount --resource-group mygroup --type connection-strings
コストの最適化
| 戦略 | 効果 |
|---|---|
| 適切なパーティションキー | ホットパーティション(RU の浪費)を回避 |
| クエリするものだけにインデックスを作成 | 書き込み時の RU コストを削減 |
| ポイント読み取りを使用 | クエリの 3 RU 以上に対し 1 RU |
| 開発/テストではサーバーレスを使用 | リクエストごとに課金 |
| 本番環境ではオートスケールを使用 | トラフィックが少ない時間帯にスケールダウン |
| 一時データには TTL を使用 | 古いアイテムを自動削除 |
有効期間(TTL)
// Enable TTL on container
await database.containers.createIfNotExists({
id: 'sessions',
partitionKey: { paths: ['/userId'] },
defaultTtl: 3600 // 1 hour
});
// Per-item TTL
const session = {
id: 'session-123',
userId: 'user-456',
ttl: 1800 // Override: 30 minutes
};
アンチパターン
- 不適切なパーティションキー - カーディナリティが低いとホットパーティションが発生します
- クロスパーティションクエリ - コストが高いため、単一パーティションのクエリ向けに設計します
- 過剰なインデックス作成 - 書き込みコストが増加するため、クエリ対象のパスだけにインデックスを作成します
- 大きすぎるアイテム - 最大 2MB です。BLOB は Azure Blob Storage に保存します
- RU コストの無視 - コストの高いクエリを監視して最適化します
- あらゆる場面で強い一貫性を使用 - 必要な場合を除き、Session(既定)を使用します
- 再試行ロジックがない - 429(スロットリング)は指数バックオフで処理します
- TTL の未設定 - 一時データやセッションデータには TTL を設定します
コメント
Markdown 対応