Напиши к моим запросам еще одно оптимизированное р...

創建於:2025年3月17日

使用 O1 回答 Chat01

問題

Напиши к моим запросам еще одно оптимизированное решение. Не используй 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

text
UNION 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.

思考

Refining queries

I’m mapping out optimized queries without using CTE, focusing on PostgreSQL compatibility and avoiding DISTINCT, GROUP BY, and UNION, aiming for cleaner, more efficient SQL code.

Crafting solution

I’m considering subqueries, window functions, and aggregator subqueries to create optimized solutions for queries 41 to 53 without using CTE, DISTINCT, or GROUP BY.

Adjusting my approach

I’m reworking the query by omitting CTEs, minimizing GROUP BY, and avoiding DISTINCT. Using subselects, I’ll sum upcoming quantities and compare them to available ones.

Adjusting my strategy

Ensuring the logic aligns with the original query's time range, summing tmts.quantity for specific orders, and focusing on material shortages for each service type.

Optimizing the approach

I’m working through query optimization by avoiding CTE, GROUP BY, and DISTINCT, focusing instead on comparing summed required quantities with available quantities for each item over the specified time period.

Adjusting thinking

I'm exploring a window function approach to avoid DISTINCT and GROUP BY. However, this might produce duplicates when multiple orders exist within the specified time period.

Selecting specific rows

I’m mapping out using ROW_NUMBER() with PARTITION BY id_types_of_services to pick one row per service, which could help avoid duplicates while summing quantities.

Weighing window functions

I'm considering window functions and ROW_NUMBER() to limit rows for each service type. This approach might align with the user's preference against DISTINCT or GROUP BY, focusing on preserving all unique titles.

Formulating the query

Crafting a SQL query to identify services lacking sufficient materials for the upcoming week, while avoiding DISTINCT or GROUP BY, and ensuring each service title is uniquely captured.

Refining the SQL query

I’m refining the SQL query to eliminate subselects and CTEs, using a window function to aggregate quantities. This adjustment ensures the query remains efficient without sacrificing clarity.

Refining the SQL query

I’m adjusting the SQL query to eliminate DISTINCT and GROUPBY, using window functions and transactions to efficiently capture the necessary data without overcomplicating the process.

Opting for a left join

Taking a closer look, a left join seems optimal, removing the need for DISTINCT or GROUP BY, while effectively excluding entries with associated types of services actions.

Refining the SQL query

I’m eliminating CTEs and DISTINCT, opting for window functions to count orders by service type this year, ensuring the count aligns with the maximum for each type.

Refining data dynamics

I’m refining the SQL query to use subselects with window functions. This approach avoids GROUP BY, efficiently represents data relationships, and adheres to the specified requirements.

Replacing CTEs

I’m thinking of using a subselect to merge client and employee data with a window function, which avoids GROUPBY, DISTINCT, and CTEs, and creates a single query.

Piecing together the strategy

I’m using window functions to calculate frequency, find min and max frequencies, and filter names with these frequencies, ultimately selecting the smallest row number for each name.

Revising the approach

I’m pivoting to window functions to rotate data into a single query, considering frequency calculations and row selection to sidestep GROUP BY and DISTINCT.

Gaining clarity

I'm pulling together client surnames, names, and patronymics, tallying total orders, identifying the top-ranked orderer and focusing solely on the top rank.

Refining the strategy

Now I'm focusing on a single, simplified query to select the top client based on total orders, ensuring the use of UNION ALL is consistent.

Tackling the query

I’m pondering whether to refine the current query by removing GROUP BY, DISTINCT, and CTE, or to use a left join approach to identify clients without recent orders.

Navigating the options

I’m considering a left join to pinpoint clients without recent orders and pondering if generate_series could track monthly orders over time.

Tracking monthly orders

I’m digging into the idea of generating monthly orders for each client by pinpointing the earliest and latest order dates, mapping out a series of months, and checking for gaps.

Navigating monthly orders

OK, let me see. Instead of generating monthly series directly, I’m working through a method to find the longest period by identifying the maximum time difference from a table of dates.

Mapping monthly patterns

I’m generating each month between the first and current date, then cross-examining these with client orders to ensure each month has precisely one distinct service.

Adapting strategies

I’m refining the approach to identify clients with consistent monthly orders, using a subselect to pinpoint a single service type for each month. Progress is steadily being made.

Implementing window functions

I’m working on window functions to tally distinct services per client monthly. It’s interesting to see how these can confirm a single service type consistently across all months.

Mapping out the details

I’m detailing a subselect approach to identify clients with consistent monthly orders, ensuring no service type mix-ups and a continuous ordering pattern. Progressing through the criteria methodically.

Formulating a strategy

I’m examining a single subselect approach using window functions to ensure clients have consistent monthly orders with a single service type. Progress is being made by tweaking the method effectively.

Piecing together clues

I'm thinking about a query to pinpoint clients with single-service monthly orders. While tricky without GROUP BY, I'm considering row_number and difference methods to filter out those with distinct services.

Figuring out the optimal solution

I'm mapping out the best way to pinpoint single-service monthly orders and absentee dates, utilizing a generate_series function to streamline the process. Progress is evident, but more refinement is needed.

Crunching numbers

I’m working through a query to tally salon services and compare individual salons to the annual total. This approach avoids Common Table Expressions, aiming for a concise, efficient solution.

Mapping salon services

I’m piecing together a query to tally salon services and compare each salon to the annual totals using window functions. This approach avoids subqueries or joins, aiming for a streamlined solution.

Calculating salon metrics

Leveraging window functions and ROW_NUMBER(), I determine each salon's service count and percentage against annual totals, ensuring the solution is straightforward and PostgreSQL compatible.

Reasoned for 3m 19s

回答

Ниже приведены возможные переписанные варианты запросов (без использования CTE и стараясь не использовать GROUP BY и DISTINCT). Там, где в изначальном запросе уже не было ни CTE, ни GROUP BY/DISTINCT, я покажу альтернативу (часто через оконные функции или LEFT JOIN вместо NOT EXISTS), чтобы запрос отличался от исходного. Также учитываю совместимость с PostgreSQL и заменяю UNION на UNION ALL, если требуется.


41. Виды работ на будущую неделю, для которых не хватает расходных материалов

Изначальный запросGROUP BY и HAVING):

sql
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);

Переписанный вариант (через оконные функции, без GROUP BY и без CTE):

sql
SELECT 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;

42. Материалы с истекшим сроком годности, которых осталось 0.5 и более от общего количества

Изначальный запрос уже не содержит GROUP BY/CTE:

sql
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 );

Вариант через оконные функции (без вложенного SELECT SUM(...), без DISTINCT/GROUP BY):

sql
SELECT 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;

43. Для каждого поставщика вывести общее количество расходных материалов

Изначальный запрос:

sql
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;

Переписанный вариант (через оконные функции, без GROUP BY):

sql
SELECT 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;

44. Акции, для которых ещё не определён набор услуг

Изначальный запрос (через NOT EXISTS):

sql
SELECT * 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:

sql
SELECT a.* FROM action a LEFT JOIN types_of_services_action tsa ON tsa.id_action = a.id_action WHERE tsa.id_action IS NULL;

45. Названия видов работ, которые в этом году заказываются чаще всех, но в прошлом году – не более 3 заказов

Изначальный код содержит подзапросы и GROUP BY.
Ниже – вариант без CTE и без явного GROUP BY, используя оконные функции и левое соединение для данных прошлого года.

sql
SELECT 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;

46. Самое редкое и самое частое имя (среди клиентов и мастеров)

Изначально был вариант с двумя CTE (name_counts, min_max).
Вот решение в одну «таблицу» (подзапрос) с оконными функциями и без GROUP BY/CTE/DISTINCT:

sql
SELECT 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);

47. Клиент, заказавший наибольшее количество услуг за последний месяц

Изначальный запрос:

sql
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;

Переписанный вариант (через оконные функции, без GROUP BY):

sql
SELECT 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;

48. Вывести в одном столбце фамилии мастеров и клиентов, во втором — «мастер» для мастеров

Изначальный запрос уже использует UNION ALL и не содержит CTE/GROUP BY:

sql
SELECT e.surname, 'мастер' AS role FROM employee e UNION ALL SELECT c.surname, NULL AS role FROM client c;

Этот запрос уже удовлетворяет: нет CTE, нет DISTINCT/GROUP BY, используется UNION ALL.
Можно оставить как есть.


49. Клиенты, которые не посещали салон более двух лет

Изначальный запрос:

sql
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' );

Он тоже не содержит CTE/GROUP BY/DISTINCT.
Альтернатива (через LEFT JOIN):

sql
SELECT 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;

50. Виды работ, которые не заказывались последний год

Изначальный запрос:

sql
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' );

Нет CTE/GROUP BY/DISTINCT.
Альтернатива (через LEFT JOIN):

sql
SELECT 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;

51. Клиент, который «каждый месяц заказывает только один вид работ»

В исходном запросе использовался рекурсивный CTE, проверка пропущенных месяцев и т. д.
Полностью без GROUP BY/CTE подобную логику очень сложно повторить «одним махом». Ниже — пример с оконными функциями (трюк с MAX(...) OVER = MIN(...) OVER позволяет проверить, что в месяце один и тот же id_types_of_services). При этом для проверки «каждый месяц без пропусков» часто используют generate_series(...), но это тоже обычно оформляют или во вложенном запросе, или через CTE.

Ниже упрощённый пример: мы проверяем, что в каждом месяце у клиента максимальный id_types_of_services равен минимальному (значит, ровно один тип услуг). Затем дополнительно убеждаемся, что количество месяцев заказов соответствует количеству календарных месяцев между первой и последней датой. Это, однако, может отличаться от вашей исходной логики, где учитывался «каждый месяц с момента первого визита до текущего». Тем не менее иллюстрирует подход без CTE.

sql
SELECT 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, то можно так (примерно):

sql
SELECT 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 на такую задачу бывает очень тяжело добиться. Приведённый код — лишь демонстрация идеи.


52. Даты прошлого месяца, когда не оказывалась услуга X

Исходный запрос – рекурсивный CTE:

sql
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 );

Переписанный вариант через generate_series() (без CTE):

sql
SELECT 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, то кастовать не нужно.)


53. Название салона, число оказанных услуг в прошлом году, и процент от общего числа

Изначальный запрос снова рекурсивный и с дополнительными CTE:

sql
WITH RECURSIVE year_months AS (...), salon_services AS (...), total_services AS (...) SELECT ... FROM salon_services ss, total_services ts;

Ниже два варианта решения без CTE:

Вариант 1: подзапрос для «общего количества» и подзапрос для «количества по салону»

sql
SELECT 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;

Вариант 2: использование оконных функций (без GROUP BY и без CTE)

Однако придётся «отсеивать» повторяющиеся строки. Можно сделать так:

sql
SELECT 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().
  • Там, где изначально не было CTE/GROUP BY, приведены альтернативы (например, через LEFT JOIN вместо NOT EXISTS или через generate_series вместо рекурсивного CTE).

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

分享這個問答