Djangoで生SQLを実行する

books_book

title price
フェルマーの最終定理 781
暗号解読(上) 590
暗号解読(下) 630
Python プロフェッショナルプログラミング 2800
エキスパートPythonプログラミング 3600
スタートアップ! 1600


こんなテーブルがあったとして、SQLで価格毎の冊数をカウントする場合、Case式を使って以下のように書けます。

SELECT CASE WHEN price < 1000 THEN '1000円未満'
            WHEN price >= 1000 AND price < 2000 THEN '1000円以上2000円未満' 
            WHEN price >= 2000 AND price < 3000 THEN '2000円以上3000円未満'
            WHEN price >= 3000 THEN '3000円以上'
            ELSE NULL END AS price_class,
            COUNT(*) AS cnt
FROM books_book
GROUP BY CASE WHEN price < 1000 THEN '1000円未満'
            WHEN price >= 1000 AND price < 2000 THEN '1000円以上2000円未満' 
            WHEN price >= 2000 AND price < 3000 THEN '2000円以上3000円未満'
            WHEN price >= 3000 THEN '3000円以上'
            ELSE NULL END;


結果

+----------------------------+-----+
| price_class                | cnt |
+----------------------------+-----+
| 1000円未満                 |   3 |
| 1000円以上2000円未満       |   1 |
| 2000円以上3000円未満       |   1 |
| 3000円以上                 |   1 |
+----------------------------+-----+

DjangoのQuerySetを使ってSQLを発行する場合、自由にsqlを書く方法としてrawがありますが

result = Book.objects.raw("""
SELECT CASE WHEN price < 1000 THEN '1000円未満'
            WHEN price >= 1000 AND price < 2000 THEN '1000円以上2000円未満' 
            WHEN price >= 2000 AND price < 3000 THEN '2000円以上3000円未満'
            WHEN price >= 3000 THEN '3000円以上'
            ELSE NULL END AS price_class,
            COUNT(*) AS cnt
FROM books_book
GROUP BY CASE WHEN price < 1000 THEN '1000円未満'
            WHEN price >= 1000 AND price < 2000 THEN '1000円以上2000円未満' 
            WHEN price >= 2000 AND price < 3000 THEN '2000円以上3000円未満'
            WHEN price >= 3000 THEN '3000円以上'
            ELSE NULL END;
""")[0]

これはエラーになります

InvalidQuery: Raw query must include the primary key

rawを使う場合は必ずprimary keyを取得しないといけないんですね。


raw以外に素の SQL クエリを直接実行するにはconnection.cursor()を使う手もあります

from django.db import connection
cursor = connection.cursor()
cursor.execute("""
SELECT CASE WHEN price < 1000 THEN '1000円未満'
            WHEN price >= 1000 AND price < 2000 THEN '1000円以上2000円未満' 
            WHEN price >= 2000 AND price < 3000 THEN '2000円以上3000円未満'
            WHEN price >= 3000 THEN '3000円以上'
            ELSE NULL END AS price_class,
            COUNT(*) AS cnt
FROM books_book
GROUP BY CASE WHEN price < 1000 THEN '1000円未満'
            WHEN price >= 1000 AND price < 2000 THEN '1000円以上2000円未満' 
            WHEN price >= 2000 AND price < 3000 THEN '2000円以上3000円未満'
            WHEN price >= 3000 THEN '3000円以上'
            ELSE NULL END;
""")
rows = cursor.fetchall()

これなら値を取得できました。



まあ、無理して一発で引く必要がなければ、これでも。

 Book.objects.filter(price__lt=1000).count()
 Book.objects.filter(price__gte=1000, price__lt=2000).count()
 Book.objects.filter(price__gte=2000, price__lt=3000).count()
 Book.objects.filter(price__gte=3000).count()