تقريبا. ترجمة. : في هذه المقالة ، يشارك قائد فريق شركة Ticketsolve في المملكة المتحدة حلاً لمشكلته المحددة للغاية ، مع توضيح الأساليب العامة لإنشاء ما يسمى بالوظائف المتراكمة باستخدام ميزات MySQL 8.0 الحديثة. قوائمه واضحة ومزودة بتفسيرات مفصلة ، مما يساعد على فهم جوهر المشكلة ، حتى بالنسبة لأولئك الذين لم يتعمقوا فيها.
تتمثل الإستراتيجية الشائعة لإجراء التحديثات باستخدام الوظائف التراكمية في MySQL في استخدام المتغيرات والنمط المخصص
UPDATE [...] SET mycol = (@myvar := EXPRESSION(@myvar, mycol))
.
هذا النمط لا يعمل بشكل جيد مع المُحسِّن (مما يؤدي إلى سلوك غير حتمي) ، لذلك قرروا التخلي عنه. والنتيجة هي نوع من الفراغ ، لأن المنطق المعقد (نسبيًا) أصبح الآن أكثر صعوبة في التنفيذ (على الأقل بنفس البساطة).
ستناقش المقالة طريقتين لتنفيذه: استخدام وظائف النافذة (النهج المتعارف عليه) واستخدام CTE العودية (تعبيرات الجدول العام).
المتطلبات والخلفية
على الرغم من أن CTEs بديهية إلى حد ما ، بالنسبة لأولئك الذين ليسوا على دراية بها ، أوصي بالإشارة إلى مشاركتي السابقة حول هذا الموضوع .
وينطبق الشيء نفسه على وظائف النافذة: سأعلق على الاستفسارات / المفاهيم بالتفصيل ، لكن الفكرة العامة لا تزال غير مؤذية. يتم تخصيص عدد كبير من الكتب والمنشورات لوظائف النوافذ (وهذا هو السبب في أنني لم أكتب عنها بعد) ؛ ومع ذلك ، في معظم الأمثلة ، يتم إجراء الحسابات إما على النتائج المالية أو على المؤشرات الديموغرافية. ومع ذلك ، في هذه المقالة سأستخدم حالة حقيقية.
من ناحية البرنامج ، أوصي باستخدام MySQL 8.0.19 (ولكن ليس مطلوبًا). يجب تنفيذ جميع التعبيرات في نفس وحدة التحكم لإعادة استخدامها
@venue_id
.
في عالم البرمجيات ، هناك معضلة معمارية معروفة: هل ينبغي تطبيق المنطق على مستوى التطبيق أم على مستوى قاعدة البيانات؟ في حين أن هذا سؤال صحيح تمامًا ، إلا أنني في حالتنا أفترض أن المنطق يجب أن يظل عند المستوى الأساسي ؛ قد يكون السبب في ذلك ، على سبيل المثال ، متطلبات السرعة (كما كان الحال في حالتنا).
مهمة
في هذه المهمة ، نخصص مقاعد في صالة معينة (مسرح).
لأغراض العمل ، يجب تخصيص ما يسمى بـ "التجميع" لكل موقع - وهو رقم إضافي يمثله.
إليك خوارزمية تحديد قيمة التجميع:
- ابدأ من 0 وأعلى اليسار ؛
- إذا كانت هناك مسافة فارغة بين الحالي والسابق ، أو كان هذا صفًا جديدًا ، فإننا نضيف 2 إلى القيمة السابقة (إذا لم يكن هذا هو المكان الأول المطلق) ، وإلا فإننا نزيد القيمة بمقدار 1 ؛
- تعيين مجموعة في مكان ما ؛
- انتقل إلى مكان جديد في نفس الصف أو إلى الصف التالي (إذا انتهى الصف السابق) وكرر من النقطة 2 ؛ نواصل كل شيء حتى تنفد الأماكن.
الخوارزمية في الكود الكاذب:
current_grouping = 0
for each row:
for each number:
if (is_there_a_space_after_last_seat or is_a_new_row) and is_not_the_first_seat:
current_grouping += 2
else
current_grouping += 1
seat.grouping = current_grouping
في الحياة الواقعية ، نريد أن يعطي التكوين الموجود على اليسار القيم الموجودة على اليمين:
x→ 0 1 2 0 1 2
y ╭───┬───┬───╮ ╭───┬───┬───╮
↓ 0 │ x │ x │ │ │ 1 │ 2 │ │
├───┼───┼───┤ ├───┼───┼───┤
1 │ x │ │ x │ │ 4 │ │ 6 │
├───┼───┼───┤ ├───┼───┼───┤
2 │ x │ │ │ │ 8 │ │ │
╰───┴───┴───╯ ╰───┴───┴───╯
تدريب
دع الجدول الأساسي يحتوي على الهيكل البسيط التالي:
CREATE TABLE seats (
id INT AUTO_INCREMENT PRIMARY KEY,
venue_id INT,
y INT,
x INT,
`row` VARCHAR(16),
number INT,
`grouping` INT,
UNIQUE venue_id_y_x (venue_id, y, x)
);
لا نحتاج حقًا إلى الأعمدة
row
و number
. من ناحية أخرى ، لا نريد استخدام جدول يحتوي على سجلاته بالكامل في الفهرس (فقط لنكون أقرب إلى المشاكل الحقيقية).
بناءً على الرسم البياني أعلاه ، فإن إحداثيات كل موقع هي (ص ، س):
- (0 ، 0) ، (0 ، 1)
- (1 ، 0) ، (1 ، 2)
- (20)
لاحظ أننا نستخدم y باعتباره الإحداثي الأول لأنه يسهل تتبع الصفوف.
يجب عليك تحميل عدد كبير من السجلات لمنع المحسن من العثور على مسارات قصيرة غير متوقعة. بالطبع ، نستخدم CTE العودي:
INSERT INTO seats(venue_id, y, x, `row`, number)
WITH RECURSIVE venue_ids (id) AS
(
SELECT 0
UNION ALL
SELECT id + 1 FROM venue_ids WHERE id + 1 < 100000
)
SELECT /*+ SET_VAR(cte_max_recursion_depth = 1M) */
v.id,
c.y, c.x,
CHAR(ORD('A') + FLOOR(RAND() * 3) USING ASCII) `row`,
FLOOR(RAND() * 3) `number`
FROM venue_ids v
JOIN (
VALUES
ROW(0, 0),
ROW(0, 1),
ROW(1, 0),
ROW(1, 2),
ROW(2, 0)
) c (y, x)
;
ANALYZE TABLE seats;
بعض الملاحظات:
- هنا ، يتم استخدام CTE بطريقة مثيرة للاهتمام (نأمل!): تمثل كل حلقة معرفًا للمكان ، ولكن نظرًا لأننا نريد إنشاء مواقع متعددة لكل مكان ، فإننا نقوم بربط مشترك مع الجدول الذي يحتوي على المواقع.
- استخدم مُنشئ الصف من الإصدار v8.0.19 (
VALUES ROW()...
) لتمثيل جدول (قابل للانضمام ) دون إنشائه بالفعل. - يولد قيمًا عشوائية للصف والرقم كعناصر نائبة.
- من أجل البساطة ، لم نقم بأي تحسينات (على سبيل المثال ، أنواع البيانات أوسع من اللازم ؛ تتم إضافة الفهارس قبل إدخال السجلات ، وما إلى ذلك).
النهج القديم
النهج القديم الجيد واضح ومباشر:
SET @venue_id = 5000; -- venue id; () id
SET @grouping = -1;
SET @y = -1;
SET @x = -1;
WITH seat_groupings (id, y, x, `grouping`, tmp_y, tmp_x) AS
(
SELECT
id, y, x,
@grouping := @grouping + 1 + (seats.x > @x + 1 OR seats.y != @y),
@y := seats.y,
@x := seats.x
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
)
UPDATE
seats s
JOIN seat_groupings sg USING (id)
SET s.grouping = sg.grouping
;
-- Query OK, 5 rows affected, 3 warnings (0,00 sec)
حسنًا ، كان ذلك سهلاً (لكن لا تنس التحذيرات)!
استطرادية صغيرة: في هذه الحالة ، أستخدم خصائص الحساب المنطقي. العبارات التالية متكافئة:
SELECT seats.x > @x + 1 OR seats.y != @y `increment`;
SELECT IF (
seats.x > @x + 1 OR seats.y != @y,
1,
0
) `increment`;
يجد البعض هذا بديهيًا ، والبعض الآخر ليس كذلك ؛ إنها مسألة ذوق. من الآن فصاعدًا سأستخدم تعبيرًا أكثر إحكاما.
دعونا نرى النتيجة:
SELECT id, y, x, `grouping` FROM seats WHERE venue_id = @venue_id ORDER BY y, x;
-- +-------+------+------+----------+
-- | id | y | x | grouping |
-- +-------+------+------+----------+
-- | 24887 | 0 | 0 | 1 |
-- | 27186 | 0 | 1 | 2 |
-- | 29485 | 1 | 0 | 4 |
-- | 31784 | 1 | 2 | 6 |
-- | 34083 | 2 | 0 | 8 |
-- +-------+------+------+----------+
نهج رائع!
للأسف ، هناك عيب "ثانوي": إنه يعمل بشكل رائع إلا عندما لا يعمل ...
النقطة المهمة هي أن مُحسِّن الاستعلام لا يقوم بالضرورة بإجراء حسابات من اليسار إلى اليمين ، لذلك قد يتم تنفيذ التعيينات (: =) بترتيب خاطئ مما يؤدي إلى نتيجة خاطئة. غالبًا ما يواجه الأشخاص هذه المشكلة بعد تحديث MySQL.
في MySQL 8.0 ، تم إهمال هذه الوظيفة بالفعل:
-- UPDATE.
--
SHOW WARNINGS\G
-- *************************** 1. row ***************************
-- Level: Warning
-- Code: 1287
-- Message: Setting user variables within expressions is deprecated and will be removed in a future release. Consider alternatives: 'SET variable=expression, ...', or 'SELECT expression(s) INTO variables(s)'.
-- [...]
حسنًا ، دعنا نصلح الموقف!
النهج الحديث رقم 1: وظائف النوافذ
كان إدخال وظائف النافذة حدثًا متوقعًا للغاية في عالم MySQL.
بشكل عام ، تعمل طبيعة "الانزلاق" لوظائف النافذة بشكل جيد مع الدوال التراكمية. ومع ذلك ، تتطلب بعض الوظائف التراكمية المعقدة نتائج التعبير الأخير - الوظيفة التي لا تدعمها وظائف النافذة لأنها تعمل على أعمدة.
هذا لا يعني أن المشكلة لا يمكن حلها ؛ إنها تحتاج فقط إلى إعادة التفكير.
في حالتنا ، يمكن تقسيم المهمة إلى جزأين. يمكن اعتبار التجميع لكل موقع مجموع قيمتين:
- الرقم التسلسلي لكل مكان ،
- القيمة التراكمية للزيادات في كل الأماكن السابقة لهذا المكان.
سيتعرف أولئك الذين يعرفون وظائف النوافذ على الأنماط النموذجية هنا.
الرقم التسلسلي لكل مقعد هو وظيفة مضمنة:
ROW_NUMBER() OVER <window>
لكن مع القيمة التراكمية ، كل شيء أكثر إثارة للاهتمام ... لحسابه ، نقوم بعملين:
- احسب الزيادة لكل مكان واكتبها على الجدول (أو CTE) ،
- ثم ، لكل موقع ، نقوم بتلخيص الزيادات الخاصة بهذا الموقع باستخدام وظيفة النافذة.
دعنا نلقي نظرة على SQL:
WITH
increments (id, increment) AS
(
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1 OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x)
)
SELECT
s.id, y, x,
ROW_NUMBER() OVER tzw + SUM(increment) OVER tzw `grouping`
FROM seats s
JOIN increments i USING (id)
WINDOW tzw AS (ORDER BY y, x)
;
-- +-------+---+---+----------+
-- | id | y | x | grouping |
-- +-------+---+---+----------+
-- | 24887 | 0 | 0 | 1 |
-- | 27186 | 0 | 1 | 2 |
-- | 29485 | 1 | 0 | 4 |
-- | 31784 | 1 | 2 | 6 |
-- | 34083 | 2 | 1 | 8 |
-- +-------+---+---+----------+
عظيم!
(لاحظ أنني أغفلت التحديث من الآن فصاعدًا من أجل البساطة.)
دعنا نحلل الطلب.
منطق رفيع المستوى
CTE التالي (محرر) :
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1 OR y != LAG(y, 1, y) OVER tzw `increment`
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x)
;
-- +-------+-----------+
-- | id | increment |
-- +-------+-----------+
-- | 24887 | 0 |
-- | 27186 | 0 |
-- | 29485 | 1 |
-- | 31784 | 1 |
-- | 34083 | 1 |
-- +-------+-----------+
… لحساب الزيادات لكل موقع من الموقع السابق (المزيد في
LAG()
وقت لاحق). يعمل على كل سجل والسجل الذي يسبقه وليس تراكمي.
الآن ، لحساب الزيادات التراكمية ، سنستخدم ببساطة وظيفة نافذة لحساب المجموع حتى كل موقع وتضمينه:
-- (CTE here...)
SELECT
s.id, y, x,
ROW_NUMBER() OVER tzw `pos.`,
SUM(increment) OVER tzw `cum.incr.`
FROM seats s
JOIN increments i USING (id)
WINDOW tzw AS (ORDER BY y, x);
-- +-------+---+---+------+-----------+
-- | id | y | x | pos. | cum.incr. | (grouping)
-- +-------+---+---+------+-----------+
-- | 24887 | 0 | 0 | 1 | 0 | = 1 + 0 (curr.)
-- | 27186 | 0 | 1 | 2 | 0 | = 2 + 0 (#24887) + 0 (curr.)
-- | 29485 | 1 | 0 | 3 | 1 | = 3 + 0 (#24887) + 0 (#27186) + 1 (curr.)
-- | 31784 | 1 | 2 | 4 | 2 | = 4 + 0 (#24887) + 0 (#27186) + 1 (#29485) + 1 (curr.)
-- | 34083 | 2 | 1 | 5 | 3 | = 5 + 0 (#24887) + 0 (#27186) + 1 (#29485) + 1 (#31784)↵
-- +-------+---+---+------+-----------+ + 1 (curr.)
وظيفة النافذة LAG ()
ترجع الدالة LAG ، في أبسط أشكالها (
LAG(x)
) ، القيمة السابقة لعمود معين. الإزعاج الكلاسيكي لمثل هذه الوظائف هو التعامل مع الإدخال (الإدخالات) الأول في النافذة. نظرًا لعدم وجود سجل سابق ، يتم إرجاع NULL. في حالة LAG ، يمكنك تحديد القيمة المرغوبة كمعامل ثالث:
LAG(x, 1, x - 1) -- `x -1`
LAG(y, 1, y) -- `y`
من خلال تحديد القيم الافتراضية ، نتأكد من أن المكان الأول في حدود النافذة سيكون له نفس منطق المكان التالي (x-1) وبدون تغيير الصف (y).
الحل البديل هو استخدام
IFNULL
، ومع ذلك ، فإن التعبيرات مرهقة للغاية:
-- , !
--
IFNULL(x > LAG(x) OVER tzw + 1 OR y != LAG(y) OVER tzw, 0)
IFNULL(x > LAG(x) OVER tzw + 1, FALSE) OR IFNULL(y != LAG(y) OVER tzw, FALSE)
المعلمة الثانية
LAG()
هي عدد المواضع المراد الرجوع إليها داخل النافذة ؛ 1 هي القيمة السابقة (وهي أيضًا القيمة الافتراضية).
الجوانب الفنية
النوافذ المسماة
يستخدم استعلامنا نفس النافذة عدة مرات. الاستعلامات التالية متكافئة رسميًا:
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x);
SELECT
id,
x > LAG(x, 1, x - 1) OVER (ORDER BY y, x) + 1
OR y != LAG(y, 1, y) OVER (ORDER BY y, x)
FROM seats
WHERE venue_id = @venue_id;
ومع ذلك ، قد ينتج عن الثاني سلوك دون المستوى الأمثل (الذي صادفته - على الأقل في الماضي): يمكن للمحسن اعتبار النوافذ مستقلة وحساب كل منها على حدة. لهذا السبب ، أنصحك دائمًا باستخدام النوافذ المسماة (على الأقل عند تكرارها).
التقسيم حسب البيان
عادة ما يتم تنفيذ وظائف النوافذ على القسم. في حالتنا ، سيبدو كما يلي:
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (PARTITION BY venue_id ORDER BY y, x); -- !
نظرًا لأن النافذة تتطابق مع مجموعة السجلات الكاملة (التي تمت تصفيتها حسب الشرط
WHERE
) ، فإننا لا نحتاج إلى تحديدها (القسم).
ولكن إذا كان لا بد من تشغيل هذا الاستعلام على الجدول بأكمله
seats
، فسيتعين القيام بذلك حتى تتم إعادة تعيين النافذة للجميع venue_id
.
فرز
ORDER BY
يتم تعيين
الطلب على مستوى النافذة:
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x)
في هذه الحالة ، يكون فرز النوافذ منفصلاً عن SELECT. انها مهمة جدا! سلوك هذا الطلب:
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS ()
ORDER BY y, x
… غير معرف. دعنا ننتقل إلى الدليل :
يتم تحديد سلاسل نتيجة الاستعلام من جملة FROM بعد تنفيذ الجمل WHERE و GROUP BY و HAVING ، ويتم التنفيذ داخل النافذة قبل ORDER BY و LIMIT و SELECT DISTINCT.
بعض الاعتبارات
بشكل عام ، بالنسبة لهذا النوع من المشاكل ، من المنطقي حساب تغير الحالة لكل سجل ثم جمعها - بدلاً من تمثيل كل سجل كدالة للسجل السابق.
هذا الحل أكثر تعقيدًا من الوظيفة التي يستبدلها ، ولكنه في نفس الوقت موثوق به. للأسف ، هذا النهج ليس دائمًا ممكنًا أو سهل التنفيذ. هذا هو المكان الذي يلعب فيه الاعتلال الدماغي المزمن العودي.
الأسلوب الحديث رقم 2: الاعتلال الدماغي الرهيب التكراري
يتطلب هذا النهج القليل من الخداع بسبب القدرات المحدودة لـ CTE في MySQL. من ناحية أخرى ، فهو حل مباشر يناسب الجميع ، لذلك لا يتطلب أي إعادة تفكير في نهج عالمي.
لنبدأ بنسخة مبسطة من الطلب النهائي:
-- `p_` `Previous`
--
WITH RECURSIVE groupings (p_id, p_venue_id, p_y, p_x, p_grouping) AS
(
(
SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
)
UNION ALL
SELECT
s.id, s.venue_id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s
WHERE s.venue_id = p_venue_id AND (s.y, s.x) > (p_y, p_x)
ORDER BY s.venue_id, s.y, s.x
LIMIT 1
)
SELECT * FROM groupings;
بنغو! هذا الاستعلام بسيط (نسبيًا) ، ولكن الأهم من ذلك أنه يعبر عن دالة التجميع التراكمية بأبسط طريقة ممكنة:
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
-- :
@grouping := @grouping + 1 + (seats.x > @x + 1 OR seats.y != @y),
@y := seats.y,
@x := seats.x
المنطق واضح حتى بالنسبة لأولئك الذين ليسوا على دراية بالاعتلال الدماغي الرضحي المزمن. الصف الأول هو المقعد الأول في القاعة ، بالترتيب:
SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
في الجزء العودي ، نكرر:
SELECT
s.id, s.venue_id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s
WHERE s.venue_id = p_venue_id AND (s.y, s.x) > (p_y, p_x)
ORDER BY s.venue_id, s.y, s.x
LIMIT 1
حالة
WHERE
جنبا إلى جنب مع المشغلين ORDER BY
و LIMIT
ببساطة العثور على مكان المقبل، وهو المكان مع نفسه venue_id
، ولكنها تستخدم ل إحداثيات lshimi (س، ص) في تسلسل (venue_id، س، ص).
الجزء
s.venue_id
في تعبير الفرز مهم جدا! يسمح لنا باستخدام فهرس.
المشغل
SELECT
:
- يؤدي التراكم (يحسب
(p_)grouping
) ، - يوفر قيمًا للوضع الحالي (
s.id
،s.venue_id
،s.y
،s.x
) في الدورة التالية.
نختار
FROM groupings
تلبية متطلبات التكرار CTE.
الأمر المثير للاهتمام هنا هو أننا نستخدم CTE العودي كمكرر ، حيث نجلب من جدول
groupings
في استعلام فرعي متكرر وربطه seats
للعثور على البيانات لمزيد من المعالجة.
JOIN
يتم عرضه رسميًا ، ولكن LIMIT
يتم إرجاع سجل واحد فقط بسبب عامل التشغيل .
نسخة العمل
لسوء الحظ ، لا يعمل الاستعلام أعلاه لأنه
ORDER BY
غير مدعوم حاليًا في الاستعلامات الفرعية العودية. بالإضافة إلى ذلك ، LIMIT
تختلف الدلالات المستخدمة هنا عن الدلالات النموذجية التي تنطبق على استعلام خارجي :
يتم دعم LIMIT الآن [..] التأثير على مجموعة البيانات الناتجة مماثل لاستخدام LIMIT مع SELECT خارجي
ومع ذلك ، هذه ليست مشكلة خطيرة. دعنا نلقي نظرة على نسخة العمل:
WITH RECURSIVE groupings (p_id, p_venue_id, p_y, p_x, p_grouping) AS
(
(
SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
)
UNION ALL
SELECT
s.id, s.venue_id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s WHERE s.id = (
SELECT si.id
FROM seats si
WHERE si.venue_id = p_venue_id AND (si.y, si.x) > (p_y, p_x)
ORDER BY si.venue_id, si.y, si.x
LIMIT 1
)
)
SELECT * FROM groupings;
-- +-------+------+------+------------+
-- | p_id | p_y | p_x | p_grouping |
-- +-------+------+------+------------+
-- | 24887 | 0 | 0 | 1 |
-- | 27186 | 0 | 1 | 2 |
-- | 29485 | 1 | 0 | 4 |
-- | 31784 | 1 | 2 | 6 |
-- | 34083 | 2 | 0 | 8 |
-- +-------+------+------+------------+
من غير المريح استخدام استعلام فرعي ، ولكن هذا النهج يعمل ويكون النموذج المعياري ضئيلًا هنا ، نظرًا لأن التعبيرات المتعددة مطلوبة على أي حال.
هنا، بدلا من القيام بالأمر والحد المرتبطة النقابة
groupings
و seats
، ونحن نفعل ذلك ضمن فرعي وتمريرها إلى الاستعلام الخارجي، أي بعد ذلك يختار فقط سجل الهدف.
تأملات في الأداء
دعنا نفحص خطة تنفيذ الاستعلام باستخدام شرح تحليل:
mysql> EXPLAIN ANALYZE WITH RECURSIVE groupings [...]
-> Table scan on groupings (actual time=0.000..0.001 rows=5 loops=1)
-> Materialize recursive CTE groupings (actual time=0.140..0.141 rows=5 loops=1)
-> Limit: 1 row(s) (actual time=0.019..0.019 rows=1 loops=1)
-> Index lookup on seats using venue_id_y_x (venue_id=(@venue_id)) (cost=0.75 rows=5) (actual time=0.018..0.018 rows=1 loops=1)
-> Repeat until convergence
-> Nested loop inner join (cost=3.43 rows=2) (actual time=0.017..0.053 rows=2 loops=2)
-> Scan new records on groupings (cost=2.73 rows=2) (actual time=0.001..0.001 rows=2 loops=2)
-> Filter: (s.id = (select #5)) (cost=0.30 rows=1) (actual time=0.020..0.020 rows=1 loops=5)
-> Single-row index lookup on s using PRIMARY (id=(select #5)) (cost=0.30 rows=1) (actual time=0.014..0.014 rows=1 loops=5)
-> Select #5 (subquery in condition; dependent)
-> Limit: 1 row(s) (actual time=0.007..0.008 rows=1 loops=9)
-> Filter: ((si.y,si.x) > (groupings.p_y,groupings.p_x)) (cost=0.75 rows=5) (actual time=0.007..0.007 rows=1 loops=9)
-> Index lookup on si using venue_id_y_x (venue_id=groupings.p_venue_id) (cost=0.75 rows=5) (actual time=0.006..0.006 rows=4 loops=9)
الخطة تتماشى مع التوقعات. في هذه الحالة ، يكمن أساس الخطة المثلى في عمليات بحث الفهرس:
-> Nested loop inner join (cost=3.43 rows=2) (actual time=0.017..0.053 rows=2 loops=2)
-> Single-row index lookup on s using PRIMARY (id=(select #5)) (cost=0.30 rows=1) (actual time=0.014..0.014 rows=1 loops=5)
-> Index lookup on si using venue_id_y_x (venue_id=groupings.p_venue_id) (cost=0.75 rows=5) (actual time=0.006..0.006 rows=4 loops=9)
... ذات أهمية قصوى. سينخفض الأداء بشكل ملحوظ إذا قمت بمسح الفهارس (أي ، فحص سجلات الفهرس خطيًا بدلاً من البحث عن السجلات الصحيحة).
وبالتالي ، لكي تعمل هذه الإستراتيجية ، يجب أن تكون الفهارس المرتبطة في مكانها وأن يستخدمها المحسن بأكبر قدر ممكن من الكفاءة.
إذا تم رفع القيود في المستقبل ، فلن تكون هناك حاجة لاستخدام استعلام فرعي ، مما يبسط المهمة للمحسن بشكل كبير.
بديل للخطط دون المستوى الأمثل
في حالة تعذر تحديد الخطة المثلى ، استخدم جدولًا مؤقتًا:
CREATE TEMPORARY TABLE selected_seats (
id INT NOT NULL PRIMARY KEY,
y INT,
x INT,
UNIQUE (y, x)
)
SELECT id, y, x
FROM seats WHERE venue_id = @venue_id;
WITH RECURSIVE
groupings (p_id, p_y, p_x, p_grouping) AS
(
(
SELECT id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
)
UNION ALL
SELECT
s.id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s WHERE s.id = (
SELECT ss.id
FROM selected_seats ss
WHERE (ss.y, ss.x) > (p_y, p_x)
ORDER BY ss.y, ss.x
LIMIT 1
)
)
SELECT * FROM groupings;
حتى إذا تم تمرير عمليات مسح الفهرس في هذا الاستعلام ، فإنها تكلف
selected_seats
الكثير ، لأن الجدول صغير جدًا.
خاتمة
يسعدني جدًا أنه يمكن الآن استبدال سير العمل الفعال ولكن المعيب بوظيفة بسيطة إلى حد ما تم تقديمها في MySQL 8.0.
في غضون ذلك ، يستمر تطوير الميزات الجديدة لـ 8.0 ، مما يجعل الإصدار الناجح بالفعل أفضل.
العودية الناجحة!
PS من المترجم
اقرأ أيضًا على مدونتنا: