クエリーの例

以下のクエリーの例は PostgreSQL Exercises のサイトから拝借してきたものです.サンプルのデータセットは getting started page にあります.

これらの例で使われているスキーマのビジュアル表現です:

../_images/schema-horizontal.png

モデルの定義

データを扱うには,この図にあるテーブルに対応するモデルクラスを定義してあげる必要があります.

Note

特定のフィールドについて,明示的にカラム名を指定しているケースがあります. これにより, 私達のモデルは postgres Exercises で使われるデータベーススキーマとの互換性があります.

from functools import partial
from peewee import *


db = PostgresqlDatabase('peewee_test')

class BaseModel(Model):
    class Meta:
        database = db

class Member(BaseModel):
    memid = AutoField()     # 自動インクリメントのプライマリキー
    surname = CharField()
    firstname = CharField()
    address = CharField(max_length=300)
    zipcode = IntegerField()
    telephone = CharField()
    recommendedby = ForeignKeyField('self', backref='recommended',
                                    column_name='recommendedby', null=True)
    joindate = DateTimeField()

    class Meta:
        table_name = 'members'


# 便宜上,通貨を格納するのに適した decimal のフィールドを宣言しています.
MoneyField = partial(DecimalField, decimal_places=2)


class Facility(BaseModel):
    facid = AutoField()
    name = CharField()
    membercost = MoneyField()
    guestcost = MoneyField()
    initialoutlay = MoneyField()
    monthlymaintenance = MoneyField()

    class Meta:
        table_name = 'facilities'


class Booking(BaseModel):
    bookid = AutoField()
    facility = ForeignKeyField(Facility, column_name='facid')
    member = ForeignKeyField(Member, column_name='memid')
    starttime = DateTimeField()
    slots = IntegerField()

    class Meta:
        table_name = 'bookings'

スキーマの生成

PostgreSQL Exercises のサイトから SQL ファイルをダウンロードしてきた場合は, 以下のコマンドを使って PostgreSQL データベースに対してデータをロードできます:

createdb peewee_test
psql -U postgres -f clubdata.sql -d peewee_test -x -q

Peewee でサンプルデータのロードを行わずにスキーマを生成する場合は以下の コマンドを使ってください:

# すでにデータベース "peewee_test" があるものとします.
db.create_tables([Member, Facility, Booking])

基本練習

このカテゴリーでは SQL の基礎について学びます.ここでカバーするのは select と where 句, case 表現, union その他いくつかのこまごましたものです.

すべてを取り出す

facilities(施設)テーブルからすべての情報を取り出します.

SELECT * FROM facilities
# デフォルトでは, select() に対してフィールドが明示的に渡されない場合,
# すべてのフィールドが取り出されます.
query = Facility.select()

テーブルから特定のカラムを取り出す

施設名とメンバーの料金を取り出します.

SELECT name, membercost FROM facilities;
query = Facility.select(Facility.name, Facility.membercost)

# 繰り返し(イテレータ)で取り出す場合:
for facility in query:
    print(facility.name)

取り出すべき行を制御する

施設からメンバー料金があるもののリストを取り出します.

SELECT * FROM facilities WHERE membercost > 0
query = Facility.select().where(Facility.membercost > 0)

取り出すべき行をコントロールする - part 2

施設の中でメンバーの料金があるものについて,その料金がひと月の維持費の50分の1 より小さなものに限ったリストを取り出します. id, name, cost, monthlymaintenance が返されます.

SELECT facid, name, membercost, monthlymaintenance
FROM facilities
WHERE membercost > 0 AND membercost < (monthlymaintenance / 50)
query = (Facility
         .select(Facility.facid, Facility.name, Facility.membercost,
                 Facility.monthlymaintenance)
         .where(
             (Facility.membercost > 0) &
             (Facility.membercost < (Facility.monthlymaintenance / 50))))

基本的な文字列検索

名前の中に ‘Tennis’ という単語を含む施設の一覧を取得するにはどうすれば よいでしょうか?

SELECT * FROM facilities WHERE name ILIKE '%tennis%';
query = Facility.select().where(Facility.name.contains('tennis'))

# または指数演算子を使う. ワイルドカードを含む必要があることに注意:
query = Facility.select().where(Facility.name ** '%tennis%')

複数の候補値に対するマッチング

施設のうちの ID が 1 または 5 のものの詳細を取り出すにはどうすればよいでしょう? OR 演算子を使わずにやってみてください.

SELECT * FROM facilities WHERE facid IN (1, 5);
query = Facility.select().where(Facility.facid.in_([1, 5]))

# または:
query = Facility.select().where((Facility.facid == 1) |
                                (Facility.facid == 5))

結果を分類してバケツに入れる

施設について,その維持費の月額が $100 を超えるかどうかで ‘cheap’ または ‘expensive’ ラベルが付いたリストを取得するにはどうすればよいでしょうか? それぞれの施設の名前と,その維持の状況を返します.

SELECT name,
CASE WHEN monthlymaintenance > 100 THEN 'expensive' ELSE 'cheap' END
FROM facilities;
cost = Case(None, [(Facility.monthlymaintenance > 100, 'expensive')], 'cheap')
query = Facility.select(Facility.name, cost.alias('cost'))

Note

詳細は Case のドキュメントを参照してください.

日付の操作

2012年9月以降に加入したメンバーのリストを取得するにはどうすればよいでしょうか? それぞれのメンバーの memid, surname, firstname, および加入日を返します.

SELECT memid, surname, firstname, joindate FROM members
WHERE joindate >= '2012-09-01';
query = (Member
         .select(Member.memid, Member.surname, Member.firstname, Member.joindate)
         .where(Member.joindate >= datetime.date(2012, 9, 1)))

重複を排除して結果をソートする

メンバーテーブルから先頭10個の名字を取り出して,それをソートしたリストを 作るにはどうしたらよいでしょうか?リストは重複してはならないものとします.

SELECT DISTINCT surname FROM members ORDER BY surname LIMIT 10;
query = (Member
         .select(Member.surname)
         .order_by(Member.surname)
         .limit(10)
         .distinct())

複数のクエリーの結果を結合する

何らかの理由により,すべての名字とすべての設備名をまとめたリストがほしいとします.

SELECT surname FROM members UNION SELECT name FROM facilities;
lhs = Member.select(Member.surname)
rhs = Facility.select(Facility.name)
query = lhs | rhs

以下の演算子を使ってクエリーを組み合わせることができます:

  • | - UNION

  • + - UNION ALL

  • & - INTERSECT

  • - - EXCEPT

シンプルな集約

最新のメンバーがサインアップした日付を知りたいとします. この情報をどうやって持ってくればいいでしょうか?

SELECT MAX(join_date) FROM members;
query = Member.select(fn.MAX(Member.joindate))
# 手軽にスカラー値を取得したい場合は "scalar()" を使う:
# max_join_date = query.scalar()

さらなる集約

最も最近サインアップしたメンバーの加入日だけでなく,名前と名字も合わせて 取得したいとします.

SELECT firstname, surname, joindate FROM members
WHERE joindate = (SELECT MAX(joindate) FROM members);
# クエリーの中で同じテーブルを複数回参照する場合は "alias()" を使う.
MemberAlias = Member.alias()
subq = MemberAlias.select(fn.MAX(MemberAlias.joindate))
query = (Member
         .select(Member.firstname, Member.surname, Member.joindate)
         .where(Member.joindate == subq))

JOIN とサブクエリー

このカテゴリーでは,主にリレーショナルデータベースにおける基本概念である結合(join)について学びます. 結合を使うと,複数のテーブルの関連する情報をまとめて質問に答えることが可能です. これは,クエリーを簡単にするために役に立つだけではありません: join 機能がないとデータを非正規化せざるを得なくなり,複雑さが増すことでデータの内部的な整合性を保つことが困難になります.

このトピックでは内部,外部,自己結合に加えてサブクエリー(クエリーの中のクエリー)にも多少説明の時間を割くことにします.

メンバーの予約開始時刻を取り出す

‘David Farrell’ という名前のメンバーの予約開始時刻のリストを取得するには どうすればよいでしょうか?

SELECT starttime FROM bookings
INNER JOIN members ON (bookings.memid = members.memid)
WHERE surname = 'Farrell' AND firstname = 'David';
query = (Booking
         .select(Booking.starttime)
         .join(Member)
         .where((Member.surname == 'Farrell') &
                (Member.firstname == 'David')))

テニスコートの予約開始時刻を使って練習

‘2012-09-21’ という日付でテニスコートの予約開始時刻のリストを取得するには どうすればよいでしょうか?開始時刻と施設名のペアを,時刻でソートして返します.

SELECT starttime, name
FROM bookings
INNER JOIN facilities ON (bookings.facid = facilities.facid)
WHERE date_trunc('day', starttime) = '2012-09-21':: date
  AND name ILIKE 'tennis%'
ORDER BY starttime, name;
query = (Booking
         .select(Booking.starttime, Facility.name)
         .join(Facility)
         .where(
             (fn.date_trunc('day', Booking.starttime) == datetime.date(2012, 9, 21)) &
             Facility.name.startswith('Tennis'))
         .order_by(Booking.starttime, Facility.name))

# 結合された施設名をイテレータを使って取り出す:
for booking in query:
    print(booking.starttime, booking.facility.name)

他のメンバーを推薦したことがあるメンバーを取り出す

他のメンバーを推薦したことがあるメンバーのリストを取り出すにはどうすればよいでしょうか? リストには重複がないものとし,結果は(surname, firstname)でソートされているものとします.

SELECT DISTINCT m.firstname, m.surname
FROM members AS m2
INNER JOIN members AS m ON (m.memid = m2.recommendedby)
ORDER BY m.surname, m.firstname;
MA = Member.alias()
query = (Member
         .select(Member.firstname, Member.surname)
         .join(MA, on=(MA.recommendedby == Member.memid))
         .order_by(Member.surname, Member.firstname))

すべてのメンバーのリストに合わせて,その推薦者も同時に取り出す

メンバーの中で(もしあれば)その人の推薦者も合わせてリスト表示するには どうすればよいでしょうか?結果は(surname,firstname)でソートされるものとします。

SELECT m.firstname, m.surname, r.firstname, r.surname
FROM members AS m
LEFT OUTER JOIN members AS r ON (m.recommendedby = r.memid)
ORDER BY m.surname, m.firstname
MA = Member.alias()
query = (Member
         .select(Member.firstname, Member.surname, MA.firstname, MA.surname)
         .join(MA, JOIN.LEFT_OUTER, on=(Member.recommendedby == MA.memid))
         .order_by(Member.surname, Member.firstname))

# To display the recommender's name when iterating:
for m in query:
    print(m.firstname, m.surname)
    if m.recommendedby:
        print('  ', m.recommendedby.firstname, m.recommendedby.surname)

テニスコートを使ったことがあるメンバーを取り出す

テニスコートを使ったことがあるメンバーのリストを取り出すにはどうすればよいでしょうか? 出力にはコートの名前とメンバー名が結合して1カラムになったものを含みます. またデータには重複がなく,またメンバー名でソートされているものとします.

SELECT DISTINCT m.firstname || ' ' || m.surname AS member, f.name AS facility
FROM members AS m
INNER JOIN bookings AS b ON (m.memid = b.memid)
INNER JOIN facilities AS f ON (b.facid = f.facid)
WHERE f.name LIKE 'Tennis%'
ORDER BY member, facility;
fullname = Member.firstname + ' ' + Member.surname
query = (Member
         .select(fullname.alias('member'), Facility.name.alias('facility'))
         .join(Booking)
         .join(Facility)
         .where(Facility.name.startswith('Tennis'))
         .order_by(fullname, Facility.name)
         .distinct())

値段が高い予約のリストを取り出す

2012-09-14 の予約のうち,メンバー(もしくはゲスト)料金が $30 を超えるものの リストを取り出すにはどうすればよいでしょうか?ゲストはメンバー(リストに現れる 料金は30分’時間枠’単位)とは別の料金体系を持っており,ゲストユーザーの ID は 常に 0 です.出力には施設名とメンバー名が結合して1カラムになったものと料金を 含みます.ソート順は利用料の降順であり,サブクエリーは使わないことにします.

SELECT m.firstname || ' ' || m.surname AS member,
       f.name AS facility,
       (CASE WHEN m.memid = 0 THEN f.guestcost * b.slots
        ELSE f.membercost * b.slots END) AS cost
FROM members AS m
INNER JOIN bookings AS b ON (m.memid = b.memid)
INNER JOIN facilities AS f ON (b.facid = f.facid)
WHERE (date_trunc('day', b.starttime) = '2012-09-14') AND
 ((m.memid = 0 AND b.slots * f.guestcost > 30) OR
  (m.memid > 0 AND b.slots * f.membercost > 30))
ORDER BY cost DESC;
cost = Case(Member.memid, (
    (0, Booking.slots * Facility.guestcost),
), (Booking.slots * Facility.membercost))
fullname = Member.firstname + ' ' + Member.surname

query = (Member
         .select(fullname.alias('member'), Facility.name.alias('facility'),
                 cost.alias('cost'))
         .join(Booking)
         .join(Facility)
         .where(
             (fn.date_trunc('day', Booking.starttime) == datetime.date(2012, 9, 14)) &
             (cost > 30))
         .order_by(SQL('cost').desc()))

# 結果をイテレートする際は,名前付きタプルが最も扱いやすいでしょう:
for row in query.namedtuples():
    print(row.member, row.facility, row.cost)

join を使ってすべてのメンバーのリストを推薦者と合わせて取り出す

メンバーのうち(もしあれば)他の人を推薦したという情報を,JOINを使わずに取り出す にはどうすればよいでしょうか?結果には重複がなく,それぞれの firstname + surname のペアが1カラムにまとめられ,かつそれを使ってソートされているものとします.

SELECT DISTINCT m.firstname || ' ' || m.surname AS member,
   (SELECT r.firstname || ' ' || r.surname
    FROM cd.members AS r
    WHERE m.recommendedby = r.memid) AS recommended
FROM members AS m ORDER BY member;
MA = Member.alias()
subq = (MA
        .select(MA.firstname + ' ' + MA.surname)
        .where(Member.recommendedby == MA.memid))
query = (Member
         .select(fullname.alias('member'), subq.alias('recommended'))
         .order_by(fullname))

サブクエリーを使って高価な予約のリストを取り出す

“高価な予約のリストを取り出す” 練習では,若干面倒なロジックを抑制しています: 予約料金を計算は WHERE 句と CASE 文の両方で行う必要がありました. これを単純化するために,サブクエリーを使って計算しています.

SELECT member, facility, cost from (
  SELECT
  m.firstname || ' ' || m.surname as member,
  f.name as facility,
  CASE WHEN m.memid = 0 THEN b.slots * f.guestcost
  ELSE b.slots * f.membercost END AS cost
  FROM members AS m
  INNER JOIN bookings AS b ON m.memid = b.memid
  INNER JOIN facilities AS f ON b.facid = f.facid
  WHERE date_trunc('day', b.starttime) = '2012-09-14'
) as bookings
WHERE cost > 30
ORDER BY cost DESC;
cost = Case(Member.memid, (
    (0, Booking.slots * Facility.guestcost),
), (Booking.slots * Facility.membercost))

iq = (Member
      .select(fullname.alias('member'), Facility.name.alias('facility'),
              cost.alias('cost'))
      .join(Booking)
      .join(Facility)
      .where(fn.date_trunc('day', Booking.starttime) == datetime.date(2012, 9, 14)))

query = (Member
         .select(iq.c.member, iq.c.facility, iq.c.cost)
         .from_(iq)
         .where(iq.c.cost > 30)
         .order_by(SQL('cost').desc()))

# イテレートする際はdicts(辞書)を使ってみてください:
for row in query.dicts():
    print(row['member'], row['facility'], row['cost'])

データの変更

データにクエリーをかけることはできるようになりましたが,次はおそらくデータベースに データを書き込みたいと思うようになるはずです! このようにデータを変更する操作は, まとめて Data Manipulation Language または DML と呼ばれています.

これまでの章では,実行したクエリーの結果を返していました.この章で述べるような 変更処理ではクエリーの結果を返しません.その代わり,意図に基づいて更新されたテーブル の内容を表示します.

データをテーブルに insert する

このクラブでは新しい施設であるスパを増やそうとしています.私達はこれを facilities テーブルに追加する必要があります.以下の値を使うことにします: facid: 9, Name: ‘Spa’, membercost: 20, guestcost: 30, initialoutlay: 100000, monthlymaintenance: 800

INSERT INTO "facilities" ("facid", "name", "membercost", "guestcost",
"initialoutlay", "monthlymaintenance") VALUES (9, 'Spa', 20, 30, 100000, 800)
res = Facility.insert({
    Facility.facid: 9,
    Facility.name: 'Spa',
    Facility.membercost: 20,
    Facility.guestcost: 30,
    Facility.initialoutlay: 100000,
    Facility.monthlymaintenance: 800}).execute()

# OR:
res = (Facility
       .insert(facid=9, name='Spa', membercost=20, guestcost=30,
               initialoutlay=100000, monthlymaintenance=800)
       .execute())

複数行データをテーブルに insert する

直前の演習では施設の追加方法を学びました.次に複数の施設を1個のコマンドで 追加してみましょう.以下の値を使います:

facid: 9, Name: ‘Spa’, membercost: 20, guestcost: 30, initialoutlay: 100000, monthlymaintenance: 800.

facid: 10, Name: ‘Squash Court 2’, membercost: 3.5, guestcost: 17.5, initialoutlay: 5000, monthlymaintenance: 80.

-- 前述 --
data = [
    {'facid': 9, 'name': 'Spa', 'membercost': 20, 'guestcost': 30,
     'initialoutlay': 100000, 'monthlymaintenance': 800},
    {'facid': 10, 'name': 'Squash Court 2', 'membercost': 3.5,
     'guestcost': 17.5, 'initialoutlay': 5000, 'monthlymaintenance': 80}]
res = Facility.insert_many(data).execute()

データの計算結果をテーブルに insert する

facilities テーブルに再度 spa を追加してみましょう.ただこの時,次の facid を定数として追加するのではなく,値を自動生成したいと思います.facid 以外は 以下の値を使います: Name: ‘Spa’, membercost: 20, guestcost: 30, initialoutlay: 100000, monthlymaintenance: 800.

INSERT INTO "facilities" ("facid", "name", "membercost", "guestcost",
  "initialoutlay", "monthlymaintenance")
SELECT (SELECT (MAX("facid") + 1) FROM "facilities") AS _,
        'Spa', 20, 30, 100000, 800;
maxq = Facility.select(fn.MAX(Facility.facid) + 1)
subq = Select(columns=(maxq, 'Spa', 20, 30, 100000, 800))
res = Facility.insert_from(subq, Facility._meta.sorted_fields).execute()

既存のいくつかのデータを update する

2つ目のテニスコートのデータを入れる際に間違えてしまいました.初期費用は 8000 ではなく 10000 でした: あなたはデータを変更してエラーを修正する必要があります.

UPDATE facilities SET initialoutlay = 10000 WHERE name = 'Tennis Court 2';
res = (Facility
       .update({Facility.initialoutlay: 10000})
       .where(Facility.name == 'Tennis Court 2')
       .execute())

# OR:
res = (Facility
       .update(initialoutlay=10000)
       .where(Facility.name == 'Tennis Court 2')
       .execute())

複数行と複数カラムを同時に update する

私達はメンバーとゲスト双方について,テニスコートの値段を上げたいと思います. メンバーの料金は 6,ゲストは 30 になるように更新してください.

UPDATE facilities SET membercost=6, guestcost=30 WHERE name ILIKE 'Tennis%';
nrows = (Facility
         .update(membercost=6, guestcost=30)
         .where(Facility.name.startswith('Tennis'))
         .execute())

別の行の内容に基づいて行を update する

2番目のテニスコートの価格を,1番目の価格の 10% 増しにしたいと思います.必要に 応じてそのステートメントを再利用できるように,これを定数を使わずにやってみてください.

UPDATE facilities SET
membercost = (SELECT membercost * 1.1 FROM facilities WHERE facid = 0),
guestcost = (SELECT guestcost * 1.1 FROM facilities WHERE facid = 0)
WHERE facid = 1;

-- または --
WITH new_prices (nmc, ngc) AS (
  SELECT membercost * 1.1, guestcost * 1.1
  FROM facilities WHERE name = 'Tennis Court 1')
UPDATE facilities
SET membercost = new_prices.nmc, guestcost = new_prices.ngc
FROM new_prices
WHERE name = 'Tennis Court 2'
sq1 = Facility.select(Facility.membercost * 1.1).where(Facility.facid == 0)
sq2 = Facility.select(Facility.guestcost * 1.1).where(Facility.facid == 0)

res = (Facility
       .update(membercost=sq1, guestcost=sq2)
       .where(Facility.facid == 1)
       .execute())

# または:
cte = (Facility
       .select(Facility.membercost * 1.1, Facility.guestcost * 1.1)
       .where(Facility.name == 'Tennis Court 1')
       .cte('new_prices', columns=('nmc', 'ngc')))
res = (Facility
       .update(membercost=SQL('new_prices.nmc'), guestcost=SQL('new_prices.ngc'))
       .with_cte(cte)
       .from_(cte)
       .where(Facility.name == 'Tennis Court 2')
       .execute())

すべての予約を削除する

データベースの掃除の一環として,bookings テーブルからすべての予約データを 削除したいと思います.

DELETE FROM bookings;
nrows = Booking.delete().execute()

cd.members テーブルからメンバーを削除する

データベースから,一切予約をしていない 37 番のメンバーを削除します.

DELETE FROM members WHERE memid = 37;
nrows = Member.delete().where(Member.memid == 37).execute()

サブクエリー基づいて削除する

より汎用的に,一切予約をしていないメンバーをすべて削除するにはどうすればよいでしょうか?

DELETE FROM members WHERE NOT EXISTS (
  SELECT * FROM bookings WHERE bookings.memid = members.memid);
subq = Booking.select().where(Booking.member == Member.memid)
nrows = Member.delete().where(~fn.EXISTS(subq)).execute()

集約関数

集約関数は,リレーショナルデータベースシステムのパワーの恩恵を受けられる機能のひとつです. これにより,単にデータを保持するだけというレベルから,意思決定にも役立てられるような 本当に興味のある質問を投げかけることができるようになります.このカテゴリーでは 集約関数を詳細にカバーします.標準的なグルーピングに加えて,最新のウィンドウ関数に ついても言及します.

設備の数をカウントする

集約関数への最初の入口として,まずは簡単な例からご紹介します.施設がどれだけ存在 するのかを知るにはどうすればよいでしょうか? - 単に合計をカウントしてみましょう.

SELECT COUNT(facid) FROM facilities;
query = Facility.select(fn.COUNT(Facility.facid))
count = query.scalar()

# または:
count = Facility.select().count()

高価な設備の数をカウントする

ゲストの料金が 10 以上である施設の数をカウントしてみます.

SELECT COUNT(facid) FROM facilities WHERE guestcost >= 10
query = Facility.select(fn.COUNT(Facility.facid)).where(Facility.guestcost >= 10)
count = query.scalar()

# または:
# count = Facility.select().where(Facility.guestcost >= 10).count()

それぞれのメンバーが行った被推薦者のカウント

それぞれのメンバーが行った被推薦者をカウントしてみます.結果はメンバー ID でソートします.

SELECT recommendedby, COUNT(memid) FROM members
WHERE recommendedby IS NOT NULL
GROUP BY recommendedby
ORDER BY recommendedby
query = (Member
         .select(Member.recommendedby, fn.COUNT(Member.memid))
         .where(Member.recommendedby.is_null(False))
         .group_by(Member.recommendedby)
         .order_by(Member.recommendedby))

施設ごとの予約時間枠の合計一覧を取得する

施設ごとの予約時間枠の合計一覧を求めてみましょう.現時点では単に,施設 ID と 時間枠で構成されるテーブルを施設 ID でソートしたものを出力してみます.

SELECT facid, SUM(slots) FROM bookings GROUP BY facid ORDER BY facid;
query = (Booking
         .select(Booking.facid, fn.SUM(Booking.slots))
         .group_by(Booking.facid)
         .order_by(Booking.facid))

指定月の施設ごとの予約時間枠の合計一覧を取得する

2012年9月の,施設ごとの予約時間枠の合計一覧を求めてみましょう.施設 ID と時間枠 で構成されるテーブルを,時間枠の数でソートしたものを出力してみます.

SELECT facid, SUM(slots)
FROM bookings
WHERE (date_trunc('month', starttime) = '2012-09-01'::dates)
GROUP BY facid
ORDER BY SUM(slots)
query = (Booking
         .select(Booking.facility, fn.SUM(Booking.slots))
         .where(fn.date_trunc('month', Booking.starttime) == datetime.date(2012, 9, 1))
         .group_by(Booking.facility)
         .order_by(fn.SUM(Booking.slots)))

月ごとの施設ごとの予約時間枠の合計一覧を取得する

2012年の,月ごと施設ごとの予約時間枠の合計一覧を求めてみましょう.施設 ID と時間枠 で構成されるテーブルを,施設 ID と月でソートしたものを出力してみます.

SELECT facid, date_part('month', starttime), SUM(slots)
FROM bookings
WHERE date_part('year', starttime) = 2012
GROUP BY facid, date_part('month', starttime)
ORDER BY facid, date_part('month', starttime)
month = fn.date_part('month', Booking.starttime)
query = (Booking
         .select(Booking.facility, month, fn.SUM(Booking.slots))
         .where(fn.date_part('year', Booking.starttime) == 2012)
         .group_by(Booking.facility, month)
         .order_by(Booking.facility, month))

最低1件の予約を行ったメンバーの数を算出する

最低1件の予約を行ったメンバー数の合計を算出してみましょう.

SELECT COUNT(DISTINCT memid) FROM bookings

-- または --
SELECT COUNT(1) FROM (SELECT DISTINCT memid FROM bookings) AS _
query = Booking.select(fn.COUNT(Booking.member.distinct()))

# または:
query = Booking.select(Booking.member).distinct()
count = query.count()  # count() wraps in SELECT COUNT(1) FROM (...)

予約された時間枠が1000個以上となる施設の一覧

予約された時間枠が1000個以上となるような施設の一覧を求めてみましょう.施設 ID と時間により構成されるテーブルを,施設 ID でソートしたものを出力してみます.

SELECT facid, SUM(slots) FROM bookings
GROUP BY facid
HAVING SUM(slots) > 1000
ORDER BY facid;
query = (Booking
         .select(Booking.facility, fn.SUM(Booking.slots))
         .group_by(Booking.facility)
         .having(fn.SUM(Booking.slots) > 1000)
         .order_by(Booking.facility))

施設ごとの売上合計を求める

施設の一覧を,それらの売上合計と合わせて求めてみましょう.出力されるテーブルは 施設名と売上により構成され,売上でソートされています.ゲストとメンバーでは 利用料が異なることに注意してください。

SELECT f.name, SUM(b.slots * (
CASE WHEN b.memid = 0 THEN f.guestcost ELSE f.membercost END)) AS revenue
FROM bookings AS b
INNER JOIN facilities AS f ON b.facid = f.facid
GROUP BY f.name
ORDER BY revenue;
revenue = fn.SUM(Booking.slots * Case(None, (
    (Booking.member == 0, Facility.guestcost),
), Facility.membercost))

query = (Facility
         .select(Facility.name, revenue.alias('revenue'))
         .join(Booking)
         .group_by(Facility.name)
         .order_by(SQL('revenue')))

売上合計が1000未満の施設の一覧

売上合計が1000未満の施設の一覧を求めてみます.出力は施設名と売上により構成され, 売上によりソートされます.ゲストとメンバーでは利用料が異なることに注意してください。

SELECT f.name, SUM(b.slots * (
CASE WHEN b.memid = 0 THEN f.guestcost ELSE f.membercost END)) AS revenue
FROM bookings AS b
INNER JOIN facilities AS f ON b.facid = f.facid
GROUP BY f.name
HAVING SUM(b.slots * ...) < 1000
ORDER BY revenue;
# Same definition as previous example.
revenue = fn.SUM(Booking.slots * Case(None, (
    (Booking.member == 0, Facility.guestcost),
), Facility.membercost))

query = (Facility
         .select(Facility.name, revenue.alias('revenue'))
         .join(Booking)
         .group_by(Facility.name)
         .having(revenue < 1000)
         .order_by(SQL('revenue')))

予約された時間枠数が最も多い施設 ID を出力する

予約された時間枠数が最も多い施設 ID を出力します.

SELECT facid, SUM(slots) FROM bookings
GROUP BY facid
ORDER BY SUM(slots) DESC
LIMIT 1
query = (Booking
         .select(Booking.facility, fn.SUM(Booking.slots))
         .group_by(Booking.facility)
         .order_by(fn.SUM(Booking.slots).desc())
         .limit(1))

# Retrieve multiple scalar values by calling scalar() with as_tuple=True.
facid, nslots = query.scalar(as_tuple=True)

月ごと施設ごとの予約時間枠の合計一覧を取得する - part 2

2012年の月ごと施設ごとの予約時間枠の合計の一覧を取得します.このバージョンでは, 出力行に施設ごとのすべての月の合計と,施設全体のすべての月の合計が現れます. 出力テーブルは施設 ID, 月, 時間枠により構成され,施設 ID と月でソートされます. すべての月とすべての施設の集約値を計算する際は,月と施設 ID は null 値を返します.

※ Postgres のみ.

SELECT facid, date_part('month', starttime), SUM(slots)
FROM booking
WHERE date_part('year', starttime) = 2012
GROUP BY ROLLUP(facid, date_part('month', starttime))
ORDER BY facid, date_part('month', starttime)
month = fn.date_part('month', Booking.starttime)
query = (Booking
         .select(Booking.facility,
                 month.alias('month'),
                 fn.SUM(Booking.slots))
         .where(fn.date_part('year', Booking.starttime) == 2012)
         .group_by(fn.ROLLUP(Booking.facility, month))
         .order_by(Booking.facility, month))

施設ごとの予約合計時間のリスト

時間枠は 30 分単位であることを考慮しつつ,施設ごとの予約時間の合計の一覧を求めます. 出力テーブルは施設 ID, 施設名, 予約時間からなり,施設 ID でソートされます.

SELECT f.facid, f.name, SUM(b.slots) * .5
FROM facilities AS f
INNER JOIN bookings AS b ON (f.facid = b.facid)
GROUP BY f.facid, f.name
ORDER BY f.facid
query = (Facility
         .select(Facility.facid, Facility.name, fn.SUM(Booking.slots) * .5)
         .join(Booking)
         .group_by(Facility.facid, Facility.name)
         .order_by(Facility.facid))

2012/09/01 以降に最初に予約したメンバーのリスト

2012/09/01 以降最初に予約したメンバーについて,メンバー名,メンバー ID および予約開始時間のリストを求めます.ソート順はメンバー ID です.

SELECT m.surname, m.firstname, m.memid, min(b.starttime) as starttime
FROM members AS m
INNER JOIN bookings AS b ON b.memid = m.memid
WHERE starttime >= '2012-09-01'
GROUP BY m.surname, m.firstname, m.memid
ORDER BY m.memid;
query = (Member
         .select(Member.surname, Member.firstname, Member.memid,
                 fn.MIN(Booking.starttime).alias('starttime'))
         .join(Booking)
         .where(Booking.starttime >= datetime.date(2012, 9, 1))
         .group_by(Member.surname, Member.firstname, Member.memid)
         .order_by(Member.memid))

各行にメンバー数の合計を記載したメンバー名の一覧

各行にメンバー数の合計を含むメンバー名の一覧です.ソートキーは加入日です.

Postgres のみ (文字通り).

SELECT COUNT(*) OVER(), firstname, surname
FROM members ORDER BY joindate
query = (Member
         .select(fn.COUNT(Member.memid).over(), Member.firstname,
                 Member.surname)
         .order_by(Member.joindate))

メンバーのリストに番号を振ったもの

メンバー一覧に対して単純に昇順の番号を振り,加入日でソートしたものを求めます. メンバー ID は昇順になっているとは限らないことに注意してください.

Postgres のみ (文字通り).

SELECT row_number() OVER (ORDER BY joindate), firstname, surname
FROM members ORDER BY joindate;
query = (Member
         .select(fn.row_number().over(order_by=[Member.joindate]),
                 Member.firstname, Member.surname)
         .order_by(Member.joindate))

再度,予約時間枠数が最大の施設 ID を出力

予約時間枠数が最大である施設の ID を求めます.複数あった場合はそれらが すべて出力されます.

Postgres のみ (文字通り).

SELECT facid, total FROM (
  SELECT facid, SUM(slots) AS total,
         rank() OVER (order by SUM(slots) DESC) AS rank
  FROM bookings
  GROUP BY facid
) AS ranked WHERE rank = 1
rank = fn.rank().over(order_by=[fn.SUM(Booking.slots).desc()])

subq = (Booking
        .select(Booking.facility, fn.SUM(Booking.slots).alias('total'),
                rank.alias('rank'))
        .group_by(Booking.facility))

# Here we use a plain Select() to create our query.
query = (Select(columns=[subq.c.facid, subq.c.total])
         .from_(subq)
         .where(subq.c.rank == 1)
         .bind(db))  # We must bind() it to the database.

# クエリー結果をイテレートする場合:
for facid, total in query.tuples():
    print(facid, total)

利用時間(概数)によるメンバーのランキング

施設を予約した時間を10時間単位で四捨五入した数とともに,メンバー一覧を出力します. この概算値を使って順位付けを行い,名,姓,概算時間数,順位を出力します.ソート順は 順位,姓,名です.

Postgres のみ (文字通り).

SELECT firstname, surname,
((SUM(bks.slots)+10)/20)*10 as hours,
rank() over (order by ((sum(bks.slots)+10)/20)*10 desc) as rank
FROM members AS mems
INNER JOIN bookings AS bks ON mems.memid = bks.memid
GROUP BY mems.memid
ORDER BY rank, surname, firstname;
hours = ((fn.SUM(Booking.slots) + 10) / 20) * 10
query = (Member
         .select(Member.firstname, Member.surname, hours.alias('hours'),
                 fn.rank().over(order_by=[hours.desc()]).alias('rank'))
         .join(Booking)
         .group_by(Member.memid)
         .order_by(SQL('rank'), Member.surname, Member.firstname))

収入合計がトップ3の施設

トップ3の売上を計上した施設の一覧を求めます(同順位も考慮).出力は施設名と 順位により構成され,順位と施設名でソートされます.

Postgres のみ (文字通り).

SELECT name, rank FROM (
    SELECT f.name, RANK() OVER (ORDER BY SUM(
        CASE WHEN memid = 0 THEN slots * f.guestcost
        ELSE slots * f.membercost END) DESC) AS rank
    FROM bookings
    INNER JOIN facilities AS f ON bookings.facid = f.facid
    GROUP BY f.name) AS subq
WHERE rank <= 3
ORDER BY rank;
total_cost = fn.SUM(Case(None, (
    (Booking.member == 0, Booking.slots * Facility.guestcost),
), (Booking.slots * Facility.membercost)))

subq = (Facility
        .select(Facility.name,
                fn.RANK().over(order_by=[total_cost.desc()]).alias('rank'))
        .join(Booking)
        .group_by(Facility.name))

query = (Select(columns=[subq.c.name, subq.c.rank])
         .from_(subq)
         .where(subq.c.rank <= 3)
         .order_by(subq.c.rank)
         .bind(db))  # Here again we used plain Select, and call bind().

値により施設を分類する

施設をそれらの売上額の高い(high),平均(average),低い(low)という3グループに分類します. ソート順は分類と施設名です.

Postgres のみ (文字通り).

SELECT name,
  CASE class WHEN 1 THEN 'high' WHEN 2 THEN 'average' ELSE 'low' END
FROM (
  SELECT f.name, ntile(3) OVER (ORDER BY SUM(
    CASE WHEN memid = 0 THEN slots * f.guestcost ELSE slots * f.membercost
    END) DESC) AS class
  FROM bookings INNER JOIN facilities AS f ON bookings.facid = f.facid
  GROUP BY f.name
) AS subq
ORDER BY class, name;
cost = fn.SUM(Case(None, (
    (Booking.member == 0, Booking.slots * Facility.guestcost),
), (Booking.slots * Facility.membercost)))
subq = (Facility
        .select(Facility.name,
                fn.NTILE(3).over(order_by=[cost.desc()]).alias('klass'))
        .join(Booking)
        .group_by(Facility.name))

klass_case = Case(subq.c.klass, [(1, 'high'), (2, 'average')], 'low')
query = (Select(columns=[subq.c.name, klass_case])
         .from_(subq)
         .order_by(subq.c.klass, subq.c.name)
         .bind(db))

再帰

クエリーの実行中に,一般的なテーブル表現を使って効率的に中間的なテーブルを生成 することができます - それらは SQL をより読みやすくしてくれるので、非常に便利な ものです.しかしながら, WITH RECURSIVE 修飾子を使うと,再帰クエリーを生成する ことができます.これはツリー構造やグラフ構造のデータを処理する際,とても都合の よいものです - たとえば,指定された深度でグラフノードのリレーション全体を探索 することを考えてみてください.

メンバー ID 27 についての推薦チェーンで上昇中のものを見つける

メンバー ID 27 について,推薦チェーンの中から上昇中のものを見つけることを考えます: すなわち,他の誰かを推薦したメンバーと,そのメンバーを推薦したメンバーを見つける といったことです.このクエリーではメンバー ID,名,姓を返します.ソート順はメンバー ID の降順です.

WITH RECURSIVE recommenders(recommender) as (
  SELECT recommendedby FROM members WHERE memid = 27
  UNION ALL
  SELECT mems.recommendedby
  FROM recommenders recs
  INNER JOIN members AS mems ON mems.memid = recs.recommender
)
SELECT recs.recommender, mems.firstname, mems.surname
FROM recommenders AS recs
INNER JOIN members AS mems ON recs.recommender = mems.memid
ORDER By memid DESC;
# 再帰 CTE の基本ケース.memid=27 の推薦者を見つける.
base = (Member
        .select(Member.recommendedby)
        .where(Member.memid == 27)
        .cte('recommenders', recursive=True, columns=('recommender',)))

# CTE の再帰部分.直近の推薦者の推薦者を見つける.
MA = Member.alias()
recursive = (MA
             .select(MA.recommendedby)
             .join(base, on=(MA.memid == base.c.recommender)))

# Combine the base-case with the recursive term.
# 基本ケースと再帰部分を結合
cte = base.union_all(recursive)

# 再帰 CTE から select し,member と結合して名前の情報を取得
query = (cte
         .select_from(cte.c.recommender, Member.firstname, Member.surname)
         .join(Member, on=(cte.c.recommender == Member.memid))
         .order_by(Member.memid.desc()))