SQL
SQLServer
SQLDatabase

SQLでカラムの合計値がある値を超えるまで出力するには

SQLで、テーブルをあるカラムの値でソートし、別のカラムの合計値が閾値を超えるまでのエントリを取り出す方法を書く。whileとかカーソルとかのループを使わずにやれないかと検索したらStack Overflowに一発で答えがあったけど、一応自分用のメモ書きで残しておく。

参考リンク

テーブル例

create table Items
(
    ID int NOT NULL primary key clustered,
    Name nvarchar(255) not null,
    Price int not null,
    LastUpdated datetime2(7) not null
)

サンプルデータ

insert into Items (ID, Name, Price, LastUpdated) values
    (1, 'Tomato', 5, '2017-06-01'),
    (2, 'Banana', 4, '2017-05-30'),
    (3, 'Apple', 3, '2017-07-05'),
    (4, 'Potato', 3, '2017-04-01'),
    (5, 'Orange', 5, '2017-07-01'),
    (6, 'Broccoli', 4, '2017-07-04')

SQL構文

上記のデータからLastUpdated順に、Priceの合計が10を超えるまで表示するには以下のSQLクエリを使う。

WITH q AS (
    SELECT *, SUM(Price) OVER (ORDER BY LastUpdated) AS TotalPrice
    FROM Items
)
SELECT * FROM q
WHERE TotalPrice <= 10
ORDER BY LastUpdated

複数テーブルでもOK

上記のサンプルは正規化されてないテーブルだったが、より実用的に正規化されたテーブルを使って同じことをする。さらにWHERE句も追加し、ある条件に合致するエントリ(InStockが1のもの)だけを取り出す。

create table Items
(
    ID int NOT NULL primary key clustered,
    Name nvarchar(255) not null
)
GO

create table Prices
(
    ItemId int FOREIGN KEY REFERENCES Items(ID),
    Price int not null,
    InStock BIT not null,
    LastUpdated datetime2(7) not null
)
GO

サンプルデータ

insert into Items (ID, Name) values
    (1, 'Tomato'),
    (2, 'Banana'),
    (3, 'Apple'),
    (4, 'Potato'),
    (5, 'Orange'),
    (6, 'Broccoli')
GO

insert into Prices (ItemId, Price, InStock, LastUpdated) values
    (1, 5, 1, '2017-06-01'),
    (2, 4, 0, '2017-05-30'),
    (3, 3, 0, '2017-07-05'),
    (4, 3, 1, '2017-04-01'),
    (5, 5, 1, '2017-07-01'),
    (6, 4, 1, '2017-07-04')
GO

SQL構文

WITH q AS (
  SELECT SUM(P.Price) OVER (ORDER BY P.LastUpdated) AS TotalPrice
      ,I.*
      ,P.*
  FROM Items AS I
  JOIN Prices AS P ON I.ID = P.ItemId
  WHERE P.InStock = 1
)
SELECT * FROM q
WHERE TotalPrice <= 10
ORDER BY LastUpdated

IFとか変数とかカーソルとか一切不要。

おまけ

上記の例のExecution Planを見るとPriceテーブルに対してテーブルスキャンがかかっている。なのでインデックスを追加する。

create nonclustered index ix_price_lastupdated on Prices (LastUpdated)

これでテーブルスキャンがインデックススキャンに置き換わった。