
يعمل نظام إدارة قواعد البيانات (DBMS) في هذا الصدد وفقًا لنفس المبادئ - " قالوا للحفر وأنا أحفر ". لا يمكن لطلبك فقط إبطاء العمليات المجاورة ، مما يؤدي إلى احتلال موارد المعالج باستمرار ، ولكن أيضًا "إسقاط" قاعدة البيانات بالكامل ، مما يؤدي إلى "أكل" كل الذاكرة المتاحة. لذلك ، تقع على عاتق المطور مسؤولية الحماية من التكرار اللانهائي .
في PostgreSQL ،
WITH RECURSIVEظهرت القدرة على استخدام الاستعلامات التكرارية منذ زمن بعيد في الإصدار 8.4 ، ولكن لا يزال بإمكانك مواجهة استعلامات "عديمة الدفاع" يحتمل أن تكون ضعيفة. كيف تنقذ نفسك من هذه الأنواع من المشاكل؟
لا تكتب استفسارات متكررة
والكتابة غير العودية. مع احترامك K.O.في الواقع ، توفر PostgreSQL قدرًا كبيرًا من الوظائف التي يمكنك استخدامها لتجنب التكرار.
استخدم نهجًا مختلفًا تمامًا للمهمة
في بعض الأحيان يمكنك فقط إلقاء نظرة على المشكلة "من الجانب الآخر". أعطيت مثالاً لمثل هذا الموقف في مقالة "SQL HowTo: 1000 وطريقة واحدة للتجميع" - ضرب مجموعة من الأرقام دون استخدام وظائف التجميع المخصصة:
WITH RECURSIVE src AS (
SELECT '{2,3,5,7,11,13,17,19}'::integer[] arr
)
, T(i, val) AS (
SELECT
1::bigint
, 1
UNION ALL
SELECT
i + 1
, val * arr[i]
FROM
T
, src
WHERE
i <= array_length(arr, 1)
)
SELECT
val
FROM
T
ORDER BY --
i DESC
LIMIT 1;
يمكن استبدال هذا الطلب بمتغير من خبراء في الرياضيات:
WITH src AS (
SELECT unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime
)
SELECT
exp(sum(ln(prime)))::integer val
FROM
src;
استخدم create_series بدلا من الحلقات
لنفترض أننا نواجه مهمة إنشاء كل البادئات الممكنة لسلسلة نصية
'abcdefgh':
WITH RECURSIVE T AS (
SELECT 'abcdefgh' str
UNION ALL
SELECT
substr(str, 1, length(str) - 1)
FROM
T
WHERE
length(str) > 1
)
TABLE T;
بالضبط، هل تحتاجون إلى العودية هنا .. إذا كنت تستخدم؟
LATERALو generate_series، ثم ليست هناك حاجة حتى CTEs:
SELECT
substr(str, 1, ln) str
FROM
(VALUES('abcdefgh')) T(str)
, LATERAL(
SELECT generate_series(length(str), 1, -1) ln
) X;
تغيير هيكل قاعدة البيانات
على سبيل المثال ، لديك جدول مشاركات المنتدى به روابط إلى من أجاب على شخص ما أو موضوع على إحدى الشبكات الاجتماعية :
CREATE TABLE message(
message_id
uuid
PRIMARY KEY
, reply_to
uuid
REFERENCES message
, body
text
);
CREATE INDEX ON message(reply_to);

حسنًا ، يبدو الطلب المعتاد لتنزيل جميع الرسائل في موضوع واحد مثل هذا:
WITH RECURSIVE T AS (
SELECT
*
FROM
message
WHERE
message_id = $1
UNION ALL
SELECT
m.*
FROM
T
JOIN
message m
ON m.reply_to = T.message_id
)
TABLE T;
ولكن نظرًا لأننا نحتاج دائمًا إلى الموضوع بأكمله من رسالة الجذر ، فلماذا لا نضيف معرفه إلى كل منشور تلقائيًا؟
--
ALTER TABLE message
ADD COLUMN theme_id uuid;
CREATE INDEX ON message(theme_id);
--
CREATE OR REPLACE FUNCTION ins() RETURNS TRIGGER AS $$
BEGIN
NEW.theme_id = CASE
WHEN NEW.reply_to IS NULL THEN NEW.message_id --
ELSE ( -- ,
SELECT
theme_id
FROM
message
WHERE
message_id = NEW.reply_to
)
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER ins BEFORE INSERT
ON message
FOR EACH ROW
EXECUTE PROCEDURE ins();

الآن يمكن اختزال الاستعلام العودي بالكامل إلى هذا فقط:
SELECT
*
FROM
message
WHERE
theme_id = $1;
استخدام "القيود" المطبقة
إذا لم نتمكن من تغيير بنية قاعدة البيانات لسبب ما ، فلنرى ما يمكننا الاعتماد عليه حتى لا يؤدي وجود خطأ في البيانات إلى تكرار لا نهائي.
العودية "عمق" عداد
نقوم فقط بزيادة العداد بمقدار واحد في كل خطوة من العودية حتى يتم الوصول إلى الحد الأقصى ، والذي نعتبره غير كافٍ بشكل واضح:
WITH RECURSIVE T AS (
SELECT
0 i
...
UNION ALL
SELECT
i + 1
...
WHERE
T.i < 64 --
)
المؤيد: عندما نحاول التكرار ، فإننا لن نجعل أكثر من الحد المعين للتكرار "في العمق".
كونترا: ليس هناك ما يضمن أننا لن نعالج نفس السجل مرة أخرى - على سبيل المثال ، في أعماق 15 و 25 ، وبعد ذلك سيكون هناك كل +10. ولم يعد أحد بأي شيء عن "الاتساع".
بشكل رسمي ، لن يكون مثل هذا التكرار غير محدود ، ولكن إذا زاد عدد السجلات في كل خطوة أضعافًا مضاعفة ، فنحن جميعًا نعرف جيدًا كيف ينتهي ...

وصي "الطريق"
نضيف واحدًا تلو الآخر جميع معرفات الكائنات التي واجهناها على طول مسار العودية في مصفوفة ، وهو "مسار" فريد لها:
WITH RECURSIVE T AS (
SELECT
ARRAY[id] path
...
UNION ALL
SELECT
path || id
...
WHERE
id <> ALL(T.path) --
)
المؤيد: إذا كانت هناك حلقة في البيانات ، فلن نعيد معالجة نفس السجل في نفس المسار.
كونترا: ولكن في نفس الوقت يمكننا تجاوز ، حرفيا ، جميع السجلات دون تكرار.

تحديد طول المسار
لتجنب حالة "الشرود" العودية على عمق غير مفهوم ، يمكننا الجمع بين الطريقتين السابقتين. أو ، إذا كنا لا نريد دعم الحقول غير الضرورية ، فقم بتكملة شرط استمرار العودية بتقدير لطول المسار:
WITH RECURSIVE T AS (
SELECT
ARRAY[id] path
...
UNION ALL
SELECT
path || id
...
WHERE
id <> ALL(T.path) AND
array_length(T.path, 1) < 10
)
اختر الطريقة التي تناسب ذوقك!