SQLite ウィンドウ関数でよくあるエラーと解決策

2025-05-31

ウィンドウ関数の基本的な考え方

ウィンドウ関数は、特定の条件に基づいて行のグループ(「ウィンドウ」または「フレーム」)を定義し、そのグループ内の行に対して関数を適用します。このグループは、OVER 句によって定義されます。

OVER 句の構成要素

OVER 句は、以下の3つの主要な部分で構成されます。

    • 結果セットを行のグループに分割します。これは、GROUP BY 句に似ていますが、GROUP BY が各グループにつき1行しか返さないのに対し、PARTITION BY は各グループ内の元の行をすべて保持します。
    • 例えば、部署ごとの給与ランキングを計算する場合、PARTITION BY 部署名 とすることで、部署ごとに独立したランキングを作成できます。
  1. ORDER BY 句 (任意)

    • 各パーティション内の行の順序を定義します。これは、ランキング関数 (ROW_NUMBER, RANK, DENSE_RANK, NTILE) や、移動平均、累積和などの計算を行う際に非常に重要です。
    • 例えば、給与の高い順に並べ替える場合、ORDER BY 給与 DESC とします。
  2. ROWS または RANGE フレーム句 (任意)

    • 各行の「ウィンドウ」または「フレーム」をさらに限定します。これは、現在の行を基準として、その前後の何行(または値の範囲)を計算対象にするかを定義します。
    • 一般的に、移動平均や累積和を計算する際に使用されます。
    • 例:
      • ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: 現在の行からパーティションの先頭まですべての行を含みます(累積和などで使用)。
      • ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING: 現在の行と、その前後の1行を含みます(3点移動平均などで使用)。
      • ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING: パーティション内のすべての行を含みます。

主なウィンドウ関数

SQLiteで利用できる主なウィンドウ関数には、以下の種類があります。

  1. ランキング関数

    • ROW_NUMBER(): 各パーティション内で、指定された順序で行に一意の連続した番号を割り当てます。
    • RANK(): 各パーティション内で、指定された順序で行にランクを割り当てます。同じ値を持つ行には同じランクが与えられ、次のランクはスキップされます。
    • DENSE_RANK(): RANK() と似ていますが、同じ値を持つ行に同じランクが与えられた後、次のランクはスキップされずに連続します。
    • NTILE(n): 各パーティション内の行を n 個のグループに均等に分割し、それぞれのグループに番号を割り当てます。
  2. 値関数

    • LEAD(expression, offset, default_value): 現在の行から指定されたオフセットだけ後の行の値を返します。
    • LAG(expression, offset, default_value): 現在の行から指定されたオフセットだけ前の行の値を返します。
    • FIRST_VALUE(expression): ウィンドウフレーム内の最初の行の値を返します。
    • LAST_VALUE(expression): ウィンドウフレーム内の最後の行の値を返します。
    • NTH_VALUE(expression, n): ウィンドウフレーム内の n 番目の行の値を返します。
  3. 集約関数をウィンドウ関数として使用

    • SUM() OVER (...): ウィンドウ内の値の合計を計算します(累積和など)。
    • AVG() OVER (...): ウィンドウ内の値の平均を計算します(移動平均など)。
    • COUNT() OVER (...): ウィンドウ内の行数をカウントします。
    • MAX() OVER (...): ウィンドウ内の最大値を返します。
    • MIN() OVER (...): ウィンドウ内の最小値を返します。

使用例

例1: 各部署での給与ランキング

SELECT
  部署名,
  従業員名,
  給与,
  RANK() OVER (PARTITION BY 部署名 ORDER BY 給与 DESC) AS 部署内給与ランク
FROM 従業員;

このクエリでは、部署名 ごとにデータをパーティションし、各部署内で 給与 が高い順にランキングを付けています。

例2: 累積売上

SELECT
  日付,
  売上,
  SUM(売上) OVER (ORDER BY 日付 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS 累積売上
FROM 日次売上;

このクエリでは、日付順に売上を累積していきます。ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW は、現在の行より前のすべての行と現在の行を含むフレームを定義しています。

例3: 移動平均

SELECT
  日付,
  株価,
  AVG(株価) OVER (ORDER BY 日付 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS 3日移動平均
FROM 株価データ;

このクエリでは、現在の日付と、その前の2日間の株価(合計3日分)の平均を計算することで、3日間の移動平均を求めています。

メリット

  • パフォーマンスの向上
    多くの場合、サブクエリや自己結合よりも最適化され、パフォーマンスが向上します。
  • 読みやすいクエリ
    分析ロジックがSQL文内で明確に表現されるため、可読性が向上します。
  • 複雑な分析の簡素化
    サブクエリや自己結合を使うことなく、複雑な集計やランキングを効率的に行うことができます。


OVER 句の構文エラー

最も一般的なエラーの一つは、OVER 句の構文ミスです。

一般的なエラーの例

  • キーワードのスペルミス: PARTITION BYPARTITIONED BY などと間違える。
  • 括弧の不一致: () の数が合わない。
  • OVER() が空: ランキング関数など、ORDER BY が必須な関数で OVER() とだけ記述してしまう。

エラーメッセージの例

  • SQL error: window function requires an ORDER BY clause (特にランキング関数で発生)
  • SQL error: near "OVER": syntax error

トラブルシューティング

  1. 構文の確認: OVER 句の内部(PARTITION BYORDER BY、フレーム句)がSQLの構文に厳密に合致しているか確認してください。
  2. ORDER BY の確認: ROW_NUMBER(), RANK(), DENSE_RANK(), NTILE() などのランキング関数、および LEAD(), LAG() などは、OVER 句内に ORDER BY 句が必須です。これが抜けていないか確認してください。
  3. 括弧の対応: 括弧の数が正しく対応しているか、目視またはエディタの機能で確認してください。

ORDER BY 句の欠如または不適切な指定

ウィンドウ関数、特にランキング関数や、前後の行を参照する関数 (LEAD, LAG) では、OVER 句内の ORDER BY が結果に大きく影響します。

一般的なエラーの例

  • 複数列の指定ミス: 複数の列でソートする場合の優先順位を誤る。
  • 順序が期待通りでない: 昇順/降順の指定 (ASC/DESC) を間違える。
  • ランキング関数で ORDER BY がない: 結果が不定になるか、エラーになります。

トラブルシューティング

  1. ORDER BY の必須性: ランキング関数を使用する場合、OVER 句内に ORDER BY が必須であることを確認してください。
  2. 順序の確認: 結果が昇順 (ASC) になるべきか降順 (DESC) になるべきか、期待する順序と一致しているか確認してください。デフォルトは ASC です。
  3. 同値の扱い: RANK()DENSE_RANK() の違いを理解し、どちらが目的に合っているか確認してください。RANK() は同値の次のランクをスキップしますが、DENSE_RANK() はスキップしません。
  4. NULL値のソート: ORDER BY 句では、NULL値の扱いに注意が必要です。デフォルトの順序 (NULLS FIRST または NULLS LAST) を確認し、必要に応じて明示的に指定 (NULLS FIRST / NULLS LAST) してください。

PARTITION BY 句の不適切な使用または欠如

PARTITION BY 句は、データを論理的なグループに分割するために使用されます。これが正しく機能しないと、期待する集計結果が得られません。

一般的なエラーの例

  • パーティションキーの誤り: 間違った列でパーティションしてしまう。
  • PARTITION BY が必要なのに省略: 全体のデータに対してウィンドウ関数が適用されてしまい、望まない結果になる。

トラブルシューティング

  1. グループ化の意図: 各グループ内で独立した計算を行いたい場合(例: 部署ごとのランキング、顧客ごとの購入履歴など)、PARTITION BY 句が適切に指定されているか確認してください。
  2. パーティションキーの確認: どの列でデータを分割したいのかを明確にし、その列が PARTITION BY 句に指定されていることを確認してください。
  3. PARTITION BY なしの場合: PARTITION BY 句を省略すると、結果セット全体が1つのパーティションとして扱われます。これにより、全体のランキングや全体の累積値などが計算されます。これが意図した動作であるか確認してください。

フレーム句 (ROWS または RANGE) の誤解と誤用

フレーム句は、ウィンドウ内の行の範囲を定義するもので、特に移動平均や累積和のような計算で重要です。

一般的なエラーの例

  • RANGEROWS の違いの誤解: RANGE は値の範囲、ROWS は行数に基づきます。通常、時系列データなどでは ROWS が使われますが、金額などの値に基づく場合は RANGE が有効な場合があります。
  • フレームの範囲の誤り: 移動平均の計算で、対象とする行数(例: 2 PRECEDING AND CURRENT ROW)を間違える。
  • ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW の誤解: 累積和を計算する際にこのフレーム句を正しく理解していない。

エラーメッセージの例

  • SQL error: frame specification cannot be used with aggregate window functions when there is no ORDER BY clause (集約関数をウィンドウ関数として使用し、フレーム句があるのに ORDER BY がない場合)

トラブルシューティング

  1. ORDER BY の必須性: フレーム句を使用する場合、多くの場合 OVER 句内に ORDER BY 句が必須です。特に集約関数をウィンドウ関数として使う場合は必須です。
  2. フレームの定義: どのような計算をしたいのか(累積、移動平均など)に基づいて、適切なフレーム句を選択してください。
    • 累積和/累積平均: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    • N点移動平均: ROWS BETWEEN (N-1) PRECEDING AND CURRENT ROW (または ROWS BETWEEN FLOOR(N/2) PRECEDING AND FLOOR(N/2) FOLLOWING など、中心化したい場合)
  3. ROWSRANGE の違い: ROWS は物理的な行数に基づいてフレームを定義します。RANGEORDER BY 句で指定された列の値に基づいてフレームを定義します。多くの場合、ROWS で十分ですが、値の範囲で定義したい場合は RANGE を検討してください。ただし、RANGE はより複雑なルールがあり、SQLiteでは使用できるケースが限られることがあります。

NULL 値の扱い

ウィンドウ関数は NULL 値をどのように扱うかによって結果が変わることがあります。

一般的なエラーの例

  • 集約関数が NULL 値を無視することで、期待しない平均値や合計値になる。
  • NULL が含まれる行がランキングに影響を与える。

トラブルシューティング

  1. NULL のフィルタリング: ウィンドウ関数を適用する前に、WHERE 句で NULL 値の行を除外する必要があるか検討してください。
  2. 集約関数の動作: SUM(), AVG(), COUNT() などはデフォルトで NULL 値を無視します。これは通常期待される動作ですが、もし NULL を0として扱いたい場合は、COALESCE(列名, 0) などを使用して NULL を変換してからウィンドウ関数を適用してください。
  3. ORDER BY における NULL のソート順: ORDER BY 句で NULLS FIRSTNULLS LAST を明示的に指定することで、NULL 値のソート順を制御できます。

大量のデータに対してウィンドウ関数を使用すると、パフォーマンスが低下することがあります。

一般的なエラーの例

  • データベースがフリーズする。
  • クエリの実行に非常に時間がかかる。
  1. インデックスの利用: PARTITION BY 句や ORDER BY 句で使用されている列にインデックスが張られているか確認してください。インデックスは、データの並べ替えやグループ化のコストを大幅に削減します。
  2. サブクエリでの事前フィルタリング: ウィンドウ関数を適用する前に、WHERE 句で不要な行をできるだけ減らしてください。これにより、ウィンドウ関数が処理するデータ量が減り、パフォーマンスが向上します。
  3. 複雑なフレームの回避: 特に大きなフレーム (UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING など) は、多くのメモリとCPUを消費する可能性があります。必要に応じて、より小さなフレームに制限できないか検討してください。
  4. SQLiteの制限: SQLiteは組み込みデータベースであり、非常に大規模なデータセットや非常に複雑なウィンドウ関数に対しては、他のよりスケーラブルなデータベースシステム(PostgreSQL, MySQLなど)の方が適している場合があります。


SQLiteのウィンドウ関数に関連するプログラミング例 (コード例)

準備: サンプルデータの作成

まず、以下のSQL文を実行して、説明に使用するサンプルテーブルとデータを準備します。

-- テーブルの作成
CREATE TABLE sales (
    sale_date DATE,
    region TEXT,
    product TEXT,
    amount INTEGER
);

-- データの挿入
INSERT INTO sales (sale_date, region, product, amount) VALUES
('2024-01-01', 'East', 'Laptop', 1000),
('2024-01-01', 'West', 'Mouse', 50),
('2024-01-02', 'East', 'Keyboard', 75),
('2024-01-02', 'West', 'Laptop', 1200),
('2024-01-03', 'East', 'Mouse', 40),
('2024-01-03', 'West', 'Keyboard', 80),
('2024-01-04', 'East', 'Laptop', 1100),
('2024-01-04', 'West', 'Mouse', 60),
('2024-01-05', 'East', 'Keyboard', 90),
('2024-01-05', 'West', 'Laptop', 1300),
('2024-01-06', 'East', 'Laptop', 950),
('2024-01-06', 'West', 'Mouse', 55);

-- 従業員テーブルの作成(ランキング関数用)
CREATE TABLE employees (
    employee_id INTEGER PRIMARY KEY,
    name TEXT,
    department TEXT,
    salary INTEGER
);

-- 従業員データの挿入
INSERT INTO employees (employee_id, name, department, salary) VALUES
(1, 'Alice', 'Sales', 50000),
(2, 'Bob', 'Sales', 60000),
(3, 'Charlie', 'Marketing', 55000),
(4, 'David', 'Sales', 50000),
(5, 'Eve', 'Marketing', 60000),
(6, 'Frank', 'IT', 70000),
(7, 'Grace', 'IT', 65000),
(8, 'Heidi', 'Sales', 70000);

ランキング関数 (Ranking Functions)

特定のグループ内で順位付けを行う場合によく使用されます。

ROW_NUMBER(): 各行に一意の番号を割り当てる

目的
各地域の売上を日付順に並べた際の、各売上記録に連番を割り当てる。

SELECT
  sale_date,
  region,
  product,
  amount,
  ROW_NUMBER() OVER (PARTITION BY region ORDER BY sale_date, amount DESC) AS row_num_in_region
FROM sales;

解説

  • ROW_NUMBER(): この指定された順序に基づいて、1から始まる連番を各行に割り当てます。
  • ORDER BY sale_date, amount DESC: 各地域内で、まずsale_dateの昇順に、次にamountの降順に並べ替えます。
  • PARTITION BY region: データをregion(地域)ごとにグループ化します。各地域内で独立した連番が振られます。

RANK(): 同順位をスキップしてランク付けする

目的
各部署の従業員を給与の高い順にランク付けする。同じ給与の従業員には同じランクが与えられ、次のランクはスキップされる。

SELECT
  employee_id,
  name,
  department,
  salary,
  RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank_dept
FROM employees;

解説

  • RANK(): 同じ給与の従業員には同じランクが与えられます(例: 1位、1位、3位)。
  • ORDER BY salary DESC: 各部署内で、salary(給与)の降順に並べ替えます。
  • PARTITION BY department: データをdepartment(部署)ごとにグループ化します。

DENSE_RANK(): 同順位をスキップせず連続してランク付けする

目的
RANK() と同じ目的だが、同順位の後もランクが連続する。

SELECT
  employee_id,
  name,
  department,
  salary,
  DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_dense_rank_dept
FROM employees;

解説

  • DENSE_RANK(): RANK() とは異なり、同じ給与の従業員に同じランクが与えられた後も、次のランクは連続します(例: 1位、1位、2位)。

NTILE(n): グループをN個のタイルに分割する

目的
全従業員を給与順に4つのグループ(四分位数)に分割する。

SELECT
  employee_id,
  name,
  salary,
  NTILE(4) OVER (ORDER BY salary DESC) AS salary_quartile
FROM employees;

解説

  • 従業員数がnで割り切れない場合、最初のグループに多くの行が割り当てられることがあります。
  • NTILE(4): 結果セット全体(ここではPARTITION BYがないため)を、salaryの降順に基づいて4つのほぼ等しいサイズのグループに分割します。

集約関数をウィンドウ関数として使用 (Aggregate Functions as Window Functions)

SUM(), AVG(), COUNT(), MAX(), MIN() などの集約関数を OVER() 句と組み合わせて、ウィンドウ内で集計を行うことができます。

累積合計 (Cumulative Sum)

目的
各地域の日付ごとの累積売上を計算する。

SELECT
  sale_date,
  region,
  amount,
  SUM(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_amount
FROM sales;

解説

  • ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: これが「フレーム句」です。現在の行より前のすべての行と現在の行を含んだ範囲でSUM()を計算します。これにより、日付が新しい行ほど前の売上も全て足し合わせた累積値が得られます。
  • ORDER BY sale_date: 日付順に並べ替えます。
  • PARTITION BY region: 地域ごとに累積を計算します。

移動平均 (Moving Average)

目的
各地域の日付ごとの3日間の移動平均売上を計算する。

SELECT
  sale_date,
  region,
  amount,
  AVG(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS 3_day_moving_avg
FROM sales;

解説

  • ROWS BETWEEN 2 PRECEDING AND CURRENT ROW: 現在の行、およびその前の2行(合計3行)を対象としたフレームを定義します。このフレーム内でAVG(amount)が計算され、3日間の移動平均が得られます。
  • PARTITION BY region ORDER BY sale_date: 上記と同様に地域と日付で順序付けます。

ウィンドウ内の他の行の値を取得するために使用されます。

LEAD(): 次の行の値を取得する

目的
各地域の売上について、次の売上高を隣の列に表示する。

SELECT
  sale_date,
  region,
  amount,
  LEAD(amount, 1, 0) OVER (PARTITION BY region ORDER BY sale_date) AS next_amount
FROM sales;

解説

  • これにより、現在の売上と次の売上を比較するなどの分析が可能になります。
  • LEAD(amount, 1, 0): amount列の値を、現在の行から1つ後の行から取得します。
    • 1: オフセット(何行後の値を取得するか)。
    • 0: デフォルト値。もし次の行が存在しない場合(パーティションの最終行など)に返される値。

LAG(): 前の行の値を取得する

目的
各地域の売上について、前の売上高を隣の列に表示する。

SELECT
  sale_date,
  region,
  amount,
  LAG(amount, 1, 0) OVER (PARTITION BY region ORDER BY sale_date) AS previous_amount
FROM sales;

解説

  • これにより、現在の売上と前の売上を比較して成長率などを計算できます。
  • LAG(amount, 1, 0): amount列の値を、現在の行から1つ前の行から取得します。
    • 1: オフセット(何行前の値を取得するか)。
    • 0: デフォルト値。もし前の行が存在しない場合(パーティションの最初の行など)に返される値。

FIRST_VALUE() と LAST_VALUE(): フレームの最初と最後の値を取得する

目的
各地域の日付ごとの売上に対して、その地域で最初に記録された売上高と、フレーム内で最後に記録された売上高を表示する。

SELECT
  sale_date,
  region,
  amount,
  FIRST_VALUE(amount) OVER (PARTITION BY region ORDER BY sale_date) AS first_sale_in_region,
  LAST_VALUE(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS last_sale_in_frame
FROM sales;
  • LAST_VALUE(amount) OVER (PARTITION BY region ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW): LAST_VALUE を使う場合、明示的にフレーム句を指定しないと、ORDER BY 句で定義されたカレント行までの範囲になるため、多くの場合、現在の行の値が返されてしまいます。上記の例では、ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW を指定することで、現在の行までの累積フレームにおける最後の値(つまり現在の行の値)を返しています。もしパーティション全体の最後の値を取得したい場合は、フレームを ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING に設定する必要があります。
  • FIRST_VALUE(amount) OVER (PARTITION BY region ORDER BY sale_date): PARTITION BY region で定義された各地域内で、ORDER BY sale_date によって順序付けられた最初のamountの値を返します。FIRST_VALUE のデフォルトフレームは RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW または ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW です。


サブクエリ (Subqueries)

最も一般的な代替手段の一つは、サブクエリ(副問い合わせ)を使用することです。特に、集計関数をウィンドウ関数のように使用する場合に有効です。

累積合計の例 (Cumulative Sum)

ウィンドウ関数 (SUM() OVER (ORDER BY ... ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) の代替。

ウィンドウ関数を使用した場合

SELECT
  sale_date,
  amount,
  SUM(amount) OVER (ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_amount
FROM sales;

サブクエリを使用した場合

SELECT
  s1.sale_date,
  s1.amount,
  (SELECT SUM(s2.amount)
   FROM sales AS s2
   WHERE s2.sale_date <= s1.sale_date
   ORDER BY s2.sale_date) AS cumulative_amount
FROM sales AS s1
ORDER BY s1.sale_date;

解説

  • これは「相関サブクエリ」と呼ばれ、外側のクエリの行ごとに内側のサブクエリが実行されるため、データ量が多い場合にはパフォーマンスが低下する可能性があります。
  • WHERE s2.sale_date <= s1.sale_date の条件により、現在の行の日付(s1.sale_date)以下のすべての売上(s2.amount)がサブクエリによって合計されます。
  • s2はサブクエリ内のすべての行を表します。
  • s1は現在の行を表します。

グループごとの最大値の例 (Group-wise Maximum)

MAX() OVER (PARTITION BY ...) の代替。

ウィンドウ関数を使用した場合

SELECT
  region,
  product,
  amount,
  MAX(amount) OVER (PARTITION BY region) AS max_amount_in_region
FROM sales;

サブクエリを使用した場合

SELECT
  s1.region,
  s1.product,
  s1.amount,
  (SELECT MAX(s2.amount) FROM sales AS s2 WHERE s2.region = s1.region) AS max_amount_in_region
FROM sales AS s1;

解説

  • WHERE s2.region = s1.region により、同じ地域の売上データのみを対象として最大値が計算されます。

自己結合 (Self-Join)

テーブルをそれ自身と結合させることで、関連する行のデータにアクセスする方法です。特に、ランキング関数や前の行/次の行の値を取得する場合に有効です。

前の行の値の取得 (LAG() の代替)

ウィンドウ関数を使用した場合

SELECT
  sale_date,
  amount,
  LAG(amount, 1, 0) OVER (ORDER BY sale_date) AS previous_amount
FROM sales;

自己結合を使用した場合

SELECT
  s1.sale_date,
  s1.amount,
  s2.amount AS previous_amount
FROM sales AS s1
LEFT JOIN sales AS s2
  ON s1.sale_date = DATE(s2.sale_date, '+1 day') -- 前の日のデータに結合
ORDER BY s1.sale_date;

解説

  • この方法は、日付が連続している場合にのみ機能します。もし日付に欠損がある場合や、特定の順序での「前の行」を定義したい場合は、より複雑な結合条件や連番を振る前処理が必要になることがあります。
  • LEFT JOIN を使用することで、s1(現在の行)のsale_dates2(前の行)のsale_dateの1日後である場合に結合します。

ランキングの代替 (ROW_NUMBER() の代替)

ウィンドウ関数を使用した場合

SELECT
  employee_id,
  name,
  department,
  salary,
  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num_in_dept
FROM employees;

自己結合とCOUNT() を使用した場合
(これは RANK() に近い動作になることが多いです)

SELECT
  e1.employee_id,
  e1.name,
  e1.department,
  e1.salary,
  COUNT(e2.salary) + 1 AS rank_in_dept
FROM employees AS e1
LEFT JOIN employees AS e2
  ON e1.department = e2.department
  AND e2.salary > e1.salary -- 自身より給与が高い従業員をカウント
GROUP BY e1.employee_id, e1.name, e1.department, e1.salary
ORDER BY e1.department, rank_in_dept;

解説

  • COUNT(e2.salary) + 1 は、自分より給与が高い人の数に1を足すことでランクを表現します。これは RANK() の動作に近いですが、厳密な ROW_NUMBER() とは異なります(同給与の場合に同じランクになるため)。
  • この自己結合は、同じ部署内で自分より給与が高い従業員の数を数えることで、その従業員のランクを算出します。

再帰CTE (Recursive Common Table Expressions)

再帰CTEは、データの階層構造を処理したり、順序付けられたリストを反復処理したりするのに非常に強力です。累積計算によく使用されます。

累積合計の例 (Cumulative Sum)

ウィンドウ関数を使用した場合

SELECT
  sale_date,
  amount,
  SUM(amount) OVER (ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_amount
FROM sales;

再帰CTEを使用した場合

WITH RECURSIVE CumulativeSales AS (
  SELECT
    sale_date,
    amount,
    amount AS cumulative_amount
  FROM sales
  ORDER BY sale_date
  LIMIT 1 -- 最初の行をアンカーメンバーとする

  UNION ALL

  SELECT
    s.sale_date,
    s.amount,
    cs.cumulative_amount + s.amount
  FROM sales AS s
  JOIN CumulativeSales AS cs ON s.sale_date = DATE(cs.sale_date, '+1 day') -- 日付が連続していることを前提
  WHERE s.sale_date > cs.sale_date
  ORDER BY s.sale_date -- このORDER BYは再帰の順序には影響しない
)
SELECT
  sale_date,
  amount,
  cumulative_amount
FROM CumulativeSales
ORDER BY sale_date;

解説

  • 再帰CTEは強力ですが、ウィンドウ関数に比べて記述が複雑になりがちです。
  • JOIN 条件 (s.sale_date = DATE(cs.sale_date, '+1 day')) は、日付が連続していることを前提としています。日付に欠損がある場合は、別の方法で次の日付を見つける必要があります(例: ROW_NUMBER() で連番を振ってから、その連番を使って結合するなど、より複雑になります)。
  • 再帰メンバー: 前のステップの結果 (cs) を参照して、次のステップの計算を行います。ここでは、前の日の累積額に現在の日の売上を足し合わせています。
  • アンカーメンバー: 再帰の最初のステップを定義します。ここでは、最も古い日付の売上を最初の累積額として設定します。

SQLクエリだけで完結させるのではなく、アプリケーション(Python, Java, Node.jsなど)のコードでデータを取得し、ループ処理やデータ構造操作によって集計やランキングを行う方法です。

メリット

  • メモリやCPUの利用状況をアプリケーション側で細かく制御できる。
  • より複雑なビジネスロジックを柔軟に組み込める。
  • SQLの知識が限定的でも処理を実装できる。

デメリット

  • 大量のデータを扱う場合、データベース側の処理(ウィンドウ関数など)の方がパフォーマンスが良いことが多い。
  • アプリケーションのコードが肥大化し、メンテナンスが複雑になる可能性がある。
  • データベースとアプリケーション間のデータ転送量が増える可能性がある。

例 (Pythonでの累積合計)

import sqlite3

conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

# サンプルデータの作成(上記と同じ)
cursor.execute('''
CREATE TABLE sales (
    sale_date DATE,
    region TEXT,
    product TEXT,
    amount INTEGER
);
''')
cursor.executemany('INSERT INTO sales VALUES (?, ?, ?, ?)', [
    ('2024-01-01', 'East', 'Laptop', 1000),
    ('2024-01-01', 'West', 'Mouse', 50),
    ('2024-01-02', 'East', 'Keyboard', 75),
    ('2024-01-02', 'West', 'Laptop', 1200),
    ('2024-01-03', 'East', 'Mouse', 40),
    # ... 他のデータも同様に挿入
])
conn.commit()

# データを取得し、Pythonで累積合計を計算
cursor.execute("SELECT sale_date, amount FROM sales ORDER BY sale_date")
rows = cursor.fetchall()

cumulative_amount = 0
results = []
for row in rows:
    sale_date, amount = row
    cumulative_amount += amount
    results.append((sale_date, amount, cumulative_amount))

print("日付 | 金額 | 累積金額")
print("----|------|--------")
for r in results:
    print(f"{r[0]} | {r[1]:>4} | {r[2]:>6}")

conn.close()

SQLiteのウィンドウ関数が利用できない(または利用したくない)場合でも、サブクエリ、自己結合、再帰CTE、またはアプリケーション層での処理といった代替手段を用いて、同様のデータ分析を行うことが可能です。

  • アプリケーション層での処理
    柔軟性は高いですが、データベースの負荷分散やデータ転送効率を考慮する必要があります。
  • 再帰CTE
    複雑なロジックや階層データの処理に強力ですが、記述が複雑になります。
  • サブクエリ / 自己結合
    比較的シンプルで、多くのケースでウィンドウ関数の代わりになりますが、パフォーマンスのボトルネックになることがあります。