Time to first response by group members

Time to first group response for topics created within a given time period

Returns the time to first response by a member of a given group to “regular” (not Personal Message) topics created by a user who is not a member of the given group. The query’s :group_name parameter is set to “staff” by default. With that value, it will give the time to first response by staff members. You can change the value of that parameter to get response times for different groups. For example “customer_support”.

Note that dates should technically be supplied in the form yyyy-mm-dd, but the query would also accept dates in the form dd-mm-yyyy.

-- [params]
-- date :start_date
-- date :end_date
-- string :group_name = staff

WITH group_response_times AS (
    SELECT
        t.category_id,
        t.id AS topic_id,
        EXTRACT(EPOCH FROM (p.created_at - t.created_at)) / 60 AS response_time_minutes,
        p.user_id AS staff_user_id,
        ROW_NUMBER() OVER (PARTITION BY t.id ORDER BY p.created_at) AS row_num
    FROM posts p
    JOIN topics t ON t.id = p.topic_id
    WHERE t.user_id NOT IN (SELECT user_id
                                FROM group_users gu JOIN groups g ON g.id = gu.group_id
                                WHERE gu.user_id > 0 AND g.name = :group_name)
        AND p.user_id IN (SELECT user_id
                              FROM group_users gu JOIN groups g ON g.id = gu.group_id
                              WHERE gu.user_id > 0 AND g.name = :group_name)
        AND t.archetype = 'regular'
        AND t.deleted_at IS NULL
        AND p.post_type = 1
        AND p.deleted_at IS NULL
        AND t.created_at BETWEEN :start_date AND :end_date
)

SELECT
    category_id,
    topic_id,
    staff_user_id,
    ROUND(response_time_minutes::numeric, 2) AS response_time_minutes
FROM group_response_times
WHERE row_num = 1
ORDER BY category_id, response_time_minutes

Average time to first group response per category:

Uses the same logic as the previous query, but returns the average time to the first response by members of the given group per category for topics created by users who are not members of the given group within the time period set by the :start_date and :end_date parameters. As with the previous query, if the :group_name parameter is left at its default value of “staff”, it will return the average staff first response times for regular topics created by non-staff users.

-- [params]
-- date :start_date
-- date :end_date
-- string :group_name = staff

WITH group_response_times AS (
    SELECT
        t.category_id,
        t.id AS topic_id,
        EXTRACT(EPOCH FROM (p.created_at - t.created_at)) / 60 AS response_time_minutes,
        p.user_id AS staff_user_id,
        ROW_NUMBER() OVER (PARTITION BY t.id ORDER BY p.created_at) AS row_num
    FROM posts p
    JOIN topics t ON t.id = p.topic_id
    WHERE t.user_id NOT IN (SELECT user_id
                                FROM group_users gu JOIN groups g ON g.id = gu.group_id
                                WHERE gu.user_id > 0 AND g.name = :group_name)
        AND p.user_id IN (SELECT user_id
                              FROM group_users gu JOIN groups g ON g.id = gu.group_id
                              WHERE gu.user_id > 0 AND g.name = :group_name)
        AND t.archetype = 'regular'
        AND t.deleted_at IS NULL
        AND p.post_type = 1
        AND p.deleted_at IS NULL
        AND t.created_at BETWEEN :start_date AND :end_date
)

SELECT
    category_id,
    ROUND(AVG (response_time_minutes)::numeric, 2) AS average_response_time_minutes,
    COUNT(*) AS num_topics_with_staff_responses
FROM group_response_times
WHERE row_num = 1
GROUP BY category_id

안녕하세요 @simon

직원별 월간 응답 시간 보고서를 생성하는 잠재적 해결책을 공유해 주셔서 감사합니다. 이 코드가 여전히 사용 가능한지, 아니면 업데이트된 버전이 있는지 궁금합니다. 또한, 이 보고서를 월별 대신 주별로 생성할 수 있는 방법이 있을까요? 정말 감사합니다.

아니요. 솔직히 말해서, 이게 어떻게 작동했는지도 잘 모르겠습니다. 곧 업데이트된 버전을 게시하고 준비가 되었다고 알려드리겠습니다.

수정: @IreneT
원래 쿼리의 수정된 버전입니다. 그 쿼리는 무시하고 이 답변에서 게시한 다른 쿼리들을 참고해 주세요. 쿼리에 대해 질문이 있거나 쿼리에 필요한 매개변수를 추가하는 과정에서 문제가 발생하면 알려 주세요. 오늘 테스트해 본 결과, Data Explorer 쿼리를 저장한 후 매개변수 입력 필드가 쿼리 아래에 표시되려면 페이지를 새로고침해야 하는 것 같습니다. (그것은 제 로컬 개발 사이트의 특이한 현상일 수도 있습니다.)

-- [params]
-- int :months_ago = 1

WITH query_period AS (
SELECT
date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' as period_start,
date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' + INTERVAL '1 month' - INTERVAL '1 second' as period_end
),
staff_responses AS (
SELECT
DISTINCT ON (p.topic_id)
p.topic_id,
p.created_at,
t.category_id,
EXTRACT(MINUTE FROM (p.created_at - t.created_at)) AS response_time
FROM posts p
JOIN topics t
ON t.id = p.topic_id
AND t.category_id = ANY ('{46,25,43,40,44,35,22,7,20,17,6,12}'::int[])
JOIN users u
ON u.id = p.user_id
WHERE p.post_number > 1
AND u.admin = 't' OR u.moderator = 't'
ORDER BY p.topic_id, p.created_at
),
user_topics AS (
SELECT
t.id
FROM topics t
JOIN users u
ON u.id = t.user_id
WHERE u.admin = 'f' AND u.moderator = 'f'
)

SELECT
sr.category_id,
AVG(sr.response_time) AS "Average First Response Time",
COUNT(1) AS "Topics Responded to"
FROM staff_responses sr
JOIN query_period qp
ON sr.created_at >= qp.period_start
AND sr.created_at <= qp.period_end
JOIN user_topics t
ON t.id = sr.topic_id
GROUP BY sr.category_id

변경된 부분은 다음과 같습니다:

--DATE_TRUNC('minute', p.created_at - t.created_at) AS response_time
EXTRACT(MINUTE FROM (p.created_at - t.created_at)) AS response_time

테스트 없이 오래된 쿼리를 작성했다면 놀라울 것입니다. 어쨌든, 업데이트된 버전은 이제 예상대로 작동합니다.

기억에 따르면, 이 쿼리는 특정 사이트를 위해 작성된 것이며 메타(Meta)에 게시할 목적으로 작성된 것이 아닙니다. 직원 응답 시간에 대한 정보를 얻기 위한 몇 가지 유용한 쿼리를 더 공유합니다:

주어진 기간 내에 생성된 토픽에 대한 첫 번째 그룹 응답까지의 시간

주어진 그룹의 멤버가 아닌 사용자가 생성한 “일반”(개인 메시지가 아닌) 토픽에 대해, 해당 그룹의 멤버가 첫 번째 응답을 하는 데 걸린 시간을 반환합니다. 쿼리의 :group_name 매개변수는 기본적으로 “staff”(직원)로 설정되어 있습니다. 이 값으로 설정하면 직원들의 첫 번째 응답 시간을 제공합니다. 이 매개변수의 값을 변경하여 다른 그룹의 응답 시간을 확인할 수 있습니다. 예를 들어 “customer_support”(고객 지원) 등으로 설정할 수 있습니다.

날짜는 기술적으로 yyyy-mm-dd 형식으로 제공되어야 하지만, 쿼리는 dd-mm-yyyy 형식의 날짜도 허용합니다.

-- [params]
-- date :start_date
-- date :end_date
-- string :group_name = staff

WITH group_response_times AS (
    SELECT
        t.category_id,
        t.id AS topic_id,
        EXTRACT(MINUTE FROM (p.created_at - t.created_at)) AS response_time_minutes,
        p.user_id AS staff_user_id,
        ROW_NUMBER() OVER (PARTITION BY t.id ORDER BY p.created_at) AS row_num
    FROM posts p
    JOIN topics t ON t.id = p.topic_id
    WHERE t.user_id NOT IN (SELECT user_id
                                FROM group_users gu JOIN groups g ON g.id = gu.group_id
                                WHERE gu.user_id > 0 AND g.name = :group_name)
        AND p.user_id IN (SELECT user_id
                              FROM group_users gu JOIN groups g ON g.id = gu.group_id
                              WHERE gu.user_id > 0 AND g.name = :group_name)
        AND t.archetype = 'regular'
        AND t.deleted_at IS NULL
        AND p.post_type = 1
        AND p.deleted_at IS NULL
        AND t.created_at BETWEEN :start_date AND :end_date
)

SELECT
    category_id,
    topic_id,
    staff_user_id,
    response_time_minutes
FROM group_response_times
WHERE row_num = 1
ORDER BY category_id, response_time_minutes

카테고리별 첫 번째 그룹 응답까지의 평균 시간:

이전 쿼리와 동일한 로직을 사용하지만, :start_date와 :end_date 매개변수로 설정된 기간 내에 주어진 그룹의 멤버가 아닌 사용자가 생성한 토픽에 대해, 카테고리별로 해당 그룹 멤버의 첫 번째 응답까지의 평균 시간을 반환합니다. 이전 쿼리와 마찬가지로, :group_name 매개변수가 기본값인 "staff"로 남아 있으면, 직원 사용자가 아닌 사용자가 생성한 일반 토픽에 대한 직원의 평균 첫 번째 응답 시간을 반환합니다.

-- [params]
-- date :start_date
-- date :end_date
-- string :group_name = staff

WITH group_response_times AS (
    SELECT
        t.category_id,
        t.id AS topic_id,
        EXTRACT(MINUTE FROM (p.created_at - t.created_at)) AS response_time_minutes,
        p.user_id AS staff_user_id,
        ROW_NUMBER() OVER (PARTITION BY t.id ORDER BY p.created_at) AS row_num
    FROM posts p
    JOIN topics t ON t.id = p.topic_id
    WHERE t.user_id NOT IN (SELECT user_id
                                FROM group_users gu JOIN groups g ON g.id = gu.group_id
                                WHERE gu.user_id > 0 AND g.name = :group_name)
        AND p.user_id IN (SELECT user_id
                              FROM group_users gu JOIN groups g ON g.id = gu.group_id
                              WHERE gu.user_id > 0 AND g.name = :group_name)
        AND t.archetype = 'regular'
        AND t.deleted_at IS NULL
        AND p.post_type = 1
        AND p.deleted_at IS NULL
        AND t.created_at BETWEEN :start_date AND :end_date
)

SELECT
    category_id,
    AVG (response_time_minutes) AS average_response_time_minutes,
    COUNT(*) AS num_topics_with_staff_responses
FROM group_response_times
WHERE row_num = 1
GROUP BY category_id

@JammyDodger, OP(원문 게시글)의 쿼리를 이 게시글의 마지막 두 쿼리로 교체하는 것이 좋을 것 같습니다. 또한 제목을 "그룹 멤버의 첫 번째 응답까지의 시간"과 같은 내용으로 업데이트하는 것이 좋겠습니다.

@simon 기대하고 있습니다.

정말 감사합니다.

아니요, 불행히도 이것은 알려진 문제입니다. 다만, 빠른 새로고침으로 해결됩니다. :+1:

완벽해요 @simon. 이번 일로 정말 큰 도움이 됐습니다. 우리 쪽에서 필요한 데이터를 확보하기 위해 조금만 더 조정하면 될 것 같고, 이제 준비가 다 된 것 같아요. 정말 감사드립니다 :heart:

안녕하세요 @simon

Data Explorer를 설치하고 공유해 주신 코드를 사용했습니다. 구체적으로 원하시는 것은 그룹 멤버 이름별로 첫 응답 시간(Time to first response)을 확인하는 것입니다. 그러나 현재 쿼리는 사용자 ID가 아닌 카테고리 ID별로 응답 시간을 그룹화하여 반환하고 있습니다.

도움 주시면 감사하겠습니다. 감사합니다

이 문제를 제기해 주셔서 감사합니다. 제가 게시한 쿼리에 오류가 있었기 때문입니다:

EXTRACT(MINUTE FROM (p.created_at - t.created_at)) AS response_time_minutes

해당 줄은 말 그대로 타임스탬프에서 분(minute)을 추출하고 있습니다. 즉, 토픽이 12:00에 생성되었다가 한 달 후 12:05에 응답이 달렸다면, 쿼리는 응답 시간을 5분으로 계산하고 있었던 것입니다.

수정 방법은 다음과 같습니다:

EXTRACT(EPOCH FROM (p.created_at - t.created_at))/ 60 AS response_time_minutes

날짜 처리는 까다로울 수 있습니다. 원본 글(OP)을 수정할 수 없으므로, 여기에서 수정 사항을 게시한 뒤, 질문하신 내용에 대한 답변은 별도의 글로 올리겠습니다.

@JammyDodger 님, OP에 있던 2개의 쿼리 업데이트 버전을 올립니다:

그룹 멤버의 토픽별 첫 응답까지 걸린 시간

-- [params]
-- date :start_date
-- date :end_date
-- string :group_name = staff

WITH group_response_times AS (
    SELECT
        t.category_id,
        t.id AS topic_id,
        EXTRACT(EPOCH FROM (p.created_at - t.created_at)) / 60 AS response_time_minutes,
        p.user_id AS staff_user_id,
        ROW_NUMBER() OVER (PARTITION BY t.id ORDER BY p.created_at) AS row_num
    FROM posts p
    JOIN topics t ON t.id = p.topic_id
    WHERE t.user_id NOT IN (SELECT user_id
                                FROM group_users gu JOIN groups g ON g.id = gu.group_id
                                WHERE gu.user_id > 0 AND g.name = :group_name)
        AND p.user_id IN (SELECT user_id
                              FROM group_users gu JOIN groups g ON g.id = gu.group_id
                              WHERE gu.user_id > 0 AND g.name = :group_name)
        AND t.archetype = 'regular'
        AND t.deleted_at IS NULL
        AND p.post_type = 1
        AND p.deleted_at IS NULL
        AND t.created_at BETWEEN :start_date AND :end_date
)

SELECT
    category_id,
    topic_id,
    staff_user_id,
    ROUND(response_time_minutes::numeric, 2) AS response_time_minutes
FROM group_response_times
WHERE row_num = 1
ORDER BY category_id, response_time_minutes

카테고리별 그룹의 첫 응답까지 평균 소요 시간

-- [params]
-- date :start_date
-- date :end_date
-- string :group_name = staff

WITH group_response_times AS (
    SELECT
        t.category_id,
        t.id AS topic_id,
        EXTRACT(EPOCH FROM (p.created_at - t.created_at)) / 60 AS response_time_minutes,
        p.user_id AS staff_user_id,
        ROW_NUMBER() OVER (PARTITION BY t.id ORDER BY p.created_at) AS row_num
    FROM posts p
    JOIN topics t ON t.id = p.topic_id
    WHERE t.user_id NOT IN (SELECT user_id
                                FROM group_users gu JOIN groups g ON g.id = gu.group_id
                                WHERE gu.user_id > 0 AND g.name = :group_name)
        AND p.user_id IN (SELECT user_id
                              FROM group_users gu JOIN groups g ON g.id = gu.group_id
                              WHERE gu.user_id > 0 AND g.name = :group_name)
        AND t.archetype = 'regular'
        AND t.deleted_at IS NULL
        AND p.post_type = 1
        AND p.deleted_at IS NULL
        AND t.created_at BETWEEN :start_date AND :end_date
)

SELECT
    category_id,
    ROUND(AVG (response_time_minutes)::numeric, 2) AS average_response_time_minutes,
    COUNT(*) AS num_topics_with_staff_responses
FROM group_response_times
WHERE row_num = 1
GROUP BY category_id

질문자님이 그룹 멤버별 평균 첫 응답 시간을 원하시는 것으로 추정됩니다. 만약 단순히 그룹 멤버들이 개별 주제(topic)에 응답하는 데 걸린 시간만 원하신다면, OP(원문 게시글)의 첫 번째 쿼리를 수정한 아래 버전을 사용하세요:

-- [params]
-- date :start_date
-- date :end_date
-- string :group_name = staff

WITH group_response_times AS (
    SELECT
        t.category_id,
        t.id AS topic_id,
        EXTRACT(EPOCH FROM (p.created_at - t.created_at))/ 60 AS response_time_minutes,
        p.user_id AS staff_user_id,
        ROW_NUMBER() OVER (PARTITION BY t.id ORDER BY p.created_at) AS row_num
    FROM posts p
    JOIN topics t ON t.id = p.topic_id
    WHERE t.user_id NOT IN (SELECT user_id
                                FROM group_users gu JOIN groups g ON g.id = gu.group_id
                                WHERE gu.user_id > 0 AND g.name = :group_name)
        AND p.user_id IN (SELECT user_id
                              FROM group_users gu JOIN groups g ON g.id = gu.group_id
                              WHERE gu.user_id > 0 AND g.name = :group_name)
        AND t.archetype = 'regular'
        AND t.deleted_at IS NULL
        AND p.post_type = 1
        AND p.deleted_at IS NULL
        AND t.created_at BETWEEN :start_date AND :end_date
)

SELECT
    category_id,
    topic_id,
    staff_user_id,
    ROUND(response_time_minutes::numeric, 2) AS response_time_minutes
FROM group_response_times
WHERE row_num = 1
ORDER BY category_id, response_time_minutes

이 쿼리는 그룹 멤버들이 개별 주제에 응답하는 데 걸린 시간을 알려줍니다. 결과는 카테고리별로 정리되어 있지만, 카테고리는 무시하셔도 됩니다.

개별 그룹 멤버에 대한 평균 응답 시간 데이터가 얼마나 의미 있는지 확신할 수 없습니다. 어려운 질문이나 다른 그룹 멤버들이 무시한 주제에 응답하는 그룹 멤버들에게 불리하게 작용할 수 있으므로, 이를任何形式的 성과 평가에 사용하는 것은 신중해야 합니다. 이러한 점을 고려하여, 그룹 멤버들의 평균 응답 시간과 그들이 그룹 내에서 첫 번째로 응답한 주제의 수를 반환하는 쿼리를 아래에 제공합니다:

-- [params]
-- date :start_date
-- date :end_date
-- string :group_name = staff

WITH group_response_times AS (
    SELECT
        t.category_id,
        t.id AS topic_id,
        EXTRACT(EPOCH FROM (p.created_at - t.created_at))/ 60 AS response_time_minutes,
        p.user_id AS staff_user_id,
        ROW_NUMBER() OVER (PARTITION BY t.id ORDER BY p.created_at) AS row_num
    FROM posts p
    JOIN topics t ON t.id = p.topic_id
    WHERE t.user_id NOT IN (SELECT user_id
                                FROM group_users gu JOIN groups g ON g.id = gu.group_id
                                WHERE gu.user_id > 0 AND g.name = :group_name)
        AND p.user_id IN (SELECT user_id
                              FROM group_users gu JOIN groups g ON g.id = gu.group_id
                              WHERE gu.user_id > 0 AND g.name = :group_name)
        AND t.archetype = 'regular'
        AND t.deleted_at IS NULL
        AND p.post_type = 1
        AND p.deleted_at IS NULL
        AND t.created_at BETWEEN :start_date AND :end_date
)

SELECT
    staff_user_id,
    ROUND(AVG(response_time_minutes)::numeric, 2) AS average_response_time_minutes,
    COUNT(*) AS number_of_topics_responded_to
FROM group_response_times
WHERE row_num = 1
GROUP BY staff_user_id
ORDER BY average_response_time_minutes

@simon 감사합니다. 항상처럼 매우 유용하고 빠른 답변에 진심으로 감사드립니다!

사실 저희의 목표는 코치들(그룹 중 하나에 추가된)의 평균 응답 시간을 측정하는 것입니다. 이를 통해 디스코urs에서 커뮤니티 지원을 얼마나 잘 제공하고 있는지, 그리고 장기적으로 멤버들의 만족도를 어떻게 유지하고 있는지에 대한 기준점을 확보하고자 합니다.

그렇군요. 올바르게 해석된다면 흥미롭거나 유용할 수 있는 데이터가 정말 많습니다. Data Explorer 쿼리를 사용할 때, 성능 보고서를 생성하는 데 사용되는 쿼리를 작성하고 있는 것은 아닌지 가끔 걱정됩니다.

저는 고객 지원 업무를 많이 해왔습니다. 최초 응답 시간(time to first response)은 유용한 지표라고 생각하지만, 응답의 품질도 반드시 고려해야 합니다.

이 주제에서 다루는 쿼리들이 알려주지 않는 한 가지 사항은 그룹 멤버로부터 응답을 받지 못한 주제에 대한 것입니다. 해당 쿼리들은 응답이 있는 주제에 대한 데이터만 반환하고 있습니다. 응답이 없는 주제에 대한 정보를 쿼리에 어떻게 가장 잘 추가할 수 있을지 잘 모르겠습니다.

@simon 그 말이 맞네요.

그 정보를 공유해 주셔서 감사합니다. 아마도 이 지표(첫 응답까지의 시간)는 항상 "응답이 없는 주제"에 대한 다른 보고서와 함께 제공되어야 할 것 같습니다.

이 보고서에서 값을 클릭했을 때, 해당 "응답이 없는 주제"의 요약을 보여주는 링크로 연결될 수 있을까요? 그렇게 하면 지정된 시간 내에 응답이 필요한 모든 주제에 응답할 수 있는지 확인할 수 있고, 실제로는 전혀 응답이 필요하지 않은 주제들은 제외할 수 있을 것입니다.

그렇지 않습니다. 답변이 없는 주제에 대한 링크를 얻으려면 Data Explorer 쿼리를 사용하는 것이 가장 좋은 방법일 수 있습니다. Meta에 해당 쿼리의 예시가 있을 수도 있지만, 검색을 해봐도 찾지 못하고 있습니다.

다른 옵션으로는 Unanswered Filter 테마 구성 요소를 설치하는 것입니다: Unanswered Filter. 이 구성 요소는 사이트의 내비게이션 메뉴에 드롭다운 항목을 추가하여 주제 목록을 답변이 없는 주제만 표시하도록 필터링할 수 있게 해줍니다.

@simon 정말 감사합니다. 항상 감사드립니다 :heart: