Однажды мне было поручено выполнить эту задачу в СУБД:
Приведены таблицы заказчик, заказ, строки заказов и товар. Все сделано с обычными полями и связями, с полем памятки комментария в таблице строки заказа.
Для одного клиента получить список всех продуктов, которые он когда-либо заказывал, с указанием названия продукта, года первой покупки, дат трех последних покупок, комментария к последнему заказу, суммы общего дохода для этой комбинации продукт-клиент за последние 12 месяцев.
Через пару дней я отказался от выполнения этого как запроса и решил просто получать каждую строку заказа для клиента и каждый продукт и обрабатывать данные процедурно, чтобы создать необходимую таблицу на стороне клиента.
Я считаю это симптомом одного или нескольких из следующих:
Правильно ли я поступил? Были ли у меня другие варианты?


В большинстве СУБД у вас есть возможность использовать временные таблицы или локальные переменные таблицы, которые можно использовать для разделения такой задачи на управляемые фрагменты.
Я не вижу способа легко сделать это в виде запроса не замужем (без некоторых подзапросов противный), но это все равно должно быть выполнено, не отвлекаясь от процедурного кода, если вы используете временные таблицы.
Ты прав. Подзапросы действительно были бы очень неприятными. Но я думаю, что просмотр одного результата, установленного курсором, и процедурная вставка строк результатов во временную таблицу результатов - лучший способ сделать это на стороне сервера. Это работает, но подтверждает, что язык запросов SQL не справляется с этой задачей.
Нет, ВСЕГО СОВЕТА временной таблицы в том, что вы все равно будете использовать логику, основанную на наборах, для взаимодействия с ней. Конечно, вставки на основе курсора являются процедурными, но весь смысл логики на основе наборов не в использовании курсоров.
@onedaywhen: «хранимая процедура» не является «процедурной» в том смысле, в котором вы думаете. Просто потому, что что-то разбито на подэтапы, каждый из подэтапов может выполняться в логике набора, а не в процедурном коде.
Set operations are not as expressive as procedural operations
Возможно, больше похоже на: «Операции Set не так знакомы разработчику, как процедурные операции, для процедурных языков» ;-)
Итеративное выполнение этого действия, как вы это сделали сейчас, подходит для небольших наборов данных, но просто не масштабируется таким же образом. Ответ на вопрос, правильно ли вы поступили, зависит от того, удовлетворены ли вы производительностью прямо сейчас и / или не ожидаете значительного увеличения объема данных.
Если бы вы могли предоставить образец кода, мы могли бы помочь вам найти решение на основе наборов, которое будет быстрее с самого начала и намного лучше масштабируется. Как упоминал GalacticCowboy, такие методы, как временные таблицы, могут помочь сделать операторы более удобочитаемыми, в значительной степени сохраняя при этом преимущества в производительности.
Согласовано. Может ли приложение Amazon.com получать все продукты и перебирать их?
Я согласен со всем, что вы говорите. Причина, по которой я не предоставил образец кода, заключается в том, что я покинул этот проект много лет назад. У меня больше нет кода. Код был чертовски уродливым. Я не хотел заранее загружать все идеи о том, как это сделать.
SQL-запросы возвращают результаты в виде единой «плоской» таблицы строк и столбцов. Требования к отчетности часто более сложны, чем это, и требуют «зубчатого» набора результатов, как в вашем примере. Нет ничего плохого в «процедурном» решении таких требований или использовании инструмента отчетности, который находится поверх базы данных. Однако вы должны использовать SQL, насколько это возможно, чтобы получить максимальную производительность от базы данных.
Другой способ мышления заключается в том, что результат запроса сам по себе является отношением (т. Е. Таблицей), даже если он не сохраняется. Следовательно, применяется определение отношения, например каждый столбец имеет одинаковое имя, тип данных и значение в каждой строке.
Я полностью согласен с производственной частью. Я думаю, что одним SQL эту проблему не решить. SQL не был создан для такого рода отчетов. Отчет очень ориентирован на человека.
SQL был создан именно для такого рода проблем - изначально он рассматривался как интерфейс специальных запросов. И с этим, безусловно, можно справиться с помощью одного запроса.
Doofledorfer, я не понимаю, как на эту проблему можно ПОЛЕЗНО ответить в одном запросе, без группировки с конкатенированными значениями или получения декартового произведения? Результат не помещается в одну таблицу.
Эта проблема могла не быть решена с помощью запроса один. Я вижу несколько отдельных частей ...
Для одного покупателя
Ваша процедура - это шаги 1–5, и SQL возвращает вам данные.
Может быть, всего 3 запроса: в одном запросе можно получить минимальный год покупки, последний комментарий и сумму покупок за последние 12 месяцев.
Я хотел бы, чтобы вы получили даты последних трех покупок для каждого продукта в одном красивом запросе ANSI-92. Как может то, что так легко объяснить, так сложно выразить на таком «популярном» языке, как SQL?
Для меня это похоже на проект хранилища данных. Если вам нужны такие вещи, как «три самых последних события» и «сумма чего-то за последние 12 месяцев», сохраните их, то есть денормализуйте.
На самом деле это очень хороший ответ. Почему бы не перенести проблему на входящие данные? Намного проще удалить самую старую покупку, когда сделана новая, а в таблице уже 3, или даже есть 3 даты и обновление, установив date3 = date2, date2 = date1 и date1 = order.date. Правило триггеров.
Ага, по крайней мере часть, кроме "хранилища данных". Для этого нужны аналитические базы данных.
(Но триггеры не управляют. Избегайте их любой ценой. Это змеи, которые прячутся в темноте и кусаются, когда вы меньше всего этого ожидаете.)
Пока ограничения SQL Server CHECK не поддерживают подзапросы (и можем ли мы также создать CREATE ASSERTION, пожалуйста), я застрял с триггерами для обеспечения соблюдения основных ограничений, например. упорядоченный ключ в таблице состояния действительного времени («история»).
Вы определенно сможете выполнить это упражнение, не выполняя работу, эквивалентную JOIN в коде приложения, то есть путем выборки всех строк как из строк заказов, так и из продуктов и повторения их по ним. Для этого не обязательно быть мастером SQL. JOIN для SQL - это то же самое, что цикл для процедурного языка - в том смысле, что обе являются фундаментальными языковыми функциями, которые вы должны знать, как использовать.
Одна из ловушек, в которую попадают люди, - это думать, что весь отчет должен быть создан с помощью одного SQL-запроса. Не правда! Как отмечает Тони Эндрюс, большинство отчетов не помещаются в прямоугольник. Существует множество сводок, сводок, особых случаев и т. д., Поэтому и проще, и эффективнее получать части отчета в отдельных запросах. Точно так же на процедурном языке вы не стали бы пытаться выполнять все свои вычисления в одной строке кода или даже в одной функции (надеюсь).
Некоторые инструменты отчетности настаивают на том, что отчет создается на основе одного запроса, и у вас нет возможности объединить несколько запросов. Если да, то вам нужно создать несколько отчетов (и если начальник хочет, чтобы они были на одной странице, вам нужно выполнить некоторую вставку вручную).
Получить список все заказанные продукты (с названием продукта), даты последних трех покупок и комментарий к последнему заказу просто:
SELECT o.*, l.*, p.*
FROM Orders o
JOIN OrderLines l USING (order_id)
JOIN Products p USING (product_id)
WHERE o.customer_id = ?
ORDER BY o.order_date;
Можно перебирать результат построчно, чтобы извлечь даты и комментарии к последним заказам, так как вы в любом случае получаете эти строки. Но упростите себе задачу, попросив базу данных вернуть результаты, отсортированные по дате.
Год первой покупки доступен из предыдущего запроса, если вы отсортируете по order_date и получите результат построчно, у вас будет доступ к первому порядку. В противном случае это можно сделать так:
SELECT YEAR(MIN(o.order_date)) FROM Orders o WHERE o.customer_id = ?;
Сумма покупок товаров за последние 12 месяцев лучше всего рассчитывать отдельным запросом:
SELECT SUM(l.quantity * p.price)
FROM Orders o
JOIN OrderLines l USING (order_id)
JOIN Products p USING (product_id)
WHERE o.customer_id = ?
AND o.order_date > CURDATE() - INTERVAL 1 YEAR;
редактировать: Вы сказали в другом комментарии, что хотели бы увидеть, как получить даты последних трех покупок в стандартном SQL:
SELECT o1.order_date
FROM Orders o1
LEFT OUTER JOIN Orders o2
ON (o1.customer_id = o2.customer_id AND (o1.order_date < o2.order_date
OR (o1.order_date = o2.order_date AND o1.order_id < o2.order_id)))
WHERE o1.customer_id = ?
GROUP BY o1.order_id
HAVING COUNT(*) <= 3;
Если вы можете использовать немного специфичных для поставщика функций SQL, вы можете использовать Microsoft / Sybase TOPп или MySQL / PostgreSQL LIMIT:
SELECT TOP 3 order_date
FROM Orders
WHERE customer_id = ?
ORDER BY order_date DESC;
SELECT order_date
FROM Orders
WHERE customer_id = ?
ORDER BY order_date DESC
LIMIT 3;
Очень подробный и приятный ответ. Баланс между заданными операциями и процедурными, как я. Самое сложное - узнать последние три даты покупки для каждого продукта, который купил клиент. Вы, как и я, решите эту проблему процедурным путем.
Мне не хватает «процедурной» части этого решения, которую определяет Гуге. Разве для этого не нужны курсоры и циклы? Это просто выглядит как несколько операторов выбора.
@ Guiness: я думаю, дело в том, что не каждый отчет должен создаваться из одного слишком сложного SQL-запроса. «Процедурная» часть - это некий прикладной клей для выполнения нескольких более простых запросов и объединения / представления их результатов.
Обновлено: Это совершенно новый подход к решению, без использования временных таблиц или странных подподподзапросов. Однако он будет работать ТОЛЬКО с SQL 2005 или новее, так как он использует новую в этой версии команду «pivot».
Основная проблема - это желаемый поворот из набора строк (в данных) в столбцы в выходных данных. Обдумывая этот вопрос, я вспомнил, что в SQL Server теперь есть оператор «поворота», который справляется с этим.
Это работает в SQL 2005 Только, используя образец данных Northwind.
-- This could be a parameter to a stored procedure
-- I picked this one because he has products that he ordered 4 or more times
declare @customerId nchar(5)
set @customerId = 'ERNSH'
select c.CustomerID, p.ProductName, products_ordered_by_cust.FirstOrderYear,
latest_order_dates_pivot.LatestOrder1 as LatestOrderDate,
latest_order_dates_pivot.LatestOrder2 as SecondLatestOrderDate,
latest_order_dates_pivot.LatestOrder3 as ThirdLatestOrderDate,
'If I had a comment field it would go here' as LatestOrderComment,
isnull(last_year_revenue_sum.ItemGrandTotal, 0) as LastYearIncome
from
-- Find all products ordered by customer, along with first year product was ordered
(
select c.CustomerID, od.ProductID,
datepart(year, min(o.OrderDate)) as FirstOrderYear
from Customers c
join Orders o on o.CustomerID = c.CustomerID
join [Order Details] od on od.OrderID = o.OrderID
group by c.CustomerID, od.ProductID
) products_ordered_by_cust
-- Find the grand total for product purchased within last year - note fudged date below (Northwind)
join (
select o.CustomerID, od.ProductID,
sum(cast(round((od.UnitPrice * od.Quantity) - ((od.UnitPrice * od.Quantity) * od.Discount), 2) as money)) as ItemGrandTotal
from
Orders o
join [Order Details] od on od.OrderID = o.OrderID
-- The Northwind database only contains orders from 1998 and earlier, otherwise I would just use getdate()
where datediff(yy, o.OrderDate, dateadd(year, -10, getdate())) = 0
group by o.CustomerID, od.ProductID
) last_year_revenue_sum on last_year_revenue_sum.CustomerID = products_ordered_by_cust.CustomerID
and last_year_revenue_sum.ProductID = products_ordered_by_cust.ProductID
-- THIS is where the magic happens. I will walk through the individual pieces for you
join (
select CustomerID, ProductID,
max([1]) as LatestOrder1,
max([2]) as LatestOrder2,
max([3]) as LatestOrder3
from
(
-- For all orders matching the customer and product, assign them a row number based on the order date, descending
-- So, the most recent is row # 1, next is row # 2, etc.
select o.CustomerID, od.ProductID, o.OrderID, o.OrderDate,
row_number() over (partition by o.CustomerID, od.ProductID order by o.OrderDate desc) as RowNumber
from Orders o join [Order Details] od on o.OrderID = od.OrderID
) src
-- Now, produce a pivot table that contains the first three row #s from our result table,
-- pivoted into columns by customer and product
pivot
(
max(OrderDate)
for RowNumber in ([1], [2], [3])
) as pvt
group by CustomerID, ProductID
) latest_order_dates_pivot on products_ordered_by_cust.CustomerID = latest_order_dates_pivot.CustomerID
and products_ordered_by_cust.ProductID = latest_order_dates_pivot.ProductID
-- Finally, join back to our other tables to get more details
join Customers c on c.CustomerID = products_ordered_by_cust.CustomerID
join Orders o on o.CustomerID = products_ordered_by_cust.CustomerID and o.OrderDate = latest_order_dates_pivot.LatestOrder1
join [Order Details] od on od.OrderID = o.OrderID and od.ProductID = products_ordered_by_cust.ProductID
join Products p on p.ProductID = products_ordered_by_cust.ProductID
where c.CustomerID = @customerId
order by CustomerID, p.ProductID
Кстати, если я закомментирую фильтр идентификатора клиента (чтобы результаты содержали всех клиентов в базе данных Northwind), сценарий все равно займет меньше секунды.
Вы понимаете, насколько короткий шаг от этого до одного запроса без временных таблиц, не так ли? И я уверен, что он будет работать еще быстрее - по крайней мере, не медленнее.
@GalacticCowboy - Снимаю перед вами шляпу. Достаточно усилий. Я прихожу к выводу, что Сорен Куклау был прав в том, что я очень процедурный, именно так я читаю между его строк. Однако я думаю, что язык запросов должен был иметь возможность делать что-то настолько простое с точки зрения процедур в более коротком выражении.
@doofledorfer - Не могу дождаться, чтобы увидеть это. Будете ли вы сначала получать самую последнюю, затем следующую последнюю - с подзапросом, а третью - с подзапросом в подзапросе? Это то, чего я не хотел в своей первоначальной программе.
Эээ, тогда не используется процедурный код временных таблиц или локальных табличных переменных? Я бы посчитал что-то «основанное на множестве» достижимым с помощью VIEW. Если бы эти подзапросы выполнялись очень быстро, вы все равно считали бы их «неприятными»?