Когда вы отказываетесь от заданных операций в SQL и переходите на процедурный? - PullRequest
2 голосов
/ 03 декабря 2008

Однажды мне было поручено выполнить эту задачу в СУБД:

Даны таблицы покупателя, заказа, строки заказа и товара. Все сделано с обычными полями и связями, с полем примечания комментария в таблице строки заказа.

Для одного клиента получить список всех продуктов, которые он когда-либо заказывал, с названием продукта, годом первой покупки, датами трех последних покупок, комментарием последнего заказа, суммой общего дохода для этой комбинации продукта и клиента за последние 12 лет. месяцев.

Через пару дней я прекратил делать это как запрос и решил просто извлекать каждую строку заказа для клиента и каждый продукт и проходил через эти процедуры процедурно для создания необходимой таблицы на стороне клиента.

Я считаю это симптомом одного или нескольких из следующих факторов:

  • Я ленивый идиот и должен был увидеть, как это сделать в SQL
  • Операции над множествами не так выразительны, как процедурные операции
  • SQL не так выразителен, как должно быть

Я правильно сделал? У меня были другие варианты?

Ответы [ 7 ]

8 голосов
/ 03 декабря 2008

Вы определенно должны быть в состоянии выполнить это упражнение, не выполняя работу, эквивалентную 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;

edit: В другом комментарии вы сказали, что хотите посмотреть, как получить даты трех последних покупок в стандартном 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 n или 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;
4 голосов
/ 03 декабря 2008

Операции над множествами не так выразительны, как процедурные операции

Возможно, больше похоже на: «Операции над множествами не так знакомы, как процедурные операции для разработчика, привыкшего к процедурным языкам»; -)

Делать это итеративно, как вы это сделали сейчас, хорошо для небольших наборов данных, но просто не масштабировать таким же образом. Ответ на вопрос, правильно ли вы поступили, зависит от того, удовлетворены ли вы сейчас производительностью и / или не ожидаете, что объем данных значительно увеличится.

Если бы вы могли предоставить пример кода, мы могли бы помочь вам найти решение на основе множеств, которое будет быстрее для начала и масштабироваться намного, намного лучше. Как упомянул GalacticCowboy, такие методы, как временные таблицы, могут сделать операторы гораздо более читабельными, сохраняя при этом преимущества в производительности.

3 голосов
/ 03 декабря 2008

В большинстве RDBMS у вас есть опция временных таблиц или переменных локальных таблиц, которые вы можете использовать, чтобы разбить такую ​​задачу на управляемые куски.

Я не вижу способа легко сделать это как одиночный запрос (без каких-либо неприятных подзапросов), но это все равно должно быть выполнимо без перехода к процедурному коду, если вы используете временные таблицы.

2 голосов
/ 04 декабря 2008

РЕДАКТИРОВАТЬ: Это совершенно новый подход к решению, без использования временных таблиц или странных запросов sub-sub-sub. Однако он будет работать ТОЛЬКО в SQL 2005 или новее, так как использует команду «pivot», которая появилась в этой версии.

Фундаментальная проблема - это требуемый стержень из набора строк (в данных) в столбцы в выходных данных. Разбираясь в этом вопросе, я вспомнил, что в SQL Server теперь есть оператор "pivot" для решения этой проблемы.

Работает только в 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
2 голосов
/ 03 декабря 2008

Для меня это звучит как проект хранилища данных. Если вам нужны такие вещи, как «три последние вещи» и «сумма чего-либо за последние 12 месяцев», сохраните их, то есть денормализуйте.

2 голосов
/ 03 декабря 2008

Эта проблема может быть не решена с помощью одного запроса. Я вижу несколько отдельных частей ...

Для одного клиента

  1. Получить список всех заказанных товаров (с названием товара)
  2. Получить год первой покупки
  3. Получить даты последних трех покупок
  4. Получить комментарий к последнему заказу
  5. Получить сумму покупок за последние 12 месяцев

Ваша процедура - шаги 1 - 5, и SQL получает вам данные.

1 голос
/ 03 декабря 2008

SQL-запросы возвращают результаты в виде единой «плоской» таблицы строк и столбцов. Требования к отчетности часто более сложны, чем это, и требуют «зазубренного» набора результатов, как в вашем примере. Нет ничего плохого в том, чтобы «идти процедурно» для решения таких требований или использовать инструмент отчетности, расположенный поверх базы данных. Тем не менее, вы должны максимально использовать SQL, чтобы получить максимальную производительность от базы данных.

...