ضغط البيانات باستخدام جداول عمودية في Azure Cosmos DB ل PostgreSQL

ينطبق على: Azure Cosmos DB ل PostgreSQL (مدعوم بملحق قاعدة بيانات Citus إلى PostgreSQL)

يدعم Azure Cosmos DB ل PostgreSQL تخزين الجدول العمودي للإلحاق فقط لأحمال العمل التحليلية وتخزين البيانات. عند تخزين الأعمدة (بدلاً من الصفوف) بشكل مستمر على القرص، تصبح البيانات أكثر قابلية للانضغاط، ويمكن للاستعلامات طلب مجموعة فرعية من الأعمدة بسرعة أكبر.

إنشاء جدول

لاستخدام التخزين العمودي، حدد USING columnar عند إنشاء جدول:

CREATE TABLE contestant (
    handle TEXT,
    birthdate DATE,
    rating INT,
    percentile FLOAT,
    country CHAR(3),
    achievements TEXT[]
) USING columnar;

يقوم Azure Cosmos DB ل PostgreSQL بتحويل الصفوف إلى تخزين عمودي في "خطوط" أثناء الإدراج. يحتفظ كل خط شريحة على بيانات بقيمة عملية واحدة، أو 150000 صف، أيهما أقل. (يمكن تغيير حجم خط الشريحة والمعلمات الأخرى للجدول العمودي باستخدام الوظيفة التجميعية alter_columnar_table_set.)

على سبيل المثال: تضع العبارة التالية جميع الصفوف الخمسة في نفس خط الشريحة نظراً إلى إدراج جميع القيم في عملية واحدة:

-- insert these values into a single columnar stripe

INSERT INTO contestant VALUES
  ('a','1990-01-10',2090,97.1,'XA','{a}'),
  ('b','1990-11-01',2203,98.1,'XA','{a,b}'),
  ('c','1988-11-01',2907,99.4,'XB','{w,y}'),
  ('d','1985-05-05',2314,98.3,'XB','{}'),
  ('e','1995-05-05',2236,98.2,'XC','{a}');

من الأفضل إنشاء خطوط كبيرة عندما يكون ذلك ممكنا، لأن Azure Cosmos DB ل PostgreSQL يضغط البيانات العمودية بشكل منفصل لكل شريط. يمكننا رؤية حقائق جدولنا العمودي مثل: معدل الضغط، وعدد خطوط الشريحة ومتوسط الصفوف لكل خط شريحة باستخدام VACUUM VERBOSE:

VACUUM VERBOSE contestant;
INFO:  statistics for "contestant":
storage id: 10000000000
total file size: 24576, total data size: 248
compression rate: 1.31x
total row count: 5, stripe count: 1, average rows per stripe: 5
chunk count: 6, containing data for dropped columns: 0, zstd compressed: 6

يظهر الإخراج أن Azure Cosmos DB ل PostgreSQL استخدم خوارزمية ضغط zstd للحصول على ضغط بيانات 1.31x. مقارنة معدل الضغط أ) حجم البيانات المدرجة على نحو تقسيمها مرحلياً في الذاكرة مقابل ب) حجم تلك البيانات المضغوطة في خط شريحتها النهائي.

بسبب أسلوب قياسه، قد يتطابق معدل الضغط أو لا يتطابق مع فرق الحجم بين الصف والتخزين العمودي لأحد الجداول. يتمثل الأسلوب الوحيدة للعثور حقاً على هذا الاختلاف في إنشاء جدول صف وعمودي يحتوي على نفس البيانات، ومقارنتهما.

قياس الضغط

لننشئ مثال جديد بمزيد من البيانات لقياس توفير الضغط.

-- first a wide table using row storage
CREATE TABLE perf_row(
  c00 int8, c01 int8, c02 int8, c03 int8, c04 int8, c05 int8, c06 int8, c07 int8, c08 int8, c09 int8,
  c10 int8, c11 int8, c12 int8, c13 int8, c14 int8, c15 int8, c16 int8, c17 int8, c18 int8, c19 int8,
  c20 int8, c21 int8, c22 int8, c23 int8, c24 int8, c25 int8, c26 int8, c27 int8, c28 int8, c29 int8,
  c30 int8, c31 int8, c32 int8, c33 int8, c34 int8, c35 int8, c36 int8, c37 int8, c38 int8, c39 int8,
  c40 int8, c41 int8, c42 int8, c43 int8, c44 int8, c45 int8, c46 int8, c47 int8, c48 int8, c49 int8,
  c50 int8, c51 int8, c52 int8, c53 int8, c54 int8, c55 int8, c56 int8, c57 int8, c58 int8, c59 int8,
  c60 int8, c61 int8, c62 int8, c63 int8, c64 int8, c65 int8, c66 int8, c67 int8, c68 int8, c69 int8,
  c70 int8, c71 int8, c72 int8, c73 int8, c74 int8, c75 int8, c76 int8, c77 int8, c78 int8, c79 int8,
  c80 int8, c81 int8, c82 int8, c83 int8, c84 int8, c85 int8, c86 int8, c87 int8, c88 int8, c89 int8,
  c90 int8, c91 int8, c92 int8, c93 int8, c94 int8, c95 int8, c96 int8, c97 int8, c98 int8, c99 int8
);

-- next a table with identical columns using columnar storage
CREATE TABLE perf_columnar(LIKE perf_row) USING COLUMNAR;

املأ كلا الجدولين بنفس مجموعة البيانات الكبيرة:

INSERT INTO perf_row
  SELECT
    g % 00500, g % 01000, g % 01500, g % 02000, g % 02500, g % 03000, g % 03500, g % 04000, g % 04500, g % 05000,
    g % 05500, g % 06000, g % 06500, g % 07000, g % 07500, g % 08000, g % 08500, g % 09000, g % 09500, g % 10000,
    g % 10500, g % 11000, g % 11500, g % 12000, g % 12500, g % 13000, g % 13500, g % 14000, g % 14500, g % 15000,
    g % 15500, g % 16000, g % 16500, g % 17000, g % 17500, g % 18000, g % 18500, g % 19000, g % 19500, g % 20000,
    g % 20500, g % 21000, g % 21500, g % 22000, g % 22500, g % 23000, g % 23500, g % 24000, g % 24500, g % 25000,
    g % 25500, g % 26000, g % 26500, g % 27000, g % 27500, g % 28000, g % 28500, g % 29000, g % 29500, g % 30000,
    g % 30500, g % 31000, g % 31500, g % 32000, g % 32500, g % 33000, g % 33500, g % 34000, g % 34500, g % 35000,
    g % 35500, g % 36000, g % 36500, g % 37000, g % 37500, g % 38000, g % 38500, g % 39000, g % 39500, g % 40000,
    g % 40500, g % 41000, g % 41500, g % 42000, g % 42500, g % 43000, g % 43500, g % 44000, g % 44500, g % 45000,
    g % 45500, g % 46000, g % 46500, g % 47000, g % 47500, g % 48000, g % 48500, g % 49000, g % 49500, g % 50000
  FROM generate_series(1,50000000) g;

INSERT INTO perf_columnar
  SELECT
    g % 00500, g % 01000, g % 01500, g % 02000, g % 02500, g % 03000, g % 03500, g % 04000, g % 04500, g % 05000,
    g % 05500, g % 06000, g % 06500, g % 07000, g % 07500, g % 08000, g % 08500, g % 09000, g % 09500, g % 10000,
    g % 10500, g % 11000, g % 11500, g % 12000, g % 12500, g % 13000, g % 13500, g % 14000, g % 14500, g % 15000,
    g % 15500, g % 16000, g % 16500, g % 17000, g % 17500, g % 18000, g % 18500, g % 19000, g % 19500, g % 20000,
    g % 20500, g % 21000, g % 21500, g % 22000, g % 22500, g % 23000, g % 23500, g % 24000, g % 24500, g % 25000,
    g % 25500, g % 26000, g % 26500, g % 27000, g % 27500, g % 28000, g % 28500, g % 29000, g % 29500, g % 30000,
    g % 30500, g % 31000, g % 31500, g % 32000, g % 32500, g % 33000, g % 33500, g % 34000, g % 34500, g % 35000,
    g % 35500, g % 36000, g % 36500, g % 37000, g % 37500, g % 38000, g % 38500, g % 39000, g % 39500, g % 40000,
    g % 40500, g % 41000, g % 41500, g % 42000, g % 42500, g % 43000, g % 43500, g % 44000, g % 44500, g % 45000,
    g % 45500, g % 46000, g % 46500, g % 47000, g % 47500, g % 48000, g % 48500, g % 49000, g % 49500, g % 50000
  FROM generate_series(1,50000000) g;

VACUUM (FREEZE, ANALYZE) perf_row;
VACUUM (FREEZE, ANALYZE) perf_columnar;

فيما يتعلق بهذه البيانات، يمكنك رؤية نسبة ضغط أفضل من 8X في الجدول العمودي.

SELECT pg_total_relation_size('perf_row')::numeric/
       pg_total_relation_size('perf_columnar') AS compression_ratio;
 compression_ratio
--------------------
 8.0196135873627944
(1 row)

مثال

التخزين العمودي يعمل بشكل جيد مع تقسيم الجدول. على سبيل المثال: راجع وثائق مجتمع Citus Engine، الأرشفة باستخدام التخزين العمودي.

Gotchas

  • يضغط التخزين العمودي لكل خط شريحة. تُنشأ خطوط الشريحة لكل عملية، لذا يؤدي إدراج صف واحد لكل عملية إلى وضع صفوف فردية في خطوط شريحتهم. سيعد ضغط خطوط شريحة الصف الفردي وأداؤه أسوأ من جدول الصفوف. أدرج دائماً بشكل مجمع في الجدول العمودي.
  • إذا أخطأت ورتبت مجموعة من خطوط الشريحة الصغيرة في جدول عمودي، فسوف تعلق. الإصلاح الوحيد هو إنشاء جدول عمودي جديد ونسخ البيانات من الأصل في معاملة واحدة:
    BEGIN;
    CREATE TABLE foo_compacted (LIKE foo) USING columnar;
    INSERT INTO foo_compacted SELECT * FROM foo;
    DROP TABLE foo;
    ALTER TABLE foo_compacted RENAME TO foo;
    COMMIT;
    
  • يمكن أن تمثل البيانات غير القابلة للضغط بشكل أساسي مشكلة، على الرغم من أن التخزين العمودي سيظل مفيداً عند تحديد أعمدة معينة. لا يحتاج إلى تحميل الأعمدة الأخرى في الذاكرة.
  • في جدول مقسم ذو مزيج من أقسام الصفوف والأعمدة، يجب استهداف التحديثات بعناية. صفهم للوصول إلى أقسام الصف فحسب.
    • في حال استهداف العملية في قسم صف معين (على سبيل المثال: UPDATE p2 SET i = i + 1)، فسوف تنجح، في حال استهدافها في قسم عمودي محدد (على سبيل المثال: UPDATE p1 SET i = i + 1)، فستبوء محاولتك بالفشل.
    • في حال استهداف العملية في الجدول المقسم وتحتوي على عبارة WHERE التي تستبعد جميع الأقسام العمودية (على سبيل المثال: UPDATE parent SET i = i + 1 WHERE timestamp = '2020-03-15')، فسوف تنجح.
    • في حال استهداف العملية في الجدول المقسم، ولكن لا تصفي على أعمدة مفتاح القسم، فستبوء محاولتك بالفشل. حتى في حال تطابق عبارات WHERE للصفوف في الأقسام العمودية فحسب، لا يعد ذلك كافياً - ويجب أيضاً تصفية مفتاح القسم.

القيود

لا تزال هذه الميزة لها قيود كبيرة:

  • يكون الضغط على القرص وليس في الذاكرة
  • ملحق فقط (لا يوجد دعم تحديث/حذف)
  • لا توجد استعادة للمساحة (على سبيل المثال، قد تستمر العمليات جرى التراجع عنها في استهلاك مساحة القرص)
  • لا يوجد دعم فهرس أو عمليات فحص فهرس أو عمليات فحص فهرس نقطي
  • لا يوجد tidscans
  • لا توجد عينات عمليات فحص
  • لا يوجد دعم TOAST (قيم كبيرة مدعومة مُضمنة)
  • لا يوجد دعم لعبارات ON CONFLICT (باستثناء إجراءات DO NOTHING من دون تحديد هدف).
  • لا يوجد دعم لأقفال تأمين المجموعة (SELECT ... FOR SHARE, SELECT ... FOR UPDATE)
  • لا يوجد دعم لمستوى العزل القابل للتسلسل
  • دعم إصدارات خادم PostgreSQL 12 رقم 12 فما فوق فقط
  • لا يوجد دعم للمفاتيح الخارجية أو القيود الفريدة أو قيود الاستبعاد
  • لا يوجد دعم لفك التشفير المنطقي
  • لا يوجد دعم للفحص المتوازي داخل العقدة
  • لا يوجد دعم لمشغّلات "بعد ... لكل صف"
  • لا توجد جداول عمودية "غير مسجلة"
  • لا توجد جداول عمودية "مؤقتة"

الخطوات التالية

  • شاهد مثالاً للتخزين العمودي في time series tutorial Citus (رابط خارجي).