지난 글에 이어서 AshPostgres 튜토리얼을 따라가며 사용법을 배워 본다. 이번 글 에서는 집계(Aggregates)와 계산(Calculations)에 대해 알아 볼 것이다.
집계(Aggregates)
집계(Aggregates)는 관계로 연결된 데이터들을 모아 요약 정보를 제공하는 도구이다. 관계로 연결된 데이터들의 합계, 수, 최대값, 최소값, 평균값 등 통계성 값 들을 얻는데 사용한다. 예제에선 Representative 와 Ticket 이 일대다 관계로 연결되었기 때문에 담당자가 몇개의 티켓을 가지고 있는지 진행중인 티켓은 몇 개 인지 같은 값을 얻는데 사용할 수 있다.
다음 코드를 담당자 리소스 선언에 추가한다.
# in lib/helpdesk/support/representative.ex
aggregates do
# 첫번째 인자는 집계의 이름, 두 번째 인자는 관계의 이름
count :total_tickets, :tickets
count :open_tickets, :tickets do
# 표현식으로 필터링 할 수 도 있다
filter expr(status == :open)
end
count :closed_tickets, :tickets do
filter expr(status == :closed)
end
end
# in iex
iex> require Ash.Query
iex> Helpdesk.Support.Representative
|> Ash.Query.filter(closed_tickets < 4) # closed_tickets 집계가 사용 됨
|> Ash.Query.sort(closed_tickets: :desc) # closed_tickets 집계가 사용 됨
|> Ash.read!()
18:44:34.441 [debug] QUERY OK source="representatives" db=15.7ms queue=0.1ms idle=1406.0ms
SELECT r0."id", r0."name" FROM "representatives" AS r0 LEFT OUTER JOIN LATERAL (SELECT st0."representative_id" AS "representative_id", coalesce(count(*), $1::bigint)::bigint AS "closed_tickets" FROM "public"."tickets" AS st0 WHERE (r0."id" = st0."representative_id") AND (st0."status"::varchar = $2::varchar) GROUP BY st0."representative_id") AS s1 ON TRUE WHERE (coalesce(s1."closed_tickets", $3::bigint)::bigint < $4::bigint) ORDER BY coalesce(s1."closed_tickets", $5::bigint)::bigint DESC [0, :closed, 0, 4, 0]
# 저장된 데이터 상황에 따라 결과는 달라질 수 있음
[
%Helpdesk.Support.Representative{
id: "f773d41f-24f8-4d38-a5ef-a98a187de034",
name: "Joe Armstrong",
total_tickets: #Ash.NotLoaded<:aggregate, field: :total_tickets>,
open_tickets: #Ash.NotLoaded<:aggregate, field: :open_tickets>,
closed_tickets: #Ash.NotLoaded<:aggregate, field: :closed_tickets>,
tickets: #Ash.NotLoaded<:relationship, field: :tickets>,
__meta__: #Ecto.Schema.Metadata<:loaded, "representatives">
},
%Helpdesk.Support.Representative{
id: "a4467ac7-6f4e-49b1-bdea-9dc98acb5ac9",
name: "Test User 2",
total_tickets: #Ash.NotLoaded<:aggregate, field: :total_tickets>,
open_tickets: #Ash.NotLoaded<:aggregate, field: :open_tickets>,
closed_tickets: #Ash.NotLoaded<:aggregate, field: :closed_tickets>,
tickets: #Ash.NotLoaded<:relationship, field: :tickets>,
__meta__: #Ecto.Schema.Metadata<:loaded, "representatives">
}
]
리소스에 total_tickets, open_tickets, closed_tickets 값이 추가된 것을 확인할 수 있다. 기본적으로 명시적 로드를 해야만 쿼리에도 반영되고 값도 획득된다. closed_tickets 값은 필터링에서 사용되었기 때문에 쿼리에 집계가 포함되었으나 해당 값 자체는 로드하지 않았기 때문에 NotLoaded 이다. 명시적으로 값을 로드하는 코드는 다음과 같다.
iex> Helpdesk.Support.Representative
|> Ash.Query.filter(closed_tickets < 4)
|> Ash.Query.sort(closed_tickets: :desc)
|> Ash.Query.load([:closed_tickets, :open_tickets, :total_tickets]) # 명시
|> Ash.read!()
계산(Calculations)
계산은 집계와 비슷하지만 개별 레코드에 대해 작동한다는 것이 다르다. 그러나 계산은 집계된 값을 참조할 수 있다. 예를들어 할당된 티켓의 상태 비율을 계산하는 코드는 다음과 같이 만들 수 있다.
# in lib/helpdesk/support/representative.ex
calculations do
# open_tickets, total_tickets 는 aggregates 값
calculate :percent_open, :float, expr(open_tickets / total_tickets)
end
iex> Helpdesk.Support.Representative
|> Ash.Query.filter(percent_open > 0.25)
|> Ash.Query.sort(:percent_open)
|> Ash.Query.load(:percent_open)
|> Ash.read!()
19:00:02.609 [debug] QUERY OK source="representatives" db=4.2ms queue=18.6ms idle=1420.5ms
SELECT r0."id", r0."name", (coalesce(s1."open_tickets", $1::bigint)::bigint::decimal / coalesce(s1."total_tickets", $2::bigint)::bigint::decimal)::float FROM "representatives" AS r0 LEFT OUTER JOIN LATERAL (SELECT st0."representative_id" AS "representative_id", coalesce(count(*) FILTER (WHERE st0."status"::varchar = $3::varchar), $4::bigint)::bigint AS "open_tickets", coalesce(count(*) FILTER (WHERE $5), $6::bigint)::bigint AS "total_tickets" FROM "public"."tickets" AS st0 WHERE (r0."id" = st0."representative_id") GROUP BY st0."representative_id") AS s1 ON TRUE WHERE ((coalesce(s1."open_tickets", $7::bigint)::bigint::decimal / coalesce(s1."total_tickets", $8::bigint)::bigint::decimal)::float > $9::float) ORDER BY (coalesce(s1."open_tickets", $10::bigint)::bigint::decimal / coalesce(s1."total_tickets", $11::bigint)::bigint::decimal)::float [0, 0, :open, 0, true, 0, 0, 0, 0.25, 0, 0]
[
%Helpdesk.Support.Representative{
id: "f773d41f-24f8-4d38-a5ef-a98a187de034",
name: "Joe Armstrong",
total_tickets: #Ash.NotLoaded<:aggregate, field: :total_tickets>,
open_tickets: #Ash.NotLoaded<:aggregate, field: :open_tickets>,
closed_tickets: #Ash.NotLoaded<:aggregate, field: :closed_tickets>,
percent_open: 0.5,
tickets: #Ash.NotLoaded<:relationship, field: :tickets>,
__meta__: #Ecto.Schema.Metadata<:loaded, "representatives">
},
%Helpdesk.Support.Representative{
id: "a4467ac7-6f4e-49b1-bdea-9dc98acb5ac9",
name: "Test User 2",
total_tickets: #Ash.NotLoaded<:aggregate, field: :total_tickets>,
open_tickets: #Ash.NotLoaded<:aggregate, field: :open_tickets>,
closed_tickets: #Ash.NotLoaded<:aggregate, field: :closed_tickets>,
percent_open: 0.5,
tickets: #Ash.NotLoaded<:relationship, field: :tickets>,
__meta__: #Ecto.Schema.Metadata<:loaded, "representatives">
}
]
집계과 계산을 잘 활용하면 리소스 별 통계성 값 들을 꽤 깔끔하게 코드로 표현할 수 있다. 예제의 코드들은 Ash 가 만들어 주는 쿼리들 이었지만 원한다면 직접 커스텀 집계 쿼리를 만들 수 도 있다. 적절히 사용하면 리소스의 명료한 표현에 큰 도움이 될 것 같다.