كيفية الانضمام إلى أحدث الصفوف من الجدول؟

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

  •  03-07-2019
  •  | 
  •  

سؤال

كثيرًا ما أواجه مشكلات من هذا النموذج ولم أجد حلاً جيدًا بعد:

افترض أن لدينا جدولين في قاعدة البيانات يمثلان نظام التجارة الإلكترونية.

userData (userId, name, ...)
orderData (orderId, userId, orderType, createDate, ...)

بالنسبة لجميع المستخدمين في النظام، حدد معلومات المستخدم الخاصة بهم، وأحدث معلومات الطلب الخاصة بهم بالنوع = '1'، ومعلومات الطلب الأحدث الخاصة بهم بالنوع = '2'.أريد أن أفعل ذلك في استعلام واحد.هنا نتيجة المثال:

(userId, name, ..., orderId1, orderType1, createDate1, ..., orderId2, orderType2, createDate2, ...)
(101, 'Bob', ..., 472, '1', '4/25/2008', ..., 382, '2', '3/2/2008', ...)
هل كانت مفيدة؟

المحلول

من المفترض أن ينجح هذا، سيتعين عليك ضبط أسماء الجدول/الأعمدة:

select ud.name,
       order1.order_id,
       order1.order_type,
       order1.create_date,
       order2.order_id,
       order2.order_type,
       order2.create_date
  from user_data ud,
       order_data order1,
       order_data order2
 where ud.user_id = order1.user_id
   and ud.user_id = order2.user_id
   and order1.order_id = (select max(order_id)
                            from order_data od1
                           where od1.user_id = ud.user_id
                             and od1.order_type = 'Type1')
   and order2.order_id = (select max(order_id)
                             from order_data od2
                            where od2.user_id = ud.user_id
                              and od2.order_type = 'Type2')

قد يكون إلغاء تسوية بياناتك فكرة جيدة أيضًا.سيكون القيام بهذا النوع من الأشياء مكلفًا إلى حد ما.لذا يمكنك إضافة أ last_order_date إلى بيانات المستخدم الخاصة بك.

نصائح أخرى

لقد قدمت ثلاث طرق مختلفة لحل هذه المشكلة:

  1. باستخدام المحاور
  2. استخدام بيانات الحالة
  3. استخدام الاستعلامات المضمنة في جملة حيث

تفترض جميع الحلول أننا نحدد الترتيب "الأحدث" بناءً على orderId عمود.باستخدام createDate سيضيف العمود تعقيدًا بسبب تضارب الطوابع الزمنية ويعيق الأداء بشكل خطير منذ ذلك الحين createDate ربما لا يكون جزءًا من المفتاح المفهرس.لقد قمت باختبار هذه الاستعلامات فقط باستخدام MS SQL Server 2005، لذلك ليس لدي أي فكرة عما إذا كانت ستعمل على الخادم الخاص بك.

يؤدي الحلان (1) و(2) أداءً متطابقًا تقريبًا.في الواقع، كلاهما يؤدي إلى نفس عدد القراءات من قاعدة البيانات.

الحل (3) هو لا النهج المفضل عند العمل مع مجموعات البيانات الكبيرة.فهو يقوم باستمرار بإجراء مئات من القراءات المنطقية أكثر من (1) و(2).عند التصفية لمستخدم واحد محدد، يكون الأسلوب (3) مشابهًا للطرق الأخرى.في حالة المستخدم الفردي، يساعد الانخفاض في وقت وحدة المعالجة المركزية في مواجهة العدد الأعلى بشكل ملحوظ من عمليات القراءة؛ومع ذلك، عندما يصبح محرك الأقراص أكثر انشغالًا وتحدث أخطاء في ذاكرة التخزين المؤقت، ستختفي هذه الميزة البسيطة.

خاتمة

بالنسبة للسيناريو المعروض، استخدم النهج المحوري إذا كان مدعومًا بواسطة نظام إدارة قواعد البيانات (DBMS) الخاص بك.يتطلب رمزًا أقل من بيان الحالة ويبسط إضافة أنواع الطلبات في المستقبل.

يرجى ملاحظة أنه في بعض الحالات، لا تتمتع PIVOT بالمرونة الكافية وأن وظائف القيمة المميزة التي تستخدم بيانات الحالة هي الحل الأمثل.

شفرة

النهج (1) باستخدام PIVOT:

select 
    ud.userId, ud.fullname, 
    od1.orderId as orderId1, od1.createDate as createDate1, od1.orderType as orderType1,
    od2.orderId as orderId2, od2.createDate as createDate2, od2.orderType as orderType2

from userData ud
    inner join (
            select userId, [1] as typeOne, [2] as typeTwo
            from (select
                userId, orderType, orderId
            from orderData) as orders
            PIVOT
            (
                max(orderId)
                FOR orderType in ([1], [2])
            ) as LatestOrders) as LatestOrders on
        LatestOrders.userId = ud.userId 
    inner join orderData od1 on
        od1.orderId = LatestOrders.typeOne
    inner join orderData od2 on
        od2.orderId = LatestOrders.typeTwo

النهج (2) باستخدام بيانات الحالة:

select 
    ud.userId, ud.fullname, 
    od1.orderId as orderId1, od1.createDate as createDate1, od1.orderType as orderType1,
    od2.orderId as orderId2, od2.createDate as createDate2, od2.orderType as orderType2

from userData ud 
    -- assuming not all users will have orders use outer join
    inner join (
            select 
                od.userId,
                -- can be null if no orders for type
                max (case when orderType = 1 
                        then ORDERID
                        else null
                        end) as maxTypeOneOrderId,

                -- can be null if no orders for type
                max (case when orderType = 2
                        then ORDERID 
                        else null
                        end) as maxTypeTwoOrderId
            from orderData od
            group by userId) as maxOrderKeys on
        maxOrderKeys.userId = ud.userId
    inner join orderData od1 on
        od1.ORDERID = maxTypeTwoOrderId
    inner join orderData od2 on
        OD2.ORDERID = maxTypeTwoOrderId

النهج (3) باستخدام الاستعلامات المضمنة في جملة المكان (استنادًا إلى استجابة Steve K.):

select  ud.userId,ud.fullname, 
        order1.orderId, order1.orderType, order1.createDate, 
        order2.orderId, order2.orderType, order2.createDate
  from userData ud,
       orderData order1,
       orderData order2
 where ud.userId = order1.userId
   and ud.userId = order2.userId
   and order1.orderId = (select max(orderId)
                            from orderData od1
                           where od1.userId = ud.userId
                             and od1.orderType = 1)
   and order2.orderId = (select max(orderId)
                             from orderData od2
                            where od2.userId = ud.userId
                              and od2.orderType = 2)

برنامج نصي لإنشاء الجداول و1000 مستخدم مع 100 طلب لكل منهم:

CREATE TABLE [dbo].[orderData](
    [orderId] [int] IDENTITY(1,1) NOT NULL,
    [createDate] [datetime] NOT NULL,
    [orderType] [tinyint] NOT NULL, 
    [userId] [int] NOT NULL
) 

CREATE TABLE [dbo].[userData](
    [userId] [int] IDENTITY(1,1) NOT NULL,
    [fullname] [nvarchar](50) NOT NULL
) 

-- Create 1000 users with 100 order each
declare @userId int
declare @usersAdded int
set @usersAdded = 0

while @usersAdded < 1000
begin
    insert into userData (fullname) values ('Mario' + ltrim(str(@usersAdded)))
    set @userId = @@identity

    declare @orderSetsAdded int
    set @orderSetsAdded = 0
    while @orderSetsAdded < 10
    begin
        insert into orderData (userId, createDate, orderType) 
            values ( @userId, '01-06-08', 1)
        insert into orderData (userId, createDate, orderType) 
            values ( @userId, '01-02-08', 1)
        insert into orderData (userId, createDate, orderType) 
            values ( @userId, '01-08-08', 1)
        insert into orderData (userId, createDate, orderType) 
            values ( @userId, '01-09-08', 1)
        insert into orderData (userId, createDate, orderType) 
            values ( @userId, '01-01-08', 1)
        insert into orderData (userId, createDate, orderType) 
            values ( @userId, '01-06-06', 2)
        insert into orderData (userId, createDate, orderType) 
            values ( @userId, '01-02-02', 2)
        insert into orderData (userId, createDate, orderType) 
            values ( @userId, '01-08-09', 2)
        insert into orderData (userId, createDate, orderType) 
            values ( @userId, '01-09-01', 2)
        insert into orderData (userId, createDate, orderType) 
            values ( @userId, '01-01-04', 2)

        set @orderSetsAdded = @orderSetsAdded + 1
    end
    set @usersAdded = @usersAdded + 1
end

مقتطف صغير لاختبار أداء الاستعلام على MS SQL Server بالإضافة إلى ملف تعريف SQL:

-- Uncomment these to clear some caches
--DBCC DROPCLEANBUFFERS
--DBCC FREEPROCCACHE

set statistics io on
set statistics time on

-- INSERT TEST QUERY HERE

set statistics time off
set statistics io off

آسف، ليس لدي أوراكل أمامي، ولكن هذا هو الهيكل الأساسي لما سأفعله في أوراكل:

SELECT b.user_id, b.orderid, b.orderType, b.createDate, <etc>,
       a.name
FROM orderData b, userData a
WHERE a.userid = b.userid
AND (b.userid, b.orderType, b.createDate) IN (
  SELECT userid, orderType, max(createDate) 
  FROM orderData 
  WHERE orderType IN (1,2)
  GROUP BY userid, orderType) 

نموذج حل T-SQL (MS SQL):

SELECT
    u.*
    , o1.*
    , o2.* 
FROM
(
    SELECT
        , userData.*
        , (SELECT TOP 1 orderId.url FROM orderData WHERE orderData.userId=userData.userId AND orderType=1 ORDER BY createDate DESC)
            AS order1Id
        , (SELECT TOP 1 orderId.url FROM orderData WHERE orderData.userId=userData.userId AND orderType=2 ORDER BY createDate DESC)
            AS order2Id
    FROM userData
) AS u
LEFT JOIN orderData o1 ON (u.order1Id=o1.orderId)
LEFT JOIN orderData o2 ON (u.order2Id=o2.orderId)

في SQL 2005، يمكنك أيضًا استخدام الدالة RANK ( ) OVER.(لكن AFAIK هي ميزة خاصة بـ MSSQL تمامًا)

قد تتمكن من إجراء استعلام موحد لهذا الغرض.يحتاج بناء الجملة الدقيق إلى بعض العمل، خاصة المجموعة حسب القسم، ولكن يجب أن يكون الاتحاد قادرًا على القيام بذلك.

على سبيل المثال:

SELECT orderId, orderType, createDate
FROM orderData
WHERE type=1 AND MAX(createDate)
GROUP BY orderId, orderType, createDate

UNION

SELECT orderId, orderType, createDate
FROM orderData
WHERE type=2 AND MAX(createDate)
GROUP BY orderId, orderType, createDate

أحدثهم تقصد كل جديد في اليوم الحالي؟يمكنك دائمًا التحقق من تاريخ الإنشاء الخاص بك والحصول على جميع بيانات المستخدم والطلب إذا كان تاريخ الإنشاء >= اليوم الحالي.

SELECT * FROM
"orderData", "userData"
WHERE
"userData"."userId"  ="orderData"."userId"
AND "orderData".createDate >= current_date;

محدث

وهذا ما تريده بعد تعليقك هنا:

SELECT * FROM
"orderData", "userData"
WHERE
"userData"."userId"  ="orderData"."userId"
AND "orderData".type = '1'
AND "orderData"."orderId" = (
SELECT "orderId" FROM "orderData"
WHERE 
"orderType" = '1'
ORDER "orderId" DESC
LIMIT 1

)

أستخدم أشياء مثل هذه في MySQL:

SELECT
   u.*,
   SUBSTRING_INDEX( MAX( CONCAT( o1.createDate, '##', o1.otherfield)), '##', -1) as o2_orderfield,
   SUBSTRING_INDEX( MAX( CONCAT( o2.createDate, '##', o2.otherfield)), '##', -1) as o2_orderfield
FROM
   userData as u
   LEFT JOIN orderData AS o1 ON (o1.userId=u.userId AND o1.orderType=1)
   LEFT JOIN orderData AS o2 ON (o1.userId=u.userId AND o2.orderType=2)
GROUP BY u.userId

باختصار، استخدم MAX() للحصول على الأحدث، عن طريق إضافة حقل المعايير (تاريخ الإنشاء) إلى الحقل (الحقول) المثيرة للاهتمام (حقل آخر).SUBSTRING_INDEX() ثم يزيل التاريخ.

OTOH، إذا كنت بحاجة إلى عدد عشوائي من الطلبات (إذا كان userType يمكن أن يكون أي رقم، وليس ENUM محدودًا)؛فمن الأفضل التعامل مع استعلام منفصل، شيء من هذا القبيل:

select * from orderData where userId=XXX order by orderType, date desc group by orderType

لكل مستخدم.

بافتراض أن معرف الطلب يزداد رتابة مع مرور الوقت:

SELECT *
FROM userData u
INNER JOIN orderData o
  ON o.userId = u.userId
INNER JOIN ( -- This subquery gives the last order of each type for each customer
  SELECT MAX(o2.orderId)
    --, o2.userId -- optional - include if joining for a particular customer
    --, o2.orderType -- optional - include if joining for a particular type
  FROM orderData o2
  GROUP BY o2.userId
    ,o2.orderType
) AS LastOrders
  ON LastOrders.orderId = o.orderId -- expand join to include customer or type if desired

ثم قم بالتركيز على العميل أو إذا كنت تستخدم SQL Server، فهناك وظيفة PIVOT

فيما يلي إحدى الطرق لنقل بيانات النوع 1 و2 إلى نفس الصف:
(عن طريق وضع معلومات النوع 1 والنوع 2 في التحديدات الخاصة بهم والتي يتم استخدامها بعد ذلك في جملة from.)

SELECT
  a.name, ud1.*, ud2.*
FROM
    userData a,
    (SELECT user_id, orderid, orderType, reateDate, <etc>,
    FROM orderData b
    WHERE (userid, orderType, createDate) IN (
      SELECT userid, orderType, max(createDate) 
      FROM orderData 
      WHERE orderType = 1
      GROUP BY userid, orderType) ud1,
    (SELECT user_id, orderid, orderType, createDate, <etc>,
    FROM orderData 
    WHERE (userid, orderType, createDate) IN (
      SELECT userid, orderType, max(createDate) 
      FROM orderData 
      WHERE orderType = 2
      GROUP BY userid, orderType) ud2

وإليك كيف أفعل ذلك.هذا هو SQL القياسي ويعمل في أي نوع من قواعد البيانات.

SELECT u.userId, u.name, o1.orderId, o1.orderType, o1.createDate,
  o2.orderId, o2.orderType, o2.createDate
FROM userData AS u
  LEFT OUTER JOIN (
    SELECT o1a.orderId, o1a.userId, o1a.orderType, o1a.createDate
    FROM orderData AS o1a 
      LEFT OUTER JOIN orderData AS o1b ON (o1a.userId = o1b.userId 
        AND o1a.orderType = o1b.orderType AND o1a.createDate < o1b.createDate)
    WHERE o1a.orderType = 1 AND o1b.orderId IS NULL) AS o1 ON (u.userId = o1.userId)
  LEFT OUTER JOIN (
    SELECT o2a.orderId, o2a.userId, o2a.orderType, o2a.createDate
    FROM orderData AS o2a 
      LEFT OUTER JOIN orderData AS o2b ON (o2a.userId = o2b.userId 
        AND o2a.orderType = o2b.orderType AND o2a.createDate < o2b.createDate)
    WHERE o2a.orderType = 2 AND o2b.orderId IS NULL) o2 ON (u.userId = o2.userId);

لاحظ أنه إذا كان لديك طلبات متعددة من أي نوع تكون تواريخها مساوية لأحدث تاريخ، فستحصل على صفوف متعددة في مجموعة النتائج.إذا كان لديك أوامر متعددة من كلا النوعين، فستحصل على صفوف N x M في مجموعة النتائج.لذا أوصي بإحضار صفوف كل نوع في استعلامات منفصلة.

ستيف ك على حق تمامًا، شكرًا!لقد قمت بإعادة كتابة إجابته قليلاً لمراعاة حقيقة أنه قد لا يكون هناك طلب لنوع معين (والذي فشلت في ذكره، لذلك لا أستطيع أن أخطئ في Steve K.)

إليك ما انتهيت من استخدامه:

select ud.name,
       order1.orderId,
       order1.orderType,
       order1.createDate,
       order2.orderId,
       order2.orderType,
       order2.createDate
  from userData ud
  left join orderData order1
   on order1.orderId = (select max(orderId)
                            from orderData od1
                           where od1.userId = ud.userId
                             and od1.orderType = '1')
  left join orderData order2
   on order2.orderId = (select max(orderId)
                            from orderData od2
                           where od2.userId = ud.userId
                             and od2.orderType = '2')
 where ...[some limiting factors on the selection of users]...;
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top