DCSIMG
January 2011 - Posts - גרי רשף

January 2011 - Posts

סטיקר מלפני הרבה שנים שנזכרתי בו לאחר שקראתי את המאמר Understanding SQL Server 2008 R2 Fixed Database Level Roles של Satnam Singh, שהוא מסוג המאמרים הטכניים שאני אוהב לקרוא ומשתדל לכתוב: הכל מוסבר בפרוטרוט בצירוף צילום מסכים, בהנחה שהקורא (כלומר- אני) אינו חכם במיוחד; או אם להיות יותר רציניים- אני משתדל לכתוב כך שזה מה שהייתי רוצה לקרוא לפני שהתעמקתי בנושא וכשלא שלטתי בחומר.

המאמר מציין שניתן לתת ל-User (ברמת הדטבייס) הרשאות קריאה בלבד DB_DataReader, או לחילופין הרשאות כתיבה בלבד - DB_DataWriter (קצת מזכיר את הבדיחה על שני השוטרים- זה שקורא וזה שכותב); ובמקרה השני המשתמש יכול להכניס נתונים לטבלאות, אך לא לקרוא מתוכן. קצת משונה, אבל מי יודע? יש עת לכל דבר וזמן לכל חפץ: הנה, באחת ההרצאות של ISUG, שמעתי את הטענה של-DBA-ים צריך לתת הרשאות מלאות על הסכימה, אך ללא יכולת לקרוא את הנתונים שיכולים להיות סודיים.. הגיוני אבל נשמע לא כל כך מעשי.

בכל מקרה: ניסיתי לבדוק את הגבולות והעמידות של הרשאות ה-DB_DataWriter ומתברר שזה אמיתי וזה עובד.

ניצור Login + User עם הרשאות DB_DataWriter,

וכן שתי טבלאות- אחת עם נתונים אותם יש לנסות ולקרוא, ואחת ריקה שלתוכה יש להכניס נתונים:

USE tempdb;
Go
 
Create Login DWL With Password='1234';
Go
 
Create User DWU for LOGIN DWL;
Go
 
Exec SP_AddroleMember 'DB_DataWriter',DWU;
Go
 
Create Table Tbl1(I Int, S Varchar(50));
Go
 
Insert Into Tbl1 Values(1,'A');
Insert Into Tbl1 Values(2,'B');
Insert Into Tbl1 Values(3,'C');
Go
 
Create Table Tbl2(S Varchar(50));
Go

וכעת ננסה לבצע פעולות שונות תחת הזהות של DWU:

Execute As User='DWU';
Go
 
Select * From Tbl1;
Go
 
Select * From sys.objects;
Go
 
Select * From sys.columns Where object_Name(Object_id)='Tbl1';
Go
 
Insert Into TBL2 Select '1';
Go
 
Insert Into TBL2 Select Cast(I As Varchar)+','+S From Tbl1;
Go
 
Insert Into TBL2 Select '1' Where Exists (Select * From Tbl1);
Go
 
Update TBL1
Set I=10*I;
Go
 
Delete From Tbl1 Output deleted.*;
Go
 
Delete From Tbl1;
Go
 
Revert;
Go

clip_image002

(הערה- אני הרצתי את הקוד בדטבייס בשם MyDB ולא ב-tempdb, אך זה לא משנה)

1. פקודת ה-Select נכשלה בשל היעדר הרשאות.

2. פקודת Select מטבלת מערכת דווקא מצליחה: די הגיוני לאור העובדה שלא מדובר בנתונים אלא באובייקטים שלמשתמש DWU יש הרשאה להכניס לתוכם נתונים.

3. כנ"ל: ניתן לקרוא מטבלאות המערכת גם את המבנה של הטבלאות (כלומר- שמות העמודות בטבלה הראשונה).

4. ניתן להכניס נתונים לטבלה השניה.

5. לא ניתן להעביר נתונים מהטבלה הראשונה לשניה על ידי Insert + Select, למרות שפעולה זו אינה מאפשרת לראות את הנתונים באופן ישיר.

6. גם פעולה שרק בודקת מה יש בטבלה הראשונה (על ידי Exist) נכשלת.

7. פעולת Update נכשלת- יש הרשאות רק ל-Insert!

8. פעולת Delete בצירוף האופרטור Output המאפשר צפייה בנתונים ללא שימוש ב-Select מפורש - אסורה (גם אם הנתונים מופנים לטבלה אחרת ולא למסך).

9. פעולת Delete ללא התחכמויות - מותרת.

לסיכום: מותר לבצע Insert & Delete, מבלי לצפות בנתונים בשום דרך (כלומר- הנתונים חייבים להיות חדשים ולא ממוחזרים..);

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

לסיום- ננקה את כל מה שעשינו:

Drop User DWU;
Drop Login DWL;
Drop Table Tbl1;
Drop Table Tbl2;
Go

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

While @@TranCount>0 Commit Tran;

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

את הפרוצדורה שכתבתי הפעלתי באמצעות פרוצדורה אחרת שכללה פתיחה של טרנזקציה,

וכתוצאה מהקוד הנ"ל- כל הטרנזקציות נסגרו, כולל זו של הפרוצדורה המפעילה, וזה יצר את השגיאה.

רציתי להיות בטוח שהכל בסדר ויריתי לעצמי כדור ברגל..

מסקנה: יותר טוב להיות עשיר וחכם מאשר עני וטיפש.:-(

Posted by גרי רשף | with no comments

הרשאה לעיין בטבלאות המערכת או באובייקטים כדוגמת פרוצדורות ו-Views ניתנת על ידי View Definition.

ניצור Login ו-User ב-tempdb, ופרוצדורה סתמית לצורך ההדגמה:

Use tempdb;
Go
 
Create Login MyLogin With Password='1234',Check_Policy=Off;
Go
 
Create User MyUser For Login MyLogin;
Go
 
Create Proc MyProc As Select GetDate() Taarih;
Go

כעת ננסה לעיין בטבלת המערכת sys.objects ולקבל את ההגדרה של הפרוצדורה MyProc הנ"ל בתור אדמיניסטרטורים:

Select * From sys.objects;
Go
 
Exec SP_HelpText 'MyProc';
Go

clip_image002

ננסה כעת לבצע זאת בתור המשתמש החדש MyUser:

Execute As User='MyUser';
Go
 
Select * From sys.objects;
Go
 
Exec SP_HelpText 'MyProc';
Go
 
Revert;
Go

clip_image004

השליפה מטבלת המערכת לא החזירה שורות,

והנסיון לקבל את ההגדרה של הפרוצדורה גרם להודעת שגיאה.

הרשאה מתאימה לראות את ההגדרה של הפרוצדורה ניתן לתת (ולבדוק שהיא פועלת) כך:

Grant View Definition On MyProc To MyUser;
Go
 
Execute As User='MyUser';
Go
 
Exec SP_HelpText 'MyProc';
Go
 
Revert;
Go

clip_image006

הרשאה כללית לעיין בכל האובייקטים וההגדרות ניתן לתת כך (גם כאן בשילוב עם בדיקה):

Grant View Definition TO MyUser;
Go
 
Execute As User='MyUser';
Go
 
Select * From sys.objects;
Go
 
Revert;
Go

clip_image008

לסיום- נבטל את ההרשאות המיוחדות שנתנו ל-MyUser:

Revoke View Definition To MyUser;
Revoke View Definition On MyProc To MyUser;
Go

כדאי לשים לב ש-Revoke שונה מ-Deny בכך שאינה מונעת הרשאות אלא מבטלת הרשאות שניתנו קודם באמצעות פקודת Grant; כלומר- מבטלת הרשאות שניתנו במפורש/במישרין (Explicitly) ולא במרומז/בעקיפין (Implicitly).

פקודת SQL אמורה להיות מעוצבת בצורה נוחה לקריאה מבחינת מעברי שורות, הזחה (Identization), שימוש ב-Capitals & Italics וכו'.
אין לכך השפעה על הביצועים של המערכת, אבל כן על הביצועים של המפתח - ובעיקר אם לא הוא זה שכתב את הקוד: קשה להבין קוד שאינו ערוך נכון, וכשאני מתבקש לדבג או לתקן קטע קוד - אני קודם כל מסדר אותו בצורה נוחה לפני שאני צולל לעובי הקורה..

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

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

לטבלאות הזמניות אותו שם כמו לקבועות בצירוף הסיומת _tmp,

ושמות כל הטבלאות הקבועות מופיעים בטבלת ניהול בשם Tavlaot.

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

מוסיף מספר שורות לכל טבלה זמנית לצורך ההמחשה,

ואת שמות הטבלאות הקבועות לטבלת הניהול:

Use tempdb;
Go
 
If Object_Id('Tbl1_tmp') Is Not Null Drop Table Tbl1_tmp;
If Object_Id('Tbl2_tmp') Is Not Null Drop Table Tbl2_tmp;
If Object_Id('Tbl1') Is Not Null Drop Table Tbl1;
If Object_Id('Tbl2') Is Not Null Drop Table Tbl2;
If Object_Id('Tavlaot') Is Not Null Drop Table Tavlaot;
Go
 
Create Table Tbl1_tmp(ID Int Identity Primary Key,
                    Txt Varchar(Max));
 
Create Table Tbl1(Taarih DateTime Not Null,
                ID Int Not Null,
                Txt Varchar(Max),
                Constraint PK_Tbl1 Primary Key Clustered(Taarih,ID));
 
Create Table Tbl2_tmp(ID Int Identity Primary Key,
                    Txt Varchar(Max),
                    Mispar Int);
 
Create Table Tbl2(Taarih DateTime Not Null,
                ID Int Not Null,
                Txt Varchar(Max),
                Mispar Int,
                Constraint PK_Tbl2 Primary Key Clustered(Taarih,ID));
Go
 
Insert
Into     Tbl1_tmp(Txt)
Select   'Adva' Union All
Select   'Beni';
Insert
Into     Tbl2_tmp(Txt,Mispar)
Select   'Carmel',10 Union All
Select   'Dan',20 Union All
Select   'Esther',30;
Go
 
Create Table Tavlaot(Tavla Varchar(50) Primary Key);
Go
 
Insert
Into    Tavlaot
Select  'Tbl1' Union All
Select  'Tbl2';
Go

כעת נבנה באופן דינאמי את פקודות ה-SQL המתאימות שיעבירו את נתוני כל הטבלאות הזמניות לטבלאות הקבועות, וירוקנו את הטבלאות הזמניות;

ולשם כך נעבור בעזרת Cursor על טבלת הניהול,

את שמות העמודות של כל טבלה נמצא בעזרת טבלת המערכת sys.columns,

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

(כדאי להריץ מחדש את סקריפט יצירת הטבלאות ואיכלוסן כדי לשוב לנקודת ההתחלה):

Declare Cr Cursor For Select * From Tavlaot;

Declare @Tavla Varchar(50),
        @SQL Varchar(Max);
 
Open Cr
Fetch Next From Cr Into @Tavla
While @@Fetch_Status=0
    Begin
    Set @SQL='';
    Select  @SQL=@SQL+','+name
    From    sys.columns
    Where   Object_Name(Object_Id)=@Tavla
            And name<>'Taarih';
    Set @SQL=Stuff(@SQL,1,1,'');
    Set @SQL='Insert Into '+@Tavla+'(Taarih,'+@SQL+') Select GetDate() Taarih,'+@SQL+' From '+@Tavla+'_tmp;';
    Set @SQL=@SQL+'Delete From '+@Tavla+'_tmp;';
    Print @SQL;
    Exec(@SQL);
    Fetch Next From Cr Into @Tavla;
    End
Close Cr;
Deallocate Cr;
Go

clip_image002

הפקודות התבצעו בהצלחה- שתי שורות הועברו לטבלה הראשונה ונמחקו מהזמנית שלה, ושלוש בשניה.

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

כדי לעצב את הקוד נכון אני משתמש ב-Char(13) למעברי שורה, וב-Char(9) (תו Tab) ליישור.

נוסיף אם כך מעברי שורות בין האופרטורים השונים (Select, From וכו'),

שני מעברי שורות בין פקודת ה-Insert ופקודת ה-Delete,

ולרשימת העמודות נוסיף גם מעברי שורות וגם תווי Tab בעזרת Replace):

Declare Cr Cursor For Select * From Tavlaot;
Declare @Tavla Varchar(50),
        @SQL Varchar(Max);
Open Cr
Fetch Next From Cr Into @Tavla
While @@Fetch_Status=0
    Begin
    Set @SQL='';
    Select  @SQL=@SQL+','+name
    From    sys.columns
    Where   Object_Name(Object_Id)=@Tavla
            And name<>'Taarih';
    Set @SQL=Stuff(@SQL,1,1,'');
    Set @SQL='Insert'+Char(13)+'Into '
            +Char(9)+@Tavla+'(Taarih,'+@SQL+')'
            +Char(13)+'Select'+Char(9)+'GetDate() Taarih,'
            +Char(13)+Char(9)+Char(9)+Replace(@SQL,',',','+Char(13)+Char(9)+Char(9))
            +Char(13)+'From '+Char(9)+@Tavla+'_tmp;';
    Set @SQL=@SQL+Char(13)+Char(13)+'Delete'+Char(13)+'From'+Char(9)+@Tavla+'_tmp;';
    Print @SQL;
    Select @SQL For XML Path('');
    Exec(@SQL);
    Fetch Next From Cr Into @Tavla;
    End
Close Cr;
Deallocate Cr;

clip_image004

לפקודת Print @SQL הוספתי פקודת Select שמאפשרת לראות את הפקודה באופן קריא גם מלשונית ה-Results. אופציה זו יכולה להיות שימושית אם שומרים את הפקודות בטבלת לוג ורוצים לשלוף אותן ולעיין בהן.

הקדמה:

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

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

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

השימוש ב-RaisError

נפתח בדוגמה לשימוש באופרטור זה-

RaisError('נסיון 1',1,1);
Go
 
RaisError('נסיון 2',16,1);
Go

clip_image002

כפי שאפשר להבחין- הפקודה הראשונה יצרה שגיאה Level 1 (=Severity) בצבע שחור,

והפקודה שניה יצרה שגיאה Level 16 (=Severity) בצבע אדום.

מה ההבדל חוץ מאשר צבע ההודעה? כאשר ה-Severity הוא בתחום 0-10 זו הודעה אינפורמטיבית שאינה מפריעה לריצת הקוד,

וכאשר ה-Severity הוא בתחום 11-18 נוצרת שגיאה.

נוכל לראות זאת כשנשתמש במנגנון Try & Catch:

Begin Try
Print 'התחלה';
RaisError('נסיון',1,1);
Print 'סיום תקין';
End Try
 
Begin Catch
Print IsNull(Object_Name(@@ProcID),'פרוצדורה לא ידועה')+': Error_Procedure()='+IsNull(Error_Procedure(),'פרוצדורה לא ידועה')+', Error_Line()='+Cast(Error_Line() As Varchar)+', Error_Message()='+Cast(Error_Message() As Varchar)+', Error_Severity()='+Cast(Error_Severity() As Varchar);
Print 'סיום שגוי'
End Catch

clip_image004

Begin Try
Print 'התחלה';
RaisError('נסיון',16,1);
Print 'סיום תקין';
End Try
 
Begin Catch
Print IsNull(Object_Name(@@ProcID),'פרוצדורה לא ידועה')+': Error_Procedure()='+IsNull(Error_Procedure(),'פרוצדורה לא ידועה')+', Error_Line()='+Cast(Error_Line() As Varchar)+', Error_Message()='+Cast(Error_Message() As Varchar)+', Error_Severity()='+Cast(Error_Severity() As Varchar);
Print 'סיום שגוי'
End Catch

clip_image006

במקרה הראשון הופיעה ההודעה והריצה המשיכה והסתיימה באופן תקין,

ובמקרה השני ה-RaisError יצר שגיאה שהעבירה את המשך הריצה לבלוק ה-Catch, שם הודפסה הודעה על תוכן ואופי השגיאה (זו פקודה שאני משתמש בה לצרכי דיבוג וכשהיא מופעלת מתוך פרוצדורה אני יכול לדעת איזו פרוצדורה הדפיסה את ההודעה, איזו פרוצדורה יצרה את השגיאה, באיזו שורה הבעייה ועוד).

כאשר ה-Severity הוא בתחום 19-25 זו שגיאה חמורה, ה-Connection מתנתק, ויש צורך להשתמש באופרטור With Log כדי שהשגיאה החמורה תיכתב ללוג השגיאות:

Print 'התחלה';
RaisError('נסיון',24,1) With Log;
Print 'סיום';

clip_image008

clip_image010

הריצה נעצרה כי ה-Connection התנתק ולכן לא הופיעה ההודעה 'סיום',

ונכתבה הודעת שגיאה ללוג (לצד הודעות שגיאה נוספות מנסיונות שערכתי במהלך כתיבת הפוסט..).

לו הייתי משתמש כאן במנגנון Try & Catch - ה-Connection לא היה מתנתק והריצה הייתה ממשיכה ב-Catch.

עד כאן הודעות השגיאה נכתבו Hard coded בפקודת ה-RaisError עצמה. ניתן לנהל באופן עצמאי טבלת הודעות שגיאה (למשל- כדי להקפיד על נוסח מוסכם או לאפשר שינוי נוסח ההודעות מבלי להיכנס לקוד), ואפשר להשתמש בטבלת הודעות השגיאה של המערכת:

Select *
From sys.messages
Order By message_id;
Go

clip_image012

כפי שאפשר לראות- כל הודעה נשמרת בשפות שונות וכך הן מתפרסמות בהתאם לשפת ה-Session.

ההודעות שמספרן עד 13,000 משמשות את המערכת, ומשם ואילך (למעט 50,000) הן לרשות המשתמשים, למשל כך:

RaisError(19129,1,1);
Go

clip_image014

או כך:

RaisError(14524,1,1,'Me','You');
Go

clip_image016

במקרה השני החלפנו בהודעת המערכת מס 14524 " Supply either %s or %s." את %s הראשון ב-Me ואת %s השני ב-You.

את טבלת ההודעות ניתן לעדכן בהודעות חדשות:

EXEC sp_AddMessage 50001,1,'פריט %s אינו קיים';
Go
 
RaisError(50001,1,1,'אבקת חשמל');
Go

clip_image018

וכמובן למחוק הודעות מיותרות:

sp_DropMessage 50001;
Go

כדאי לציין שהפרמטר השלישי בהודעות RaisError (State) הוא לנוחות המתכנת והוא יכול להציב שם ערכים שונים בתחום 1-255 ולתת להם משמעות לפי צרכיו.

מה קורה אם משתמשים בקוד שגיאה שאינו קיים? תקלה אופיינית להעברה לא מסודרת בין סביבות כשאת קוד ה-SQL מעבירים אבל לא מעדכנים כנדרש את טבלת sys.messages: תקלה זו עצמה תיצור שגיאה - ולא זו שהתכוונו ליצור בעצמנו באופן מבוקר, אלא אם כן זה קרה בתוך בלוק Try ואז המערכת תתנהג כאילו קוד השגיאה קיים:

Print '10';
RaisError(13005,1,1);
Print '20';
 
Begin Try
Print '110';
RaisError(13005,1,1);
Print '120';
RaisError(13005,16,1);
Print '130';
End Try
 
Begin Catch
Print IsNull(Object_Name(@@ProcID),'פרוצדורה לא ידועה')+': Error_Procedure()='+IsNull(Error_Procedure(),'פרוצדורה לא ידועה')+', Error_Line()='+Cast(Error_Line() As Varchar)+', Error_Message()='+Cast(Error_Message() As Varchar)+', Error_Severity()='+Cast(Error_Severity() As Varchar);
Print '999'
End Catch

clip_image020

קוד 13005 אינו קיים במערכת (הקוד הורץ בגרסת 2008, בגרסת Denali שתוזכר בהמשך יש קוד כזה ויש להחליפו בדוגמה ב-13007). ההפעלה הראשונה שלו - בין 10 ל-20 - יצרה שגיאה 18054.

לעומת זאת ההפעלה שלו ב-Severity=1 בתוך בלוק ה-Try לא יצרה שגיאה או חיווי כלשהו,

וההפעלה שלו ב-Severity=16 בתוך בלוק ה-Try העבירה את המשך הריצה לבלוק ה-Catch עם מספר שגיאה 13005;

כלומר- בתוך בלוק ה-Try המערכת מתעלמת מהעובדה שהקוד אינו קיים ומטפלת בו בהתאם ל-Severity שלו.

השימוש ב-Throw

החל מגרסת SQL Server הבאה (שם קוד Denali לגרסת ה-CTP) מתווספת אפשרות נוספת- האופרטור Throw שמתנהג בצורה שונה מ-RaisError. אופרטור זה אינו נעזר בטבלת Sys.messages (הוא חסין מפני תקלות בהעברה בין סביבות) ולכן יש לציין קוד שגיאה (מ-50,000 ומעלה), טקסט שגיאה, ו-State.

ה-Severity תמיד יהיה 16, והשימוש באופרטור רק במסגרת Try & Catch:

Begin Try
Print '110';
Throw 50000,'שגיאה',1;
Print '120';
End Try
 
Begin Catch
Print IsNull(Object_Name(@@ProcID),'פרוצדורה לא ידועה')+': Error_Procedure()='+IsNull(Error_Procedure(),'פרוצדורה לא ידועה')+', Error_Line()='+Cast(Error_Line() As Varchar)+', Error_Message()='+Cast(Error_Message() As Varchar)+', Error_Severity()='+Cast(Error_Severity() As Varchar);
Print '999';
End Catch

clip_image022

הופיעה הודעת שגיאה שאינה קיימת בטבלת הודעות השגיאה (ניתן להציג באופן דומה גם את את קוד השגיאה - 50000).

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

בדוגמה שלהלן אני יוזם שגיאה ומנסה להפוך תו X למספר:

Begin Try
Print '110';
Select Cast('X' As Int);
Print '120';
End Try
 
Begin Catch
Print IsNull(Object_Name(@@ProcID),'פרוצדורה לא ידועה')+': Error_Procedure()='+IsNull(Error_Procedure(),'פרוצדורה לא ידועה')+', Error_Line()='+Cast(Error_Line() As Varchar)+', Error_Message()='+Cast(Error_Message() As Varchar)+', Error_Severity()='+Cast(Error_Severity() As Varchar);
Print '999';
Throw;
End Catch

clip_image024

כשהשגיאה נוצרה - המשך הריצה עבר לבלוק ה-Catch, ולאחר שבוצעה מניפולציה כלשהי - משתמשים ב-Throw להצגת הודעת השגיאה והחזרת קוד השגיאה המקוריים.

הכותרת המעט מסורבלת של הפוסט מתייחסת לשאלה ששאל הרב דוט נט:

כיצד מאחדים את הסט

1

2

3

4

עם הסט

A

B

C

D

לסט מאוחד

1,A

2,B

3,C

4,D

והתשובה הנכונה כעקרון שהוא הציג היא למספר כל סט באמצעות Row_Number ולבצע Join בינהם.

נכון כעקרון מכיוון שמדובר במקרה פרטי בו יש שני סטים בלבד, ואורכם זהה; אבל מה יקרה אם יהיו יותר סטים ואורכם לא זהה?

נניח שאנחנו מעוניינים להציג סט בו בעמודה אחת יופיעו הטבלאות, בעמודה השניה ה-Views, בשלישית הפרוצדורות וברביעית הטריגרים; ולכל רשימה אורך אחר?

נשתמש ב-Full Outer join בין הסטים, והתנאי יהיה התאמה בין המספר בטבלה ל-Coalesce של המספרים בטבלאות הקודמות:

Select  *
From    (Select Row_Number() Over(Order By name) Mispar,
        name
        From sys.tables) T
Full Join (Select Row_Number() Over(Order By name) Mispar,
        name
        From sys.views) V
        On T.Mispar=V.Mispar
Full Join (Select Row_Number() Over(Order By name) Mispar,
        name
        From sys.Procedures) P
        On Coalesce(T.Mispar,V.Mispar)=P.Mispar
Full Join (Select Row_Number() Over(Order By name) Mispar,
        name
        From sys.triggers) Tr
        On Coalesce(T.Mispar,V.Mispar,P.Mispar)=Tr.Mispar;
Go

clip_image002

Posted by גרי רשף | with no comments

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

ניצור שוב שתי טבלאות, הפעם בנות 100 עמדות Int, כשבראשונה הן Sparse ובשניה לא:

Use tempdb;
Go
 
If Object_Id('T1') Is Not Null Drop Table T1;
If Object_Id('T2') Is Not Null Drop Table T2;
Go
 
Create Table T1(I00 Int Sparse Null,
                I01 Int Sparse Null,
                I02 Int Sparse Null,
                I03 Int Sparse Null,
                I04 Int Sparse Null,
                I05 Int Sparse Null,
                I06 Int Sparse Null,
                I07 Int Sparse Null,
                I08 Int Sparse Null,
                I09 Int Sparse Null,
                I10 Int Sparse Null,
                I11 Int Sparse Null,
                I12 Int Sparse Null,
                I13 Int Sparse Null,
                I14 Int Sparse Null,
                I15 Int Sparse Null,
                I16 Int Sparse Null,
                I17 Int Sparse Null,
                I18 Int Sparse Null,
                I19 Int Sparse Null,
                I20 Int Sparse Null,
                I21 Int Sparse Null,
                I22 Int Sparse Null,
                I23 Int Sparse Null,
                I24 Int Sparse Null,
                I25 Int Sparse Null,
                I26 Int Sparse Null,
                I27 Int Sparse Null,
                I28 Int Sparse Null,
                I29 Int Sparse Null,
                I30 Int Sparse Null,
                I31 Int Sparse Null,
                I32 Int Sparse Null,
                I33 Int Sparse Null,
                I34 Int Sparse Null,
                I35 Int Sparse Null,
                I36 Int Sparse Null,
                I37 Int Sparse Null,
                I38 Int Sparse Null,
                I39 Int Sparse Null,
                I40 Int Sparse Null,
                I41 Int Sparse Null,
                I42 Int Sparse Null,
                I43 Int Sparse Null,
                I44 Int Sparse Null,
                I45 Int Sparse Null,
                I46 Int Sparse Null,
                I47 Int Sparse Null,
                I48 Int Sparse Null,
                I49 Int Sparse Null,
                I50 Int Sparse Null,
                I51 Int Sparse Null,
                I52 Int Sparse Null,
                I53 Int Sparse Null,
                I54 Int Sparse Null,
                I55 Int Sparse Null,
                I56 Int Sparse Null,
                I57 Int Sparse Null,
                I58 Int Sparse Null,
                I59 Int Sparse Null,
                I60 Int Sparse Null,
                I61 Int Sparse Null,
                I62 Int Sparse Null,
                I63 Int Sparse Null,
                I64 Int Sparse Null,
                I65 Int Sparse Null,
                I66 Int Sparse Null,
                I67 Int Sparse Null,
                I68 Int Sparse Null,
                I69 Int Sparse Null,
                I70 Int Sparse Null,
                I71 Int Sparse Null,
                I72 Int Sparse Null,
                I73 Int Sparse Null,
                I74 Int Sparse Null,
                I75 Int Sparse Null,
                I76 Int Sparse Null,
                I77 Int Sparse Null,
                I78 Int Sparse Null,
                I79 Int Sparse Null,
                I80 Int Sparse Null,
                I81 Int Sparse Null,
                I82 Int Sparse Null,
                I83 Int Sparse Null,
                I84 Int Sparse Null,
                I85 Int Sparse Null,
                I86 Int Sparse Null,
                I87 Int Sparse Null,
                I88 Int Sparse Null,
                I89 Int Sparse Null,
                I90 Int Sparse Null,
                I91 Int Sparse Null,
                I92 Int Sparse Null,
                I93 Int Sparse Null,
                I94 Int Sparse Null,
                I95 Int Sparse Null,
                I96 Int Sparse Null,
                I97 Int Sparse Null,
                I98 Int Sparse Null,
                I99 Int Sparse Null);
 
Create Table T2(I00 Int Null,
                I01 Int Null,
                I02 Int Null,
                I03 Int Null,
                I04 Int Null,
                I05 Int Null,
                I06 Int Null,
                I07 Int Null,
                I08 Int Null,
                I09 Int Null,
                I10 Int Null,
                I11 Int Null,
                I12 Int Null,
                I13 Int Null,
                I14 Int Null,
                I15 Int Null,
                I16 Int Null,
                I17 Int Null,
                I18 Int Null,
                I19 Int Null,
                I20 Int Null,
                I21 Int Null,
                I22 Int Null,
                I23 Int Null,
                I24 Int Null,
                I25 Int Null,
                I26 Int Null,
                I27 Int Null,
                I28 Int Null,
                I29 Int Null,
                I30 Int Null,
                I31 Int Null,
                I32 Int Null,
                I33 Int Null,
                I34 Int Null,
                I35 Int Null,
                I36 Int Null,
                I37 Int Null,
                I38 Int Null,
                I39 Int Null,
                I40 Int Null,
                I41 Int Null,
                I42 Int Null,
                I43 Int Null,
                I44 Int Null,
                I45 Int Null,
                I46 Int Null,
                I47 Int Null,
                I48 Int Null,
                I49 Int Null,
                I50 Int Null,
                I51 Int Null,
                I52 Int Null,
                I53 Int Null,
                I54 Int Null,
                I55 Int Null,
                I56 Int Null,
                I57 Int Null,
                I58 Int Null,
                I59 Int Null,
                I60 Int Null,
                I61 Int Null,
                I62 Int Null,
                I63 Int Null,
                I64 Int Null,
                I65 Int Null,
                I66 Int Null,
                I67 Int Null,
                I68 Int Null,
                I69 Int Null,
                I70 Int Null,
                I71 Int Null,
                I72 Int Null,
                I73 Int Null,
                I74 Int Null,
                I75 Int Null,
                I76 Int Null,
                I77 Int Null,
                I78 Int Null,
                I79 Int Null,
                I80 Int Null,
                I81 Int Null,
                I82 Int Null,
                I83 Int Null,
                I84 Int Null,
                I85 Int Null,
                I86 Int Null,
                I87 Int Null,
                I88 Int Null,
                I89 Int Null,
                I90 Int Null,
                I91 Int Null,
                I92 Int Null,
                I93 Int Null,
                I94 Int Null,
                I95 Int Null,
                I96 Int Null,
                I97 Int Null,
                I98 Int Null,
                I99 Int Null);
Go

וכעת נמלא את שתיהן ב-100,000 שורות ריקות:

Declare    @N Int=1000001;
 
With T As
(Select 1 I
Union All
Select I+1
From T
Where I<1000000)
Insert Into T1
Select    Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null
From T
Where    I%@N<>0
Union All
Select    I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I,I
From T
Where    I%@N=0
Option (MaxRecursion 0);
 
With T As
(Select 1 I
Union All
Select I+1
From T
Where I<1000000)
Insert Into T2
Select    Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null,Null
From T
Option (MaxRecursion 0);
 
Exec sp_SpaceUsed T1;
Exec sp_SpaceUsed T2;
Go

רגע- מה זו ההתחכמות המשונה בשאילתה הראשונה?
כאשר N@=1,000,001 רק ה-Insert של ה-Nulls מתבצע (כי השארית לעולם לא תהיה 0) ואחוז המילוי בטבלת ה-Sparse יהיה 0%.
כשהמשתנה יהיה שווה 100 - אחוז המילוי יהיה 1%,
וכך הלאה..

התוצאות שהתקבלו עבור אחוזי מילוי שונים של טבלת Sparse:

Name Rows Reserved Data Index_Size Unused
T1 0% 1000000 10888 KB 10872 KB 8 KB 8 KB
T1 5% 1000000 54792 KB 54776 KB 8 KB 8 KB
T1 10% 1000000 98696 KB 98672 KB 8 KB 16 KB
T1 20% 1000000 186568 KB 186480 KB 8 KB 80 KB
T1 50% 1000000 449992 KB 449880 KB 8 KB 104 KB
T1 100% 1000000 889032 KB 888896 KB 8 KB 128 KB
T2 0% 1000000 421128 KB 421056 KB 8 KB 64 KB

ניתן לראות שטבלת ה-Sparse משתווה בגודלה לטבלה הרגילה (T2) ב-50% מילוי,
וב-5% מילוי גודלה הוא רק 1/8 ממנה;
ואני מזכיר שטבלה T2 תהיה באותו גודל בים נמלא אותה ב-Nulls או בערכים אמיתיים שכן המקום בכל מקרה מוקצה.

שורה תחתונה: בטבלאות גדולות בנות עשרות עמודות בהן אחוזים בודדים של ערכים - השימוש ב-Sparse Columns מצדיק את עצמו.

האופרטור Execute As מאפשר לבצע פעולות תחת זהות אחרת מזו שלנו, ללא צורך להתחבר בזהות אחרת,

למשל- כדי לבצע בדיקות שהקוד שנכתב על ידי המפתח שיש לא הרשאות יחסית גבוהות יעבוד גם כשיופעל על ידי משתמש שהרשאותיו נמוכות,

או לחילופין- להגדיר שקוד מסויים ירוץ בזהות גבוהה מזו של המשתמש.

כמובן שבעל הרשאות נמוכות לא יכול להשתמש ב-Execute As כדי לאמץ לעצמו זהות של System Administartor..

דוגמה- ניצור Login חדש במערכת ו-User המשוייך לו:

Use tempdb;
Go
 
Create Login MyLogin With Password='1234',Check_Policy=Off;
Go
 
Create User MyUser For Login MyLogin;
Go

כעת ניצור פרוצדורה פשוטה ששולפת את פרטי המשתמש שמריץ אותה ונעניק ל-User הנ"ל הרשאה להריץ אותה:

Create Proc MyProc1 As
Select sUser_name() [sUser_name()],
       User_name() [User_name()];
Go
 
Grant Execute On MyProc1 To MyUser;
Go

כעת נריץ אותה תוך שאנחנו מזדהים כ-MyUser, ומבטלים לאחר מכן את ההזדהות באמצעות Revert:

Execute As User='MyUser';
Go
 
Exec MyProc1;
Go
 
Revert;
Go

clip_image002

כפי שאפשר לראות- המערכת זיהתה את מי שמריץ את הפרוצדורה בתור MyLogin & MyUser הנ"ל ולא בתור האדמיניסטראטור (אני), וזו דרך טובה לבדוק אם יש למשתמשים הרשאות מתאימות כדי שלא נקבל הודעות שגיאה לאחר העלייה לאוויר.

אם אריץ אותה בזהותי האמיתית (ללא שימוש ב-Execute As) היא תזהה אותי כצפוי:

Exec MyProc1;
Go

clip_image004

כעת נבצע פעולה הפוכה- ניצור פרוצדורה שבתוכה יופיע האופרטור Execute As כך שמי שמריץ יזוהה בתור ה-Owner שלה, גם אם אינו אלא משתמש פשוט, וניתן הרשאות הרצה ל-MyUser (לא לטעות- המשתמש החדש עדיין זקוק להרשאות הרצה):

Create Proc MyProc2 With Execute As Owner As
Select sUser_name() [sUser_name()],
       User_name() [User_name()];
Go
 
Grant Execute On MyProc2 To MyUser;
Go

כעת אריץ אותה תחת הזהות של MyUser, כפי שהרצתי את הפרוצדורה הראשונה בהתחלה:

Execute As User='MyUser';
Go
 
Exec MyProc2;
Go
 
Revert;
Go

clip_image006

אפשר להבין מכך שבלי לתת ל-MyUser הרשאות מפורשות, כל מה שהפרוצדורה תבצע יהיה תחת הזהות של ה-Admin.

clip_image002

לאובייקטים שונים ב-SQL Server ניתן להוסיף ערכים והסברים באמצעות Extended Properties.

דרך הממשק הגרפי של ה-SSMS - קליק ימני על האובייקט ו-Properties,

בחלון שנפתח בוחרים ב- Extended Properties,

וניתן להוסיף שורות באופן חופשי הכוללות Name (סעיף) ו-Value (תיאור, ערך).

מידע שניתן להוסיף- מקור הנתונים האובייקט, תיאור כללי שלו, שם האפיון במערכת וכו'.

רצוי כמובן שה-Name יהיה סטנדרטי כדי שיהיה ניתן לתחקר את ה-Extended Properties בקלות: בדוגמה הזו הכותרת של תיאור הטבלה הוא MS_Description, ורצוי שזו תהיה הכותרת בכל תיאורי האובייקטים כדי שניתן יהיה לשלוף אותם בקלות מטבלאות המערכת.

ניתן להוסיף Extended Properties בעזרת קוד, למשל- נתונה טבלת dbo.Employees ובה עמודת Gender (מין, מגדר..) ואנחנו מעוניינים להוסיף תיאור לטבלה ותיאור לעמודה:

Exec sp_addextendedproperty 'Description','Actual employees', 'User', 'dbo', 'Table', 'Employees';
 
Exec sp_addextendedproperty 'Description','0=woman, 1=man, Null=unknown', 'User', 'dbo', 'Table', 'Employees', 'Column','Gender';
Go

שליפת ה-Extended Properties מתבצעת כך:

Select Object_Name(major_id) Tbl,
       Col_Name(major_id,minor_id) Col,
       *
From   sys.extended_properties
Where  Object_Name(major_id)='Employees';
Go

clip_image004

או כך:

Select *
From   Fn_ListExtendedProperty('Description','schema','dbo','Table','Employees','column','Gender');
Go
 
Select *
From   Fn_ListExtendedProperty('Description','schema','dbo','Table','Employees',Null,Null);
Go

clip_image006