보기: ORMS 및 SQL 보기

본고는 데이터베이스, Ruby, Rails의 작업 방식에 대해 대체적으로 알고 있다고 가정한다. 아래의 예시 사용ActiveRecord 라이브러리는 Rails 응용 프로그램의 상하문에서 데이터베이스 관계를 Ruby 대상에 비추었다.보기example repository, 그들의 실제 행동을 봐라!(주의: 관심이 있으시다면 JavaScript 예시here를 설정했지만 SQL 보기에 대해 일류로 지원되는 JS ORM을 아직 찾지 못했습니다.)

집결 가중


웹 응용 프로그램은 일반적으로 "요약"또는 집합 데이터를 표시하는데, 이 데이터는 여러 데이터베이스 테이블에서 정보를 추출하여 여러 데이터베이스 테이블에 걸쳐 계산할 수 있다.ORM(객체 관계 매핑) 데이터베이스 모델링 라이브러리는 일반적으로 한 테이블에서 한 번에 읽도록 설계됩니다.비록 이런 모델은 대량의 용례 (win의 기본 CRUD)에 매우 유용하지만, 우리가 데이터를 집합하려고 시도할 때, 우리는 복잡한 응용 프로그램 코드와/또는 비싼 데이터베이스 조회의 위험에 직면하게 될 것이다.SQL 보기는 우리가 조회의 수를 줄이고 데이터 집합 논리를 데이터베이스로 아래로 밀어내는 데 잠재적으로 도움을 줄 수 있다.
예를 들어 수의사가 고객과 의사소통을 할 수 있도록 프로그램을 만들기 위해 고용되었다고 가정해 보세요.이러한 워크플로우(씬) 예제 서버 노드를 생성한 경우
user = User.includes(:pets).find(1)

render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pets.map do |pet|
    {
      id: pet.id,
      name: pet.name
    }
  end
}

위의 코드는 매우 간단합니다. 사용자를 불러오고 애완동물을 불러오며 이전에 기대했던 형식의 서열화 데이터를 불러옵니다.우리는 두 가지 조회를 하고 있지만, 그것들은 간단하고 빠르다. 이것은 합리적인 기대이다.
빨리 몇 주 들어가자, 우리는 새로운 요구가 생겼다.사용자는 그들이 당신에게 어떤 애완동물을 등록했는지 볼 수 있지만, 다음 예약 시간이 언제인지 알고 싶어 합니다!따라서 끝점을 업데이트해야 합니다.
user = User
  .includes(:pets)
  .find(1)

# Pre-load the user's pets' appointments that are scheduled for the future
upcoming_appointments = user
  .appointments
  .order(date: :asc)  
  .where('date >= ?', Time.current)

render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pets.map do |pet|
    # Use Array#find to return the first of this pet's appointments
    next_appointment_date = upcoming_appointments.find do |appt|
      appt.pet_id == pet.id
    end

    {
      id: pet.id,
      name: pet.name,
      next_appointment_date: next_appointment_date
    }
  end
}
솔직히 말하면 이것은 그리 나쁘지 않다.우리는 다른 조회를 추가했다. 이번에는 데이트였다. 정렬과 필터 논리가 있었다.우리는 또한 서열화 절차에 순환 논리를 추가하여 모든 주인의 애완동물을 위해 첫 번째 데이트를 이끌어냈다.그것은 좀 어수선하지만, 야, 그것은 쓸모가 있어.
개인적으로 나는 응용 프로그램 코드에서 이 모든 행위를 인코딩하는 것을 좋아하지 않는다.이것은 나로 하여금 매우 혼란스럽게 하고, 사악한 물건을 그 속에 잠입시키기 쉽다.예를 들어, 다음과 같이 가정합니다.
render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pets.map do |pet|
    next_appointment_date = user
      .appointments
      .order(date: :asc)
      .find_by('date >= ?', Time.current)
      &.date

    {
      id: pet.id,
      name: pet.name,
      next_appointment_date: next_appointment_date
    }
  end
}
🙀아, 아니야!누군가가 몰래 우리의 서열화 절차에 대해 조회했다. 비록 이 단계는 처음에는 어떤 경종도 울리지 않을 수도 있지만, (사용자 한 명당 한두 마리의 애완동물을 처리할 때) 동물수용소가 당신의 고객 중 하나가 되어 100여 마리의 애완동물을 등록했을 때 무슨 일이 일어날까?페이지당 100개가 넘는 조회를 불러옵니다.
이것은 분명히 인위적인 예이지만, 나는 더욱 복잡한 상황을 만났다. 그 중에서 N+1 조회는 서비스 대상과 단독 파일에 숨겨져 있다.나는 개인적으로 이런 논리가 더욱 복잡해지기 시작하거나 응용 프로그램의 다른 곳에서 그것을 필요로 할 때 데이터베이스로 밀어내는 것을 좋아한다.이것이 바로 SQL 뷰의 용무지!

SQL 뷰란 무엇입니까?


SQL 뷰의 가장 기본적인 심리 모델은 "데이터베이스에 저장된 조회입니다. 그 행위는 표와 유사합니다."그리고 a lotmore to it, 하지만 이 간단한 이해는 당신을 아주 긴 길을 걷게 할 수 있습니다.예를 들어, 데이터베이스에서 다음 문장을 수행합니다.
CREATE VIEW silly_users AS
  SELECT 
    id, 
    first_name,
    first_name || ' Mc' || first_name || 'erson' AS silly_name
  FROM users;
More on the || concatenation operator
이 뷰에서 테이블과 동일한 구문을 사용하여 결과를 질의할 수 있습니다.
# SELECT * FROM silly_users;
  id   | first_name |       silly_name       
------------+------------+------------------------
     1 | Melissa    | Melissa McMelissaerson
     2 | Colleen    | Colleen McColleenerson
     3 | Vince      | Vince McVinceerson
     4 | David      | David McDaviderson
     5 | Dennis     | Dennis McDenniserson
...etc
이 보기의 동작은 시계와 비슷하기 때문에 ORM과 잘 어울려 사용할 수 있다.우리는 보기가 지원하는 모델을 만들 수 있습니다!
silly_user = SillyUser.find(1)
silly_user.silly_name // => 'Melissa McMelissaerson'
Ruby를 사용하는 경우 ActiveRecord ORM을 사용하는 항목에 뷰 버전 제어 및 기타 유용한 기능을 제공하는 것을 강력히 권장합니다.

ThoughtBot의 풍경 보석 새로운 시각


응용 프로그램 코드 에서 검색하지 않고 데이터베이스 보기를 작성해 보겠습니다.
-- Grabs one record per pet, returning the earliest future appointment date 
CREATE VIEW pets_with_upcoming_appointments AS
SELECT DISTINCT ON (pets.id)
  pets.id AS id,
  users.id AS user_id,
  pets.name AS name,
  MIN(appointments.date) AS next_appointment_date
FROM users
INNER JOIN pets
  ON user_id = users.id
LEFT JOIN appointments
  ON pets.id = pet_id
  AND appointments.date >= CURRENT_DATE
GROUP BY (
  users.id,
  pets.id,
  pets.name
);

너무 좋아요.이제 우리는 이 보기에서 읽을 수 있다.
# SELECT * FROM pets_with_upcoming_appointments;

 user_id | pet_id | pet_name |  next_appointment_date   
--------------+--------+----------+-----------------------
       1 |      1 | Flannery |   2018-11-22 13:00:00
       2 |      2 | Porkchop |   2018-11-22 16:30:00
       2 |      3 | Gravy    |   2018-12-01 09:00:00
       3 |      4 | Magnus   | 
       4 |      5 | Huey     |   2018-12-15 10:45:00
       4 |      6 | Duey     |   2018-12-15 10:45:00
       4 |      7 | Louie    |   2018-12-15 10:45:00

# SELECT * FROM pets_with_upcoming_appointments WHERE user_id = 1;

 user_id | pet_id | pet_name |  next_appointment_date   
--------------+--------+----------+-----------------------
       1 |      1 | Flannery |   2018-11-22 13:00:00
이제 보기를 설정했습니다. 앞에서 언급한 장면gem를 사용하여 이동을 만들고 데이터베이스에서 지원하는 ORM 모델에 연결할 수 있습니다.
class PetWithUpcomingAppointment < ActiveRecord::Base
  self.table_name = 'pets_with_upcoming_appointments'
end
우리의 보기는 DISTINCT ON 필드가 있기 때문에 사용자 모델에서 하나의 관계를 연결하는 것은 매우 간단하다.
class User < ActiveRecord::Base
  has_many :pets
  # wooooot
  has_many :pet_with_upcoming_appointments
  has_many :appointments, through: :pets
end
이제 애플리케이션 코드를 얻기 위해 데이터를 정리할 수 있습니다.
user = User
  .includes(:pet_with_upcoming_appointments)
  .find(params[:id])

render json: {
  user: {
    id: user.id,
    name: "#{user.first_name} #{user.last_name}"
  },
  pets: user.pet_with_upcoming_appointments.map do |pet|
    {
      id: pet.id,
      name: pet.name,
      next_appointment_date: pet.next_appointment_date
    }
  end
}
좋다!우리는 컨트롤러에 정렬/날짜 비교 논리가 없는 두 개의 조회로 돌아왔다💪 더 좋은 것은, 우리는 응용 프로그램의 다른 부분에서 이 모델을 중복 사용할 수 있으며, 중복 검색 논리가 필요하지 않다는 것이다.더 복잡한 집합을 실행하고 다른 테이블에서 데이터를 추출하기 시작할 때, 이 점은 확실히 드러나기 시작한다.

(user\u id를 사용하는 GROUP BY에 대한 추가 정보 SQL 뷰의 트랩



비록 나는 분명히 팬이지만, 당신이 응용 프로그램의 보기를 고려하기 시작할 때, 주의해야 할 일이 있다.

과도한 시각화


이것은'나는 망치가 있는데, 모든 것이 못이다'는 또 다른 방면이다문제처음 그것을 사용하기 시작할 때, 데이터베이스에 너무 많은 논리를 넣는 것을 발견할 수 있습니다.응용 프로그램이 제공하는 서비스를 변경할 때마다 새 보기를 업데이트하거나 만들어야 하기 때문에 대량의 마이그레이션을 실행해야 합니다.😬 당신이 하고 있는 일을 하나의 관점으로 바꾸기 전에, 당신이 얻고 있는 이익이 평가할 만한 가치가 있는지 자신에게 물어보세요.

겹쳐진 뷰의 레벨이 너무 많음


보기는 일반적으로 표에서 나온 결과로 구성되지만, 다른 보기 (원보기!?) 에서 데이터를 추출할 수 있는 보기를 만들 수도 있습니다.이것은 좋은 생각인 것 같다. ("활동"사용자가 무엇인지 이 보기에서 정의한 다음에 사용자가 필요한 곳으로 끌어다 놓을 것이다!)그러나 실천 과정에서 나는 그것이 각급 보기를 갱신하는 것을 더욱 어렵게 하는 것을 보았다.당신은 또한 자신을 곤경에 빠뜨릴 수 있습니다. 한 보기의 변경이 다른 보기에 어떻게 영향을 미치는지 찾아내서 여러 단계에서 데이터를 도입할 수 있습니다😵 이것은 사실상 첫 번째 문제일 뿐이다.

전파의 저효율


SQL 쿼리 분석에 익숙한 경우 EXPLAIN/EXPLAIN ANALYZE를 사용하여 문제를 찾거나 위 뷰의 LEFT JOIN appointments 에서 불안해하는 경우😂), 지금은 이 기술을 사용하기에 좋은 시기입니다! ,indexes,hash joins이 당신을 현기증나게 한다면, 데이터베이스에서 더 많은 기록을 얻기 시작하면, 간단한 검색이 매우 느리게 실행되는 것을 발견할 수 있을 것이다.만약 당신이 조심하지 않는다면, 비효율적인 검색은 결국 대량의 응용 프로그램의 기능을 지원하여 전체적인 성능을 떨어뜨릴 수 있다.적어도 보기를 도입하는 것이 그것을 사용하는 과정 속도에 미치는 영향을 보자.

순차 스캔 이제 어떡하지?


만약 당신이 흥미가 있다면, 여기에 내가 추천한 자원(그리고 내가 이 글에서 인용한 링크)이 있어서 더 많은 것을 알 수 있습니다!장수, 번영, 데이터베이스를 두려워하지 마라🖖
  • Example Repo
  • What is a Relational Database View?
  • How to Use Views
  • Ordering Within a SQL Group By Clause
  • Depesz's tool for explaining EXPLAIN output
  • Intro to Indexes
  • Hash Joins
  • Sequential Scans
  • 좋은 웹페이지 즐겨찾기