en-UShe-IL
You are here:   Blog
Register   |  Login

Blog Archive:

Maximize
* Can be used in order to search for older blogs Entries

Search in blogs


Blog Categories:

Maximize
* Can be used in order to search for blogs Entries by Categories

Blog Tags:

Maximize
* Can be used in order to search for blogs by keywords

TNWikiSummit


Awared MVP

 


Microsoft® Community Contributor 


Microsoft® Community Contributor


 Read first, before you you use the blog! Maximize

Recent Entries

Minimize
נוב11

Written by: ronen ariely
11/11/2011 18:34 RssIcon

כיצד לאפשר דפדוף (חלוקת הרשומות לדפים ומעבר בין הדפים) כאשר אנו רוצים לקבל את הרשומות בסדר אקראי?

הקדמה:

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

בואו נעבור לדון בבעיה, אציג בקצרה את הפתרון המיידי שאינו נותן מענה מעשי ונעבור לפתרון הבעיה בצורה יעילה ופשוטה. כל כך פשוט שלא נאמין עד כמה שברנו את הראש אולי ועד כמה המדריך ארוך בעוד כל התשובה יכולה להינתן בשורה אחת :-)

הבעיה:

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

אחרי הכול לא נרצה שהמשמש יעבור לעמוד 2 ויקבל שוב נתונים מעמוד 1 או שכאשר המשתמש יחזור לעמוד 2 אחרי שביקר בעמוד 3 הוא פתאום יקבל תוצאה שונה של רשומות. אחרי הכול המשתמש כבר יודע מה אמור להיות בעמוד 2 וחזר כדי לראות את מה שראה קודם.

אפשרות 1: הרעיון המיידי שהוצג, אינו מומלץ במסד נתונים כבד או עבור מספר משתמשים רב

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

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

הערה: הדיון כאן אינו נוגע בשליפת הנתונים בסדר אקראי על מנת להכניס אותם לטבלה הזמנית. המדריך הבא למי שמעוניין דן בנושא זה

סיכום:

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

אפשרות 2: פתרון יעיל

הרעיון הבסיסי: נשלוף בכל כניסה ראשונה לעמוד נתונים בצורה אקראית בכמות שאנו רוצים להציג בעמוד (נניח PagSize ישמש כמשתנה שיכיל את כמות הנתונים בעמוד). בזמן השליפה נוודא שהנתונים שאנחנו שולפים לא משויכים כבר לעמוד אחר בו צפינו. בכל כניסה לעמוד שכבר היינו בו נבדוק מי היו הרשומות שאותן ראינו בפעם הקודמת ונציג/נשלוף את רשומות אלו בלבד שוב.

שלב 1: ברמת האפליקציה נגדיר משתנה ברמת ה Session של המשתמש מסוג רשימה פשוטה או מערך דינאמי דו מימדי בשם ArrayRowsPerPage. במערך זה נכניס בעמודה ראשונה את מספר העמוד ובעמודה שנייה את הזיהוי של הרשומה. יש לוודא לכן שסוג העמודה השנייה במערך תתאים מחינת סוג האיברים שלו לסוג של המפתח הראשי בטבלה שלנו. לשם ההדגמה נניח שבטבלה שלנו יש לנו שדה של מפתח ראשי ID מסוג INT.

שלב 2: שליפת הנתונים ממסד הנתונים עבור עמוד מסוים תבוצע בפשטות על ידי בחירת רשומות בכמות נתונים שרוצים להציג בעמוד (PagSize). אם כבר היינו בעמוד הנוכחי אז נשלוף את הנתונים המסויימים שראינו בפעם הקודמת ואם זה עמוד חדש פשוט נבחר PagSize רשומות שעדיין לא נבחרו עד כה.

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

כניסה לעמוד שכבר היינו בו: השאילתה זהה דיוק למקרה של עמוד חדש רק שהפעם האפליקציה תזהה תחילה מהמערך שלנו שיש כבר רשומות בעמוד זה שצפינו בהם ולכן בשאילתה ברמת הסינון יכנס התנאי של בחירת הרשומות עם המפתח הייחודי המסוים של העמוד הנוכחי

* הערה: באופן תיאורטי ניתן לשמור ברמת האפליקציה לא רק את המפתח הייחודי אלא את כל התוכן של הרשומות ובכך בכלל להימנע מהצורך לגשת למסד הנתונים שוב כדי לצפות ברשומות של עמוד שכבר צפינו בו. לדבר זה יש כמובן יתרונות וחסרונות כשנקודה המרכזית שיש לשים לב זה עניין הזכרון! שיטה זו תשמור עבור כל משתמש פעיל (עם סשן פתוח) את כל הרשומות שהם צפה עד כה. בעוד שלמשתמש בודד זה יכול להיות מטוב בסדרי גודל לאפליקציה הרי שבמצב של ריבוי משתמשים ורשומות כבדות או כמות רשומות רב מדובר בחסרון ענק ובדרך בלתי מומלצת לחלוטין. ברירת המחדל שאני מציע היא שמירת רק מספרי ה ID. היה ובחרנו בשיטת עבודה זו אז ברמת האפליקציה נגדיר אוסף מורכב יותר ונוכל לנצל את הכוח של מחלקות גנריות. למשל אם יש לנו מחלקה בשם MyRow המתאימה לרשומה במסד הנתונים אז נוכל העזר ב List.

נקודות נוספות למחשבה

* הערה: במדריך זה בחרתי לעבוד עם מערך דומימדי אבל ניתן לעבוד עם מערך פשוט חד מימדי. במקרה זה כשחוזרים לעמוד שכבר צפינו נאלץ לבצע יותר עבודה ברמת האפליקציה על מנת לבודד את הנתונים המשויכים לעמוד זה.

* row_number הוא שיטה יעילה לביצוע חלוקה לעמודים ובחירת נתונים המשוייכים לעמוד מסויים במקרים בהם כמות הנתונים אינה גדולה מדי. יש לזכור ששימוש ב row_number בלי סינון מבצע מעבר ומספור של כל הנתונים בטבלה ולא הייתי מציע לנסות זאת בטבלה עם טרה רשומות רק כדי להחזיר 10 נתונים שרוצים להציג בעמוד מסוים!

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

* שיטת עבודה המוצגת במדריך זה תאפשר לעבוד גם עם טרות רשומות ללא בעיה וללא זמן שליפה מיותרת או בנייה של טבלאות מיותרות, בהנחה שמבצעים את הבחירה האקראית של הרשומות בצורה יעילה, ולזה יש לי קישור מעניין שיכול לעזור ונותן פתרונות יעילים במקרים שונים:

http://ariely.info/dnn/Blog/tabid/83/EntryId/31/Select-a-random-row-from-table.aspx

קישור זה נוגע בנקודה מתי כן ומתי לא מומלץ לעבוד עם ROW_NUMBER ומציג פתרונות אחרים יעילים הרבה יותר גם ל SQL וגם לשרתים אשר בכלל לא מכירים את הפונקציה ROW_NUMBER. כמו כן בקישור זה נמצא דרכים יעילות יותר משימוש ב NEWID על מנת לקבוע את סדר כל הרשומות בזמן שאנו מעוניינים בסך הכל בכמות רשומות קטנה ולכן אין טעם לעבור על כל הרשומות.

* גישה דרך טכנולוגיית ASP קלאסי (כפי שהתבקש בשאלה המקורית) נעשית על ידי משתמש מסוים ללא חשיבות כמה גולשים מבצעים את הגישה. למעשה האפליקציה ניגשת לשרת ה SQL ומהווה "לקוח" אשר פותח סשן עם הגישה שלו לשרת. הסשן הרלוונטי במקרה של עבודה עם טבלאות זמניות בשרת מסדי הנתונים הוא לא הסשן של הגולש בשרת ה IIS אלא הסשן של האפליקציה על ה SQL. טבלה זמנית המוגדרת ב SQL באמצעות סימון # קיימת ברמת סשן של ה SQL... ניתן לחשוב על ההשלכה של נקודה זו ועל שימוש בטבלה זמנית מסוג # על ידי מספר משתמשים? ניתן דרך אגב בפרופיילר של ה SQL לוודא מתי מבוצע פתיחת סשן או סגירת סשן על ידי בחירת LOGIN כמובן.

* אחד הייתרונות באפשרות 2 שהוצגה כאן היא שבזמן שמירת הנתונים לשם דפדוף בעמודים שכבר נצפו אנחנו נעזרים רק בנתונים שכבר הועברו אל האפליקציה בכל מקרה ולכן אין כאן פניות נוספות למסד הנתונים או פעולות של יצירת אלמנטים חדשים במסד הנתונים כגון טבלה זמנית כפי שהוצגה באפשרות ראשונה לפתרון.

* נתונים שכבר הועבו אל האפליקציה ניתן לשמור בשיטות שונות ולא בהכרח ברמת השרת כפי שהוצג כדוגמה במדריך. הדבר יכול לחסוך משאבים רבים של זכרון. אחת השיטות היעילות בעבודה באפליקציית WEB היא שמירה של הנתונים בקבצי עוגייה. בצורה זו נוכל לשמור את הרשומות עצמן ולא רק את המספרי ID. גם ברמת השרת ישנן שיטות נוספות לשמיר הנתונים (נתונים מלאים או רק נתוני ה ID בהתאם לאפיון שלנו) כגון שמירה ברמת Cache התלוי בפרמטר של העמוד בו צופים + פרמטר של המשתמש (בדרך כלל יעיל הרבה פחות משימוש ב Session שכבר מכסה את נושא המשתמש המסוים), ניתן לשמור ברמת ה Cache של הדפדפן ועוד...

* שאלות של האם החסרונות שהוצגו אכן מהווים חיסרון עבור האפיון שלנו הם מרכזיות וכדאי תמיד לחשוב עליהן. למשל השאלה הבאה יכולה להשפיע על בחירת הפתרון. האם מפריע לנו שבין אלף אנשים שנכנסים לאתר שלנו 2 אנשים שנכנסו בזה אחר זה בהפרש של שנייה יקבלו את אותה חלוקה לעמודים בעקבות שימוש בטבלה זמית ברמת ה SQL? האם הורדת הרנדומליות מעט על ידי שמירת תוצאות למשל X זמן או על פי פרמטר אחר ובכך מיטוב עבודת האפליקציה אולי מהווה חיסרון או ייתרון?

* הנקודה המרכזית תמיד בבחירת האפשרות לפתרון היא כמובן מילת המפתח היא תמיד אפיון! אפיון נכון ומלא והכרה של כל האפשרויות העומדות בפני המפתח על החסרונות הוייתרונות של כל שיטה הם הבסיס לפיתוח יעיל ומותאם.

לסיכום

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

מושגים:

האפליקציה: האפליקציה שאמורה לקבל את הנתונים. יכולה להיות ה SSMS עצמו או אפליקציה חיצונית כמו אתר אינטרנט.. הדבר אינו חשוב לצורך העניין. לשם ההדגמה אשתמש מושגים המתאימים לאפליקציית WEB בשפת C#.