SQLでの集合演算をいつ放棄して手続き型になりますか?
-
22-07-2019 - |
質問
かつて、RDBMSで行うこのタスクを与えられました:
顧客、注文、注文ライン、および製品の表。オーダーラインテーブルのコメントメモフィールドを使用して、通常のフィールドと関係で行われたすべて。
1人の顧客が、顧客がこれまでに注文したすべての製品のリストを、製品名、最初の購入年、最後の3つの購入日、最後の注文のコメント、最後の12の製品と顧客の組み合わせの合計収入の合計で取得します月。
数日後、クエリとしてそれをやめ、顧客のすべてのオーダーラインとすべての製品を取得し、必要なテーブルのクライアントサイドを構築するためにデータを手続き的に実行することを選択しました。
これは、次の1つ以上の症状であると考えています。
- 私は怠け者のバカです。SQLでそれを行う方法を見たことがあるはずです
- 集合演算は手続き型演算ほど表現力がありません
- SQLは本来あるべき表現力がありません
私は正しいことをしましたか?他のオプションはありましたか
解決
アプリケーションコードの JOIN
と同等の作業を行わずに、つまり、オーダーラインと製品の両方からすべての行をフェッチし、それらを反復処理することで、この演習を確実に実行できるはずです。そのためにSQLウィザードである必要はありません。 JOIN
は、手続き型言語に対するループとはSQLに対するものです-どちらも使用方法を知っておくべき基本的な言語機能です。
人々が陥るOneの1つは、レポート全体を1つのSQLクエリで作成する必要があると考えることです。違います!トニー・アンドリュースが指摘するように、ほとんどのレポートは長方形に収まりません。ロールアップ、要約、特殊なケースなどが多数あるため、レポートの一部を個別のクエリで取得する方が簡単で効率的です。同様に、手続き型言語では、すべての計算を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;
他のヒント
集合演算は手続き型演算ほど表現力がありません
おそらく次のようになります。&quot; Set操作は、手続き言語に使用される開発者にとって手続き的な操作ほど馴染みがありません&quot; ;-)
これまでのように繰り返し実行することは、小さなデータセットには適していますが、単純に同じ方法でスケーリングすることはできません。正しいことをしたかどうかの答えは、現在のパフォーマンスに満足しているかどうか、および/またはデータ量が大幅に増加することを期待していないかどうかによって異なります。
サンプルコードを提供できれば、セットベースのソリューションを見つけるのに役立つ場合があります。 GalacticCowboyが述べたように、一時テーブルなどのテクニックは、パフォーマンスの利点をほぼ維持しながら、ステートメントをはるかに読みやすくするのに役立ちます。
ほとんどのRDBMSには、このようなタスクを管理可能なチャンクに分割するために使用できる一時テーブルまたはローカルテーブル変数のオプションがあります。
(厄介なサブクエリなしで)単一クエリとしてこれを簡単に行う方法はありませんが、手続きにドロップアウトすることなく実行できるはずです一時テーブルを使用する場合のコード。
この問題は、 1つのクエリでは解決できなかった可能性があります。いくつかの異なる部分があります...
1人の顧客向け
- 注文したすべての製品のリストを(製品名とともに)取得します
- 最初の購入年を取得
- 過去3回の購入日を取得
- 最新の注文に関するコメントを取得
- 過去12か月間の製品購入の合計を取得する
手順はステップ1〜5で、SQLがデータを取得します。
データウェアハウスプロジェクトのように聞こえます。 「最近の3つのこと」などが必要な場合および「過去12か月間の何かの合計」その後、それらを保存します。つまり、非正規化します。
編集:これは、一時テーブルや奇妙なサブ-サブ-サブクエリを使用しない、完全に新しいソリューションです。ただし、「ピボット」を使用するため、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
SQLクエリは、単一の&quot; flat&quot;の形式で結果を返します。行と列のテーブル。多くの場合、レポート要件はこれよりも複雑で、「ギザギザ」が要求されます。あなたの例のような結果のセット。 「手続きを進める」ことには何の問題もありません。そのような要件を解決するか、データベースの上にあるレポートツールを使用します。ただし、データベースから最高のパフォーマンスを得るには、可能な限りSQLを使用する必要があります。