T-CREATOR

Prisma で複雑な集計クエリを簡単に書く方法

Prisma で複雑な集計クエリを簡単に書く方法

近年の Web アプリケーション開発において、データベースから効率的に情報を取得し、集計処理を行うことは不可欠な要件となっています。特にダッシュボードやレポート機能、分析システムを構築する際には、複雑な集計クエリを正確かつ効率的に記述する必要があります。

従来の SQL 文では、複雑な集計処理を記述する際に長大で読みにくいクエリになってしまうことが多く、また型安全性の確保も困難でした。一方、Prisma の ORM 機能を活用することで、TypeScript の型システムと連携した直感的な集計クエリの記述が可能になります。

背景

Prisma における集計機能の位置づけ

Prisma は Modern Database Toolkit として設計されており、その中核機能の一つが強力な集計機能です。Prisma の集計機能は単なるクエリビルダーではなく、データベースの性能を最大限に活用しながら、開発者にとって理解しやすい API を提供することを目的としています。

以下の図は、Prisma における集計機能の全体像を示しています。

mermaidflowchart TD
  app[アプリケーション] -->|集計要求| prisma[Prisma Client]
  prisma -->|最適化されたSQL| db[(データベース)]
  db -->|集計結果| prisma
  prisma -->|型安全な結果| app

  prisma --> aggregate[aggregate メソッド]
  prisma --> groupby[groupBy メソッド]
  prisma --> count[count メソッド]
  prisma --> relations[リレーション集計]

Prisma Client は開発者の集計要求を受け取り、データベースに最適化された SQL を生成します。その結果を型安全な形でアプリケーションに返却するのが基本的なフローです。

TypeScript との親和性の高さ

Prisma の最大の特徴の一つは、TypeScript との優れた統合です。スキーマファイルから自動生成される型情報により、集計クエリの記述時にも IntelliSense による補完や型チェックの恩恵を受けることができます。

typescript// Prismaが生成する型情報の例
type OrderAggregateResult = {
  _sum: {
    totalAmount: number | null;
  };
  _avg: {
    totalAmount: number | null;
  };
  _count: {
    id: number;
  };
};

この型情報により、集計結果の値が null になる可能性も含めて、コンパイル時に型安全性を確保できます。

パフォーマンスとメンテナンス性のバランス

Prisma は開発効率と実行効率のバランスを重視した設計となっています。集計クエリにおいても、直感的な API を保ちながら、データベースレベルで最適化されたクエリを生成します。

以下のような最適化が自動的に適用されます:

  • インデックスを活用した効率的なクエリプラン
  • 不要なカラムの除外
  • 集計専用の SQL 関数の活用

課題

複雑な集計クエリを書く際の一般的な困難

従来の SQL 文を直接記述する場合、以下のような困難に直面することが多くありました。

複雑な集計処理では、多数のテーブルを結合し、条件分岐を含む集計ロジックを実装する必要があります。以下の図は、一般的な集計処理の複雑さを示しています。

mermaidflowchart LR
  users[users テーブル] --> join1[JOIN]
  orders[orders テーブル] --> join1
  join1 --> join2[JOIN]
  order_items[order_items テーブル] --> join2
  join2 --> aggregate[集計処理]
  aggregate --> conditions[条件分岐]
  conditions --> result[集計結果]

このような複雑な処理を SQL で記述する場合、以下の問題が発生します:

コードの可読性低下: 長大な SQL クエリは理解とメンテナンスが困難になります エラーの発見困難: 構文エラーや論理エラーの特定に時間がかかります
再利用性の欠如: 似たような集計処理でも、一から記述する必要があります

Raw SQL vs Prisma の選択基準

開発現場では、Raw SQL と Prisma のどちらを選択すべきかという判断に迫られることがあります。適切な選択をするための基準を理解することが重要です。

判断要素Raw SQL が適しているPrisma が適している
クエリの複雑度極めて複雑な分析クエリ一般的なビジネスロジック
開発効率パフォーマンス最優先開発速度重視
メンテナンス性SQL 専門家が担当チーム全体で保守
型安全性不要・手動で管理必須要件

型安全性を保ちながらの集計処理

TypeScript プロジェクトにおいて、集計処理の結果に対する型安全性を確保することは、バグの早期発見と保守性向上のために不可欠です。しかし、従来のアプローチでは以下のような課題がありました:

動的な集計結果の型定義が困難: 集計処理の内容に応じて結果の型が変化するため、正確な型定義が困難でした null ハンドリングの複雑さ: 集計関数の結果が null になる可能性を考慮した型設計が必要でした コンパイル時チェックの限界: 実行時にのみ発見できる型関連のエラーが存在しました

解決策

Prisma の集計メソッド群の活用法

Prisma は集計処理を効率的に行うための専用メソッドを提供しています。これらのメソッドを適切に組み合わせることで、複雑な集計要件にも対応できます。

以下は、主要な集計メソッドの概要を示した図です:

mermaidflowchart TD
  prisma[Prisma Client] --> methods{集計メソッド}
  methods --> aggregate[aggregate]
  methods --> groupby[groupBy]
  methods --> count[count]
  methods --> relations[リレーション集計]

  aggregate --> sum[sum - 合計]
  aggregate --> avg[avg - 平均]
  aggregate --> min[min - 最小値]
  aggregate --> max[max - 最大値]

  groupby --> group_fields[グループ化フィールド]
  groupby --> having[HAVING 条件]

  count --> simple_count[シンプル集計]
  count --> conditional[条件付き集計]

各メソッドは特定の集計要件に最適化されており、組み合わせることで複雑な要件にも対応できます。

基本的な集計メソッドの実装例を見てみましょう:

typescript// Prisma Clientのインポート
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
typescript// 基本的な集計処理の設定
async function setupAggregation() {
  // データベース接続の確立
  await prisma.$connect();

  console.log('集計処理の準備が完了しました');
}

aggregate、groupBy、count の使い分け

各集計メソッドには明確な用途があり、適切に使い分けることが重要です。

aggregate メソッド は、テーブル全体または条件に合致するレコードに対する集計値を取得する際に使用します:

typescript// aggregate メソッドの基本的な使用法
async function getBasicAggregation() {
  const result = await prisma.order.aggregate({
    _sum: {
      totalAmount: true,
    },
    _avg: {
      totalAmount: true,
    },
    _count: {
      id: true,
    },
  });

  return result;
}

groupBy メソッド は、特定のフィールドでグループ化した集計結果が必要な場合に使用します:

typescript// groupBy メソッドの実装
async function getGroupedAggregation() {
  const result = await prisma.order.groupBy({
    by: ['status', 'userId'],
    _sum: {
      totalAmount: true,
    },
    _count: {
      id: true,
    },
  });

  return result;
}

count メソッド は、レコード数の取得に特化した効率的な方法です:

typescript// count メソッドの活用
async function getRecordCounts() {
  // シンプルなカウント
  const totalOrders = await prisma.order.count();

  // 条件付きカウント
  const activeOrders = await prisma.order.count({
    where: {
      status: 'ACTIVE',
    },
  });

  return { totalOrders, activeOrders };
}

ネストした関係での集計方法

Prisma の強力な機能の一つは、リレーションを跨いだ集計処理を直感的に記述できることです。

以下の図は、リレーションを跨いだ集計処理のデータフローを示しています:

mermaidflowchart TD
  user[User] -->|1:N| orders[Orders]
  orders -->|1:N| items[OrderItems]
  items -->|N:1| products[Products]

  user --> user_aggregate[ユーザー別集計]
  orders --> order_aggregate[注文別集計]
  items --> item_aggregate[商品別集計]

  user_aggregate --> total_spent[総支出額]
  order_aggregate --> avg_order[平均注文額]
  item_aggregate --> popular_products[人気商品]

ネストした関係での集計処理の実装方法を見てみましょう:

typescript// リレーションを含む集計クエリ
async function getNestedAggregation() {
  const result = await prisma.user.findMany({
    include: {
      orders: {
        include: {
          _count: {
            select: {
              orderItems: true,
            },
          },
        },
      },
      _count: {
        select: {
          orders: true,
        },
      },
    },
  });

  return result;
}
typescript// より複雑なネストした集計
async function getAdvancedNestedAggregation() {
  const result = await prisma.user.findMany({
    select: {
      id: true,
      name: true,
      orders: {
        select: {
          totalAmount: true,
          orderItems: {
            select: {
              quantity: true,
              price: true,
            },
          },
        },
      },
    },
  });

  return result;
}

具体例

基本的な集計クエリ(合計、平均、最大値など)

実際の EC サイトの売上分析を例に、基本的な集計クエリの実装を見てみましょう。

まず、売上の合計値を取得する処理から始めます:

typescript// 売上合計の計算
async function getTotalSales() {
  const salesTotal = await prisma.order.aggregate({
    _sum: {
      totalAmount: true,
    },
    where: {
      status: 'COMPLETED',
    },
  });

  return salesTotal._sum.totalAmount || 0;
}
typescript// 平均注文額の計算
async function getAverageOrderValue() {
  const averageOrder = await prisma.order.aggregate({
    _avg: {
      totalAmount: true,
    },
    where: {
      status: 'COMPLETED',
      createdAt: {
        gte: new Date('2024-01-01'),
      },
    },
  });

  return averageOrder._avg.totalAmount || 0;
}
typescript// 最高注文額と最低注文額の取得
async function getOrderValueRange() {
  const orderRange = await prisma.order.aggregate({
    _max: {
      totalAmount: true,
    },
    _min: {
      totalAmount: true,
    },
    where: {
      status: 'COMPLETED',
    },
  });

  return {
    maxOrder: orderRange._max.totalAmount || 0,
    minOrder: orderRange._min.totalAmount || 0,
  };
}

これらの基本的な集計クエリは、ダッシュボードの主要指標として活用できます。

グループ化を伴う複雑な集計

続いて、より実用的なグループ化を伴う集計処理を実装してみましょう。

月別売上分析の実装例:

typescript// 月別売上集計
async function getMonthlySales() {
  const monthlySales = await prisma.order.groupBy({
    by: ['createdAt'],
    _sum: {
      totalAmount: true,
    },
    _count: {
      id: true,
    },
    where: {
      status: 'COMPLETED',
      createdAt: {
        gte: new Date('2024-01-01'),
      },
    },
  });

  return monthlySales;
}
typescript// ユーザー種別と地域での集計
async function getSalesByUserTypeAndRegion() {
  const segmentedSales = await prisma.order.groupBy({
    by: ['userId'],
    _sum: {
      totalAmount: true,
    },
    _count: {
      id: true,
    },
    having: {
      totalAmount: {
        _sum: {
          gt: 10000, // 総購入額が1万円以上のユーザー
        },
      },
    },
  });

  return segmentedSales;
}

商品カテゴリ別の売上分析も重要な指標です:

typescript// 商品カテゴリ別売上集計
async function getCategorySales() {
  const categorySales = await prisma.orderItem.groupBy({
    by: ['productId'],
    _sum: {
      price: true,
      quantity: true,
    },
    include: {
      product: {
        select: {
          name: true,
          category: true,
        },
      },
    },
  });

  return categorySales;
}

関連テーブルを跨いだ集計処理

実際のビジネス要件では、複数のテーブルを跨いだ集計処理が頻繁に必要になります。

顧客生涯価値(CLV)の計算例:

typescript// 顧客生涯価値の計算
async function calculateCustomerLifetimeValue() {
  const customers = await prisma.user.findMany({
    select: {
      id: true,
      name: true,
      email: true,
      orders: {
        select: {
          totalAmount: true,
          createdAt: true,
          orderItems: {
            select: {
              quantity: true,
              price: true,
            },
          },
        },
        where: {
          status: 'COMPLETED',
        },
      },
    },
  });

  return customers;
}
typescript// 商品別利益率分析
async function getProductProfitAnalysis() {
  const productAnalysis = await prisma.product.findMany({
    select: {
      id: true,
      name: true,
      costPrice: true,
      sellPrice: true,
      orderItems: {
        select: {
          quantity: true,
          price: true,
          order: {
            select: {
              createdAt: true,
              status: true,
            },
          },
        },
      },
    },
  });

  return productAnalysis;
}

在庫と売上の相関分析:

typescript// 在庫と売上の相関分析
async function getInventorySalesCorrelation() {
  const correlation = await prisma.product.findMany({
    select: {
      id: true,
      name: true,
      stockQuantity: true,
      orderItems: {
        select: {
          quantity: true,
          createdAt: true,
        },
        where: {
          createdAt: {
            gte: new Date(
              Date.now() - 30 * 24 * 60 * 60 * 1000
            ), // 過去30日
          },
        },
      },
    },
  });

  return correlation;
}

条件付き集計とフィルタリング

ビジネス要件に応じた柔軟な条件設定による集計処理を見てみましょう。

typescript// 期間とステータスによる条件付き集計
async function getConditionalSalesReport(
  startDate: Date,
  endDate: Date,
  status: string[]
) {
  const report = await prisma.order.aggregate({
    _sum: {
      totalAmount: true,
    },
    _count: {
      id: true,
    },
    _avg: {
      totalAmount: true,
    },
    where: {
      AND: [
        {
          createdAt: {
            gte: startDate,
            lte: endDate,
          },
        },
        {
          status: {
            in: status,
          },
        },
        {
          totalAmount: {
            gt: 0, // 金額が0より大きい注文のみ
          },
        },
      ],
    },
  });

  return report;
}
typescript// 複雑な条件を組み合わせた集計
async function getAdvancedFilteredAggregation() {
  const result = await prisma.order.groupBy({
    by: ['status', 'userId'],
    _sum: {
      totalAmount: true,
    },
    _count: {
      id: true,
    },
    where: {
      OR: [
        {
          totalAmount: {
            gte: 5000, // 高額注文
          },
        },
        {
          AND: [
            {
              user: {
                membershipLevel: 'PREMIUM',
              },
            },
            {
              totalAmount: {
                gte: 1000,
              },
            },
          ],
        },
      ],
    },
    having: {
      id: {
        _count: {
          gte: 3, // 3回以上注文しているユーザー
        },
      },
    },
  });

  return result;
}

動的な条件構築の実装例:

typescript// 動的な条件構築を使った柔軟な集計
async function getDynamicAggregation(filters: {
  dateRange?: { start: Date; end: Date };
  userTypes?: string[];
  minAmount?: number;
  productCategories?: string[];
}) {
  const whereConditions: any = {};

  if (filters.dateRange) {
    whereConditions.createdAt = {
      gte: filters.dateRange.start,
      lte: filters.dateRange.end,
    };
  }

  if (filters.minAmount) {
    whereConditions.totalAmount = {
      gte: filters.minAmount,
    };
  }

  if (filters.userTypes && filters.userTypes.length > 0) {
    whereConditions.user = {
      membershipLevel: {
        in: filters.userTypes,
      },
    };
  }

  const result = await prisma.order.aggregate({
    _sum: {
      totalAmount: true,
    },
    _count: {
      id: true,
    },
    _avg: {
      totalAmount: true,
    },
    where: whereConditions,
  });

  return result;
}

まとめ

Prisma を活用した集計クエリの実装により、従来の SQL 文では困難だった型安全で保守性の高い集計処理システムを構築することができます。

図で理解できる要点:

  • Prisma の集計機能は開発効率とパフォーマンスのバランスを実現
  • 複雑な関係性を持つデータも直感的な API で集計可能
  • TypeScript との連携により実行時エラーを大幅に削減

本記事で紹介したパターンを活用することで、以下のようなメリットを得ることができます:

開発効率の向上: 直感的な API により、複雑な集計ロジックも短時間で実装可能になります

保守性の確保: TypeScript の型システムとの連携により、リファクタリング時の安全性が向上します

パフォーマンスの最適化: Prisma が自動生成する最適化された SQL により、効率的な集計処理を実現できます

スケーラビリティ: 基本的なパターンを組み合わせることで、より複雑な要件にも対応可能です

実際のプロジェクトでは、これらのパターンを組み合わせて、ビジネス要件に最適な集計システムを構築してください。また、定期的なパフォーマンス監視を行い、必要に応じてクエリの最適化やインデックスの追加を検討することをお勧めします。

関連リンク