2

RDBMS で次のタスクを実行するように与えられたことがあります。

テーブル customer、order、orderlines、および product を指定します。すべて通常のフィールドと関係で行われ、オーダーライン テーブルにコメント メモ フィールドがあります。

ある顧客について、その顧客がこれまでに注文したすべての製品のリストを、製品名、最初の購入年、最後の 3 回の購入日、最新の注文のコメント、その製品と顧客の組み合わせの過去 12 か月間の総収入の合計で取得します。

数日後、私はそれをクエリとして行うことをあきらめ、顧客のすべてのオーダーラインとすべての製品を取得し、手続き的にデータを実行して、必要なクライアント側のテーブルを構築することを選択しました.

これは、次の 1 つまたは複数の症状であると考えています。

  • 私は怠け者で、SQL でそれを行う方法を見ておくべきでした
  • 集合操作は手続き型操作ほど表現力がない
  • SQL は本来あるべきほど表現力がありません

私は正しいことをしましたか?他に選択肢はありましたか?

4

7 に答える 7

8

JOINアプリケーションコードと同等の作業を行わなくても、つまり、オーダーラインと製品の両方からすべての行をフェッチしてそれらを反復処理することにより、この演習を確実に実行できるはずです。それを行うのにSQLウィザードである必要はありません。 JOINSQLにとっては、手続き型言語に対するループとは何ですか。どちらも基本的な言語機能であり、使用方法を知っておく必要があります。

人々が陥る罠の1つは、レポート全体を1つのSQLクエリで作成する必要があると考えていることです。違います!Tony Andrewsが指摘しているように、ほとんどのレポートは長方形に収まりません。ロールアップ、要約、特殊なケースなどがたくさんあるので、レポートの一部を個別のクエリでフェッチする方が簡単で効率的です。同様に、手続き型言語では、すべての計算を1行のコードで、または1つの関数で(うまくいけば)実行しようとはしません。

一部のレポートツールでは、レポートは単一のクエリから生成されると主張しており、複数のクエリにマージする機会はありません。その場合は、複数のレポートを作成する必要があります(また、上司が1ページにレポートを作成したい場合は、手動で貼り付ける必要があります)。

注文したすべての製品のリスト(製品名付き)、最後の3回の購入日、および最新の注文に関するコメントを取得するのは簡単です。

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で最後の3つの購入の日付を取得する方法を確認したいとおっしゃいました。

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 回のクエリでは解決できなかった可能性があります。いくつかの異なる部分が見えます...

お一人様につき

  1. 注文した全製品のリストを取得する (製品名付き)
  2. 最初の購入年を取得する
  3. 過去 3 回の購入の日付を取得する
  4. 最新の注文に関するコメントを取得
  5. 過去 12 か月間の製品購入の合計を取得する

手順はステップ 1 から 5 で、SQL がデータを取得します。

于 2008-12-03T14:11:02.847 に答える
2

編集:これは、一時テーブルや奇妙なサブサブサブクエリを使用しない、ソリューションに対するまったく新しい見方です。ただし、SQL 2005 以降でのみ機能します。これは、そのバージョンで新しく追加された「ピボット」コマンドを使用するためです。

基本的な問題は、一連の行 (データ内) から出力内の列への望ましいピボットです。この問題について考えているうちに、SQL Server にはこれに対処するための "ピボット" 演算子があることを思い出しました。

これは、Northwind サンプル データを使用して、SQL 2005でのみ機能します。

-- 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 に答える
2

私にはデータ ウェアハウス プロジェクトのように思えます。「最近の 3 つのこと」や「過去 12 か月間の合計」などの情報が必要な場合は、それらを保存します。つまり、非正規化します。

于 2008-12-03T16:14:24.283 に答える
1

SQL クエリは、行と列の単一の「フラット」テーブルの形式で結果を返します。多くの場合、レポート要件はこれよりも複雑で、例のような「ギザギザ」の結果セットが必要です。このような要件を解決するために「手続き的に」行ったり、データベースの最上位にあるレポート ツールを使用したりすることに問題はありません。ただし、データベースから最高のパフォーマンスを得るには、可能な限り SQL を使用する必要があります。

于 2008-12-03T14:09:23.817 に答える