متى تتخلى عن العمليات المحددة في SQL وتنتقل إلى الإجراءات؟

StackOverflow https://stackoverflow.com/questions/337175

سؤال

لقد تم تكليفي ذات مرة بهذه المهمة لأقوم بها في 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 لديك خيار الجداول المؤقتة أو متغيرات الجدول المحلية التي يمكنك استخدامها لتقسيم مهمة مثل هذه إلى أجزاء يمكن التحكم فيها.

لا أرى أي طريقة للقيام بذلك بسهولة كـ أعزب الاستعلام (بدون بعض مقرف الاستعلامات الفرعية)، ولكن لا يزال من المفترض أن يكون ذلك ممكنًا دون الانقطاع عن التعليمات البرمجية الإجرائية، إذا كنت تستخدم الجداول المؤقتة.

ربما لم تكن هذه المشكلة قابلة للحل بواسطة واحد استفسار.أرى عدة أجزاء متميزة..

لعميل واحد

  1. الحصول على قائمة بجميع المنتجات المطلوبة (مع اسم المنتج)
  2. احصل على سنة الشراء الأولى
  3. احصل على تواريخ آخر ثلاث عمليات شراء
  4. الحصول على تعليق على أحدث النظام
  5. احصل على مجموع مشتريات المنتجات لآخر 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 قدر الإمكان للحصول على أفضل أداء من قاعدة البيانات.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top