לדלג לתוכן

עימוד תוצאות ב-API - Pagination Patterns מוסבר

בהתחלה זה עובד מצוין. יש לכם endpoint שמחזיר GET /users, ומקבלים בחזרה מערך עם כל המשתמשים. עשרה משתמשים, מאה משתמשים, אפילו אלף - זה עדיין רץ. אבל ברגע שהטבלה גדלה למאה אלף שורות, אותו endpoint פשוט חייב להתנהג אחרת, כי "תחזיר לי הכל" הופך לבעיה אמיתית של ביצועים וזיכרון, אצלכם ואצל הלקוח כאחד.

למה "תחזיר הכל" נשבר

שאילתה שמביאה את כל השורות מטבלה גדולה לוקחת זמן, צורכת המון זיכרון בצד השרת כדי להחזיק את כל התוצאות לפני ששולחים אותן החוצה, ומחזירה תשובה ענקית שהלקוח צריך להוריד ולפרסר במלואה. גם אם המשתמש בפועל רוצה לראות רק עשרים פריטים במסך הראשון שלו, הוא עדיין מקבל את כולם. זו בזבזנות מיותרת שרק הולכת ומחמירה ככל שהמערכת גדלה, ולכן כמעט כל API רציני שמחזיר רשימות צריך עימוד (pagination) מהיום הראשון, לא כתוספת מאוחרת.

עימוד לפי Offset ו-Limit

הגישה הכי אינטואיטיבית היא עימוד לפי offset ו-limit. הלקוח מבקש, למשל, limit=20&offset=40, ומקבל בחזרה עשרים שורות, החל מהשורה ה-41. זה קל להבנה, קל למימוש, וקל לבנות ממנו ממשק "עמוד 1, עמוד 2, עמוד 3" עם מספרים.

הבעיה מתחילה כשנתונים משתנים בין בקשה לבקשה. תארו לעצמכם משתמש שעובר בין עמודים, וממש בין הבקשות, מישהו אחר מוחק שורה שהייתה קודם בתוצאות. כל השורות אחריה "זזות" עמדה אחת קדימה, מה שגורם לשורה אחת להידלג עליה, או לחלופין להופיע פעמיים בשני עמודים שונים. בנוסף, ככל שה-offset גדל, מסד הנתונים בפועל צריך "לספור ולדלג" על כל השורות שלפניו לפני שהוא מגיע לעמוד המבוקש, מה שהופך עמודים מאוחרים בטבלה גדולה לאיטיים משמעותית מהעמוד הראשון.

עימוד לפי Cursor

עימוד מבוסס cursor פותר את שתי הבעיות האלה בגישה שונה לגמרי. במקום לבקש "תן לי מהשורה ה-40", הלקוח מבקש "תן לי את השורות שבאות אחרי הפריט הזה שכבר ראיתי", כשה-cursor הוא בדרך כלל ערך ייחודי וממוין כמו מזהה או חותמת זמן של הפריט האחרון שהתקבל.

מכיוון שהמצביע הזה מצביע על פריט קונקרטי ולא על מיקום מספרי בתוצאות, הוא נשאר יציב גם אם שורות נמחקות או נוספות באמצע. השאילתה עצמה גם נשארת מהירה בכל עומק, כי מסד הנתונים יכול לקפוץ ישר לנקודה אחרי ה-cursor בעזרת אינדקס, בלי לספור את כל מה שקדם לה. המחיר הוא שקשה יותר לבנות ממשק עם "קפוץ לעמוד 47" - עימוד cursor מתאים הרבה יותר לגלילה קדימה ואחורה, לא לניווט חופשי בין עמודים.

אגב, אם הכרתם ממשקי גלילה אינסופית (infinite scroll) בצד לקוח, זה בדיוק העיקרון המקביל בצד השרת - כל "עוד קצת נתונים" שנטען כשמגלגלים למטה הוא בקשה חדשה עם ה-cursor האחרון שהתקבל.

מטא-דאטה בתגובה

תגובת API עם עימוד צריכה לכלול לא רק את הנתונים עצמם, אלא גם מידע על איך להמשיך הלאה. בעימוד מבוסס cursor, זה בדרך כלל שדה next_cursor שהלקוח שולח בבקשה הבאה. בעימוד מבוסס offset, זה יכול להיות קישור מלא לעמוד הבא, או פשוט המספר של העמוד הבא.

שאלה שכדאי לחשוב עליה בנפרד היא האם להחזיר גם ספירה כוללת של כל התוצאות (total count). זה נשמע כמו מידע שימושי, וזה בהחלט יכול להיות, אבל בטבלה גדולה מאוד, שאילתת ספירה מדויקת יכולה להיות יקרה כמעט כמו השאילתה המקורית עצמה, לפעמים אפילו יותר. לכן במערכות בקנה מידה גדול נהוג לוותר על ספירה מדויקת, או להסתפק בהערכה מקורבת, במקום לשלם את מחיר הביצועים על מספר שרוב המשתמשים בכלל לא מסתכלים עליו.

לסיכום

עימוד תוצאות הוא לא פרט טכני שולי - הבחירה בין offset לבין cursor משפיעה ישירות על יציבות התוצאות ועל ביצועי המערכת שלכם תחת עומס אמיתי. offset פשוט למימוש אבל שביר כשנתונים משתנים ואיטי בעמודים רחוקים, ו-cursor יציב ומהיר אבל מגביל את חוויית הניווט. בקורס צד שרת בונים endpoints עם עימוד אמיתי, כולל ההתלבטות הזו על סוג ה-pagination הנכון לכל מקרה.

אם אתם לא בטוחים איזו גישה מתאימה ל-API שאתם בונים כרגע, שווה לשתף בדיסקורד ולשמוע איך אחרים פתרו בעיה דומה.

הצטרפו לקהילה בדיסקורד