2

我曾经被赋予在 RDBMS 中执行此任务:

给定表格客户、订单、订单线和产品。使用通常的字段和关系完成所有操作,并在订单表上使用注释备注字段。

对于一个客户,检索客户曾经订购过的所有产品的列表,其中包含产品名称、首次购买的年份、最后三次购买的日期、最新订单的评论、该产品-客户组合过去 12 个月的总收入总和。

几天后,我放弃了将其作为查询,并选择只为客户获取每个订单和每个产品,并按程序运行数据以构建所需的表客户端。

我认为这是以下一项或多项的症状:

  • 我是个懒惰的白痴,应该知道如何在 SQL 中做到这一点
  • 集合操作不如过程操作富有表现力
  • SQL 没有应有的表现力

我做对了吗?我有其他选择吗?

4

7 回答 7

8

您绝对应该能够在不执行与JOIN应用程序代码中的 a 等效的工作的情况下完成此练习,即从订单行和产品中获取所有行并遍历它们。您不必成为 SQL 向导也能做到这一点。 JOIN循环之于 SQL 就像循环之于过程语言——因为两者都是您应该知道如何使用的基本语言特性。

人们陷入的一个陷阱是认为必须在单个 SQL 查询中生成整个报告。不对!正如托尼·安德鲁斯(Tony Andrews)所指出的,大多数报告都不适合矩形。有很多汇总、摘要、特殊情况等,因此在单独的查询中获取报告的部分内容既简单又高效。同样,在程序语言中,您不会尝试在一行代码中进行所有计算,甚至在单个函数中(希望如此)。

一些报告工具坚持认为报告是从单个查询生成的,您没有机会合并多个查询。如果是这样,那么您需要生成多个报告(如果老板想要在一页上,那么您需要手动进行一些粘贴)。

要获得所有订购产品的列表(带有产品名称)、最近三笔购买的日期以及对最新订单的评论,非常简单:

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 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;
于 2008-12-03T18:15:05.083 回答
4

集合操作不如过程操作富有表现力

也许更像:“对于习惯于过程语言的开发人员来说,集合操作不像过程操作那样熟悉”;-)

像您现在所做的那样迭代地执行它对于小型数据集很好,但根本不会以相同的方式扩展。您是否做了正确的事情的答案取决于您是否对现在的性能感到满意和/或不期望数据量会增加太多。

如果您可以提供一些示例代码,我们或许可以帮助您找到一个基于集合的解决方案,该解决方案将更快开始并且可以更好地扩展。正如 GalacticCowboy 所提到的,诸如临时表之类的技术可以帮助使语句更具可读性,同时在很大程度上保留性能优势。

于 2008-12-03T14:08:31.370 回答
3

在大多数 RDBMS 中,您可以选择临时表或本地表变量,您可以使用它们将此类任务分解为可管理的块。

我看不到任何方法可以轻松地将其作为单个查询(没有一些讨厌的子查询),但如果您使用临时表,它仍然应该是可行的,而无需退出程序代码。

于 2008-12-03T14:06:37.253 回答
2

这个问题可能不是一个查询就能解决的。我看到几个不同的部分......

对于一位客户

  1. 获取所有订购产品的列表(带有产品名称)
  2. 获取首次购买年份
  3. 获取最近三笔购买的日期
  4. 获取最新订单评论
  5. 获取过去 12 个月的产品购买总和

您的过程是步骤 1 - 5,SQL 为您获取数据。

于 2008-12-03T14:11:02.847 回答
2

对我来说,这听起来像是一个数据仓库项目。如果您需要诸如“最近的三件事”和“过去 12 个月内某件事的总和”之类的内容,请存储它们,即非规范化。

于 2008-12-03T16:14:24.283 回答
2

编辑:这是一个全新的解决方案,不使用临时表或奇怪的子子子查询。但是,它只适用于 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
于 2008-12-03T21:26:44.823 回答
1

SQL 查询以行和列的单个“平面”表的形式返回结果。报告要求通常比这更复杂,需要像您的示例一样的“锯齿状”结果集。“程序化”来解决此类需求或使用位于数据库顶部的报告工具并没有错。但是,您应该尽可能使用 SQL 以从数据库中获得最佳性能。

于 2008-12-03T14:09:23.817 回答