كيفية الانضمام إلى أحدث الصفوف من الجدول؟
سؤال
كثيرًا ما أواجه مشكلات من هذا النموذج ولم أجد حلاً جيدًا بعد:
افترض أن لدينا جدولين في قاعدة البيانات يمثلان نظام التجارة الإلكترونية.
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
إلى بيانات المستخدم الخاصة بك.
نصائح أخرى
لقد قدمت ثلاث طرق مختلفة لحل هذه المشكلة:
- باستخدام المحاور
- استخدام بيانات الحالة
- استخدام الاستعلامات المضمنة في جملة حيث
تفترض جميع الحلول أننا نحدد الترتيب "الأحدث" بناءً على 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]...;