У вас есть таблицы Orders(order_id, customer_id, amount, date) и Customers(customer_id, region). Напишите и объясните SQL-решение, которое для каждого заказа вычисляет суммарную годовую покупку клиента и ранг клиента по сумме в его регионе; сравните реализацию через JOIN+GROUP BY и оконные функции, обсудите индексацию и последствия для согласованности под высокой нагрузкой

10 Окт в 13:31
6 +1
0
Ответы
1

Задача: для каждой строки заказа вернуть 111 суммарную годовую покупку клиента суммаповсемзаказамэтогоклиентазатотжегод,чтоиузаказасумма по всем заказам этого клиента за тот же год, что и у заказасуммаповсемзаказамэтогоклиентазатотжегод,чтоиузаказа и 222 ранг клиента в его регионе по этой годовой сумме.

Я покажу два подхода: 1) "JOIN + GROUP BY + коррелированный подзапрос" чистоагрегирующий+ранжированиебезоконныхфункцийчисто агрегирующий + ранжирование без оконных функцийчистоагрегирующий+ранжированиебезоконныхфункций и 2) "агрегация + оконная функция" рекомендуемый—чищеиобычноэффективнеерекомендуемый — чище и обычно эффективнеерекомендуемыйчищеиобычноэффективнее. После — рекомендации по индексам и обсуждение поведения под высокой нагрузкой / требований по согласованности.

Обозначения: Ordersorderid,customerid,amount,dateorder_id, customer_id, amount, dateorderi d,customeri d,amount,date, Customerscustomerid,regioncustomer_id, regioncustomeri d,region. Под year я понимаю EXTRACTYEARFROMdateYEAR FROM dateYEARFROMdate.

1) JOIN + GROUP BY + коррелированный подзапрос безоконныхфункцийбез оконных функцийбезоконныхфункций Принцип: сначала соберём суммарную годовую покупку per customer/year, затем присоединим её к Orders и посчитаем ранг коррелированным подзапросом countдругихклиентоввтомжерегионесбольшейсуммойcount других клиентов в том же регионе с большей суммойcountдругихклиентоввтомжерегионесбольшейсуммой.

SQL примерпримерпример:
WITH cust_year_sum AS SELECTcustomerid,EXTRACT(YEARFROMdate)ASyear,SUM(amount)ASannualsumFROMOrdersGROUPBYcustomerid,EXTRACT(YEARFROMdate) SELECT
customer_id,
EXTRACT(YEAR FROM date) AS year,
SUM(amount) AS annual_sum
FROM Orders
GROUP BY customer_id, EXTRACT(YEAR FROM date)
SELECTcustomeri d,EXTRACT(YEARFROMdate)ASyear,SUM(amount)ASannuals umFROMOrdersGROUPBYcustomeri d,EXTRACT(YEARFROMdate)
SELECT
o.,
cys.annual_sum,
(
SELECT 1 + COUNT()
FROM cust_year_sum cys2
JOIN Customers cu2 ON cu2.customer_id = cys2.customer_id
WHERE cys2.year = cys.year
AND cu2.region = cu.region
AND cys2.annual_sum > cys.annual_sum
) AS region_rank
FROM Orders o
JOIN cust_year_sum cys
ON cys.customer_id = o.customer_id
AND cys.year = EXTRACTYEARFROMo.dateYEAR FROM o.dateYEARFROMo.date JOIN Customers cu
ON cu.customer_id = o.customer_id;

Пояснения:

cust_year_sum — одна запись на customer,yearcustomer, yearcustomer,year с суммой.Коррелированный подзапрос считает сколько других customer в том же region имеют annual_sum больше текущего → ранг = count+1.
Плюсы/минусы:Простой SQL и понятная семантика.Но коррелированный подзапрос может быть дорогим: для каждой уникной записи cust_year_sum авыприсоединяетееёккаждойстрокезаказаа вы присоединяете её к каждой строке заказаавыприсоединяетееёккаждойстрокезаказа выполняется перебор/скан по всем customer в том же году/регионе — ON2N^2N2 в худшем случае, что плохо на больших данных.Некоторым СУБД оптимизация такого шаблона работает плохо.

2) Рекомендуемая версия — сначала агрегируем, затем используем оконную функцию для ранжирования, затем джоиним обратно к Orders
Порядок: aaa агрегируем per customer/year, bbb присоединяем region, ccc считаем ранг по region+year через DENSE_RANK/RANK, ddd джоиним к Orders.

SQL рекомендованныйрекомендованныйрекомендованный:
WITH cust_year AS SELECTo.customerid,EXTRACT(YEARFROMo.date)ASyear,SUM(o.amount)ASannualsumFROMOrdersoGROUPBYo.customerid,EXTRACT(YEARFROMo.date) SELECT
o.customer_id,
EXTRACT(YEAR FROM o.date) AS year,
SUM(o.amount) AS annual_sum
FROM Orders o
GROUP BY o.customer_id, EXTRACT(YEAR FROM o.date)
SELECTo.customeri d,EXTRACT(YEARFROMo.date)ASyear,SUM(o.amount)ASannuals umFROMOrdersoGROUPBYo.customeri d,EXTRACT(YEARFROMo.date)
,
cust_year_with_region AS SELECTcy.<em>,cu.regionFROMcustyearcyJOINCustomerscuONcu.customerid=cy.customerid SELECT cy.<em>, cu.region
FROM cust_year cy
JOIN Customers cu ON cu.customer_id = cy.customer_id
SELECTcy.<em>,cu.regionFROMcusty earcyJOINCustomerscuONcu.customeri d=cy.customeri d
,
ranked AS SELECTcywr.</em>,DENSERANK()OVER(PARTITIONBYregion,yearORDERBYannualsumDESC)ASregionrankFROMcustyearwithregioncywr SELECT
cywr.</em>,
DENSE_RANK() OVER (PARTITION BY region, year ORDER BY annual_sum DESC) AS region_rank
FROM cust_year_with_region cywr
SELECTcywr.</em>,DENSER ANK()OVER(PARTITIONBYregion,yearORDERBYannuals umDESC)ASregionr ankFROMcusty earw ithr egioncywr
SELECT
o.*,
r.annual_sum,
r.region_rank
FROM Orders o
JOIN ranked r
ON r.customer_id = o.customer_id
AND r.year = EXTRACTYEARFROMo.dateYEAR FROM o.dateYEARFROMo.date;

Пояснения:

На этапе ranked мы имеем ровно одну строку на customer,yearcustomer, yearcustomer,year и применяем оконную функцию в разрезе region,yearregion, yearregion,year. Это даёт ранги DENSERANKилиRANKвзависимостиотжелаемойсемантикиDENSE_RANK или RANK в зависимости от желаемой семантикиDENSER ANKилиRANKвзависимостиотжелаемойсемантики.Затем JOIN восстанавливает per-order строки, повторяя annual_sum и region_rank для каждого заказа этого клиента в этом году.
Плюсы/минусы:Чище, обычно эффективнее: ранжирование делается один раз над множеством агрегированных строк кол−во=numberofcustomers×yearsкол-во = number of customers × yearsколво=numberofcustomers×years, а не многократно.Оконные функции требуют сортировки/партиционирования; при больших объёмах может потребоваться память/внешний сорт, но это обычно масштабируется лучше, чем квадратичные коррелированные подзапросы.Поддерживается большинством современных СУБД Postgres,MySQL8+,SQLServerит.д.Postgres, MySQL 8+, SQL Server и т.д.Postgres,MySQL8+,SQLServerит.д..

Вариант без промежуточного GROUP BY для annual_sum черезоконнуюфункциюнаOrdersчерез оконную функцию на OrdersчерезоконнуюфункциюнаOrders Можно также получить annual_sum прямо в строке заказа так:
SUMamountamountamount OVER PARTITIONBYcustomerid,EXTRACT(YEARFROMdate)PARTITION BY customer_id, EXTRACT(YEAR FROM date)PARTITIONBYcustomeri d,EXTRACT(YEARFROMdate) AS annual_sum_per_order
Но ранжировать по region удобнее на агрегированном уровне одназаписьнаcustomer/yearодна запись на customer/yearодназаписьнаcustomer/year. Нативное вложение оконных функций ранжированиепоvalue,которыйсампосчитаноконнойфункцией ранжирование по value, который сам посчитан оконной функцией ранжированиепоvalue,которыйсампосчитаноконнойфункцией нельзя во многих диалектах, поэтому обычно делают CTE агрегацию → оконная функция → join.

Индексация рекомендациирекомендациирекомендации

PK/индексы:
Customers.customer_id — PK обычноестьобычно естьобычноесть.Customersregionregionregion — индекс, если часто фильтруете или джойните по region на большом наборе.Для ускорения GROUP BY по customer и year:
Индекс на Orderscustomerid,datecustomer_id, datecustomeri d,date или customerid,date,amountcustomer_id, date, amountcustomeri d,date,amount — помогает быстро выбирать строки для одного customer и диапазона дат/года.Если СУБД поддерживает функциональные/выраженные индексы, можно создать индекс на customerid,EXTRACT(YEARFROMdate)customer_id, EXTRACT(YEAR FROM date)customeri d,EXTRACT(YEARFROMdate) или на date_trunc′year′,date'year', dateyear,date вместе с amount для ускорения группировки по году PostgresподдерживаетexpressionindexPostgres поддерживает expression indexPostgresподдерживаетexpressionindex.Для частых запросов только по текущему году: частичный индекс WHERE date >= '2025-01-01'.Если аналитика — частый сценарий, рассмотрите агрегированную таблицу materializedview/summarytablematerialized view / summary tablematerializedview/summarytable с индексом по year,region,customeridyear, region, customer_idyear,region,customeri d и периодическим обновлением batch/CDC/triggerbatch/CDC/triggerbatch/CDC/trigger.

Последствия для согласованности и поведения под высокой нагрузкой

Снимок данных и уровни изоляции:
В большинстве MVCC СУБД например,PostgreSQLнапример, PostgreSQLнапример,PostgreSQL одиночный SELECT выполняется в рамках одного консистентного снимка — вы получите внутренне согласованные суммы и ранги на момент начала запроса. Однако при уровне READ COMMITTED могут быть различия между несколькими SQL-выражениями в рамках одной транзакции вPostgresREADCOMMITTEDберётновыйснимокдлякаждоговыраженияв Postgres READ COMMITTED берёт новый снимок для каждого выражениявPostgresREADCOMMITTEDберётновыйснимокдлякаждоговыражения.Если вам нужна гарантия, что все части отчёта видят один и тот же момент времени данных, запустите запрос в транзакции с REPEATABLE READ илииспользуйтемеханизмыsnapshotили используйте механизмы snapshotилииспользуйтемеханизмыsnapshot. Но длинные снимки тормозят VACUUM и могут создавать нагрузку.Атомарность и свежесть:
Для аналитики "на лету" вы, вероятно, будете видеть все уже зафиксированные транзакции. Невозможно одновременно получить абсолютную неделимую картину при постоянно прибывающих записях без либо приостановки записей, либо использования snapshot изоляции.Если требуется строгая консистентность при конкурентных модификациях например,выхотитевмоментTполучитьточныйрангнесмотрянаодновременныевставкинапример, вы хотите в момент T получить точный ранг несмотря на одновременные вставкинапример,выхотитевмоментTполучитьточныйрангнесмотрянаодновременныевставки, можно:запускать отчёт на snapshot-изолированном транзакции витаетрискдолгихтранзакцийвитает риск долгих транзакцийвитаетрискдолгихтранзакций,либо использовать materialized view, обновляемый транзакционно/пакетно,либо иметь потоковую агрегацию incremental/CDCincremental / CDCincremental/CDC и читать предагрегированные значения.Блокировки и влияние на запись:
Чтение агрегатов обычно не блокирует вставки/обновления при MVCC, но если вы применяете явные блокировки или повышенную изоляцию — записи могут блокироваться или транзакции будут отклоняться вSERIALIZABLEв SERIALIZABLEвSERIALIZABLE.Долгие аналитические запросы в режиме REPEATABLE/ SERIALIZABLE могут замедлить очистку старых версий строк VACUUMVACUUMVACUUM, приводя к росту хранения/IO.Производительность:
GROUP BY по большой таблице = скан илииндексныйобходили индексный обходилииндексныйобход + агрегация. Если считывать весь Orders,— высокая нагрузка.Оконные функции добавляют сортировку/партиционирование; память/внешний сорт может потребоваться. Но 한 раз выполнить сортировку по агрегированному набору customer×yearcustomer × yearcustomer×year чаще дешевле, чем N коррелированных подзапросов.При экстремальной нагрузке: предпочтительнее поддерживать предварительные агрегаты summarytablesummary tablesummarytable, которые быстро джойнятся и индексируются.

Резюме / рекомендации

Для читаемости и производительности используйте агрегацию per customer/year + оконная функция DENSERANK/RANKDENSE_RANK/RANKDENSER ANK/RANK для ранга, затем джойн к Orders. Это и семантически чисто, и обычно эффективнее.Создайте индексы: Orderscustomerid,datecustomer_id, datecustomeri d,date / функциональный индекс по году, Customers.customer_id PK и индекс по region.Если данные очень большие и/или запись идёт постоянно:
рассмотрите materialized view / summary table ежечасная/ежедневнаяагрегацияилиинкрементальныеобновлениячерезCDCежечасная/ежедневная агрегация или инкрементальные обновления через CDCежечасная/ежедневнаяагрегацияилиинкрементальныеобновлениячерезCDC,или используйте транзакционную snapshot-изоляцию, если нужна консистентность на весь запрос имейтеввидувлияниенаVACUUM/долгиетранзакцииимейте в виду влияние на VACUUM/долгие транзакцииимейтеввидувлияниенаVACUUM/долгиетранзакции.Избегайте коррелированных подзапросов, которые сравнивают каждый customer с каждым другим на больших наборах — это быстро станет узким местом.

Если хотите, могу:

дать tuned-версии запросов для конкретной СУБД Postgres/MySQL/SQLServerPostgres / MySQL / SQL ServerPostgres/MySQL/SQLServer,предложить схему materialized view и скрипт для его инкрементального обновления,предложить конкретные индексы с синтаксисом для вашей СУБД.
10 Окт в 13:55
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир