متى تتخلى عن العمليات المحددة في SQL وتنتقل إلى الإجراءات؟
-
22-07-2019 - |
سؤال
لقد تم تكليفي ذات مرة بهذه المهمة لأقوم بها في RDBMS:
إعطاء جداول العملاء والطلب وخطوط الطلب والمنتج.يتم كل شيء باستخدام الحقول والعلاقات المعتادة، مع حقل مذكرة التعليق في جدول سطر الطلب.
بالنسبة لعميل واحد، يمكنك الحصول على قائمة بجميع المنتجات التي طلبها العميل على الإطلاق مع اسم المنتج، وسنة الشراء الأولى، وتواريخ آخر ثلاث عمليات شراء، والتعليق على آخر طلب، ومجموع إجمالي الدخل لمجموعة المنتج والعميل تلك خلال 12 شهرًا الماضية.
بعد بضعة أيام تخليت عن القيام بذلك كاستعلام واخترت فقط جلب كل سطر طلب للعميل وكل منتج وتشغيل البيانات من الناحية الإجرائية لإنشاء الجدول المطلوب من جانب العميل.
أنا أعتبر هذا أحد أعراض واحد أو أكثر مما يلي:
- أنا أحمق كسول وكان ينبغي أن أرى كيفية القيام بذلك في SQL
- العمليات المحددة ليست معبرة مثل العمليات الإجرائية
- SQL ليست معبرة كما ينبغي
هل فعلت الشيء الصحيح؟هل كان لدي خيارات أخرى؟
المحلول
من المؤكد أنك يجب أن تكون قادرًا على القيام بهذا التمرين دون القيام بالعمل المعادل لـ 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;
نصائح أخرى
العمليات المحددة ليست معبرة مثل العمليات الإجرائية
وربما أشبه:"مجموعة العمليات ليست مألوفة مثل العمليات الإجرائية لمطور يستخدم اللغات الإجرائية" ؛-)
يعد القيام بذلك بشكل متكرر كما فعلت الآن أمرًا جيدًا بالنسبة لمجموعات صغيرة من البيانات، ولكنه ببساطة لا يتم قياسه بنفس الطريقة.تعتمد الإجابة على ما إذا كنت قد فعلت الشيء الصحيح على ما إذا كنت راضيًا عن الأداء الآن و/أو لا تتوقع زيادة كمية البيانات كثيرًا.
إذا كان بإمكانك تقديم بعض نماذج التعليمات البرمجية، فقد نتمكن من مساعدتك في العثور على حل قائم على المجموعة، والذي سيكون أسرع في البداية والتوسع بشكل أفضل بكثير.كما ذكر GalacticCowboy، يمكن لتقنيات مثل الجداول المؤقتة أن تساعد في جعل البيانات أكثر قابلية للقراءة مع الاحتفاظ بفوائد الأداء إلى حد كبير.
في معظم أنظمة RDBMS لديك خيار الجداول المؤقتة أو متغيرات الجدول المحلية التي يمكنك استخدامها لتقسيم مهمة مثل هذه إلى أجزاء يمكن التحكم فيها.
لا أرى أي طريقة للقيام بذلك بسهولة كـ أعزب الاستعلام (بدون بعض مقرف الاستعلامات الفرعية)، ولكن لا يزال من المفترض أن يكون ذلك ممكنًا دون الانقطاع عن التعليمات البرمجية الإجرائية، إذا كنت تستخدم الجداول المؤقتة.
ربما لم تكن هذه المشكلة قابلة للحل بواسطة واحد استفسار.أرى عدة أجزاء متميزة..
لعميل واحد
- الحصول على قائمة بجميع المنتجات المطلوبة (مع اسم المنتج)
- احصل على سنة الشراء الأولى
- احصل على تواريخ آخر ثلاث عمليات شراء
- الحصول على تعليق على أحدث النظام
- احصل على مجموع مشتريات المنتجات لآخر 12 شهرًا
الإجراء الخاص بك هو الخطوات من 1 إلى 5 وسيقوم SQL بتزويدك بالبيانات.
يبدو لي وكأنه مشروع مستودع بيانات.إذا كنت بحاجة إلى أشياء مثل "أحدث ثلاثة أشياء" و"مجموع شيء على مدار الـ 12 شهرًا الماضية"، فقم بتخزينها، على سبيل المثال.إزالة التطبيع.
يحرر: يعد هذا حلًا جديدًا تمامًا، دون استخدام جداول مؤقتة أو استعلامات فرعية فرعية غريبة.ومع ذلك، فإنه سيعمل فقط على SQL 2005 أو الإصدارات الأحدث، لأنه يستخدم الأمر "المحوري" الجديد في هذا الإصدار.
المشكلة الأساسية هي المحور المطلوب من مجموعة الصفوف (في البيانات) إلى الأعمدة في الإخراج.أثناء التفكير في هذه المشكلة، تذكرت أن 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
تقوم استعلامات SQL بإرجاع النتائج في شكل جدول واحد "مسطح" من الصفوف والأعمدة.غالبًا ما تكون متطلبات إعداد التقارير أكثر تعقيدًا من ذلك، حيث تتطلب مجموعة من النتائج "المتعرجة" مثل المثال الذي قدمته.ولا حرج في "اتخاذ الإجراءات الإجرائية" لحل هذه المتطلبات، أو استخدام أداة إعداد التقارير الموجودة أعلى قاعدة البيانات.ومع ذلك، يجب عليك استخدام SQL قدر الإمكان للحصول على أفضل أداء من قاعدة البيانات.