Статистика решенных и нерешенных тем с параметрами даты и тега

Этот отчет Data Explorer предоставляет комплексный анализ решённых и нерешённых тем на сайте за указанный период времени и с возможностью фильтрации по конкретному тегу.

:discourse: Для работы этого отчёта необходимо включить плагин Discourse Solved.

Этот отчёт особенно полезен для администраторов и модераторов, которые хотят оценить оперативность работы сообщества и выявить области для улучшения поддержки пользователей и вовлечённости.

Статистика решённых и нерешённых тем с параметрами даты и тега

--[params]
-- date :start_date = 2022-01-01
-- date :end_date = 2024-01-01
-- text :tag_name = all

WITH valid_topics AS (
    SELECT 
        t.id,
        t.user_id,
        t.title,
        t.views,
        (SELECT COUNT(*) FROM posts WHERE topic_id = t.id AND deleted_at IS NULL AND post_type = 1) - 1 AS "posts_count", 
        t.created_at,
        (CURRENT_DATE::date - t.created_at::date) AS "total_days",
        STRING_AGG(tags.name, ', ') AS tag_names, -- Агрегация тегов для каждой темы
        c.name AS category_name
    FROM topics t
    LEFT JOIN topic_tags tt ON tt.topic_id = t.id
    LEFT JOIN tags ON tags.id = tt.tag_id
    LEFT JOIN categories c ON c.id = t.category_id
    WHERE t.deleted_at IS NULL
        AND t.created_at::date BETWEEN :start_date AND :end_date
        AND t.archetype = 'regular'
    GROUP BY t.id, c.name
),

solved_topics AS (
    SELECT 
        vt.id,
        dsst.created_at
    FROM discourse_solved_solved_topics dsst
    INNER JOIN valid_topics vt ON vt.id = dsst.topic_id
),

last_reply AS (
    SELECT p.topic_id, p.user_id FROM posts p
    INNER JOIN (SELECT topic_id, MAX(id) AS post FROM posts p
                WHERE deleted_at IS NULL
                AND post_type = 1
                GROUP BY topic_id) x ON x.post = p.id
),

first_reply AS (
    SELECT p.topic_id, p.user_id, p.created_at FROM posts p
    INNER JOIN (SELECT topic_id, MIN(id) AS post FROM posts p
                WHERE deleted_at IS NULL
                AND post_type = 1
                AND post_number > 1
                GROUP BY topic_id) x ON x.post = p.id
)

SELECT
    CASE 
        WHEN st.id IS NOT NULL THEN 'solved'
        ELSE 'unsolved'
    END AS status,
    vt.tag_names, 
    vt.category_name,
    vt.id AS topic_id,
    vt.user_id AS topic_user_id,
    ue.email,
    vt.title,
    vt.views,
    lr.user_id AS last_reply_user_id,
    ue2.email AS last_reply_user_email,
    vt.created_at::date AS topic_create,
    COALESCE(TO_CHAR(fr.created_at, 'YYYY-MM-DD'), '') AS first_reply_create,
    COALESCE(TO_CHAR(st.created_at, 'YYYY-MM-DD'), '') AS solution_create,
    COALESCE(fr.created_at::date - vt.created_at::date, 0) AS "time_first_reply(days)",
    COALESCE(CEIL(EXTRACT(EPOCH FROM (fr.created_at - vt.created_at)) / 3600.00), 0) AS "time_first_reply(hours)",
    COALESCE(st.created_at::date - vt.created_at::date, 0) AS "time_solution(days)",
    COALESCE(CEIL(EXTRACT(EPOCH FROM (st.created_at - vt.created_at)) / 3600.00), 0) AS "time_solution(hours)",
    vt.created_at::date,
    vt.posts_count AS number_of_replies,
    vt.total_days AS total_days_without_solution
FROM valid_topics vt
LEFT JOIN last_reply lr ON lr.topic_id = vt.id
LEFT JOIN first_reply fr ON fr.topic_id = vt.id
LEFT JOIN solved_topics st ON st.id = vt.id
INNER JOIN user_emails ue ON vt.user_id = ue.user_id AND ue."primary" = true
LEFT JOIN user_emails ue2 ON lr.user_id = ue2.user_id AND ue2."primary" = true
WHERE (:tag_name = 'all' OR vt.tag_names ILIKE '%' || :tag_name || '%')
GROUP BY st.id, vt.tag_names, vt.category_name, vt.id, vt.user_id, ue.email, vt.title, vt.views, lr.user_id, ue2.email, vt.created_at, fr.created_at, st.created_at, vt.posts_count, vt.total_days
ORDER BY topic_create, vt.total_days DESC

Пояснение к SQL-запросу

Отчёт генерируется с помощью сложного SQL-запроса, использующего общие табличные выражения (CTE) для эффективной организации и обработки данных. Запрос структурирован следующим образом:

  • valid_topics: Это CTE фильтрует темы по указанному диапазону дат и архетипу (‘regular’), исключая удалённые темы. Также оно агрегирует теги, связанные с каждой темой, для последующей фильтрации по имени тега, если оно указано.
  • solved_topics: Определяет темы, помеченные как решённые.
  • last_reply: Определяет пользователя, сделавшего последний ответ в каждой теме, находя максимальный ID сообщения (указывающий на самое последнее сообщение), которое не удалено и имеет тип сообщения 1 (обычное сообщение).
  • first_reply: Аналогично last_reply, но определяет первого пользователя, ответившего на тему после исходного сообщения.

Основной запрос объединяет эти CTE для формирования детального отчёта по каждой теме, включая статус (решена или нет), имена тегов, название категории, идентификаторы темы и пользователя, адреса электронной почты, количество просмотров, количество ответов и временные метки первого ответа и решения.

Параметры

  • start_date: Начало диапазона дат для генерации отчёта.
  • end_date: Конец диапазона дат для генерации отчёта.
  • tag_name: Конкретный тег для фильтрации тем. Используйте ‘all’, чтобы включить темы с любым тегом.

Результаты

Отчёт предоставляет следующую информацию по каждой теме в рамках указанных параметров:

  • status: Указывает, решена ли тема или остаётся нерешённой.
  • tag_names: Показывает теги, связанные с темой.
  • category_name: Показывает категорию, связанную с темой.
  • topic_id: Уникальный идентификатор темы.
  • topic_user_id: Идентификатор пользователя, создавшего тему.
  • user_email: Адрес электронной почты создателя темы.
  • title: Заголовок темы.
  • views: Количество просмотров темы.
  • last_reply_user_id: Идентификатор пользователя, сделавшего последний ответ в теме.
  • last_reply_user_email: Адрес электронной почты пользователя, сделавшего последний ответ.
  • topic_create: Дата создания темы.
  • first_reply_create: Дата первого ответа на тему.
  • solution_create: Дата помеченного решения для темы (если применимо).
  • time_first_reply(days/hours): Время до получения первого ответа в днях и часах.
  • time_solution(days/hours): Время до решения темы в днях и часах.
  • created_at: Дата создания темы.
  • number_of_replies: Общее количество ответов на тему.
  • total_days_without_solution: Общее количество дней, в течение которых тема была активна без решения.

Пример результатов

status tag_names category_name topic_id topic_user_id user_email title views last_reply_user_id last_reply_user_email topic_create first_reply_create solution_create time_first_reply(days) time_first_reply(hours) time_solution(days) time_solution(hours) created_at number_of_replies total_days_without_solution
solved support, password category1 101 1 user1@example.com Как сбросить пароль? 150 3 user3@example.com 2022-01-05 2022-01-06 2022-01-07 1 24 2 48 2022-01-05 5 2
unsolved support, account category2 102 2 user2@example.com Проблема с активацией аккаунта 75 4 user4@example.com 2022-02-10 2022-02-12 2 48 0 0 2022-02-10 3 412
solved support category3 103 5 user5@example.com Не могу загрузить фото профиля 200 6 user6@example.com 2022-03-15 2022-03-16 2022-03-18 1 24 3 72 2022-03-15 8 3
unsolved NULL category4 104 7 user7@example.com Ошибка при публикации 50 8 user8@example.com 2022-04-20 0 0 0 0 2022-04-20 0 373

Ещё один отличный запрос и ещё одна просьба от меня. :slight_smile:

Можешь добавить поле выбора для фильтрации по категории/подкатегории?
Я бы хотел иметь возможность запускать этот отчёт только по категории «Мои заявки».

Кроме того, я обнаружил необычный пограничный случай. Вы, возможно, сможете его учесть, а возможно, и нет, но спросить не помешает.

У меня есть тема, на которую я ответил и отметил ответ как решение на следующий день после публикации. Затем другой техник дал иной ответ и отметил его как решение примерно через 10 дней.

В отчёте время до решения указано как 1 день, а общее время без решения — 10 дней.

PNG image

Привет @tknospdr,

Отвечаю здесь на оба ваших вопроса:

Для решения этой задачи можно использовать следующий запрос:

--[params]
-- date :start_date = 2022-01-01
-- date :end_date = 2024-01-01
-- text :tag_name = all
-- null category_id :category_id

WITH valid_topics AS (
    SELECT 
        t.id,
        t.user_id,
        t.title,
        t.views,
        (SELECT COUNT(*) FROM posts WHERE topic_id = t.id AND deleted_at IS NULL AND post_type = 1) - 1 AS "posts_count", 
        t.created_at,
        (CURRENT_DATE::date - t.created_at::date) AS "total_days",
        STRING_AGG(tags.name, ', ') AS tag_names,
        c.name AS category_name,
        t.category_id
    FROM topics t
    LEFT JOIN topic_tags tt ON tt.topic_id = t.id
    LEFT JOIN tags ON tags.id = tt.tag_id
    LEFT JOIN categories c ON c.id = t.category_id
    WHERE t.deleted_at IS NULL
        AND t.created_at::date BETWEEN :start_date AND :end_date
        AND t.archetype = 'regular'
    GROUP BY t.id, c.name, t.category_id
),

solved_topics AS (
    SELECT 
        dsst.topic_id,
        MIN(dsst.created_at) AS first_solution_at, -- Получаем самое раннее решение
        MAX(dsst.created_at) AS latest_solution_at -- Получаем самое позднее решение
    FROM discourse_solved_solved_topics dsst
    GROUP BY dsst.topic_id
),

last_reply AS (
    SELECT p.topic_id, p.user_id FROM posts p
    INNER JOIN (SELECT topic_id, MAX(id) AS post FROM posts p
                WHERE deleted_at IS NULL
                AND post_type = 1
                GROUP BY topic_id) x ON x.post = p.id
),

first_reply AS (
    SELECT p.topic_id, p.user_id, p.created_at FROM posts p
    INNER JOIN (SELECT topic_id, MIN(id) AS post FROM posts p
                WHERE deleted_at IS NULL
                AND post_type = 1
                AND post_number > 1
                GROUP BY topic_id) x ON x.post = p.id
)

SELECT
    CASE 
        WHEN st.topic_id IS NOT NULL THEN 'solved'
        ELSE 'unsolved'
    END AS status,
    vt.tag_names, 
    vt.category_name,
    vt.id AS topic_id,
    vt.user_id AS topic_user_id,
    ue.email,
    vt.title,
    vt.views,
    lr.user_id AS last_reply_user_id,
    ue2.email AS last_reply_user_email,
    vt.created_at::date AS topic_create,
    COALESCE(TO_CHAR(fr.created_at, 'YYYY-MM-DD'), '') AS first_reply_create,
    COALESCE(TO_CHAR(st.first_solution_at, 'YYYY-MM-DD'), '') AS first_solution_create,
    COALESCE(TO_CHAR(st.latest_solution_at, 'YYYY-MM-DD'), '') AS latest_solution_create,
    COALESCE(fr.created_at::date - vt.created_at::date, 0) AS "time_first_reply(days)",
    COALESCE(CEIL(EXTRACT(EPOCH FROM (fr.created_at - vt.created_at)) / 3600.00), 0) AS "time_first_reply(hours)",
    COALESCE(st.first_solution_at::date - vt.created_at::date, 0) AS "time_to_first_solution(days)",
    COALESCE(CEIL(EXTRACT(EPOCH FROM (st.first_solution_at - vt.created_at)) / 3600.00), 0) AS "time_to_first_solution(hours)",
    COALESCE(st.latest_solution_at::date - vt.created_at::date, 0) AS "time_to_latest_solution(days)",
    COALESCE(CEIL(EXTRACT(EPOCH FROM (st.latest_solution_at - vt.created_at)) / 3600.00), 0) AS "time_to_latest_solution(hours)",
    vt.created_at::date,
    vt.posts_count AS number_of_replies,
    CASE
        WHEN st.topic_id IS NULL THEN vt.total_days
        ELSE COALESCE(st.latest_solution_at::date - vt.created_at::date, 0)
    END AS total_days_without_solution
FROM valid_topics vt
LEFT JOIN last_reply lr ON lr.topic_id = vt.id
LEFT JOIN first_reply fr ON fr.topic_id = vt.id
LEFT JOIN solved_topics st ON st.topic_id = vt.id
INNER JOIN user_emails ue ON vt.user_id = ue.user_id AND ue."primary" = true
LEFT JOIN user_emails ue2 ON lr.user_id = ue2.user_id AND ue2."primary" = true
WHERE (:tag_name = 'all' OR vt.tag_names ILIKE '%' || :tag_name || '%')
  AND (:category_id ISNULL OR vt.category_id = :category_id)
GROUP BY st.topic_id, st.first_solution_at, st.latest_solution_at, vt.tag_names, vt.category_name, vt.id, vt.user_id, ue.email, vt.title, vt.views, lr.user_id, ue2.email, vt.created_at, fr.created_at, vt.posts_count, vt.total_days
ORDER BY topic_create, vt.total_days DESC

Параметр -- null category_id :category_id можно использовать для (опционального) выбора категории, для которой запускается отчёт, а в результатах отслеживаются как первое, так и последнее решения.

Кроме того, значение total_days_without_solution теперь будет использовать дату последнего решения вместо даты первого.

Отлично, спасибо! Выглядит здорово.