معظم إصدارات Postgres عبارة عن قائمة مهذّبة من التحسينات التدريجية. أما PostgreSQL 18 فليس كذلك. إنه يغيّر طريقة حوار المحرّك مع القرص، ويمنحك نوع مفتاح أساسي يتوقف عن تفتيت فهارسك، ويلغي بهدوء صنفًا كاملًا من الفهارس لم تعد بحاجة إلى إنشائه.
إذا كنت تشغّل قاعدة Postgres خلف تطبيق Next.js أو واجهة خلفية بـ Laravel أو وكيل ذكاء اصطناعي يقصفها ببحوث المتجهات، فإن ثلاثة من هذه التغييرات ستظهر في رسوم زمن الاستجابة لديك. إليك ما هي، وكيف تفعّلها، وأيها يمكنك تجاهله.
الإدخال والإخراج غير المتزامن: التغيير الأبرز
طوال تاريخه، كان PostgreSQL يقرأ البيانات من القرص باستدعاء حاجب واحد في كل مرة. يطلب صفحة من نظام التشغيل، ينتظر، يستلمها، ثم يطلب التالية. كان نظام التشغيل يحاول المساعدة عبر التخمين المسبق (readahead)، لكنه لا يعرف كيف يبدو اجتياز شجرة B-tree، فبقيت تخميناته متواضعة.
يقدّم PostgreSQL 18 إدخالًا وإخراجًا غير متزامن حقيقيًا للقراءة. صار بإمكان الخادم إصدار عدة طلبات قراءة دفعة واحدة ومواصلة العمل بينما تملؤها النواة. في المسح التسلسلي وbitmap heap scans، المكاسب المُبلّغ عنها بين ضعفين وثلاثة أضعاف، ويكون الأثر أكبر ما يكون حيث يؤلم اليوم: التخزين الشبكي، وأقراص السحابة، وكل سياق تكلّف فيه القراءة الواحدة زمن استجابة حقيقيًا.
يتم التحكم عبر io_method:
-- تحقق من الإعداد الحالي
SHOW io_method;
-- ثلاثة خيارات:
-- sync : السلوك الحاجب القديم
-- worker : عمليات إدخال/إخراج مخصّصة (الافتراضي في 18)
-- io_uring: حلقات النواة المباشرة (Linux 5.1+، خيار عند البناء)worker هو الافتراضي ويعمل في كل مكان. يسلّم طلبات القراءة إلى مجموعة عمليات خلفية يحدد حجمها io_workers. على لينكس بنواة حديثة، وإذا جرى بناء Postgres بدعم io_uring، فإن io_method = io_uring يتجاوز عمليات الـ workers تمامًا ويتحدث إلى النواة عبر حلقات مشتركة: تبديل سياق أقل، وكلفة أدنى لكل قراءة.
# postgresql.conf — نقطة انطلاق معقولة على NVMe سحابي
io_method = worker
io_workers = 3
effective_io_concurrency = 16 # كانت 1 افتراضيًا في الإصدارات السابقةالمعامل الذي ينساه الجميع هو effective_io_concurrency. يخبر المخطِّط بعدد القراءات المتزامنة التي يستوعبها التخزين. تاريخيًا كانت قيمته 1، وهي على قرص SSD كذبة تكلّفك إنتاجية حقيقية. والآن بعد أن صار الإدخال غير المتزامن مُنفَّذًا فعلًا، صار هذا الإعداد يعني ما يوحي به اسمه. ارفعه، وقِس، وراقب عمليات المسح التسلسلي.
تحذير قبل أن تندفع: الإدخال غير المتزامن في الإصدار 18 يغطي القراءات لا الكتابات. إذا كانت أحمالك مهيمَنة بالكتابة — إدراجات كثيفة أو طابور مهام مزدحم — فلن يصلح هذا الإصدار ذلك، ولن تظهر المكاسب التي تقرأ عنها في لوحاتك.
UUIDv7: المفتاح الأساسي الذي كان يجب أن تستعمله
إذا كنت تولّد المفاتيح الأساسية بـ uuid_generate_v4() أو في كود التطبيق عبر crypto.randomUUID()، فأنت تدفع ضريبة ربما لم تقِسها قط.
المعرّفات العشوائية موزَّعة، بحكم تصميمها، بانتظام عبر فضاء المفاتيح. كل إدراج يهبط في منطقة مختلفة من شجرة الفهرس. يعني ذلك كتابات صفحات عشوائية، ومحلّية ذاكرة مخبئية رديئة، وانقسامات صفحات، وفهرسًا ينمو أسرع مما ينبغي. هذا هو السبب الكلاسيكي الذي يدفع الفرق في النهاية إلى هجر UUID والعودة إلى bigserial.
يعالج UUIDv7 ذلك دون التخلي عن التفرّد العالمي. أول 48 بت طابع زمني بالميلي ثانية، والبقية عشوائية. الصفوف المُدرَجة في الوقت نفسه تستقر متجاورة في الفهرس — المحلّية ذاتها التي يمنحها عدّاد تصاعدي — مع بقائها آمنة للتوليد على العميل، أو في عامل طابور، أو عبر شظايا متعددة دون تنسيق.
يشحنه PostgreSQL 18 أصليًا، دون أي امتداد:
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
customer_id uuid NOT NULL,
total_cents bigint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO orders (customer_id, total_cents)
VALUES (uuidv7(), 4500)
RETURNING id;مكافأتان تستحقان المعرفة. يمكنك استخراج الطابع الزمني من المفتاح نفسه، ما يجعل المفتاح بمثابة created_at مجاني لاستعلامات المدى:
SELECT id, uuid_extract_timestamp(id) AS created
FROM orders
ORDER BY id DESC
LIMIT 10;كما يقبل uuidv7() إزاحة زمنية، وهي مفيدة فعلًا في عمليات التعبئة الرجعية وبيانات الاختبار حين تحتاج معرّفات تُرتَّب في الماضي:
SELECT uuidv7(INTERVAL '-1 day');المقايضة ليست مجانية: يفشي UUIDv7 وقت الإنشاء التقريبي لكل من يراه. بالنسبة لمعرّف طلب في لوحة إدارة داخلية، لا ضرر. أما لرمز إعادة تعيين كلمة مرور أو رابط مشاركة عام، فلا — أبقِ هذه على v4.
Skip scan في B-tree: فهرس لم تعد تحتاجه
لنفترض أن لديك هذا الفهرس، المبني لاستعلام يرشّح حسب المستأجر أولًا:
CREATE INDEX idx_events ON events (tenant_id, event_type, created_at);ثم يصل استعلام جديد يرشّح على event_type فقط. في PostgreSQL 17 وما قبله، كان هذا الفهرس المركّب عديم الفائدة للمخطِّط، لأن العمود الأول ليس في الشرط. لم يبقَ أمامك سوى مسح تسلسلي أو فهرس ثانٍ: مساحة أكبر، وكتابات أبطأ، وصيانة أثقل.
يضيف PostgreSQL 18 خاصية skip scan إلى فهارس B-tree. حين يكون العمود الأول منخفض التمايز — قائمة مستأجرين، أو حالة، أو رمز بلد — صار بإمكان المخطِّط المرور على كل قيمة متمايزة والقفز مباشرة إلى الأقسام المطابقة تحتها:
-- يمكنه الآن استعمال idx_events حتى بدون tenant_id في شرط WHERE
EXPLAIN ANALYZE
SELECT * FROM events
WHERE event_type = 'checkout_completed'
AND created_at > now() - INTERVAL '7 days';النتيجة العملية: راجع فهارسك المكرّرة. كثير من الفرق تحمل فهرسين أو ثلاثة فهارس مركّبة متداخلة لمجرد تغطية أعمدة أولى مختلفة. بعضها يمكن حذفه الآن، ما يسرّع كل عملية كتابة على الجدول.
التحفّظ يتعلق بالتمايز. يربح skip scan حين يكون للعمود الأول قيم متمايزة قليلة. أما إذا كان عمودك الأول user_id بمليوني قيمة متمايزة، فالقفز بينها كلها ليس أرخص من المسح التسلسلي، وسيرفضه المخطِّط عن حق.
الأعمدة المولّدة الافتراضية، وتغيير افتراضي سيوقعك
كانت الأعمدة المولّدة موجودة قبل الإصدار 18، لكنها كانت دائمًا STORED: تُحسب عند الكتابة وتُخزَّن فعليًا. يضيف PostgreSQL 18 الأعمدة المولّدة VIRTUAL، التي تُحسب عند القراءة ولا تشغل أي مساحة قرص.
CREATE TABLE users (
first_name text NOT NULL,
last_name text NOT NULL,
full_name text GENERATED ALWAYS AS (first_name || ' ' || last_name) VIRTUAL
);وهنا الفخ: في PostgreSQL 18، صار VIRTUAL هو الافتراضي عند إغفال الكلمة المفتاحية. إذا كانت ملفات الترحيل لديك تحتوي GENERATED ALWAYS AS (...) دون تحديد STORED، فسيتغيّر سلوكها عند الترقية: تفقد العمود الفعلي، وتفقد القدرة على فهرسته. فتّش ملفات الترحيل قبل الترقية، وكن صريحًا:
-- كن صريحًا. دائمًا.
price_with_tax numeric GENERATED ALWAYS AS (price * 1.19) STORED;استعمل STORED حين تحتاج فهرسة العمود أو حين يكون التعبير مكلفًا. واستعمل VIRTUAL حين تكون القيمة رخيصة الحساب وتريدها فقط ضمن SELECT *.
تحسينات الراحة اليومية
بضعة تغييرات أصغر ستظهر في عملك اليومي:
RETURNING صار يرى نسختَي الصف المعدَّل. سابقًا كنت تحصل على الصف الجديد فحسب. الآن تحصل على الاثنين، ما يلغي صنفًا كاملًا من دورات القراءة-التعديل-الكتابة:
UPDATE accounts SET balance = balance - 100
WHERE id = $1
RETURNING old.balance AS before, new.balance AS after;pg_upgrade يحتفظ بإحصاءات المخطِّط. في كل ترقية كبرى سابقة، كنت تنهي الترحيل ثم تجلس في قاعدة بطيئة ورديئة التخطيط حتى ينتهي ANALYZE من كل الجداول. ينقل PostgreSQL 18 الإحصاءات عبر الترقية. وفي قاعدة كبيرة، يكفي هذا وحده لتحويل نافذة صيانة مرعبة إلى نافذة مملة.
NOT NULL ... NOT VALID. يمكنك إضافة قيد NOT NULL دون مسح كامل للجدول، ثم التحقق منه لاحقًا خارج ساعات الذروة:
ALTER TABLE foo ADD CONSTRAINT name_nn NOT NULL name NOT VALID;
-- ...لاحقًا، في ساعات الهدوء:
ALTER TABLE foo VALIDATE CONSTRAINT name_nn;مصادقة OAuth 2.0 صارت مدمجة، وهو ما يهم إن كنت تتخلى عن كلمات مرور قواعد البيانات طويلة الأمد لصالح رموز قصيرة يصدرها مزوّد الهوية لديك.
هل تُرقّي؟
رقِّ إن كانت: أحمالك مهيمَنة بالقراءة على تخزين سحابي (الإدخال غير المتزامن مكسب حقيقي)، أو تستعمل UUID عشوائية كمفاتيح أساسية (UUIDv7 وحده يبرّر الترقية)، أو تصون كومة من الفهارس المركّبة المتداخلة (skip scan يتيح حذف بعضها).
انتظر إن كانت: أحمالك مهيمَنة بالكتابة، أو تعتمد على امتدادات لم تُعتمد بعد للإصدار 18، أو لديك أعمدة مولّدة في الإنتاج لم تراجعها في ضوء تغيير الافتراضي إلى VIRTUAL.
ومن يستعمل Postgres كمخزن خلفي لميزات الذكاء الاصطناعي — تضمينات في pgvector، أو ذاكرة وكلاء، أو طوابير مهام — فالإدخال غير المتزامن هو أول ما ينبغي اختباره. بحث التشابه المتجهي مهيمَن بالقراءة والمسح، وهو تحديدًا شكل الحمل الذي صُمّم له هذا الإصدار. وإن كنت لا تزال تختار مخزن المتجهات، فمقارنتنا لـ قواعد البيانات المتجهية توضح أين يفوز pgvector وأين يخسر، ودليلنا حول Drizzle مقابل Prisma يغطي طبقة الـ ORM فوق كل هذا.
قِس على نسخة مطابقة، ببياناتك واستعلاماتك أنت. أرقام الثلاثة أضعاف حقيقية، لكنها أضعاف شخص آخر.
تحتاج مساعدة في التخطيط لترقية PostgreSQL أو ضبط قاعدة بيانات تجاوزت إعداداتها الافتراضية؟ تحدّث إلى فريق نقطة.