تجميع السجلات المجاورة فقط باستخدام T-SQL
-
04-07-2019 - |
سؤال
لدي (مبسط على سبيل المثال) جدول يحتوي على البيانات التالية
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-04 01 38
4 2008-10-04 2008-10-05 01 23
5 2008-10-05 2008-10-06 03 14
6 2008-10-06 2008-10-07 02 3
7 2008-10-07 2008-10-08 02 8
8 2008-10-08 2008-11-08 03 19
تمثل التواريخ فترة زمنية، والمعرف هو الحالة التي كان عليها النظام خلال تلك الفترة، والمبلغ عبارة عن قيمة مرتبطة بتلك الحالة.
ما أريد القيام به هو تجميع المبالغ مجاور صفوف مع نفس رقم المعرف، ولكن احتفظ بنفس التسلسل العام بحيث يمكن دمج عمليات التشغيل المتجاورة.وبالتالي أريد أن ينتهي بي الأمر ببيانات مثل:
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-05 01 61
4 2008-10-05 2008-10-06 03 14
5 2008-10-06 2008-10-08 02 11
6 2008-10-08 2008-11-08 03 19
أنا أبحث عن حل T-SQL يمكن وضعه في SP، ولكن لا يمكنني رؤية كيفية القيام بذلك من خلال الاستعلامات البسيطة.أظن أن الأمر قد يتطلب تكرارًا من نوع ما ولكني لا أريد السير في هذا المسار.
السبب وراء رغبتي في القيام بهذا التجميع هو أن الخطوة التالية في العملية هي إجراء SUM() و Count() مجمعة بواسطة المعرفات الفريدة التي تحدث ضمن التسلسل، بحيث تبدو بياناتي النهائية كما يلي:
ID Counts Total
-- ------ -----
01 2 71
02 2 31
03 2 33
ولكن إذا قمت بعمل بسيط
SELECT COUNT(ID), SUM(Amount) FROM data GROUP BY ID
على الطاولة الأصلية أحصل على شيء من هذا القبيل
ID Counts Total
-- ------ -----
01 3 71
02 3 31
03 2 33
وهذا ليس ما أريد.
لا يوجد حل صحيح
نصائح أخرى
إذا قرأت كتاب "تطوير تطبيقات قواعد البيانات الموجهة نحو الوقت في SQL" من تأليف آر تي سنودجراس (يتوفر ملف pdf الخاص به على موقع الويب الخاص به ضمن المنشورات)، وتصل إلى الشكل 6.25 في الصفحات 165-166، وسوف تجد SQL غير التافه الذي يمكن استخدامه في المثال الحالي لتجميع الصفوف المختلفة مع نفس قيمة المعرف والفواصل الزمنية المستمرة.
تطوير الاستعلام أدناه قريب من التصحيح، ولكن هناك مشكلة تم رصدها في النهاية، مصدرها في عبارة SELECT الأولى.لم أتتبع بعد سبب إعطاء الإجابة غير الصحيحة. [إذا كان بإمكان شخص ما اختبار SQL على نظام إدارة قواعد البيانات (DBMS) الخاص به وإخباري ما إذا كان الاستعلام الأول يعمل بشكل صحيح هناك، فسيكون ذلك مفيدًا جدًا!]
يبدو شيء مثل:
-- Derived from Figure 6.25 from Snodgrass "Developing Time-Oriented
-- Database Applications in SQL"
CREATE TABLE Data
(
Start DATE,
Finish DATE,
ID CHAR(2),
Amount INT
);
INSERT INTO Data VALUES('2008-10-01', '2008-10-02', '01', 10);
INSERT INTO Data VALUES('2008-10-02', '2008-10-03', '02', 20);
INSERT INTO Data VALUES('2008-10-03', '2008-10-04', '01', 38);
INSERT INTO Data VALUES('2008-10-04', '2008-10-05', '01', 23);
INSERT INTO Data VALUES('2008-10-05', '2008-10-06', '03', 14);
INSERT INTO Data VALUES('2008-10-06', '2008-10-07', '02', 3);
INSERT INTO Data VALUES('2008-10-07', '2008-10-08', '02', 8);
INSERT INTO Data VALUES('2008-10-08', '2008-11-08', '03', 19);
SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)));
الإخراج من هذا الاستعلام هو:
01 2008-10-01 2008-10-02
01 2008-10-03 2008-10-05
02 2008-10-02 2008-10-03
02 2008-10-06 2008-10-08
03 2008-10-05 2008-10-06
03 2008-10-05 2008-11-08
03 2008-10-08 2008-11-08
تم تحريره:هناك مشكلة في الصف قبل الأخير - لا ينبغي أن يكون موجودًا.وليس واضحًا (حتى الآن) من أين يأتي.
نحتاج الآن إلى التعامل مع هذا التعبير المعقد كتعبير استعلام في عبارة FROM الخاصة بعبارة SELECT أخرى، والتي ستجمع قيم المبلغ لمعرف معين على الإدخالات التي تتداخل مع النطاقات القصوى الموضحة أعلاه.
SELECT M.ID, M.Start, M.Finish, SUM(D.Amount)
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)))) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
ORDER BY M.ID, M.Start;
هذا يعطي:
ID Start Finish Amount
01 2008-10-01 2008-10-02 10
01 2008-10-03 2008-10-05 61
02 2008-10-02 2008-10-03 20
02 2008-10-06 2008-10-08 11
03 2008-10-05 2008-10-06 14
03 2008-10-05 2008-11-08 33 -- Here be trouble!
03 2008-10-08 2008-11-08 19
تم تحريره:هذا هو بالكاد مجموعة البيانات الصحيحة التي سيتم من خلالها إجراء تجميع COUNT وSUM المطلوب بواسطة السؤال الأصلي، وبالتالي فإن الإجابة النهائية هي:
SELECT I.ID, COUNT(*) AS Number, SUM(I.Amount) AS Amount
FROM (SELECT M.ID, M.Start, M.Finish, SUM(D.Amount) AS Amount
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS
(SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish) OR
(T2.Start <= L.Finish AND L.Finish < T2.Finish)))
) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
) AS I
GROUP BY I.ID
ORDER BY I.ID;
id number amount
01 2 71
02 2 31
03 3 66
مراجعة:أوه!Drat... الإدخال لـ 3 يحتوي على ضعف "المبلغ" الذي يجب أن يحتوي عليه.تشير الأجزاء "المعدلة" السابقة إلى أين بدأت الأمور تسوء.يبدو كما لو أن الاستعلام الأول خاطئ تمامًا (ربما كان مخصصًا لسؤال مختلف)، أو أن المُحسِّن الذي أعمل معه يسيء التصرف.ومع ذلك، ينبغي أن تكون هناك إجابة وثيقة الصلة بهذا الأمر تعطي القيم الصحيحة.
للسجل:تم اختباره على IBM Informix Dynamic Server 11.50 على Solaris 10.ومع ذلك، يجب أن تعمل بشكل جيد على أي نظام إدارة قواعد بيانات SQL آخر متوافق مع المعايير القياسية.
وربما تحتاج إلى إنشاء مؤشر وحلقة من خلال النتائج، التي تتبع معرف كنت تعمل مع وتراكم البيانات على طول الطريق. عندما يتغير معرف يمكنك إدراج البيانات المتراكمة في جدول مؤقت والعودة الجدول في نهاية الإجراء (تحديد كافة من ذلك). وظيفة على أساس الجدول قد يكون أفضل ما يمكنك ثم إدراج فقط في الجدول المقابل كما تذهب على طول.
وأظن أنه قد تتطلب التكرار من نوع ما لكنني لا أريد أن تسلك هذا المسار.
اقتباس فقرة>وأعتقد أن هذا هو الطريق عليك أن تتخذ، استخدام المؤشر لتجميع متغير الجدول. إذا كان لديك عدد كبير من السجلات التي يمكن استخدام جدول دائمة لتخزين النتائج ثم عند الحاجة إلى استرداد البيانات التي يمكن معالجة فقط البيانات الجديدة.
وأود أن أضيف حقل قليلا مع الافتراضية 0 إلى الجدول المصدر لتتبع والتي تم تجهيزها السجلات. افتراض عدم وجود واحد هو استخدام حدد * على الطاولة، إضافة عمود مع قيمة افتراضية لن يؤثر على بقية طلبك.
إضافة تعليق على هذا المنصب اذا كنت تريد مساعدة الترميز الحل.
وحسنا قررت السير في طريق التكرار باستخدام مزيج من الصلات والمؤشرات. من خلال الانضمام إلى جدول البيانات على نفسه يمكنني إنشاء قائمة الارتباط من تلك السجلات فقط التي هي على التوالي.
INSERT INTO #CONSEC
SELECT a.ID, a.Start, b.Finish, b.Amount
FROM Data a JOIN Data b
ON (a.Finish = b.Start) AND (a.ID = b.ID)
وبعد ذلك يمكنني استرخاء القائمة عن طريق بالتكرار أكثر من ذلك مع المؤشر، والقيام التحديثات إلى جدول البيانات إلى ضبط (وحذف السجلات الآن غريبة من جدول البيانات)
DECLARE CCursor CURSOR FOR
SELECT ID, Start, Finish, Amount FROM #CONSEC ORDER BY Start DESC
@Total = 0
OPEN CCursor
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
WHILE @FETCH_STATUS = 0
BEGIN
@Total = @Total + @Amount
@Start_Last = @Start
@Finish_Last = @Finish
@ID_Last = @ID
DELETE FROM Data WHERE Start = @Finish
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
IF (@ID_Last<> @ID) OR (@Finish<>@Start_Last)
BEGIN
UPDATE Data
SET Amount = Amount + @Total
WHERE Start = @Start_Last
@Total = 0
END
END
CLOSE CCursor
DEALLOCATE CCursor
وهذا كله يعمل ولديه أداء مقبول للبيانات النموذجية التي أستخدمه.
وأنا لم تجد قضية واحدة صغيرة مع رمز أعلاه. أصلا كنت تحديث جدول البيانات في كل حلقة من خلال المؤشر. ولكن لم يكن هذا العمل. يبدو أنك لا يمكن الا ان تحديث واحد على المحضر، وأن تحديثات متعددة (من أجل الحفاظ على إضافة البيانات) تعود مرة أخرى إلى قراءة محتويات الأصلي من السجل.