שימושים שגויים בפונקציות תאריך
- Geri Reshef
- Nov 22, 2023
- 4 min read
Updated: Dec 20, 2023
פונקציות תאריך הן פונקציות כדוגמת DateAdd, DateDiff, DatePart ונגזרות שלהן, כלומר - פונקציות שמופעלות על תאריכים בגלל היחודיות של התאריכים, שלכאורה נראים כסוג של מספרים, אבל מתבצעים עליהם חישובים ויש להם מאפיינים שאינם קיימים באריתמטיקה.
את הטעויות ניתן לתאר בקצרה כשימוש בעמודות מאונדקסות בפונקציות תאריך, דבר המונע שימוש באינדקס, וכתוצאה מכך מתבצע scan על הטבלה.
לצורך הדוגמאות הכינותי מראש את הטבלה הבאה, כולל עמודת תאריך ו-Clustered Index עליה:
Drop Table If Exists T_Messages;
Select *,
DateAdd(Day,2*severity,DateAdd(Hour,message_id,DateAdd(Day,language_id,DateAdd(Year,-14,Cast(GetDate() As SmallDateTime))))) [DateTime]
Into T_Messages
From sys.messages
Order By [DateTime];
Create Clustered Index Idx_T_Messages
On T_Messages([DateTime])
With(OnLine=On,Data_Compression=Page);
וכעת אנחנו מחפשים את השורות שהתאריך שלהן חל בחודש הנוכחי. יש כאלה שפותרים זאת כך - השורות שהשנה שלהן היא השנה הנוכחית, והחודש שלהן הוא החודש הנוכחי, למשל אופן הבא:
Select *
From T_Messages
Where Year([DateTime])=Year(GetDate())
And Month([DateTime])=Month(GetDate());
הניסוח ה-SQL-י די ברור ואינני מזלזל בזה, אבל המערכת תבצע scan על הטבלה ולא תוכל להשתמש באינדקס.
הדרך הנכונה היא לחפש את השורות שהתאריך שלהן גדול שווה לתחילת החודש הנוכחי, וקטן מתחילת החודש הבא.
כדי למצוא את תחילת החודש הנוכחי נחשב בעזרת DateDiff כמה חודשים חלפו מתחילת הספירה ע"פ מיקרוסופט (תאריך 0 = 01/01/1900), ואת התוצאה נוסיף בחודשים - שוב לתאריך ה-0. כלומר- אם חלפו 99 חודשים מאז ה-0 (לא כולל שארית), הרי אם נוסיף לתאריך ה-0 99 חודשים - נגיע לתחילת החודש הזה. והפתרון:
Select *
From T_Messages
Where [DateTime]>=DateAdd(Month,DateDiff(Month,0,GetDate()),0)
And [DateTime]<DateAdd(Month,DateDiff(Month,0,GetDate())+1,0);
יש להודות שה-SQL קצת פחות ברור, גם בגלל החישוב המורכב עם DateAdd & DateDiff, וגם בגלל שהחישוב מתבצע פעמיים בשינוי מינורי.
ניתן לנסח את זה באופן קצת יותר אלגנטי בעזרת Outer Apply כך:
Select *
From T_Messages T
Outer Apply (Select DateAdd(Month,DateDiff(Month,0,GetDate()),0) BOM) OA --BOM=Beginning of month
החישוב של תחילת החודש מתבצע פעם אחת, וב-Where נעשה עליו חישוב משלים של הוספת חודש כדי לקבל את תחילת החודש הבא.
מה השיפור בביצועים?
נעיין ב-Execution Plan וב-IO Statistics:
גם התמחור של ה-Execution Plan ב-% וגם מספר הדפים שנקראו ממחישים את ההשפעה הדרמטית על הביצועים.
במקרה ספציפי זה, בו אנחנו רוצים לקבל נתונים מתחילת החודש ועד סוף החודש (החודש הזה, החודש הקודם, החודש של פרמטר שהועבר לפרוצדורה) ניתן להשתמש בפונקצייה EOM (קיצור של End Of Month) וממנה לגזור על פי הצורך את תחילת החודש, אבל זה יעבוד אם נרצה את כל השורות בשנה מסויימת או בחודש מסויים מכיוון שאין פונקציות שמחזירות סוף יום או סוף שנה.
כעת אנחנו רוצים לקבל את כל השורות מהיום.
טעות שנתקלתי בה גם לגבי מקרה זה וגם לגבי הדוגמה הקודמת הוא להפוך את התאריך למחרוזת הכוללת את השנה, החודש והיום (במקרה הזה) או השנה והחודש (בדוגמה הקודמת), למשל:
Select *
From T_Messages
Where Format([DateTime],'yyyyMMdd')=Format(GetDate(),'yyyyMMdd');
יש שתי דרכים נכונות לעשת זאת ולהימנע מ-scan על כל הטבלה:
אפשרות אחת היא כמו בדוגמה הקודמת, אלא שנחשב את התאריכים של תחילת היום ותחילת היום הבא, ונחפש את כל מה שבינהם;
והאפשרות השנייה היא להפוך את התאריך מ-DateTime ל-Date, שינוי שאינו מונע שימוש באינדקס:
Select *
From T_Messages
Where Cast([DateTime] As Date)=Cast(GetDate() As Date);
הערה: פעולת ה-Cast as Date לא תמנע את השימוש באינדקס, אבל כן תמנע את השימוש בסטטיסטיקה, וכתוצאה מכך המערכת עלולה לבחור ב-Plan לא אופטימלי.
ולבסוף: אנחנו מחפשים את כל השורות שהתאריך שלהן הוא במהלך 21 הימים האחרונים ואילך.
פתרון לא יעיל הוא לחשב את מספר הימים מהתאריך בטבלה להיום, ולכתוב בתנאי שהוא קטן או שווה ל-21:
Select *
From T_Messages
Where DateDiff(Day,[DateTime],GetDate())<=21;
וכך גם להוסיף 21 יום לתריך שבטבלה ולכתוב בתנאי שזה גדול או שווה ל-GetDate:
Select *
From T_Messages
Where DateAdd(Day,21,[DateTime])>=GetDate();
הדוגמה האחרונה נותנת רמז עבה מה צריך להיות הניסוח הנכון של השאילתה - במקום להוסיף 21 לתאריך בטבלה, נוריד 21 יום מ-GetDate:
Select *
From T_Messages
Where [DateTime]>=DateAdd(Day,-21,GetDate());
באופן דומה לכל הדוגמאות עד כאן ניתן לשלוף את כל השורות מהשנה שעברה, מלפני היום, מהשבוע הקודם או משבעת הימים האחרונים וכו'.
ודוגמה אחרונה - פילטר על פי הגיל.
אנחנו מעוניינים לשלוף את כל השורות שחלפו מעל 10 שנים מאז (לו היה מדובר בתאריכי לידה - היינו אומרים שגילם מעל 10). חישוב הגיל נעשה על פי ההפרש ביו שנת הלידה לשנה הנוכחית, כך שאם מישהו נולד ב-31/12/2020, הרי למחרת ב-01/01/2021 הוא בן שנה, ולחילופין - מי שנולד ב-01/01/2020 - הרי ב-31/12/2020 הוא עדיין בן 0.
והשליפה האינטואיטיבית אך השגויה:
Select *,
DateDiff(Year,[DateTime],GetDate()) DateDiffYears
From T_Messages
Where DateDiff(Year,[DateTime],GetDate())>10;
מכיוון שמתבצע חישוב על העמודה המאונדקסת DateTime - לא ניתן לנצל את האינדקס ומתבצע scan על הטבלה.
הפתרון אינו [DateTime] < DateAdd(Year,-10,GetDate()) מכיוון שאנחנו רוצים לחשב הפרש בשנים עגולות, ולכן נשתמש בפטנט מהדוגמה הראשונה, אבל הפעם עם הפרש השנים (ולא החודשים):
Select *,
DateDiff(Year,[DateTime],GetDate()) DateDiffYears
From T_Messages
Where [DateTime]<DateAdd(Year,DateDiff(Year,0,GetDate())-10,0);
יש להדגיש שהבעיה אינה הביטוי שבפסוקית ה-Select שהוא רק חישוב, אלא התנאי שב-Where.
האם ניתן לפתור בצורה כזו את כל הבעיות ולהבטיח שימוש יעיל באינדקס? התשובה היא לא.
להלן מספר דוגמאות שלי אין פתרונות עבורם המאפשרים שימוש יעיל באינדקס, ולהלן מספר דוגמאות לבעיות פתוחות:
שליפה הכוללת השוואה בין שתי עמודות, מכיוון שבמקרה זה - אם נחלץ עמודה אחת מפונקציית התאריך - השנייה תישאר בפנים, וגם אם זו שחילצנו היא המאונדקסת - בצד השני לא נקבל ערך קבוע, אלא ערך שישתנה משורה לשורה, ולכן המערכת תצטרך לבצע scan ולבדוק את כל השורות.
באופן דומה לנ"ל - שליפה הכוללת חישוב המתבסס על עמודת תאריך ועמודה המציינת מספר ימים. למשל: לכל הלוואה יש תאריך פרעון ומספר ימי חסד שיש להמתין לפני שפונים ללווה, ואנחנו רוצים לשלוף את כל ההלוואות שהתאריך + מספר ימי החסד קטן מהיום. לא משנה את מי נמקם באיזה צד של ההשוואה ב-Where, המערכת תבצע scan על הטבלה כדי לבדוק כל שורה בנפרד. כמובן שאין הכוונה למקרה בו מספר ימי החסד זהה לכולם, אלא כשלכל הלוואה מספר שונה.
שליפה הכוללת תנאי על חלק מהיממה או מהשנה וכו'. למשל - כל השורות שהן בין חצות ל-6:00 בבוקר, או כל השורות שהן בחודש ינואר (לא משנה השנה). הבעייה כאן היא שמדובר בהרבה טווחי תאריכים (ינואר השנה, ינואר אשתקד, וכו') ולא באחד כמו בדוגמאות שהעליתי.
הבעיות הנ"ל פתירות בשיטות "לא קונבנציונליות" כמו עמודה מחושבת או Indexed View, שאינני עוסק בהם כאן.
לבסוף כדאי להדגיש: השימושיות של האינדקס על עמודת התאריך באה לידי ביטוי בפסוקיות ה-Where ה-Join On, ה-Order By, ובמקרים מסויימים גם ה-Group By ופונקציות חלון; ובמקרים האלו יש לשים לב שמאפשרים למערכת להשתמש בו. לעומת זאת ב-Select לא אמורה להיות כל בעיה, ולכן אם יש בשאילתה העמודה מחושבת הכוללת עמודת תאריך ופונקציית תאריך - לא אמורה להיות כל בעיה עם זה.
Tham gia sv 388, anh em sẽ được tận hưởng hệ thống cá cược hiện đại, tốc độ live ổn định và hình ảnh sắc nét. Những trận chọi gà hấp dẫn kết hợp khuyến mãi khủng giúp sv388 trở thành điểm hẹn lý tưởng của người yêu giải trí trực tuyến.
Xin88 ghi dấu ấn trên thị trường Việt Nam nhờ công nghệ tiên tiến, bảo mật thông tin tuyệt đối và dịch vụ tận tâm. Không chỉ giải trí, xin88.vegas còn tạo môi trường cá cược trực tuyến công bằng, minh bạch và uy tín hàng đầu.
https://88vv88.tech/ từ lâu đã trở thành một cái tên quen thuộc với cộng đồng giải trí tại Việt Nam. Ngay khi truy cập vào nền tảng, người dùng dễ dàng bị thu hút bởi giao diện hiện đại, thân thiện cùng trải nghiệm mượt mà. Với định hướng phát triển lâu dài, 88Vv không ngừng đổi mới để mang đến một không gian giải trí chất lượng và an toàn cho mọi đối tượng.
Không nhà cái nào chịu chơi như 888b. Truy cập 888baz com là bạn đã bước chân vào thế giới cược rực rỡ, nơi mọi giới hạn đều có thể bị phá vỡ.
https://go8.onl/ là thương hiệu cá cược trực tuyến uy tín tại châu Á, nổi bật với kho trò chơi đa dạng và dịch vụ chất lượng. Người chơi chỉ cần đăng ký trên website chính thức để trải nghiệm bảo mật an toàn, giao dịch nạp rút tự động, nhanh chóng. Hệ thống liên tục cập nhật khuyến mãi hấp dẫn cùng các điều khoản rõ ràng, mang đến trải nghiệm công bằng và minh bạch.