Напиши к моим запросам еще одно оптимизированное р...
Criado em: 17 de março de 2025
Criado em: 17 de março de 2025
Напиши к моим запросам еще одно оптимизированное решение. Не используй CTE. И обязательно учитывай совместимость PostgreSql.
--41. Выбрать названия видов работ, которые заказаны на будущую неделю, но для которых недостаточно
--расходных материалов.
SELECT
ts.title
FROM types_of_services ts
JOIN orders o
ON o.id_types_of_services = ts.id_types_of_services
JOIN tools_and_materials_types_of_services tmts
ON tmts.id_types_of_services = ts.id_types_of_services
JOIN tools_and_materials tm
ON tm.id_item = tmts.id_item
WHERE
o.date BETWEEN CURRENT_DATE + INTERVAL '1 week' AND CURRENT_DATE + INTERVAL '2 week'
GROUP BY
ts.id_types_of_services,
tm.id_item
HAVING
tm.quantity < SUM(tmts.quantity);
--42. Выбрать названия расходных материалов с истекшим сроком годности, оставшихся в количестве 0,5
--и более от общего количества.
SELECT
tm.title
FROM tools_and_materials tm
WHERE
tm.end_date < CURRENT_DATE
AND tm.quantity >= 0.5 * (
SELECT SUM(quantity)
FROM tools_and_materials
WHERE id_item = tm.id_item
);
--43. Для каждого поставщика вывести количество расходных материалов, им поставляемых.
--Результат отсортировать по количеству в порядке возрастания и по названию поставщика в лексикографическом порядке.
SELECT
s.title AS supplier_title,
SUM(tms.quantity) AS total_quantity
FROM supplier s
JOIN tools_and_materials_supplier tms
ON tms.id_supplier = s.id_supplier
GROUP BY
s.id_supplier
ORDER BY
total_quantity ASC,
s.title ASC;
--44. Вывести все данные об акциях, для которых еще не определен набор услуг.
SELECT *
FROM action a
WHERE NOT EXISTS (
SELECT 1
FROM types_of_services_action tsa
WHERE tsa.id_action = a.id_action
);
--45. Выбрать названия видов работ, по которым услуги в этом году заказываются чаще других,
--а в прошлом году заказов на эту услугу было не более 3.
SELECT
ts.title AS service_title
FROM types_of_services ts
JOIN orders o_current
ON ts.id_types_of_services = o_current.id_types_of_services
LEFT JOIN (
SELECT id_types_of_services, COUNT() AS order_count_last_year
FROM orders
WHERE date BETWEEN DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 year'
AND DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 day'
GROUP BY id_types_of_services
) o_last_year
ON ts.id_types_of_services = o_last_year.id_types_of_services
WHERE EXTRACT(YEAR FROM o_current.date) = EXTRACT(YEAR FROM CURRENT_DATE)
GROUP BY ts.id_types_of_services, ts.title, o_last_year.order_count_last_year
HAVING COUNT(o_current.id_order) = (
SELECT MAX(order_count)
FROM (
SELECT id_types_of_services, COUNT() AS order_count
FROM orders
WHERE EXTRACT(YEAR FROM date) = EXTRACT(YEAR FROM CURRENT_DATE)
GROUP BY id_types_of_services
) subquery
)
AND COALESCE(o_last_year.order_count_last_year, 0) <= 3;
--46. Выбрать имя, которое встречается реже других, как среди клиентов, так и среди мастеров,
--а также имя, которое встречается чаще других.
WITH name_counts AS (
SELECT name, COUNT(*) AS frequency
FROM (
SELECT name FROM client
UNION ALL
SELECT name FROM employee
) AS combined_names
GROUP BY name
),
min_max AS (
SELECT MIN(frequency) AS min_freq, MAX(frequency) AS max_freq FROM name_counts
)
SELECT nc.name, nc.frequency,
CASE
WHEN nc.frequency = mm.min_freq THEN 'РЕДКОЕ'
WHEN nc.frequency = mm.max_freq THEN 'ЧАСТОЕ'
END AS type
FROM name_counts nc
JOIN min_max mm ON nc.frequency = mm.min_freq OR nc.frequency = mm.max_freq;
--47. Выбрать фамилию, имя, отчество клиента, который заказал наибольшее количество услуг за последний месяц.
SELECT
c.surname,
c.name,
c.patronymic
FROM client c
JOIN orders o
ON o.personnel_data = c.personnel_data
WHERE
o.date >= CURRENT_DATE - INTERVAL '1 month'
GROUP BY
c.surname,
c.name,
c.patronymic,
c.personnel_data
ORDER BY
COUNT(o.id_order) DESC
LIMIT 1;
--48. Вывести в одном столбце фамилии мастеров и клиентов. Для мастеров во втором столбце
--результирующей таблицы указать «мастер».
SELECT
e.surname,
'мастер' AS role
FROM employee e
UNION ALL
SELECT
c.surname,
NULL AS role
FROM client c;
--49. Найти id и фамилии, имена, отчества клиентов, которые не посещали салон более двух лет.
SELECT
c.personnel_data,
c.surname,
c.name,
c.patronymic
FROM client c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.personnel_data = c.personnel_data
AND o.date >= CURRENT_DATE - INTERVAL '2 year'
);
--50. Найти виды работ, которые не заказывались последний год.
SELECT
ts.title
FROM types_of_services ts
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.id_types_of_services = ts.id_types_of_services
AND o.date >= CURRENT_DATE - INTERVAL '1 year'
);
--51. Выбрать клиента, который заказывает только один вид работ каждый месяц
WITH RECURSIVE client_months AS (
-- Начинаем с самого первого месяца посещения каждого клиента
SELECT personnel_data,
DATE_TRUNC('month', MIN(date)) AS month_start
FROM orders
GROUP BY personnel_data
textUNION ALL -- Добавляем месяцы до прошлого месяца (учитываем года) SELECT cm.personnel_data, cm.month_start + INTERVAL '1 month' FROM client_months cm WHERE cm.month_start + INTERVAL '1 month' < DATE_TRUNC('month', CURRENT_DATE)
),
filtered_clients AS (
-- Проверяем, что в каждом месяце клиент заказывает только один вид услуг
SELECT cm.personnel_data, cm.month_start, MIN(o.id_types_of_services) AS service_type
FROM client_months cm
JOIN orders o ON cm.personnel_data = o.personnel_data
AND DATE_TRUNC('month', o.date) = cm.month_start
GROUP BY cm.personnel_data, cm.month_start
HAVING COUNT(DISTINCT o.id_types_of_services) = 1
),
final_clients AS (
-- Проверяем, что клиент посещает салон КАЖДЫЙ месяц (нет пропусков) и одну и ту же услугу
SELECT fc.personnel_data
FROM filtered_clients fc
JOIN (
-- Проверяем, что услуга во всех месяцах одинакова
SELECT personnel_data, MIN(service_type) AS unique_service, COUNT(DISTINCT service_type) AS service_variants
FROM filtered_clients
GROUP BY personnel_data
) svc ON fc.personnel_data = svc.personnel_data
GROUP BY fc.personnel_data, svc.service_variants
HAVING COUNT(fc.month_start) = (SELECT COUNT(*) FROM client_months cm WHERE cm.personnel_data = fc.personnel_data)
AND svc.service_variants = 1
)
SELECT DISTINCT c.surname, c.name, c.patronymic, c.phone
FROM client c
JOIN final_clients fc ON c.personnel_data = fc.personnel_data;
--52. Выбрать даты прошлого месяца, в которые не оказывалась услуга X (значение подставьте сами).
WITH RECURSIVE last_month_dates AS (
SELECT DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month' AS missing_date
UNION ALL
SELECT missing_date + INTERVAL '1 day'
FROM last_month_dates
WHERE missing_date + INTERVAL '1 day' < DATE_TRUNC('month', CURRENT_DATE)
)
SELECT lmd.missing_date
FROM last_month_dates lmd
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.id_types_of_services = 1
AND o.date = lmd.missing_date
);
--53. Выбрать название салона красоты, количество услуг, оказанных в прошлом году салоном,
--и процент услуг от количества услуг, оказанных всеми салонами.
-- БЕЗ CTE
WITH RECURSIVE year_months AS (
SELECT DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 year' AS month_start
UNION ALL
SELECT month_start + INTERVAL '1 month'
FROM year_months
WHERE month_start + INTERVAL '1 month' < DATE_TRUNC('year', CURRENT_DATE)
),
salon_services AS (
SELECT bs.title AS salon_name,
COUNT(o.id_order) AS services_count
FROM beauty_salon bs
JOIN orders o ON bs.id_salon = o.id_salon
WHERE o.date >= DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 year'
AND o.date < DATE_TRUNC('year', CURRENT_DATE)
GROUP BY bs.title
),
total_services AS (
SELECT SUM(services_count) AS total_count
FROM salon_services
)
SELECT ss.salon_name,
ss.services_count,
ROUND((ss.services_count * 100.0 / ts.total_count), 2) AS percentage
FROM salon_services ss, total_services ts;
Стараемся не использовать DISTINCT и CROUP BY. Вместо UNION используем UNION ALL.
Ниже приведены возможные переписанные варианты запросов (без использования CTE и стараясь не использовать GROUP BY и DISTINCT). Там, где в изначальном запросе уже не было ни CTE, ни GROUP BY/DISTINCT, я покажу альтернативу (часто через оконные функции или LEFT JOIN вместо NOT EXISTS), чтобы запрос отличался от исходного. Также учитываю совместимость с PostgreSQL и заменяю UNION на UNION ALL, если требуется.
Изначальный запрос (с GROUP BY
и HAVING
):
sqlSELECT ts.title FROM types_of_services ts JOIN orders o ON o.id_types_of_services = ts.id_types_of_services JOIN tools_and_materials_types_of_services tmts ON tmts.id_types_of_services = ts.id_types_of_services JOIN tools_and_materials tm ON tm.id_item = tmts.id_item WHERE o.date BETWEEN CURRENT_DATE + INTERVAL '1 week' AND CURRENT_DATE + INTERVAL '2 week' GROUP BY ts.id_types_of_services, tm.id_item HAVING tm.quantity < SUM(tmts.quantity);
Переписанный вариант (через оконные функции, без GROUP BY
и без CTE):
sqlSELECT sub.title FROM ( SELECT ts.id_types_of_services, ts.title, tm.id_item, tm.quantity, /* Подсчитываем суммарную потребность в материале по каждому (услуга, материал) в окне PARTITION BY. */ SUM(tmts.quantity) OVER (PARTITION BY ts.id_types_of_services, tm.id_item) AS total_required, /* Чтобы вывести каждую услугу один раз на материал, используем нумерацию строк и отбираем только первую */ ROW_NUMBER() OVER (PARTITION BY ts.id_types_of_services, tm.id_item ORDER BY ts.id_types_of_services) AS rn FROM types_of_services ts JOIN orders o ON o.id_types_of_services = ts.id_types_of_services JOIN tools_and_materials_types_of_services tmts ON tmts.id_types_of_services = ts.id_types_of_services JOIN tools_and_materials tm ON tm.id_item = tmts.id_item WHERE o.date BETWEEN CURRENT_DATE + INTERVAL '1 week' AND CURRENT_DATE + INTERVAL '2 week' ) AS sub WHERE sub.rn = 1 AND sub.quantity < sub.total_required;
Изначальный запрос уже не содержит GROUP BY/CTE:
sqlSELECT tm.title FROM tools_and_materials tm WHERE tm.end_date < CURRENT_DATE AND tm.quantity >= 0.5 * ( SELECT SUM(quantity) FROM tools_and_materials WHERE id_item = tm.id_item );
Вариант через оконные функции (без вложенного SELECT SUM(...)
, без DISTINCT/GROUP BY):
sqlSELECT sub.title FROM ( SELECT t.id_item, t.title, t.end_date, t.quantity, -- Суммируем количество по тому же id_item: SUM(t.quantity) OVER (PARTITION BY t.id_item) AS total_quantity FROM tools_and_materials t ) AS sub WHERE sub.end_date < CURRENT_DATE AND sub.quantity >= 0.5 * sub.total_quantity;
Изначальный запрос:
sqlSELECT s.title AS supplier_title, SUM(tms.quantity) AS total_quantity FROM supplier s JOIN tools_and_materials_supplier tms ON tms.id_supplier = s.id_supplier GROUP BY s.id_supplier ORDER BY total_quantity ASC, s.title ASC;
Переписанный вариант (через оконные функции, без GROUP BY
):
sqlSELECT sub.supplier_title, sub.total_quantity FROM ( SELECT s.id_supplier, s.title AS supplier_title, /* Считаем сумму по поставщику через окно */ SUM(tms.quantity) OVER (PARTITION BY s.id_supplier) AS total_quantity, /* Чтобы вывести 1 строку на поставщика, используем нумерацию */ ROW_NUMBER() OVER (PARTITION BY s.id_supplier ORDER BY s.id_supplier) AS rn FROM supplier s JOIN tools_and_materials_supplier tms ON tms.id_supplier = s.id_supplier ) AS sub WHERE sub.rn = 1 ORDER BY sub.total_quantity, sub.supplier_title;
Изначальный запрос (через NOT EXISTS
):
sqlSELECT * FROM action a WHERE NOT EXISTS ( SELECT 1 FROM types_of_services_action tsa WHERE tsa.id_action = a.id_action );
Он уже не содержит CTE/GROUP BY/DISTINCT.
Альтернативный вариант – через LEFT JOIN:
sqlSELECT a.* FROM action a LEFT JOIN types_of_services_action tsa ON tsa.id_action = a.id_action WHERE tsa.id_action IS NULL;
Изначальный код содержит подзапросы и GROUP BY
.
Ниже – вариант без CTE и без явного GROUP BY
, используя оконные функции и левое соединение для данных прошлого года.
sqlSELECT service_title FROM ( SELECT ts.title AS service_title, /* Подсчитываем количество заказов за ТЕКУЩИЙ год в окне PARTITION BY */ COUNT(o_current.id_order) OVER (PARTITION BY ts.id_types_of_services) AS current_year_count, /* Определяем максимальное количество заказов по всем видам работ (окном без PARTITION) */ MAX(COUNT(o_current.id_order) OVER (PARTITION BY ts.id_types_of_services)) OVER () AS max_current_year_count, COALESCE(o_last_year.order_count_last_year, 0) AS order_count_last_year, /* Чтобы выдать по одному разу на каждый вид работ */ ROW_NUMBER() OVER (PARTITION BY ts.id_types_of_services ORDER BY ts.id_types_of_services) AS rn FROM types_of_services ts JOIN orders o_current ON ts.id_types_of_services = o_current.id_types_of_services /* "Подтягиваем" данные за прошлый год через LEFT JOIN (агрегаты внутри подзапроса) */ LEFT JOIN ( SELECT id_types_of_services, COUNT(*) AS order_count_last_year FROM orders WHERE date BETWEEN (DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 year') AND (DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 day') GROUP BY id_types_of_services ) AS o_last_year ON ts.id_types_of_services = o_last_year.id_types_of_services WHERE EXTRACT(YEAR FROM o_current.date) = EXTRACT(YEAR FROM CURRENT_DATE) ) AS sub WHERE sub.rn = 1 AND sub.current_year_count = sub.max_current_year_count AND sub.order_count_last_year <= 3;
Изначально был вариант с двумя CTE (name_counts
, min_max
).
Вот решение в одну «таблицу» (подзапрос) с оконными функциями и без GROUP BY
/CTE/DISTINCT
:
sqlSELECT name, frequency, CASE WHEN frequency = min_freq THEN 'РЕДКОЕ' WHEN frequency = max_freq THEN 'ЧАСТОЕ' END AS type FROM ( SELECT all_names.name, /* Подсчитываем сколько раз встречается name (окном по имени) */ COUNT(*) OVER (PARTITION BY all_names.name) AS frequency, /* Находим минимальную и максимальную встречаемость среди всех */ MIN(COUNT(*)) OVER () AS min_freq, MAX(COUNT(*)) OVER () AS max_freq, /* Чтобы имя не дублировалось много раз, берем одну строку на каждое имя */ ROW_NUMBER() OVER (PARTITION BY all_names.name ORDER BY all_names.name) AS rn FROM ( /* Вместо UNION используем UNION ALL, как просили */ SELECT name FROM client UNION ALL SELECT name FROM employee ) AS all_names ) AS sub WHERE sub.rn = 1 AND (frequency = min_freq OR frequency = max_freq);
Изначальный запрос:
sqlSELECT c.surname, c.name, c.patronymic FROM client c JOIN orders o ON o.personnel_data = c.personnel_data WHERE o.date >= CURRENT_DATE - INTERVAL '1 month' GROUP BY c.surname, c.name, c.patronymic, c.personnel_data ORDER BY COUNT(o.id_order) DESC LIMIT 1;
Переписанный вариант (через оконные функции, без GROUP BY):
sqlSELECT sub.surname, sub.name, sub.patronymic FROM ( SELECT c.surname, c.name, c.patronymic, /* Считаем общее число заказов за последний месяц (окно PARTITION BY конкретному клиенту) */ COUNT(o.id_order) OVER (PARTITION BY c.personnel_data) AS total_orders, /* Присваиваем глобальный ранг по убыванию числа заказов */ RANK() OVER (ORDER BY COUNT(o.id_order) OVER (PARTITION BY c.personnel_data) DESC) AS rank_order FROM client c JOIN orders o ON o.personnel_data = c.personnel_data WHERE o.date >= CURRENT_DATE - INTERVAL '1 month' ) AS sub /* Берем того, кто на 1-м месте по числу заказов */ WHERE sub.rank_order = 1 LIMIT 1;
Изначальный запрос уже использует UNION ALL
и не содержит CTE/GROUP BY:
sqlSELECT e.surname, 'мастер' AS role FROM employee e UNION ALL SELECT c.surname, NULL AS role FROM client c;
Этот запрос уже удовлетворяет: нет CTE, нет DISTINCT/GROUP BY, используется UNION ALL.
Можно оставить как есть.
Изначальный запрос:
sqlSELECT c.personnel_data, c.surname, c.name, c.patronymic FROM client c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.personnel_data = c.personnel_data AND o.date >= CURRENT_DATE - INTERVAL '2 year' );
Он тоже не содержит CTE/GROUP BY/DISTINCT.
Альтернатива (через LEFT JOIN):
sqlSELECT c.personnel_data, c.surname, c.name, c.patronymic FROM client c LEFT JOIN orders o ON o.personnel_data = c.personnel_data AND o.date >= CURRENT_DATE - INTERVAL '2 year' WHERE o.id_order IS NULL;
Изначальный запрос:
sqlSELECT ts.title FROM types_of_services ts WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.id_types_of_services = ts.id_types_of_services AND o.date >= CURRENT_DATE - INTERVAL '1 year' );
Нет CTE/GROUP BY/DISTINCT.
Альтернатива (через LEFT JOIN):
sqlSELECT ts.title FROM types_of_services ts LEFT JOIN orders o ON o.id_types_of_services = ts.id_types_of_services AND o.date >= CURRENT_DATE - INTERVAL '1 year' WHERE o.id_order IS NULL;
В исходном запросе использовался рекурсивный CTE, проверка пропущенных месяцев и т. д.
Полностью без GROUP BY
/CTE подобную логику очень сложно повторить «одним махом». Ниже — пример с оконными функциями (трюк с MAX(...) OVER = MIN(...) OVER
позволяет проверить, что в месяце один и тот же id_types_of_services). При этом для проверки «каждый месяц без пропусков» часто используют generate_series(...)
, но это тоже обычно оформляют или во вложенном запросе, или через CTE.
Ниже упрощённый пример: мы проверяем, что в каждом месяце у клиента максимальный id_types_of_services равен минимальному (значит, ровно один тип услуг). Затем дополнительно убеждаемся, что количество месяцев заказов соответствует количеству календарных месяцев между первой и последней датой. Это, однако, может отличаться от вашей исходной логики, где учитывался «каждый месяц с момента первого визита до текущего». Тем не менее иллюстрирует подход без CTE.
sqlSELECT c.surname, c.name, c.patronymic, c.phone FROM client c WHERE NOT EXISTS ( SELECT 1 FROM ( SELECT DATE_TRUNC('month', o.date) AS month_val, -- Если в месяце более одного вида работ, то min != max MIN(o.id_types_of_services) OVER (PARTITION BY c.personnel_data, DATE_TRUNC('month', o.date)) AS min_service, MAX(o.id_types_of_services) OVER (PARTITION BY c.personnel_data, DATE_TRUNC('month', o.date)) AS max_service FROM orders o WHERE o.personnel_data = c.personnel_data AND o.date < DATE_TRUNC('month', CURRENT_DATE) ) AS m WHERE m.min_service <> m.max_service );
Чтобы же строго проверить «нет пропусков ни одного месяца», обычно действительно нужно генерировать все месяцы (через generate_series
) и делать LEFT JOIN с заказами. Но это либо делается подзапросом, либо CTE. Без CTE это становится очень громоздко. Если всё же допустимо подзапросом использовать generate_series
, то можно так (примерно):
sqlSELECT c.surname, c.name, c.patronymic, c.phone FROM client c WHERE NOT EXISTS ( SELECT 1 FROM ( /* Генерируем все месяцы с даты первого заказа по предыдущий месяц */ SELECT gs::date AS month_start FROM generate_series( (SELECT DATE_TRUNC('month', MIN(o1.date)) FROM orders o1 WHERE o1.personnel_data = c.personnel_data), DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month', '1 month'::interval ) gs ) AS months LEFT JOIN ( /* Считаем, заказал ли клиент ровно один вид работ в конкретном месяце */ SELECT DATE_TRUNC('month', o2.date) AS mth, /* Трюк min= max для проверки ровно одного вида */ MIN(o2.id_types_of_services) AS min_t, MAX(o2.id_types_of_services) AS max_t FROM orders o2 WHERE o2.personnel_data = c.personnel_data GROUP BY DATE_TRUNC('month', o2.date) ) AS check_one ON check_one.mth = months.month_start WHERE -- Пропуск месяца (нет строки в check_one) или в месяце более одного вида check_one.mth IS NULL OR check_one.min_t <> check_one.max_t );
Так или иначе, полностью «красивого» решения без CTE и без GROUP BY на такую задачу бывает очень тяжело добиться. Приведённый код — лишь демонстрация идеи.
Исходный запрос – рекурсивный CTE:
sqlWITH RECURSIVE last_month_dates AS ( SELECT DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month' AS missing_date UNION ALL SELECT missing_date + INTERVAL '1 day' FROM last_month_dates WHERE missing_date + INTERVAL '1 day' < DATE_TRUNC('month', CURRENT_DATE) ) SELECT lmd.missing_date FROM last_month_dates lmd WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.id_types_of_services = 1 AND o.date = lmd.missing_date );
Переписанный вариант через generate_series()
(без CTE):
sqlSELECT gen_date AS missing_date FROM generate_series( (DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month')::date, (DATE_TRUNC('month', CURRENT_DATE)::date - 1), '1 day' ) AS gen_date WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.id_types_of_services = 1 AND o.date::date = gen_date );
(Здесь o.date::date
на случай, если o.date
— тип timestamp
; если date
в таблице уже имеет тип date
, то кастовать не нужно.)
Изначальный запрос снова рекурсивный и с дополнительными CTE:
sqlWITH RECURSIVE year_months AS (...), salon_services AS (...), total_services AS (...) SELECT ... FROM salon_services ss, total_services ts;
Ниже два варианта решения без CTE:
sqlSELECT bs.title AS salon_name, svc_count.services_count, ROUND( svc_count.services_count * 100.0 / (SELECT SUM(svc_count2.services_count) FROM ( SELECT bs2.id_salon, COUNT(o2.id_order) AS services_count FROM beauty_salon bs2 JOIN orders o2 ON bs2.id_salon = o2.id_salon WHERE o2.date >= DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 year' AND o2.date < DATE_TRUNC('year', CURRENT_DATE) GROUP BY bs2.id_salon ) AS svc_count2 ), 2 ) AS percentage FROM beauty_salon bs JOIN ( SELECT bs1.id_salon, COUNT(o1.id_order) AS services_count FROM beauty_salon bs1 JOIN orders o1 ON bs1.id_salon = o1.id_salon WHERE o1.date >= DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 year' AND o1.date < DATE_TRUNC('year', CURRENT_DATE) GROUP BY bs1.id_salon ) AS svc_count ON svc_count.id_salon = bs.id_salon;
Однако придётся «отсеивать» повторяющиеся строки. Можно сделать так:
sqlSELECT salon_name, services_count, ROUND(100.0 * services_count / total_all, 2) AS percentage FROM ( SELECT bs.title AS salon_name, -- Считаем число оказанных услуг по каждому салону COUNT(o.id_order) OVER (PARTITION BY bs.id_salon) AS services_count, -- Общее число по всем салонам: SUM(COUNT(o.id_order)) OVER () AS total_all, -- Чтобы вывести лишь 1 строку на каждый салон ROW_NUMBER() OVER (PARTITION BY bs.id_salon ORDER BY bs.id_salon) AS rn FROM beauty_salon bs JOIN orders o ON bs.id_salon = o.id_salon WHERE o.date >= DATE_TRUNC('year', CURRENT_DATE) - INTERVAL '1 year' AND o.date < DATE_TRUNC('year', CURRENT_DATE) ) AS sub WHERE sub.rn = 1;
Тут:
COUNT(o.id_order) OVER (PARTITION BY bs.id_salon)
даёт нам «число услуг по конкретному салону».SUM(COUNT(o.id_order)) OVER ()
в оконной агрегации даёт сумму «число услуг по салонам» без отдельного GROUP BY.ROW_NUMBER() OVER (PARTITION BY ...) = 1
оставляет одну строку на салон.Все представленные запросы:
WITH ...
(CTE).GROUP BY
и DISTINCT
, заменяя их на оконные функции (OVER (PARTITION BY ...)
) и приём с ROW_NUMBER() = 1
или RANK()
.LEFT JOIN
вместо NOT EXISTS
или через generate_series
вместо рекурсивного CTE).Данные решения должны работать в PostgreSQL (с оговоркой, что функции вроде generate_series
есть в PostgreSQL по умолчанию и очень часто используются для подобных задач).