Случайный выбор строк с взвешенными фильтрами в SQL / PostgreSQL - PullRequest
0 голосов
/ 05 июля 2018

У меня есть таблица вопросов, и мне нужно получить X вопросов для подготовки теста. Вопросы должны быть отфильтрованы по нескольким критериям (предмет, учреждение, область и т. Д.), Каждый из которых имеет разный вес.

Вес фильтров динамически устанавливается и нормализуется вне запроса. Ex.:

  1. Тема 1 - 0,4
  2. Тема 2 - 0,1
  3. Тема 3 - 0,5
  4. Учреждение 1 - 0,2
  5. Учреждение 2 - 0,04
  6. Учреждение 3 - 0,76
  7. Площадь 1 - 1

Некоторые другие пункты:

  • Сегодня у меня есть 10 различных фильтров (тема, учебное заведение, область и т. Д.), Но пользователь может выбрать несколько и смешанные методы (например, 10 предметов, 5 учебных заведений, 30 областей и т. Д.), Например: в приведенном выше примере.
  • Таблица вопросов содержит ~ 500 тыс. Строк;
  • Фильтры N - N с вопросами;
  • После фильтрации я хочу ограничить количество возвращаемых строк;
  • Если какой-то фильтр не может предложить больше вопросов, нужно рассмотреть другие (помните: я хочу подготовить тест - если у меня остались вопросы, их нужно использовать)
  • Я очень обеспокоен выполнением этого запроса.

Для иллюстрации, если бы я не хотел взвешивать фильтры, я бы сделал что-то вроде этого:

SELECT
    *
FROM
    public.questions q
    INNER JOIN public.subjects_questions sq ON q.id = sq.question_id
    INNER JOIN public.subjects s ON s.id = sq.subject_id
    INNER JOIN public.institutions_questions iq ON iq.question_id = q.id
    INNER JOIN public.institutions i ON i.id = iq.institution_id
    INNER JOIN public.areas_questions aq ON aq.question_id = q.id
    INNER JOIN public.areas a ON a.id = aq.area_id
WHERE
    s.id IN :subjects
    AND a.id IN :areas
    AND i.id IN :institutions
ORDER BY
    random() limit 200

Желаемый вывод:

Question — Subject — Institution — Area

Я думал что-то вроде:

  1. Создать CTE с вопросами, возвращаемыми фильтром; необходимо учитывать, что один и тот же вопрос может быть возвращен более чем одним фильтром - нужно ли мне оценивать каждый фильтр отдельно и затем UNION ALL, чтобы решить эту проблему? Нужно также указать, из какого фильтра возник вопрос;
  2. Создайте еще один CTE с весами и соответствующим фильтром;
  3. ПРИСОЕДИНЯЙТЕСЬ К CTE, но на этом этапе вопросы должны быть сгруппированы, а веса суммированы;
  4. Примените оконную функцию и верните результаты, ограниченные X строками (LIMIT X).

Как бы вы написали такой запрос / решили эту проблему?

Ответы [ 2 ]

0 голосов
/ 06 июля 2018

Хорошо. Успел ее решить. В основном, использовали стратегию, уже описанную в этом вопросе, и небольшую помощь от здесь - я уже видел этот пост раньше, но я пытался (и до сих пор пытаюсь решить) более элегантным способом - что-то вроде это , но для нескольких строк - не нужно создавать «границы» вручную.

Давайте попробуем пошагово:

Поскольку фильтры с весами поступают извне схемы, давайте создадим CTE:

WITH filters (type, id, weight) AS (
    SELECT 'subject', '148232e0-dece-40d9-81e0-0fa675f040e5'::uuid, 0.5
    UNION SELECT 'subject', '854431bb-18ee-4efb-803f-185757d25235'::uuid, 0.4
    UNION SELECT 'area', 'e12863fb-afb7-45cf-9198-f9f58ebc80cf'::uuid, 1
    UNION SELECT 'institution', '7f56c89f-705e-45c7-98fb-fee470550edf'::uuid, 0.5
    UNION SELECT 'institution', '0066257b-b2e3-4ee8-8075-517a2aa1379e'::uuid, 0.5
)

Теперь давайте отфильтруем строки, игнорируя вес (пока), поэтому позже нам не нужно работать со всей таблицей:

WITH filtered_questions AS (
    SELECT
        q.id,
        s.id subject_id,
        a.id area_id,
        i.id institution_id
    FROM
        public.questions q
        INNER JOIN public.subjects_questions sq ON q.id = sq.question_id
        INNER JOIN public.subjects s ON s.id = sq.subject_id
        INNER JOIN public.institutions_questions iq ON iq.question_id = q.id
        INNER JOIN public.institutions i ON i.id = iq.institution_id
        INNER JOIN public.areas_questions aq ON aq.question_id = q.id
        INNER JOIN public.areas a ON a.id = aq.area_id
    WHERE
        subject_id IN (SELECT id from filters where type = 'subject')
        and institution_id IN (SELECT id from filters where type = 'institution')
        and area_id IN (SELECT id from filters where type = 'area')
)

Один и тот же вопрос может быть выбран несколькими фильтрами, что увеличивает вероятность его выбора. Мы должны обновить веса, чтобы решить эту проблему.

WITH filtered_questions_weights_sum AS (
    SELECT
        q.id,
        SUM(filters.weight) weight_sum
    FROM filtered_questions q
    INNER JOIN filters
    ON (filters.type = 'subject' AND q.subject_id IN(filters.id))
    OR (filters.type = 'area' AND q.area_id IN(filters.id))
    OR (filters.type = 'institution' AND q.institution_id IN(filters.id))
    GROUP BY q.id
)

Генерация границ, как выставлено здесь .

WITH cumulative_prob AS (
    SELECT
        id,
        SUM(weight_sum) OVER (ORDER BY id) AS cum_prob
    FROM filtered_questions_weights_sum
),
cumulative_bounds AS (
    SELECT
        id,
        COALESCE( lag(cum_prob) OVER (ORDER BY cum_prob, id), 0 ) AS lower_cum_bound,
        cum_prob AS upper_cum_bound
    FROM cumulative_prob
)

Генерация случайного ряда. Пришлось перенормировать (random() * (SELECT SUM(weight_sum)), потому что веса были обновлены на предыдущем шаге. 10 - это количество строк, которые мы хотим вернуть.

WITH random_series AS (
    SELECT generate_series (1,10),random() * (SELECT SUM(weight_sum) FROM filtered_questions_weights_sum) AS R
)

И наконец:

SELECT
      id, lower_cum_bound, upper_cum_bound, R
FROM random_series
JOIN cumulative_bounds
ON R::NUMERIC <@ numrange(lower_cum_bound::NUMERIC, upper_cum_bound::NUMERIC, '(]')

И мы получаем следующее распределение:

id                                   lower_cum_bound upper_cum_bound r                   
------------------------------------ --------------- --------------- ------------------- 
380f46e9-f373-4b89-a863-05f484e6b3b6 0               2.0             0.41090718149207534 
42bcb088-fc19-4272-8c49-e77999edd01c 2.0             3.9             3.4483200465794654  
46a97f1d-789f-46e7-9d3b-bd881a22a32e 3.9             5.9             5.159445870062337   
46a97f1d-789f-46e7-9d3b-bd881a22a32e 3.9             5.9             5.524481557868421   
972d0296-acc3-4b44-b67d-928049d5e9c2 5.9             7.8             6.842470594821498   
bdcc26f7-ccaf-4f8f-9e0b-81b9a6d29cdb 11.6            13.5            12.207371663767844  
bdcc26f7-ccaf-4f8f-9e0b-81b9a6d29cdb 11.6            13.5            12.674184153741226  
c935e3de-f1b6-4399-b5eb-ed3a9194eb7b 15.5            17.5            17.16804686235264   
e5061aeb-53b7-4247-8404-87508c5ac723 21.4            23.4            22.622627633158118  
f8c37700-0c3a-457e-8882-7c65269482ea 25.4            27.3            26.841821723571048  

Собираем все вместе:

WITH filters (type, id, weight) AS (
        SELECT 'subject', '148232e0-dece-40d9-81e0-0fa675f040e5'::uuid, 0.5
        UNION SELECT 'subject', '854431bb-18ee-4efb-803f-185757d25235'::uuid, 0.4
        UNION SELECT 'area', 'e12863fb-afb7-45cf-9198-f9f58ebc80cf'::uuid, 1
        UNION SELECT 'institution', '7f56c89f-705e-45c7-98fb-fee470550edf'::uuid, 0.5
        UNION SELECT 'institution', '0066257b-b2e3-4ee8-8075-517a2aa1379e'::uuid, 0.5
        )
    ,
    filtered_questions AS
    (
        SELECT
            q.id,
            SUM(filters.weight) weight_sum
        FROM
        public.questions q
        INNER JOIN public.subjects_questions sq ON q.id = sq.question_id
        INNER JOIN public.subjects s ON s.id = sq.subject_id
        INNER JOIN public.institutions_questions iq ON iq.question_id = q.id
        INNER JOIN public.institutions i ON i.id = iq.institution_id
        INNER JOIN public.activity_areas_questions aq ON aq.question_id = q.id
        INNER JOIN public.activity_areas a ON a.id = aq.activity_area_id
        INNER JOIN filters
            ON (filters.type = 'subject' AND s.id IN(filters.id))
            OR (filters.type = 'area' AND a.id IN(filters.id))
            OR (filters.type = 'institution' AND i.id IN(filters.id))
        WHERE
            s.id IN (SELECT id from filters where type = 'subject')
            and i.id IN (SELECT id from filters where type = 'institution')
            and a.id IN (SELECT id from filters where type = 'area')
        GROUP BY q.id
    )
    ,
    cumulative_prob AS (
        SELECT
            id,
            SUM(weight_sum) OVER (ORDER BY id) AS cum_prob
        FROM filtered_questions
    )
    ,
    cumulative_bounds AS (
        SELECT
            id,
            COALESCE( lag(cum_prob) OVER (ORDER BY cum_prob, id), 0 ) AS lower_cum_bound,
            cum_prob AS upper_cum_bound
        FROM cumulative_prob
    )
    ,
    random_series AS
    (
        SELECT generate_series (1,14),random() * (SELECT SUM(weight_sum) FROM filtered_questions) AS R
    )
SELECT id, lower_cum_bound, upper_cum_bound, R
FROM random_series
JOIN cumulative_bounds
ON R::NUMERIC <@ numrange(lower_cum_bound::NUMERIC, upper_cum_bound::NUMERIC, '(]')
0 голосов
/ 05 июля 2018

Как насчет этого? Это просто для демонстрации идеи, я оставлю детали до вас. Если вы не знакомы с этим методом случайного выбора, если вы случайным образом генерируете число от 0 до 1, вероятность того, что он окажется ниже 0,4, будет 40%. Поэтому rand () <= .4 вернет true в 40% случаев. </p>

Предположим, у вас есть или вы можете создать сущность "Фильтры", которая выглядит примерно так

CREATE TABLE Filters
  ( FieldName VARCHAR(100), 
    FieldValue VARCHAR(100),
    Prob Float -- probability of selection based on Name and Value
  );

SELECT DISTINCT TMP.* -- The fields you want. Distinct needed to get rid of 
                      -- records which pass multiple conditions.
  FROM (SELECT YRSWF.*,
               RAND() AS rnd
          FROM YourResultSetWithoutFilters YRSWF -- You can code the details
       ) TMP  
 INNER
  JOIN Filters F
    ON (
       TMP.Subject = F.FieldValue
   AND F.FieldName = 'Subject'
   AND TMP.rnd <= F.prob
       )
    OR (
       TMP.Institution = F.FieldValue
   AND F.FieldName = 'Institution'
   AND TMP.rnd <= F.prob
       )
    OR ( 
       TMP.Area = F.FieldValue
   AND F.FieldName = 'Area'
   AND TMP.rnd <= F.prob
       );
...