У меня есть следующий пример структуры данных клиента, который может быть частью нескольких групп, используя соединительную таблицу и данные:
CREATE TABLE customer (
id INT NOT NULL
);
CREATE TABLE groups (
id INT NOT NULL
);
CREATE TABLE customers_to_groups (
id serial,
group_id INT,
customer_id INT
);
INSERT INTO customer(id) VALUES(0);
INSERT INTO customer(id) VALUES(1);
INSERT INTO customer(id) VALUES(2);
INSERT INTO customer(id) VALUES(3);
INSERT INTO groups(id) VALUES(1);
INSERT INTO groups(id) VALUES(3);
INSERT INTO groups(id) VALUES(5);
INSERT INTO groups(id) VALUES(6);
INSERT INTO customers_to_groups(customer_id, group_id) VALUES(0, 1);
INSERT INTO customers_to_groups(customer_id, group_id) VALUES(0, 5);
INSERT INTO customers_to_groups(customer_id, group_id) VALUES(1, 1);
INSERT INTO customers_to_groups(customer_id, group_id) VALUES(1, 90);
INSERT INTO customers_to_groups(customer_id, group_id) VALUES(2, 1);
INSERT INTO customers_to_groups(customer_id, group_id) VALUES(3, 3);
INSERT INTO customers_to_groups(customer_id, group_id) VALUES(3, 5);
INSERT INTO customers_to_groups(customer_id, group_id) VALUES(3, 90);
Мне нужно получить клиентов, у которых есть определенные группы, частью которых они являются, и мне нужно получить список всех клиентов, которые входят хотя бы в одну группу, в нескольких списках групп. Например, я хочу получить всех клиентов, которые находятся в группе [5 OR 6] AND [1 OR 3]
, поэтому, например, клиент из группы 5 и 1 будет возвращен, а кто-то из группы 1 и 90 или просто группы 1 — нет. С помощью предоставленных образцов данных мы получим клиентов с идентификаторами 0 и 3 только в том случае, если они соответствуют приведенным выше правилам.
Просто сделать WHERE group_id IN (5,6) AND group_id IN (1,3)
, кажется, не работает, поэтому я ищу альтернативу.
Я получил это до сих пор, что работает:
SELECT DISTINCT c.id
FROM customer c
INNER JOIN customers_to_groups at1 ON c.id = at1.customer_id
INNER JOIN customers_to_groups at2 ON c.id = at2.customer_id
WHERE at1.group_id IN (5, 6)
AND at2.group_id IN (1, 3);
Ожидаемые результаты:
Есть ли способ сделать это более производительным?
Добавил больше контекста ^^
Выборочные данные — это здорово, но можете ли вы также указать ожидаемый результат?
Обновил его там, поэтому с новым примером данных я получу 2 строки: одну с идентификатором клиента 0 и одну с клиентом 3, поскольку клиент 0 является частью группы 5 и 1, а клиент 3 является частью 3 и 5. Клиенты может быть дополнительная группа, мы просто игнорируем их здесь, как у клиента 3, имеющего дополнительную группу 7
Можете ли вы отредактировать и добавить результат в виде правильно отформатированного текста? (Строки и столбцы, вы знаете.)
Готово @jarlh Спасибо, что терпели меня ^^
Отличный пример кода, но я думаю, что вы используете group_id 6 и 7 взаимозаменяемо.
Итак, в примере я использовал группу 7 в предложении, чтобы показать, что ее нет ни в одном из двух списков. Я мог бы выбрать 20, 90 или 8. Я выбрал 7 наугад, возможно, не лучший из-за близости 7.
В этом примере у нас есть клиент 3, входящий в группу 3, 5, 7, и он должен вернуть этого клиента, поскольку этот клиент находится в группе 3 или 1, а также является частью групп 5 или 6. Меня не волнуют дополнительные группы. в этом случае.
Я могу обновить вопрос, указав другой номер, чтобы его было легче увидеть. Позвольте мне сделать это, обновлю до 90
Желаемого результата можно добиться с помощью более оптимизированного запроса, используя предложения GROUP BY и HAVING. Этот подход позволяет избежать необходимости многократного самосоединения.
SELECT customer_id
FROM customers_to_groups
WHERE group_id IN (5, 6, 1, 3)
GROUP BY customer_id
HAVING COUNT(DISTINCT CASE WHEN group_id IN (5, 6) THEN 1 END) > 0
AND COUNT(DISTINCT CASE WHEN group_id IN (1, 3) THEN 1 END) > 0;
Рассмотрение индекса: Чтобы еще больше повысить производительность, убедитесь, что у вас есть индекс для столбцов customer_id и group_id в таблице customer_to_groups:
CREATE INDEX idx_customer_group ON customers_to_groups (customer_id, group_id);
Мы можем GROUP BY
идентификатор клиента и использовать предложение HAVING
. Там мы можем использовать CASE
или FILTER
, если ваша СУБД поддерживает это. Постгрес должен.
Там вам будут поставлены условия.
Запрос будет:
SELECT c.id
FROM customer c
INNER JOIN customers_to_groups ctg
ON c.id = ctg.customer_id
AND ctg.group_id IN (1,3,5,6)
GROUP BY c.id
HAVING
COUNT(CASE WHEN ctg.group_id IN (1,3) THEN 1 END) > 0
AND COUNT(CASE WHEN ctg.group_id IN (5,6) THEN 1 END) > 0;
или
SELECT c.id
FROM customer c
INNER JOIN customers_to_groups ctg
ON c.id = ctg.customer_id
AND ctg.group_id IN (1,3,5,6)
GROUP BY c.id
HAVING
COUNT(*) FILTER(WHERE ctg.group_id IN (1,3)) > 0
AND COUNT(*) FILTER(WHERE ctg.group_id IN (5,6)) > 0;
Примечание. Приведенные выше запросы предполагают, что вам действительно нужно включить таблицы customers
и customers_to_groups
и соединить их. Если вам не нужно включать таблицу customers
, просто удалите ее и выберите из таблицы customers_to_groups
только для улучшения производительности:
SELECT customer_id
FROM customers_to_groups ctg
WHERE ctg.group_id IN (1,3,5,6)
GROUP BY customer_id
HAVING
COUNT(*) FILTER(WHERE ctg.group_id IN (1,3)) > 0
AND COUNT(*) FILTER(WHERE ctg.group_id IN (5,6)) > 0;
Эта демонстрация с примерами данных и множеством дополнительных строк показывает разницу в производительности.
Использование FILTER
или CASE
намного быстрее (поскольку позволяет избежать второго JOIN
в таблице customer_to_groups). Точная разница в производительности зависит от реальных данных в ваших таблицах и от того, какие индексы вы используете.
Принимаю это как ответ, но всем спасибо за ответы в будущем, если вы используете Postgress и хотите, чтобы динамический ответ @Charlieface был идеальным. Еще раз всем спасибо, я проголосовал за все ответы ^^
Кроме того, как уже упоминалось, если вам не нужны данные о клиенте, вы можете просто удалить использование клиента, которое будет выглядеть как ответ @rahul-jangid и @ jarlh.
Это проблема реляционного деления с той особенностью, что у вас есть несколько делителей. То, что у вас есть, неплохо с точки зрения эффективности.
Но если вы хотите иметь возможность делать это динамически и передавать разное количество групп и блоков групп, тогда вам нужно другое решение.
Вы можете передать его как параметр массива JSONB.
SELECT
cg.customer_id
FROM customers_to_groups cg
JOIN jsonb_array_elements('[[5,6],[1,3]]'::jsonb) sections(value)
ON cg.group_id IN (SELECT value::int FROM jsonb_array_elements(sections.value))
GROUP BY
cg.customer_id
HAVING COUNT(*) = MIN(jsonb_array_length('[[5,6],[1,3]]'::jsonb));
В качестве альтернативы, если у вас есть входные данные в таблице, вы можете использовать полное реляционное деление.
SELECT
c.id
FROM (
SELECT
c.id,
COUNT(cg.group_id) AS count_matches
FROM customer c
CROSS JOIN input_groups ig
LEFT JOIN customers_to_groups cg
ON cg.group_id = ig.group_id
AND cg.customer_id = c.id
GROUP BY
c.id,
ig.section_id
) c
GROUP BY
c.id
HAVING COUNT(*) FILTER (WHERE count_matches = 0) = 0; -- all sections have a match
Сделайте GROUP BY
, используйте предложение HAVING
, чтобы убедиться, что присутствует хотя бы один из (5, 6) и хотя бы один из (1, 3).
SELECT customer_id
FROM customers_to_groups
WHERE group_id IN (5, 6, 1, 3)
GROUP BY customer_id
HAVING MAX(group_id) >= 5
AND MIN(group_id) <= 3;
Вы также можете использовать INTERSECT
:
SELECT customer_id FROM customers_to_groups WHERE group_id IN (5, 6)
INTERSECT
SELECT customer_id FROM customers_to_groups WHERE group_id IN (1, 3)
Минимальный воспроизводимый пример — отличное начало при обращении за помощью по SQL.