【入門編】 APIのフィルタリング・ソート・フィールド選択のクエリ設計 – Web APIアーキテクチャ・データ連携実践ガイド

こんにちは!技術メディア編集部のシニア・インフラアーキテクトです。

日々のインフラ構築やネットワーク設計、本当にお疲れ様です。パケットがルーターを飛び交い、ファイアウォールを潜り抜けてアプリケーションに届くまでのダイナミクスを愛する私ですが、今回は少し視点を変えて、私たちが日々何気なく叩いている「Web API」の世界に飛び込んでみたいと思います。

特に「APIのフィルタリング・ソート・フィールド選択のクエリ設計」という、一見すると開発者向けのお話を取り上げます。「あれ、それってプログラマの仕事じゃないの?」と思ったインフラエンジニアや初学者のあなた、ちょっと待ってください!

実はこのクエリ設計、裏側のデータベース(DB)のインデックス設計や、ネットワークを流れるデータ量(帯域幅)に直結する、インフラエンジニアにとっても超重要なお話なんです。

難しい用語はできるだけ排除して、身近な例えを交えながら一歩ずつ紐解いていきましょう。それでは、楽しいAPI設計の旅に出発です!

—

1. 郵便配達で例える「全部もらう」と「必要な分だけもらう」の違い

皆さんの手元に、日本全国の全住民データが載った電話帳(のような分厚い冊子)が届いたと想像してみてください。
もしあなたが「東京都に住んでいる、山田さんという苗字の人だけを知りたい」と思ったとき、分厚い電話帳の最初から最後までペラペラとめくって探しますか?そんなことをしていたら、日が暮れてしまいますよね。

現実の世界では、役所やデータベースに対して「東京都の」「山田さんを」と条件を指定して、欲しい情報ピンポイントで引き出します。

Web APIの世界でも全く同じことが言えます。
例えば、ユーザー一覧を取得する GET /api/users というエンドポイントがあったとします。このシステムが大きくなり、登録ユーザーが100万人を超えたとしましょう。

何も考えずに GET /api/users を叩いてしまうと、ネットワークの向こう側にあるサーバーは、100万人分の名前、住所、メールアドレス、クレジットカード情報、購入履歴などの膨大なデータを一気に詰め込んで、あなたの手元に送り返そうとします。

これでは、まるで一通の手紙を届けるために、100トンのトラックを走らせるようなものです。ネットワークの帯域は圧迫され、サーバーのメモリはパンクし、クライアント側のアプリはフリーズしてしまいます。「一歩ずつ理解していきましょう!」ということで、これをスマートに解決するのが 「フィルタリング」「ソート」「フィールド選択」 という3つの魔法のクエリパラメータなのです。

—

2. 柔軟なクエリ設計:3つの基本テクニック

URLの後ろに ? をつけて、 key=value の形式で条件を付与していくルールを覚えていますか?このクエリパラメータを使って、サーバーにお願いする内容を細かく指定していきましょう。

2.1 フィルタリング(条件で絞り込む)

まずは、膨大なデータから「欲しいものだけ」を抽出するフィルタリングです。例えば、「ステータスが有効(active)なユーザーで、かつ、東京(tokyo)に住んでいる人」を取得したい場合は、次のようなURLを設計します。

GET /api/users?status=active&location=tokyo

このように & で繋ぐことで、複数条件の絞り込み(AND条件)を直感的に表現できます。サーバー側は、この条件をもとにデータベースへ「条件に合うデータだけちょうだい!」と効率よくお願いできるようになります。

2.2 プレフィックスや範囲の指定

完全一致だけでなく、「特定の文字から始まる」や「ある数値以上」といった条件もよく使われます。実務では次のような命名規則が好まれます。

  • name_like=Yamada: 名前が「Yamada」を含むもの(部分一致)
  • age_gte=20: 年齢が「20歳以上」(gteは Greater Than or Equal の略)

2.3 ソート(並び替える)

次に、取得したデータをどのような順番で並べるかを指定する「ソート」です。「登録日の新しい順(降順)」や「名前のアルファベット順(昇順)」などを指定します。

# 登録日の古い順(昇順)
GET /api/users?sort=created_at

# 登録日の新しい順(降順:ハイフンをつけるのが一般的です)
GET /api/users?sort=-created_at

2.4 フィールド選択(必要な項目だけに絞る)

ここがインフラ視点でも非常に重要なポイントです。
ユーザー一覧を表示するだけなのに、わざわざ「重たいプロフィール画像データのURL」や「詳細な自己紹介文」まで一緒に取得していませんか?

レスポンスのデータ量をダイエットさせるために、必要な項目(フィールド)だけを指定して取得できるようにします。

# id, name, email の3つの項目だけを返してほしい場合
GET /api/users?fields=id,name,email

これにより、ネットワーク上を流れるパケットのペイロードサイズを劇的に削減でき、モバイル回線などの細いパイプラインでも高速に表示させることができます。

—

3. データベースとインデックス設計の深い関係

さて、ここからがインフラ・ネットワークスペシャリストの腕の見せどころです。
フロントエンドやAPIを開発するエンジニアが「便利なクエリを何でも自由に生やす」と、裏側のデータベースはどうなるでしょうか?

結論から言うと、インデックス(索引)が貼られていない列でフィルタリングやソートを行うと、データベースは「フルテーブルスキャン(全件走査)」という最も重い処理を走らせます。

フルテーブルスキャンがもたらす悲劇

先ほどの100万人のユーザーの例に戻りましょう。
もし GET /api/users?location=tokyo というリクエストが飛んできたとき、データベースの location 列にインデックス(索引)が用意されていなかったらどうなるでしょう?

データベースは、100万人分のデータを上から順に1行ずつ「東京ですか?」「違います」「東京ですか?」「違います」と目視で確認していくことになります。これが同時多数のリクエストとして押し寄せたら……。CPU使用率は一瞬で100%に張り付き、データベースはダウン、APIはタイムアウト(504 Gateway Time-out)の嵐です。

インフラ・DBエンジニアからの提言:クエリ設計とインデックスの歩み寄り

美しいAPIのクエリ設計を行うときは、必ず以下のインフラ的視点をセットで考えるようにしましょう。

1. よく使われるフィルタ条件にはインデックスを張る

  • status や location など、絞り込みに使われやすい列(カラム)には、あらかじめデータベース側でB-Treeインデックスなどを構築しておきます。

2. ソートのコストを意識する

  • 大量のデータに対するソート(ORDER BY)はメモリを大量消費します。インデックス順にデータを読み出せるようなクエリ設計、あるいはインデックス設計を意識することが大切です。

3. ページネーション(分割取得)を組み合わせる

  • フィルタリングしてもなお数万件ヒットしてしまう場合は、limit と offset(またはカーソルベースのページネーション)を必ず併用させ、一度に取得する上限を強制します。

—

4. 実装サンプル:Python(FastAPI)で見る安全なクエリ設計

百聞は一見にしかず。実際に、クエリパラメータを受け取り、安全にデータを絞り込む簡単なWeb APIのコード例(PythonのFastAPIフレームワーク)を見てみましょう。コード内のコメントに注目してください。

from typing import Optional
from fastapi import FastAPI, Query

app = FastAPI()

# モックのユーザーデータベース
users_db = [
    {"id": 1, "name": "Yamada Taro", "location": "tokyo", "status": "active", "age": 28},
    {"id": 2, "name": "Tanaka Hanako", "location": "osaka", "status": "inactive", "age": 34},
    {"id": 3, "name": "Suzuki Ichiro", "location": "tokyo", "status": "active", "age": 42},
]

@app.get("/api/users")
def get_users(
    # フィルタリング用のパラメータ(オプショナル)
    status: Optional[str] = Query(None, description="ステータスで絞り込み"),
    location: Optional[str] = Query(None, description="地域で絞り込み"),
    # フィールド選択用のパラメータ
    fields: Optional[str] = Query(None, description="カンマ区切りで必要なフィールドを指定")
):
    # 1. フィルタリングの処理(実務ではSQLのWHERE句に相当します)
    filtered_users = users_db
    
    if status:
        filtered_users = [u for u in filtered_users if u["status"] == status]
    
    if location:
        filtered_users = [u for u in filtered_users if u["location"] == location]

    # 2. フィールド選択の処理(必要なキーだけにダイエットさせる)
    if fields:
        field_list = fields.split(",")
        # 指定されたフィールドのみを抽出した新しい辞書リストを作る
        result = [
            {key: user[key] for key in field_list if key in user}
            for user in filtered_users
        ]
        return result

    # フィールド指定がない場合は全項目を返す
    return filtered_users

このコードでは、クライアントから送られてきた status や location、そして fields の指定を受け取り、動的にレスポンスを構築しています。実務の現場では、これをそのままPythonだけで処理するのではなく、SQLAlchemyなどのORMや、背後のRDB(PostgreSQLやMySQLなど)のクエリビルダーへと安全に翻訳して渡すことになります。

—

5. おわりに:美しいAPI設計は、美しいインフラ基盤から生まれる

今回は、APIのフィルタリング・ソート・フィールド選択のクエリ設計について、郵便配達の例えやデータベースのインデックスの話を交えながら解説してきましたがいかがでしたでしょうか?

「必要なものを、必要な分だけ、綺麗な並びでスマートに受け取る」。
これは、ネットワークのパケット制御の思想とまったく同じです。

エンドポイントのURLを美しく設計し、適切なクエリパラメータを用意することは、単にプログラムを書きやすくするだけでなく、「データベースの負荷を抑え、ネットワークの帯域を守り、ユーザーに最高のレスポンスを返す」という、極めてインフラストラクチャ寄りの最適化そのものです。

次にAPIを設計、あるいは叩くときは、ぜひ「このクエリの裏で、データベースのインデックスはどう動いているかな?」と一歩踏み込んで想像してみてください。きっと、ネットワークとアプリケーションの繋がりがより一層面白く感じられるはずです。

それでは、また次回の深淵なプロトコルの世界でお会いしましょう!

コメント

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