LSI مقابل GSI في DynamoDB: متى تستخدم كل نوع من الفهارس الثانوية؟

عندما تحتاج إلى الاستعلام عن بيانات DynamoDB باستخدام حقل غير مفتاح الجدول الأساسي، يقع كثير من المهندسين في فخ الاختيار العشوائي بين LSI وGSI — ثم يكتشفون لاحقاً أن الخيار الخاطئ يعني إما قيوداً لا يمكن تجاوزها أو تكاليف غير متوقعة في الإنتاج.

ملخص سريع: LSI مقابل GSI في DynamoDB

الفرق الجوهري: LSI يشارك مفتاح التقسيم مع الجدول الأساسي ويُنشأ فقط عند إنشاء الجدول، بينما GSI يملك مفتاح تقسيم مستقلاً تماماً ويمكن إضافته في أي وقت. الاختيار بينهما ليس مجرد تفضيل — له تداعيات على الاتساق والتكلفة وحدود الحجم.

المعيار LSI (فهرس ثانوي محلي) GSI (فهرس ثانوي عام)
مفتاح التقسيم نفس مفتاح التقسيم للجدول الأساسي أي حقل في الجدول
وقت الإنشاء عند إنشاء الجدول فقط في أي وقت
اتساق القراءة يدعم القراءة المتسقة بشكل قوي (Strongly Consistent) قراءة متسقة في نهاية المطاف فقط (Eventually Consistent)
حد حجم التقسيم 10 GB لكل قيمة مفتاح تقسيم (يشمل الجدول والفهارس) لا يوجد حد مرتبط بمفتاح التقسيم الأصلي
سعة الإنتاجية تستهلك من سعة الجدول الأساسي سعة مستقلة قابلة للضبط
الحد الأقصى لكل جدول 5 فهارس 20 فهرساً (قابل للزيادة عبر طلب دعم)

كيف تعمل الفهارس الثانوية في DynamoDB

DynamoDB جدول مفتاح-قيمة في جوهره، لكنه يدعم نمط الاستعلام عبر الفهارس الثانوية. كل فهرس ثانوي هو في الحقيقة بنية بيانات منفصلة تحتفظ DynamoDB بها تلقائياً — عند كتابة عنصر في الجدول الأساسي، تُحدَّث الفهارس المرتبطة به بشكل متزامن (LSI) أو غير متزامن (GSI).

الفهرس لا يخزن نسخة كاملة من كل عنصر بالضرورة — يمكنك تحديد Projection لتحديد الحقول المُنسخة إلى الفهرس: إما كل الحقول (ALL)، أو المفاتيح فقط (KEYS_ONLY)، أو حقول محددة (INCLUDE). هذا الاختيار يؤثر مباشرة على تكلفة التخزين وعمليات القراءة.

graph TD Table["الجدول الأساسي PK: UserId | SK: OrderId"] LSI["LSI: CreatedAtIndex PK: UserId | SK: CreatedAt"] GSI["GSI: StatusCreatedAtIndex PK: Status | SK: CreatedAt"] Write["عملية الكتابة"] Write -->|"متزامن"| Table Write -->|"متزامن"| LSI Write -->|"غير متزامن"| GSI Table -->|"نفس مفتاح التقسيم"| LSI Table -->|"مفتاح تقسيم مستقل"| GSI
  1. الجدول الأساسي: يحتوي على البيانات الكاملة مع مفتاح التقسيم الأصلي (PK) ومفتاح الفرز (SK).
  2. LSI: يستخدم نفس PK لكن مع مفتاح فرز مختلف — الاستعلام محدود دائماً بقيمة PK واحدة.
  3. GSI: يملك PK مستقلاً تماماً — يمكن الاستعلام عبر كل التقسيمات في الجدول.
  4. التحديثات: LSI يُحدَّث بشكل متزامن مع الكتابة، GSI يُحدَّث بشكل غير متزامن مما يعني تأخراً قصيراً في الاتساق.

متى تختار LSI؟

LSI مناسب في حالة واحدة أساسية: تحتاج إلى ترتيب أو تصفية البيانات بحقل مختلف عن مفتاح الفرز الأساسي، لكن استعلاماتك دائماً محددة بنفس مفتاح التقسيم. مثال كلاسيكي: جدول طلبات حيث PK هو معرف المستخدم، SK هو معرف الطلب — وتريد أيضاً الاستعلام عن طلبات مستخدم معين مرتبة حسب تاريخ الإنشاء.

LSI يشبه فهرساً في كتاب: يساعدك على إيجاد المعلومات بطريقة مختلفة، لكنه لا يزال مقيداً بنفس الفصل (مفتاح التقسيم). GSI يشبه كتاباً مرجعياً منفصلاً يمكنه الإحالة إلى أي فصل.

القيد الحرج لـ LSI: حد الـ 10 GB لكل قيمة مفتاح تقسيم يشمل الجدول الأساسي وكل LSIs المرتبطة به معاً. إذا كان مستخدم واحد يمكن أن يتجاوز بياناته هذا الحد، LSI ليس الخيار الصحيح.

إنشاء جدول مع LSI

aws dynamodb create-table \
  --table-name Orders \
  --attribute-definitions \
    AttributeName=UserId,AttributeType=S \
    AttributeName=OrderId,AttributeType=S \
    AttributeName=CreatedAt,AttributeType=S \
  --key-schema \
    AttributeName=UserId,KeyType=HASH \
    AttributeName=OrderId,KeyType=RANGE \
  --local-secondary-indexes '[{
    "IndexName": "CreatedAtIndex",
    "KeySchema": [
      {"AttributeName": "UserId", "KeyType": "HASH"},
      {"AttributeName": "CreatedAt", "KeyType": "RANGE"}
    ],
    "Projection": {"ProjectionType": "ALL"}
  }]' \
  --billing-mode PAY_PER_REQUEST \
  --region us-east-1

الاستعلام عبر LSI

aws dynamodb query \
  --table-name Orders \
  --index-name CreatedAtIndex \
  --key-condition-expression 'UserId = :uid AND CreatedAt BETWEEN :start AND :end' \
  --expression-attribute-values '{
    ":uid": {"S": "user-123"},
    ":start": {"S": "2024-01-01"},
    ":end": {"S": "2024-12-31"}
  }' \
  --region us-east-1

متى تختار GSI؟

GSI هو الخيار الافتراضي في معظم حالات الاستخدام الحديثة. إذا كنت تحتاج إلى الاستعلام بحقل لا علاقة له بمفتاح التقسيم الأصلي — مثل البحث عن كل الطلبات بحالة معينة بغض النظر عن المستخدم — فأنت تحتاج GSI.

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

إضافة GSI إلى جدول موجود

aws dynamodb update-table \
  --table-name Orders \
  --attribute-definitions \
    AttributeName=Status,AttributeType=S \
    AttributeName=CreatedAt,AttributeType=S \
  --global-secondary-index-updates '[{
    "Create": {
      "IndexName": "StatusCreatedAtIndex",
      "KeySchema": [
        {"AttributeName": "Status", "KeyType": "HASH"},
        {"AttributeName": "CreatedAt", "KeyType": "RANGE"}
      ],
      "Projection": {"ProjectionType": "KEYS_ONLY"},
      "ProvisionedThroughput": {
        "ReadCapacityUnits": 5,
        "WriteCapacityUnits": 5
      }
    }
  }]' \
  --region us-east-1

ملاحظة: إذا كان الجدول يستخدم PAY_PER_REQUEST، احذف حقل ProvisionedThroughput من الأمر أعلاه.

الاستعلام عبر GSI

aws dynamodb query \
  --table-name Orders \
  --index-name StatusCreatedAtIndex \
  --key-condition-expression '#s = :status AND CreatedAt > :date' \
  --expression-attribute-names '{"#s": "Status"}' \
  --expression-attribute-values '{
    ":status": {"S": "PENDING"},
    ":date": {"S": "2024-06-01"}
  }' \
  --region us-east-1

تشخيص مشكلة حقيقية: GSI لا يُرجع البيانات المتوقعة

أحد أكثر المشاكل شيوعاً في الإنتاج: تكتب عنصراً في الجدول، ثم تستعلم فوراً عبر GSI ولا تجد النتيجة. الافتراض الأول دائماً أن هناك خطأ في الكود أو في مفاتيح الاستعلام.

السبب الفعلي: GSI يعمل بنموذج الاتساق في نهاية المطاف (Eventually Consistent). التحديث يصل إلى الفهرس بعد تأخير قصير — وهذا التأخير غير محدد بقيمة ثابتة في توثيق AWS. الحل ليس إعادة المحاولة بشكل عشوائي، بل إعادة تصميم التطبيق ليتعامل مع هذا النموذج: إما قراءة من الجدول الأساسي مباشرة بعد الكتابة، أو تصميم تجربة المستخدم لتتحمل التأخير.

للتحقق من حالة GSI وما إذا كان لا يزال في طور البناء:

aws dynamodb describe-table \
  --table-name Orders \
  --query 'Table.GlobalSecondaryIndexes[*].{Name:IndexName,Status:IndexStatus,Backfilling:Backfilling}' \
  --region us-east-1

الفهرس لن يكون متاحاً للاستعلام حتى يصل IndexStatus إلى ACTIVE. أثناء بناء الفهرس على جدول موجود، قد تستمر العملية لفترة تعتمد على حجم البيانات.

sequenceDiagram participant App as التطبيق participant DDB as DynamoDB Table participant LSI as LSI participant GSI as GSI App->>DDB: PutItem DDB-->>LSI: تحديث متزامن DDB-->>App: استجابة النجاح Note over GSI: تأخير غير محدد DDB-->>GSI: تحديث غير متزامن App->>LSI: Query (Strongly Consistent متاح) LSI-->>App: النتيجة فورية App->>GSI: Query (Eventually Consistent فقط) GSI-->>App: قد لا تظهر البيانات الجديدة فوراً
  1. الكتابة في الجدول الأساسي: تتم بشكل متزامن وتُرجع استجابة النجاح فور اكتمالها.
  2. تحديث LSI: يحدث بشكل متزامن مع الكتابة — القراءة المتسقة بشكل قوي متاحة فوراً.
  3. تحديث GSI: يحدث بشكل غير متزامن — هناك فجوة زمنية قبل ظهور البيانات في الفهرس.
  4. القراءة من GSI: دائماً Eventually Consistent — لا يمكن طلب Strongly Consistent من GSI.

اعتبارات التكلفة والأداء

GSI له سعة إنتاجية مستقلة — هذا ميزة ومسؤولية في نفس الوقت. إذا كانت استعلامات الفهرس كثيفة، يمكنك تخصيص سعة أعلى له دون التأثير على الجدول الأساسي. لكن كل كتابة في الجدول الأساسي تستهلك وحدات كتابة من كل GSI مرتبط به أيضاً — لذا كلما زاد عدد GSIs، زادت تكلفة كل عملية كتابة.

اختيار Projection المناسب يؤثر بشكل مباشر على التكلفة: KEYS_ONLY يقلل حجم الفهرس وتكلفة تخزينه، لكن إذا احتاج تطبيقك إلى حقول إضافية، ستضطر إلى قراءة إضافية من الجدول الأساسي (Fetch) مما يزيد الكمون والتكلفة. ALL يبسط الكود لكنه يضاعف التخزين.

دليل القرار: LSI أم GSI في DynamoDB؟

graph TD Start(["هل تحتاج إلى فهرس ثانوي؟"]) Q1{"هل الجدول موجود بالفعل؟"} Q2{"هل تحتاج للاستعلام عبر تقسيمات مختلفة؟"} Q3{"هل تحتاج إلى Strongly Consistent Read؟"} Q4{"هل حجم البيانات لكل تقسيم قد يتجاوز 10 GB؟"} GSI1["استخدم GSI"] GSI2["استخدم GSI"] GSI3["استخدم GSI"] LSI["استخدم LSI"] Start --> Q1 Q1 -->|"نعم"| GSI1 Q1 -->|"لا"| Q2 Q2 -->|"نعم"| GSI2 Q2 -->|"لا"| Q3 Q3 -->|"لا"| GSI3 Q3 -->|"نعم"| Q4 Q4 -->|"نعم"| GSI3 Q4 -->|"لا"| LSI
  1. إذا كنت تحتاج إلى الاستعلام عبر قيم مفتاح تقسيم مختلفة، GSI هو الخيار الوحيد الممكن.
  2. إذا كانت استعلاماتك دائماً محددة بمفتاح تقسيم واحد وتحتاج إلى قراءة متسقة بشكل قوي، LSI هو الخيار الأنسب.
  3. إذا كان الجدول موجوداً بالفعل، LSI غير متاح — الخيار الوحيد هو GSI.
  4. إذا كانت البيانات لكل مفتاح تقسيم قد تتجاوز 10 GB، تجنب LSI تماماً.

صلاحيات IAM المطلوبة للاستعلام عبر الفهارس

الاستعلام عبر فهرس ثانوي يتطلب صلاحية dynamodb:Query على مورد الفهرس تحديداً، وليس فقط على الجدول. هذا خطأ شائع يؤدي إلى رفض الطلبات في بيئات الإنتاج.

🔽 عرض سياسة IAM للاستعلام عبر الفهارس
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:Query"
      ],
      "Resource": [
        "arn:aws:dynamodb:us-east-1:123456789012:table/Orders",
        "arn:aws:dynamodb:us-east-1:123456789012:table/Orders/index/StatusCreatedAtIndex",
        "arn:aws:dynamodb:us-east-1:123456789012:table/Orders/index/CreatedAtIndex"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/Orders"
    }
  ]
}

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

LSI وGSI في DynamoDB ليسا خيارين متكافئين — لكل منهما حالة استخدام محددة. القاعدة العملية: ابدأ بـ GSI كخيار افتراضي لمرونته وقابليته للإضافة لاحقاً، وانتقل إلى LSI فقط عندما تحتاج إلى اتساق قوي في القراءة وأنت متأكد أن حجم البيانات لكل تقسيم لن يتجاوز 10 GB.

للتعمق أكثر، راجع توثيق AWS الرسمي:

مسرد المصطلحات

المصطلح التعريف
Partition Key (PK) مفتاح التقسيم — يحدد التقسيم الفيزيائي الذي يُخزن فيه العنصر في DynamoDB
Sort Key (SK) مفتاح الفرز — يُستخدم لترتيب العناصر داخل نفس التقسيم والاستعلام عنها بنطاق
Projection مجموعة الحقول المنسوخة من الجدول الأساسي إلى الفهرس الثانوي
Eventually Consistent نموذج اتساق يضمن وصول التحديثات إلى كل النسخ في نهاية المطاف، لكن مع تأخير محتمل
Strongly Consistent نموذج اتساق يضمن أن القراءة تعكس آخر كتابة ناجحة فوراً

Related Posts

تعليقات

المشاركات الشائعة من هذه المدونة

استرجاع معرّف نسخة EC2 عبر خدمة البيانات الوصفية IMDSv2

أوضاع سعة DynamoDB: متى تختار On-Demand ومتى تختار Provisioned؟

فهم مهلة الرؤية في SQS: لماذا تُعالَج رسائلك مرتين؟