ActiveRecord를 사용하여 FROM 절의 하위 쿼리를 작성하는 방법

ActiveRecord에서 여러 테이블을 JOIN 하고 GROUP BY 만났습니다.

SQL이라고 간단하게 쓸 수 있습니다만 ActiveRecord라고 어떻게 써야할지 몰랐기 때문에, 정리합니다.

결론



먼저 서브 쿼리에 상당하는 처리를 ActiveRecord로 기술해 변수에 대입해 두고,
ModelClass.from("(#{subquery.to_sql}) AS sub").select('sub.columnA, sub.columnB')
GROUP BY 메서드를 호출하여 FROM 절의 하위 쿼리로 처리할 수 있습니다.

만난 상황





각 테이블은 다음과 같이 되어 있습니다.
요약하면 이런 상황입니다.
  • 일식과 이탈리안을 취급하는 레스토랑(대중계 레스토랑)과 이탈리안만 취급하는 레스토랑(고급 레스토랑)이 있다
  • 각각 3건씩 예약이 들어 있다
  • 이탈리안을 취급하는 레스토랑의 예약 건수(3+3=6)와 일식을 취급하는 레스토랑의 예약 건수(3)를 취득하고 싶다
  • SELECT id, restaurant_id, guest_name, reserved_at FROM reservations;
    1|1|岩瀬 好近|2019-08-01 13:00:00
    2|1|戸田 良市|2019-08-01 14:00:00
    3|2| 信好|2019-08-01 15:00:00
    4|2|平山 利次|2019-08-01 11:00:00
    5|1|金城 義勝|2019-08-01 18:00:00
    6|2| 知実|2019-08-01 19:00:00
    
    SELECT id, name FROM restaurants;
    1|大衆系レストラン
    2|高級レストラン
    
    SELECT id, restaurant_id, name FROM courses;
    1|1|焼き魚コース
    2|1|串カツ三昧
    3|1|寿司セット
    4|1|コラボピザ
    5|2|高級ピッツア
    6|2|色とりどりのパスタ
    7|2|イタリアンフルコース
    
    SELECT id, restaurant_id, name, category_id FROM courses;
    1|1|焼き魚コース|1
    2|1|串カツ三昧|1
    3|1|寿司セット|1
    4|1|コラボピザ|2
    5|2|高級ピッツア|2
    6|2|色とりどりのパスタ|2
    7|2|イタリアンフルコース|2
    
    SELECT id, name  FROM categories;
    1|和食
    2|イタリアン
    

    해결 방법



    각 테이블을 JOIN하여 DISTINCTfromcategories.name 하는 하위 쿼리를 정의합니다. (subquery 변수에 할당)
    reservations.id 메서드에 subquery를 하위 쿼리로 확장하고 전달하고 SELECT DISTINCTfrom를 호출하여 범주별 예약 수를 검색 할 수있었습니다.
    subquery = Reservation.all
                          .joins(restaurant: { courses: :category })
                          .select(%(
                            distinct categories.name AS category_name,
                                     reservations.id AS reservation_id
                          ))
    
    reservations = Reservation.from("(#{subquery.to_sql}) AS reservations")
                              .group('reservations.category_name')
                              .select(%(
                                reservations.category_name AS category_name,
                                COUNT(reservations.reservation_id) AS reservation_count
                              ))
    
    reservations.map do|reservation|
      [reservation.category_name, reservation.reservation_count]
    end
    => [["イタリアン", 6], ["和食", 3]]
    

    (2019/8/21 13:38 추가)
    @ j 치토 님이 group 를 사용하는 방법을 코멘트했습니다.
    확실히, 이쪽이 깨끗이 쓸 수 있습니다. 감사합니다.
    Reservation
      .joins(restaurant: { courses: :category })
      .group('categories.name')
      .distinct
      .count(:id)
    

    (참고) 하위 쿼리를 사용하지 않고 잘못된 값을 얻은 사례



    참고까지 처음 쓴 코드를 노출합니다. 이 코드는 올바른 예약 수를 얻을 수 없습니다.
    reservations = Reservation.all
                              .joins(restaurant: { courses: :category })
                              .group('categories.name')
                              .select(%(
                                categories.name AS category_name, 
                                COUNT(reservations.id) AS reservation_count
                              ))
    
    reservations.map do|reservation|
      [reservation.category_name, reservation.reservation_count]
    end
    => [["イタリアン", 12], ["和食", 9]]
    

    원인은 GROUP BY하기 직전의 테이블을 참조하면 일목요연합니다.
    하나의 레스토랑에 여러 코스가 존재하면 JOIN했을 때 각 레스토랑의 코스 수만큼 레코드가 중복되어 버리는 것이 원인입니다.
    SELECT reservations.id,
           restaurants.NAME,
           courses.NAME,
           categories.NAME
    FROM   "reservations"
           INNER JOIN "restaurants"
                   ON "restaurants"."id" = "reservations"."restaurant_id"
           INNER JOIN "courses"
                   ON "courses"."restaurant_id" = "restaurants"."id"
           INNER JOIN "categories"
                   ON "categories"."id" = "courses"."category_id";
    1|大衆系レストラン|焼き魚コース|和食
    2|大衆系レストラン|焼き魚コース|和食
    5|大衆系レストラン|焼き魚コース|和食
    1|大衆系レストラン|串カツ三昧|和食
    2|大衆系レストラン|串カツ三昧|和食
    5|大衆系レストラン|串カツ三昧|和食
    1|大衆系レストラン|寿司セット|和食
    2|大衆系レストラン|寿司セット|和食
    5|大衆系レストラン|寿司セット|和食
    1|大衆系レストラン|コラボピザ|イタリアン
    2|大衆系レストラン|コラボピザ|イタリアン
    5|大衆系レストラン|コラボピザ|イタリアン
    3|高級レストラン|高級ピッツア|イタリアン
    4|高級レストラン|高級ピッツア|イタリアン
    6|高級レストラン|高級ピッツア|イタリアン
    3|高級レストラン|色とりどりのパスタ|イタリアン
    4|高級レストラン|色とりどりのパスタ|イタリアン
    6|高級レストラン|色とりどりのパスタ|イタリアン
    3|高級レストラン|イタリアンフルコース|イタリアン
    4|高級レストラン|イタリアンフルコース|イタリアン
    6|高級レストラン|イタリアンフルコース|イタリアン
    

    이전 섹션과 같이 하위 쿼리에서 select 한 다음 COUNT(DISTINCT some_column) 하면 중복을 제거한 건수를 얻을 수 있습니다.

    참고


  • Constructing a select * from subquery using ActiveRecord
  • 좋은 웹페이지 즐겨찾기