DCSIMG
April 2010 - Posts - גרי רשף

April 2010 - Posts

הפעלה של ג'ובים או של פרוצדורות מערכת שפועלות מול ה-File System (המחיצות והקבצים) הן בעייתיות מכיוון שמי שמבצע את הפקודות מזדהה בתור ה-Agent של ה-SQL Server ולא בתור המשתמש הספציפי שהריץ את הפקודה, וכך המשתמש מקבל הרשאות חריגות שעלולות להיות מנוצלות לרעה: הוא יכול להגיע למחיצות וקבצים שאין לו הרשאה להגיע אליהם או לשנותם, הוא יכול להריץ פקודות SQL בעזרת SQLCmd ולעשות ככל העולה על רוחו.

הפתרון לבעייה הוא ליצור Proxy Account כך שהכלים הנ"ל יזדהו איתו ויקבלו את ההרשאות של ה-Credential המשוייך אליו, הרשאות שניתן להגביל אותן בהתאם לצורך.

הפעלת ג'ובים

הדוגמה להלן מניחה שהשרת מוגדר ב-Properties => Security בתור SQL Server and Windows Authentication mode.

1. יצרתי Login בשם Try001 עם SQL Server Authentication וסיסמה 1234 (הורדתי את הסימונים בתיבות לגבי ה-Policy),
וב-User Mapping שייכתי אותו ל-msdb כדי שיוכל בהמשך לקבל הרשאות להפעלת ג'ובים.

2. יצרתי ג'וב ששוייך ל-Try001 הנ"ל (Owner) ובו צעד אחד מסוג (Operating System (CmdExec והפקודה Dir C:\ > C:\Try001.txt (פקודה פשוטה שמריצה פקודת Dir ומפנה את התוצאה לקובץ Try001.txt).
בסעיף Run As כתוב SQL Agent Service Account, כלומר- הפקודה מתוכננת להיות מופעלת על ידי ה-Agent עם ההרשאות שלו.

3. ב-SSMS פתחתי New Query ושיניתי את ה-Connection על ידי קליק ימני ל-Try001 עם הסיסמה הנ"ל (1234).

4. ניסיתי ב-Query החדש שפתחתי להפעיל את הג'וב:

Exec msdb..sp_start_job 'Job001';

ההפעלה נכשלה בשל העדר הרשאות Exec של משתמש Try001 לפרוצדורה sp_start_job.

5. נתתי למשתמש הנ"ל הרשאות מתאימות ב-Query "שלי" (לא זה שפתחתי בסעיף 3 עבור Try001):

Use msdb;
Go
Grant Execute On Object::sp_start_job To Try001;
Go

6. כעת ההפעלה של הג'וב על ידי המשתמש Try001 הצליחה, אבל הג'וב עצמו נכשל (ניתן לראות זאת על ידי קליק ימני על הג'וב ו-History) מכיוון שה-Owner שלו הוא Try001 ואינו יכול להפעיל פקודות שמורצות על ידי ה-Agent. גם אם אפיל אותו עם הרשאות ה-Admin שלי הוא יכשל בגלל ה-Owner.
כדי לפתור את הבעייה יש להגדיר Credential שיזהה משתמש עם הרשאות מתאימות, וחשבון Proxy שבעזרתו יופעל הצעד של הג'וב ושההרשאות שלו הן של ה-Credential שישוייך לו.

7. יצרתי Credential (תחת Security) בשם Cred001 עם המשתמש שלי (ניתן לראות אותו על ידי Select System_User) והסיסמה שלי.

8. יצרתי Proxy (תחת SQL Server Agent) מסוג (Operating System (CmdExec ששמו Proxy001,
ציינתי את Cred001 ב-General,
ואת Try001 ב-Principals (בהמשך נוכל לראות את הג'וב Job001 ב-References).

9. בג'וב, בצעד שהגדרתי בו, ציינתי את Proxy001 ב-Run As (במקום AQL Agent Service Account).

10. הפעלתי שוב את הג'וב בתור Try001 והוא הופעל ורץ בהצלחה (כדאי לוודא שהקובץ Try001.txt באמת נוצר).

הפעלה של xp_CmdShell

הפרוצדורה הזו מאפשרת להריץ פקודות שורה (Command Lines) מתוך ה-SQL Server, פקודה שימושית מאוד אך גם מסוכנת!

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

EXEC sp_configure 'show advanced options', 1
GO
RECONFIGURE --RECONFIGURE WITH OVERRIDE אם זה לא עובד אז לנסות
GO
EXEC sp_configure 'xp_cmdshell', 1
GO
RECONFIGURE --RECONFIGURE WITH OVERRIDE אם זה לא עובד אז לנסות
GO

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

Exec xp_CmdShell 'Dir C:\ > C:\Try001.txt';
Go

אולם את ננסה להריץ אותה בתור Try001 ב-Query שלו, נקבל הודעת שגיאה על העדר הרשאות על xp_CmdShell.
ניתן לו אותן:

Use master;
Go
Create User Try001 For Login Try001;
Go
Grant Execute On Object::xp_CmdShell To Try001;
Go

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

Msg 15153, Level 16, State 1, Procedure xp_cmdshell, Line 1
The xp_cmdshell proxy account information cannot be retrieved or is invalid. Verify that the '##xp_cmdshell_proxy_account##' credential exists and contains valid information.

4. כדי לאפשר להפעיל את הפקודה עם Proxy שמתאים לה ספציפית, נריץ בתור אדמיניסטרטורים את הפקודה הבאה:

Exec master..sp_xp_CmdShell_Proxy_Account 'MyLogin','MyPassword';
Go

את MyLogin יש להחליף ב-Login עם ההרשאות המתאימות מול מערכת ההפעלה. שלנו, למשל, יתקבל על ידי Select SYSTEM_USER.
כעת הפעלת xp_CmdShell על ידי Try001 אמורה להצליח.

5. אם מתחרטים ורוצים לבטל את שיוך ה-Proxy לפקודה נפעיל את הפקודה:

Exec master..sp_xp_CmdShell_Proxy_Account Null;
Go
כאשר קולטים רשומות הכוללות Null – הם (ה-Nulls) תופסים מקום, בניגוד למה שאולי היינו מצפים. למשל בעמודות מסוג Varchar ששומרות רק את המחרוזת שקלטנו ללא מקומות פנויים לתווים המיותרים (אם נניח קלטנו מחרוזת באורך 10 תווים לשדה שיכול לקלוט עד 50 תווים) יש צורך לשמור בכל רשומה מידע לגבי אורך המחרוזת השמורה באותה עמודה. החל מגרסת SQL Server 2008 יש אפשרות להגדיר שדות כ-Sparse Columns ואז לא מבוזבז מקום כשיש Null, אלא רק כשאין ערך (כלומר- טוב להשתמש בהן כשברוב המקרים יש Null). פטנט זה מגיע יחד עם האופציה ליצור Column_Set שזו מעין עמודה מחושבת המציגה בתור Xml את כל שדות ה-Sparse:
Create Table #Tmp(ID Int Identity Primary Key,
                S Varchar(Max) Sparse Null,
                I Int Sparse Null,
                B Bit Sparse Null,
                D DateTime Sparse Null,
                SIBD XML COLUMN_SET FOR ALL_SPARSE_COLUMNS);
Go
כפי שאפשר לראות- העמודות S,I,B,D הן עמודות Sparse מסוגי נתונים שונים, ועמודה SIBD היא Column_Set שתציג את ארבעתן כ-Xml. נכניס כעת מספר שורות לטבלה:
Insert Into #Tmp(S) Values('Try it');
Insert Into #Tmp(I) Values(123);
Insert Into #Tmp(B) Values(1);
Insert Into #Tmp(D) Values(GETDATE());
וכעת נכניס נתונים לעמודות ה-Sparse דרך ה-Column_Set (ערך Xml בודד הכולל ארבעה ערכים לארבע העמודות):
Insert Into #Tmp(SIBD) Values(Cast('<S>Another try</S><I>456</I><B>0</B><D>2010-04-01T12:00:00</D>' As Xml));
כעת נשלוף את נתוני הטבלה, ויש לשים לב שכדי לעיין בעמודות ה-Sparse יש לציין אותן במפורש ב-Select:
Select    * ,
        S,
        I,
        B,
        D
From    #Tmp;

נפתח ביצירת Event Session (יש לוודא שהמחיצה קיימת!):

 
CREATE EVENT SESSION MyEventSession ON SERVER
ADD EVENT sqlserver.sql_statement_completed(ACTION (sqlserver.database_id, sqlserver.sql_text, sqlserver.session_id,sqlserver.tsql_stack, sqlserver.username, sqlserver.client_hostname) WHERE sqlserver.database_id=2),
ADD EVENT sqlserver.sql_statement_starting(ACTION (sqlserver.database_id, sqlserver.sql_text, sqlserver.session_id,sqlserver.tsql_stack, sqlserver.username, sqlserver.client_hostname) WHERE sqlserver.database_id=2)
ADD TARGET package0.asynchronous_file_target(set FileName = 'c:\tmp\EventSession.xel',MetaDataFile = 'c:\tmp\EventSession.xem')
GO

1. נאסוף נתונים על אירועי sql_statement_starting ו-sql_statement_completed.
כדי לקבל פרטים על אירועים אחרים שניתן לתחקר:

 
Select    *
From    sys.dm_xe_objects 
Where    object_type='event'
Order By name

2. הנתונים שיאספו – מלבד אלו שנאספים אוטומטית – הם אלו שמופיעים בתוך הסוגריים לאחר פקודת Action,
כדי לקבל פרטים על נתונים אחרים שניתן לאסוף:

 
Select    *
From    sys.dm_xe_objects 
Where    object_type='action'
Order By name

3. ניתן לסנן את האירועים – כפי שמופיע לאחר הפרדיקט Where,
וכדי לקבל פרטים על אפשרויות סינון נוספות:

 
Select    *
From    sys.dm_xe_objects 
Where    object_type='pred_source'
Order By name

4. ה-Target (קובץ היעד במקרה זה) הוא קובץ אסינכרוני,
אך יש אופציות ליעדים אחרים (במקום או בנוסף):

 
Select    *
From    sys.dm_xe_objects 
Where    object_type='target'
Order By name

כעת נפעיל את ה-Event Session שיצרנו:

 
ALTER EVENT SESSION MyEventSession ON SERVER STATE = Start
GO

וכדי לוודא שהוא עוקב אחרינו- נריץ שתי שליפות לדוגמה:

 
Select * From sys.objects
Go
Select * From sys.tables
Go

..ונעצור את ה-Event Session, ונבטל אותו (אל דאגה- הקובץ קיים יחד עם הנתונים שנאספו):

 
ALTER EVENT SESSION MyEventSession ON SERVER STATE = Stop
GO
DROP EVENT SESSION MyEventSession ON SERVER
GO

נשלוף את הנתונים מהקובץ:

 
Select    *,
        CAST(event_data as XML) as event_data_xml
From    sys.fn_xe_file_target_read_file('c:\tmp\EventSession*.xel','c:\tmp\EventSession*.xem', null, null)
Go

המידע הרלוונטי נמצא בעמודה event_data ובהמה שלה ל-xml,
וכדי לעיין בו בצורה נוחה נמיר אותו לתצוגה טבלאית:

With T As
(SELECT    X.value('(./@name)', 'varchar(1000)') as event_name,
        X.value('(./data[@name="source_database_id"]/value)[1]', 'int') as source_database_id,
        X.value('(./data[@name="object_id"]/value)[1]', 'int') as object_id,
        X.value('(./data[@name="object_type"]/value)[1]', 'varchar(25)') as object_type,
        X.value('(./data[@name="duration"]/value)[1]', 'bigint') as duration,
        X.value('(./data[@name="cpu"]/value)[1]', 'bigint') as cpu,
        X.value('(./data[@name="reads"]/value)[1]', 'bigint') as reads,
        X.value('(./data[@name="writes"]/value)[1]', 'bigint') as writes,
        X.value('(./action[@name="sql_text"]/value)[1]', 'varchar(Max)') as sql_text,
        X.value('(./action[@name="database_id"]/value)[1]', 'int') as database_id,
        X.value('(./action[@name="session_id"]/value)[1]', 'int') as session_id,
        X.value('(./action[@name="username"]/value)[1]', 'varchar(Max)') as username,
        X.value('(./action[@name="client_hostname"]/value)[1]', 'varchar(Max)') as client_hostname
FROM    (SELECT CAST(event_data AS XML) xml_event_data 
        FROM sys.fn_xe_file_target_read_file('c:\tmp\data2*.xel','c:\tmp\data2*.xem', NULL, NULL)) AS event_table
        CROSS APPLY xml_event_data.nodes('//event') (X))
Select    event_name,
        source_database_id,
        DB_NAME(source_database_id) source_database_name,
        object_id,
        object_type,
        duration,
        cpu,
        reads,
        writes,
        sql_text,
        database_id,
        DB_NAME(database_id) database_name,
        session_id,
        username,
        client_hostname
From    T;
Go

לסיום נמחק את הקבצים כך (או בדרך אחרת):

XP_CMDSHELL 'del c:\tmp\EventSession.*'
GO

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

הפתרון הפשוט ביותר הוא להשתמש ב-IsNull כך שאם לא נספק ערך לפילטר – ה-ID יהיה שווה לעצמו:

Declare    @ID1 Int;
Set        @ID1=3;
Select    *
From    sys.objects
Where    object_id=IsNull(@ID1,object_id);
 
Declare    @ID2 Int;
Select    *
From    sys.objects
Where    object_id=IsNull(@ID2,object_id);

במקרה הראשון הפרמטר ID1@ מקבל את הערך 3 והשליפה מחזירה רק את הרשומה המתאימה,
ובמקרה השני הפרמטר ID2@ אינו מקבל ערך ונשאר Null, וכל הרשומות חוזרות (התנאי Where object_id=object_id הוא חסר משמעות).

מכיוון שלהסתמך על ערכי Null של פרמטרים זו משענת קנה רצוץ, פתרון יותר בטוח הוא לקבוע ערך ברירת מחדל או ערך מחוץ לתחום שמשמעותו תהיה "כל הרשומות" (נניח אם בונים Combo Box עם כל ה-ID ורוצים לכלול גם את האפשרות של "כל הרשומות"), ואז נשתמש בתנאי Case כך (הפעם נבנה פרוצדורה מתאימה):

Create Proc P_Objects(@ID Int=-1) As
Select    *
From    sys.objects
Where    object_id=Case When @ID=-1 Then object_id Else @ID End;

ונפעיל אותה פעם עם ערך ופעם ללא ערך כדי לוודא שהיא תקינה:

Exec P_Objects 3;
Exec P_Objects;
Posted by גרי רשף | with no comments
נניח שאנחנו רוצים לחפש שורות בהן מופיע הצירוף 100% בשדה טקסטואלי, למשל במקרה הזה:
Create Table #Ovdim(ID Int Not Null Primary Key,
                    Shem VarChar(50),
                    Heara NVarChar(Max));
Go
Insert Into #Ovdim Values(1,'Ada','העובדת הראשונה');
Insert Into #Ovdim Values(2,'Beni','הוא 100% בסדר');
Insert Into #Ovdim Values(3,'Galit','גם היא בסדר');
Insert Into #Ovdim Values(4,'Dan','יש לו 100 במתימטיקה');
Insert Into #Ovdim Values(5,'Hila',Null);
לכאורה קלי קלות (כדי לקבל את בּנִי):
Select    *
From    #Ovdim
Where    Heara Like '%100%%'
דא עקא שבמקרה זה נקבל את כל השורות בהן מופיע הצירוף 100, מכיוון ש-% הוא "ג'וקר" שמחליף כל תו אחר. בעייה תהיה גם למי שינסה לחפש צירוף תווים התחום בתוך סוגריים מרובעים ויקבל את כל השורות בהן מופיע אחד התווים שבסוגריים (כאן). לגבי הדוגמה של 100% אפשר לחפש כך:
Select    *
From    #Ovdim
Where    Heara Like '%100~%%' ESCAPE '~'
ולגבי הסוגריים המרובעים ניתן לעיין בקישור שצירפתי, אבל אם לא רוצים להסתבך או לא זוכרים את הפתרון המתוחכם לכל בעייה- הנה פתרון אחיד לכל הבעיות: נחפש רשומות שבהן החלפה של המחרוזת המבוקשת במחרוזת ריקה- תשנה את אורך הטקסט בעמודה. למשל לגבי 100%:
Select    *
From    #Ovdim
Where    Len(Heara)<>Len(Replace(Heara,'100%',''))
כלומר- האורך של שדה ההערה שונה מאורכו לאחר שנחליף את '100%' ב-''.. בדקתי את חוות דעתו של ה-Execution Plan מול טבלה גדולה יחסית:
Select    *
From    AdventureWorks.Person.Address
Where    AddressLine1 Like '%100~%%' ESCAPE '~'


Select    *
From    AdventureWorks.Person.Address
Where    Len(AddressLine1)<>Len(Replace(AddressLine1,'100%',''))
ה-Estimated Execution Plan היה זהה בשני המקרים, אם כי דומה שהאומדנים היו מופרכים (לפחות לגבי ה-Estimated Numer of Rows), אבל בכל מקרה לא נראה לי שאמור להיות הבדל דראסטי בינהם (מעבר על כל הטבלה ובדיקה של כל רשומה). השלמות (21/05/2010): ניתן לשלוף על ידי תחימת התווים הבעייתיים בסוגריים מרובעים, למשל מחרוזות בהן מופיע %-

Where MyField Like '%[%]%'

נשאלתי כיצד ניתן לשלוף פרטים על מבנה טבלה זמנית.
הטבלאות הזמניות נשמרות ב-tempdb ושם גם הפרטים עליהן.

ניצור טבלה זמנית בדטבייס אחר:

Use AdventureWorks;
Go
Create Table #TmpTbl(MyInt Int, MyString Varchar(10));
Go

ונשלוף פרטים לגבי המבנה שלה:

Select * From tempdb.sys.columns Where object_id=OBJECT_ID('tempdb.dbo.#TmpTbl');

יש לשים לב שבשליפה מופיע פעמיים tempdb ללא קשר לדטבייס בו היינו עת הטבלה נוצרה.
אם ניצור Session חדש (למשל- על ידי פתיחת New Query ב-Management Studio) ונריץ משם את השליפה הנ"ל – לא נקבל תוצאות מכיוון שרק ה-Session בו הטבלה הזמנית נוצרה "רואה" אותה.
לחילופין נוכל ליצור טבלה זמנית גלובלית באופן דומה לנ"ל אך בשם TmpTbl## (#סולמית אחת מציינת טבלה זמנית מקומית, ##סולמית כפולה מציינת טבלה זמנית גלובלית),
ואז ניתן יהיה לשלוף את המבנה שלה מכל ה-Session-ים.

לא יפה להסתכל בטבלאות של אחרים, ובעזרת מנגנון ה-Audit נוכל ללכוד את מי שהציץ ונפגע. ניצור מחיצה מתאימה בדיסק של השרת לשם יופנו נתוני המעקב, ונגדיר Audit (אובייקט ברמת השרת) למחיצה זו. את המחיצות ניתן ליצור ידנית אם ה-xp_CmdShell אינו מאופשר:
Use Master
Go
xp_cmdshell 'md C:\MyAudit\'
Go
CREATE SERVER AUDIT My_Server_Audit
    TO FILE (FILEPATH='C:\MyAudit\', MAXSIZE = 5MB,
    MAX_ROLLOVER_FILES = 100, RESERVE_DISK_SPACE = ON);
Go
ALTER SERVER AUDIT My_Server_Audit with (STATE = ON);
Go
כעת נגדיר Database Audit Specification (אובייקט ברמת הדטבייס) שבעזרת הנ"ל יעקוב אחר מה שנחליט, כאשר לצורך העניין נעקוב אחר פעולות DML שכוללות Select, Update, Delete, Insert; ובמחשבה שניה – כדי להדגים שינוי בדיעבד בהגדרה – נעקוב גם אחרי פעולות DDL (שינויי סכימה), כל זה בדטבייס AdventureWorks:
Use AdventureWorks
Go
CREATE DATABASE AUDIT SPECIFICATION My_DB_Audit_Spec
    FOR SERVER AUDIT My_Server_Audit
    ADD (SELECT,DELETE,INSERT,UPDATE ON DATABASE::[AdventureWorks] BY [public])
    WITH (STATE = Off)
GO
ALTER  DATABASE AUDIT SPECIFICATION My_DB_Audit_Spec
    FOR SERVER AUDIT My_Server_Audit
    ADD (SCHEMA_OBJECT_CHANGE_GROUP)
Go
ALTER DATABASE AUDIT SPECIFICATION My_DB_Audit_Spec
    FOR SERVER AUDIT My_Server_Audit WITH (STATE = ON)
Go
כעקרון קבצי ה-Audit ריקים כרגע חוץ מרשומת פתיחה סימלית:
SELECT    *
FROM    sys.fn_get_audit_file ('C:\MyAudit\My_Server_Audit*.sqlaudit',default,default)
Order By event_time;
כעת נבצע שורה של פעולות כדי לוודא שהיד פתוח וה-Audit רושמת: ניצור ב-AdventureWorks טבלה, נכניס נתונים, נשלוף, נעדכן, נפתח טרנזקציה + נמחק נתונים + נתחרט, נמחוק על ידי Truncate, ונבטל את הטבלה; ניצור View, נשנה אותו, נשלוף ממנו, ונבטל אותו; ובין הפקודות נפזר פקודות המתנה WaitFor Delay כדי שהן לא יתבצעו באותה אלפית שניה ושנוכל לשלוף לפי סדר הביצוע:
Use AdventureWorks
Go
Create Table dbo.Try001(I Int);WaitFor Delay '00:00:01';
Insert Into Try001 Select 1;WaitFor Delay '00:00:01';
Select * From Try001;WaitFor Delay '00:00:01';
Update Try001 Set I=2;WaitFor Delay '00:00:01';
Begin Tran
Delete From Try001;WaitFor Delay '00:00:01';
RollBack;
Truncate Table Try001;WaitFor Delay '00:00:01';
Drop Table Try001;WaitFor Delay '00:00:01';
Go
Create View dbo.Try002 As Select 2 D;
Go
WaitFor Delay '00:00:01';
Go
Alter View dbo.Try002 As Select 3 D;
Go
WaitFor Delay '00:00:01';
Go
Select * From Try002;WaitFor Delay '00:00:01';
Drop View dbo.Try002;
Go
וכעת ניתן לשלוף שוב את הנתונים, וניתן לראות שהכל נרשם- כולל הטרנזקציה שבוטלה על ידי RollBack ופקודת ה-Truncate:
SELECT    *
FROM    sys.fn_get_audit_file ('C:\MyAudit\My_Server_Audit*.sqlaudit',default,default)
Order By event_time;
את ה-Audit ניתן לעצור כך:
Use Master
ALTER SERVER AUDIT My_Server_Audit with (STATE = ON)
Go
ואם רוצים להעלים ראיות, מבטלים את כל מה שנוצר:
Use Master
If (Select Count(*) From sys.server_audits Where name='My_Server_Audit')>0
    Begin
    ALTER SERVER AUDIT My_Server_Audit with (STATE = OFF);
    Drop SERVER AUDIT My_Server_Audit;
    End
Go
Use AdventureWorks
Go
If (Select Count(*) From sys.Database_audit_specifications Where name='My_DB_Audit_Spec')>0
    Begin
    ALTER DATABASE AUDIT SPECIFICATION My_DB_Audit_Spec with (STATE = OFF);
    Drop DATABASE AUDIT SPECIFICATION My_DB_Audit_Spec;
    End
Go
xp_cmdshell 'del C:\MyAudit\/q'
Go
xp_cmdshell 'rd C:\MyAudit\'
Go

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

ניצור דטבייס חדש לצורך ההדגמה:

Use tempdb
Go
Create Database PartDB;
Go
Use PartDB;
Go

כעת ניצור את התשתית לשימוש ב-Partitions:
Partition Function – קובעת לפי איזה שדה תתבצע החלוקה, ומה יהיו נקודות החלוקה. במקרה זה החלוקה תהיה לשני תחומים כך שכל הנתונים עד סוף 2002 הם נתונים הסטוריים, ומ-2002 ואילך נתונים עדכניים.
Partition Scheme – קובעת היכן יאוחסן כל Partition של הפונקציה הנ"ל. בשלב זה שניהם יהיו על ה-Filegroup הראשי, כלומר- Primary:

CREATE PARTITION FUNCTION PART_FN (datetime)
AS RANGE LEFT FOR VALUES ('20021231');
Go
CREATE PARTITION SCHEME PART_SCH
AS PARTITION PART_FN TO ([Primary],[Primary]);
Go

ניצור טבלה לפי סכימת החלוקה הנ"ל ונכניס לתוכה נתונים מטבלה ב-AdventureWorks:

CREATE TABLE CurrencyRate(CurrencyRateID int,
                        CurrencyRateDate datetime,
                        FromCurrencyCode nchar(3),
                        ToCurrencyCode nchar(3),
                        AverageRate money,
                        EndOfDayRate money,
                        ModifiedDate datetime) ON PART_SCH (CurrencyRateDate);
GO
Insert Into CurrencyRate
Select    * 
From    AdventureWorks.Sales.CurrencyRate T 
Order By T.CurrencyRateDate;
Go

* נוכל לעיין בנתוני הטבלה ולראות לאיזה משני ה-Partitins הופנתה כל שורה:

SELECT    $PARTITION.PART_FN(CurrencyRateDate) Partition,
        MIN(CurrencyRateDate) [Min],
        MAX(CurrencyRateDate) [Max],
        COUNT(CurrencyRateDate) [Count]
FROM    CurrencyRate
Group By $PARTITION.PART_FN(CurrencyRateDate)
Order By $PARTITION.PART_FN(CurrencyRateDate);
Go

נגדיל כעת את מספר ה-Partitions וניצור חדש שיכיל את נתוני 2004 ואילך על ידי שינוי הפונקציה והסכימה.
בנוסף, ניצור עבורו Filegroup חדש על דיסק אחר (בניגוד לשני הנ"ל ששוכנים על ה-Primary) משיקולים של ביצועים:

ALTER DATABASE PartDB
ADD FILEGROUP FGCurrFile
Go
ALTER DATABASE PartDB
ADD FILE(NAME = CurrFile,FILENAME = 'E:\CurrFile.ndf',SIZE = 1MB)
TO FILEGROUP FGCurrFile
GO
ALTER PARTITION SCHEME PART_SCH
NEXT USED [FGCurrFile]
GO
ALTER PARTITION FUNCTION PART_FN() 
SPLIT RANGE ('20031231')
GO 

כדאי לשוב לאחר כל שינוי כזה לשליפה *הנ"ל כדי לבחון את השינוי.

התחרטנו? ניתן לאחד חזרה את שני ה-Partitions ולבטל את ה-Filegroup שיצרנו:

ALTER PARTITION FUNCTION PART_FN()
MERGE RANGE ('20031231')
GO
ALTER DATABASE PartDB Remove FILE CurrFile;
Go
ALTER DATABASE PartDB Remove FILEGROUP FGCurrFile
Go

כעת ניפטר מחלק מהנתונים- נהפוך את ה-Partition הישן לטבלה נפרדת, שנוכל לטפל בה מבלי להשפיע על הטבלה הקיימת,
ולאחר מכן נבדוק כמה שורות יש בכל טבלה כדי להיווכח שהנתונים עברו:

CREATE TABLE CurrencyRate_Out(CurrencyRateID int,
                        CurrencyRateDate datetime,
                        FromCurrencyCode nchar(3),
                        ToCurrencyCode nchar(3),
                        AverageRate money,
                        EndOfDayRate money,
                        ModifiedDate datetime);
Go
ALTER TABLE dbo.CurrencyRate
SWITCH PARTITION 1
TO dbo.CurrencyRate_Out;
Go
Select COUNT(*) [Count] From CurrencyRate;
Go
Select COUNT(*) [Count] From CurrencyRate_Out;

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

CREATE TABLE CurrencyRate_In(CurrencyRateID int,
                        CurrencyRateDate datetime CHECK (CurrencyRateDate >= '20050101' AND CurrencyRateDate IS NOT NULL),
                        FromCurrencyCode nchar(3),
                        ToCurrencyCode nchar(3),
                        AverageRate money,
                        EndOfDayRate money,
                        ModifiedDate datetime);
Go
Insert Into CurrencyRate_In Values(15000,'20050101','USD','ARS',0.99,0.999,'20050101');
Go
ALTER PARTITION SCHEME PART_SCH
NEXT USED [Primary]
Go
ALTER PARTITION FUNCTION PART_FN() 
SPLIT RANGE ('20041231');
Go
ALTER TABLE CurrencyRate_In switch TO CurrencyRate PARTITION 3;
Go
Select * From CurrencyRate_In;
Select * From CurrencyRate Where $PARTITION.PART_FN(CurrencyRateDate)=3

בהצלחה!

טריק עתיק יומין ופשוט ששלפתי מהבוידעם לאחר שנתקלתי בדוגמה הזו שמצליחה לסבך את הבעייה ללא צורך. הטריק הוא למצוא את ההפרש בין אורך המחרוזת המקורית, לבין אורך המחרוזת לאחר שהסרנו ממנה את תת המחרוזת. למשל- כמה פעמים מופיעה האות e בשם Geri Reshef? אורך השם Geri Reshef הוא 11 תווים, אם נוריד את e נקבל Gri Rshf שאורכו 8 תווים, ומכאן ש-e מופיע 3=11-8 פעמים. לו הייתי מחפש תת מחרוזת שאורכה גדול מ-1, היה עלי לחלק את התוצאה באורך תת המחרוזת. דוגמה לחיפוש מחרוזת באורך תו אחד:
Declare    @S1 Varchar(Max),
        @S2 Varchar(Max);
Select    @S1='Geri Reshef',
        @S2='e';
Select DataLength(@S1)-DataLength(Replace(@S1,@S2,'')) Occurences;
דוגמה לחיפוש מחרוזת בכל אורך שהוא:
Declare    @S1 Varchar(Max),
        @S2 Varchar(Max);
Select    @S1='Count appearance of a particular string within the outer string',
        @S2='String';
Select (DataLength(@S1)-DataLength(Replace(@S1,@S2,'')))/DataLength(@S2) Occurences;
כמובן שב"מקרה אמיתי" (נניח- שליפה מטבלה) – תופיע רק שורת ה-Select האחרונה עם שמות השדות הרלוונטיים במקום המשתנים שאני הגדרתי.
משחק מטופש למדי שמהר מאוד ממצה את עצמו לאחר שמבינים שהוא מוביל לתיקו (אלא אם כן מדובר בטירון), אך מה לעשות? לפעמים תכנות ו-DBA-ות הם גם עיסוקים מטופשים.. נתחיל ביצירת טבלה T_Mazavim שתכלול את כל המצבים החוקיים האפשריים של הלוח: 1. בהנחה ש-X מתחיל, ההפרש בין מספר ה-X-ים וה-O-ים הוא לכל היותר 1. 2. כשמישהו מנצח או כשנוצר תיקו- עוצרים (לא יתכן ששני הצדדים מנצחים, למשל- שלושה X-ים בשורה העליונה ושלושה O-ים בשורה התחתונה).
With T1 As    --3 posible signs ("-" = empty)
(Select    '-' IMD
Union All
Select    'X' IMD
Union All
Select    'O' IMD),
T2 As
(Select    1 N,
        CAST(IMD As Varchar(9)) IMD,
        IMD IMD1,
        '-' IMD2,
        '-' IMD3,
        '-' IMD4,
        '-' IMD5,
        '-' IMD6,
        '-' IMD7,
        '-' IMD8,
        '-' IMD9,
        CAST(Null As SmallInt) As Tozaa
From    T1
Union All
Select    *,
        Case When 'XXX' In (IMD1+IMD2+IMD3,IMD4+IMD5+IMD6,IMD7+IMD8+IMD9,IMD1+IMD4+IMD7,IMD2+IMD5+IMD8,IMD3+IMD6+IMD9,IMD1+IMD5+IMD9,IMD3+IMD5+IMD7)
                Then Cast(1 As SmallInt) Else Cast(0 As SmallInt) End --X wins
        +Case When 'OOO' In (IMD1+IMD2+IMD3,IMD4+IMD5+IMD6,IMD7+IMD8+IMD9,IMD1+IMD4+IMD7,IMD2+IMD5+IMD8,IMD3+IMD6+IMD9,IMD1+IMD5+IMD9,IMD3+IMD5+IMD7)
                Then Cast(3 As SmallInt) Else Cast(0 As SmallInt) End --O wins
        +Case When IMD1+IMD2+IMD3 Like '%X%' And IMD4+IMD5+IMD6 Like '%X%' And IMD7+IMD8+IMD9 Like '%X%' And IMD1+IMD4+IMD7 Like '%X%' And IMD2+IMD5+IMD8 Like '%X%' And IMD3+IMD6+IMD9 Like '%X%' And IMD1+IMD5+IMD9 Like '%X%' And IMD3+IMD5+IMD7 Like '%X%'
                And IMD1+IMD2+IMD3 Like '%O%' And IMD4+IMD5+IMD6 Like '%O%' And IMD7+IMD8+IMD9 Like '%O%' And IMD1+IMD4+IMD7 Like '%O%' And IMD2+IMD5+IMD8 Like '%O%' And IMD3+IMD6+IMD9 Like '%O%' And IMD1+IMD5+IMD9 Like '%O%' And IMD3+IMD5+IMD7 Like '%O%'
                Then Cast(2 As SmallInt) Else Cast(0 As SmallInt) End --Equal
        Tozaa
From    (Select    T2.N+1 N,
                Cast(T2.IMD+T1.IMD As Varchar(9)) IMD,
                T2.IMD1,
                Case When N+1=2 Then T1.IMD When N+1>2 Then T2.IMD2 Else '-' End IMD2,
                Case When N+1=3 Then T1.IMD When N+1>3 Then T2.IMD3 Else '-' End IMD3,
                Case When N+1=4 Then T1.IMD When N+1>4 Then T2.IMD4 Else '-' End IMD4,
                Case When N+1=5 Then T1.IMD When N+1>5 Then T2.IMD5 Else '-' End IMD5,
                Case When N+1=6 Then T1.IMD When N+1>6 Then T2.IMD6 Else '-' End IMD6,
                Case When N+1=7 Then T1.IMD When N+1>7 Then T2.IMD7 Else '-' End IMD7,
                Case When N+1=8 Then T1.IMD When N+1>8 Then T2.IMD8 Else '-' End IMD8,
                Case When N+1=9 Then T1.IMD When N+1>9 Then T2.IMD9 Else '-' End IMD9
        From    T2,
                T1
        Where    N<9
                And Len(T2.IMD+T1.IMD)-Len(Replace(T2.IMD+T1.IMD,'X',''))<=5
                And Len(T2.IMD+T1.IMD)-Len(Replace(T2.IMD+T1.IMD,'O',''))<=4
                And IsNull(Tozaa,2) In (0,1,2,3)) T),
T3 As
(Select    *
From    T2
Where    IsNull(Tozaa,0) In (0,1,2,3)
        And Len(IMD)=9
        And (9-Len(Replace(IMD,'X','')))-(9-Len(Replace(IMD,'O',''))) In (0,1))
Select    *
Into    T_Mazavim
From    T3;

כעת ניצור את טבלת T_Ksharim שתכלול את הקשרים בין המצבים השונים, כלומר- בין מצב אפשרי של הלוח (IMD) למצבו הקודם (IMDp כאשר בדרך כלל יש לכל מצב כמה קודמים).

לכאורה הייתי יכול לעשות זאת במכה אחת ותוך כדי בניית המצבים הנ"ל לציין מה המצב הקודם, אולם זה גם יותר איטי וגם יוצר כפילויות רבות; ולכן במקום ליצור את המצבים בדרך של סימולציה של משחקים (X מתחיל ויש לו 9 אפשרויות, O ממשיך ויש לו 8 אפשרויות..) יצרתי צירופים חוקיים של (X O –) וכעת אנסה למצוא מי קשור למי:
Select    Mb.*,
        Ma.IMD IMDp
Into    T_Ksharim
From    T_Mazavim Ma
Inner Join T_Mazavim Mb
        On Case When Ma.IMD1=Mb.IMD1 Then 1 When Ma.IMD1='-' Then 0 Else 999 End
            +Case When Ma.IMD2=Mb.IMD2 Then 1 When Ma.IMD2='-' Then 0 Else 999 End
            +Case When Ma.IMD3=Mb.IMD3 Then 1 When Ma.IMD3='-' Then 0 Else 999 End
            +Case When Ma.IMD4=Mb.IMD4 Then 1 When Ma.IMD4='-' Then 0 Else 999 End
            +Case When Ma.IMD5=Mb.IMD5 Then 1 When Ma.IMD5='-' Then 0 Else 999 End
            +Case When Ma.IMD6=Mb.IMD6 Then 1 When Ma.IMD6='-' Then 0 Else 999 End
            +Case When Ma.IMD7=Mb.IMD7 Then 1 When Ma.IMD7='-' Then 0 Else 999 End
            +Case When Ma.IMD8=Mb.IMD8 Then 1 When Ma.IMD8='-' Then 0 Else 999 End
            +Case When Ma.IMD9=Mb.IMD9 Then 1 When Ma.IMD9='-' Then 0 Else 999 End=8
        And Ma.Tozaa=0;
כעת ליד כל מצב מופיעה אחת התוצאות הבאות: 0 – המשחק טרם הסתיים (התוצאה לא ידועה) 1- X ניצח 2- תיקו 3- O ניצח ויש לחשב את התוצאה בעזרת אלגוריתם MinMax, כלומר- לגבי כל מצב שהתוצאה שלו לא ידועה אך ידועות התוצאות של כל מצבי ההמשך- השחקן באותו שלב יבחר בתוצאה הכי טובה לו (ניצחון בעדיפות ראשונה, תיקו בעדיפות משנית, והפסד אם אין ברירה אחרת..). לשם כך יש להיעזר בלולאה – שזו דרך מאוד לא אלגנטית בשעה שמדובר ב-SQL אך הכרח בל יגונה במקרה זה:
Declare    @RC Int;
Set        @RC=1;
While    @RC>0
        Begin
        Update T_Ksharim
        Set        Tozaa=Case When (9-LEN(Replace(IMD,'-',''))) In (1,3,5,7,9) Then Mn Else Mx End --MinMax algorithm
        From    T_Ksharim K
        Inner Join (Select    IMDp,
                            MIN(Tozaa) Mn,
                            MAX(Tozaa) Mx
                    From    T_Ksharim
                    Group By IMDp
                    Having    MIN(Tozaa)<>0) T
                On K.IMD=T.IMDp
        Where    K.Tozaa=0;
        Set        @RC=@@ROWCOUNT;
        End;
הלולאה מעדכנת בכל איטרציה את כל המצבים שאין להם תוצאה אך תוצאות מצבי ההמשך שלהם ידועות, וחוזרת על כך עד שלא נותר מה לעדכן. כעת ניגש למשחק, ובואו נניח שאנחנו מתחילים, ואנחנו שולפים מתוך טבלת הקשרים את כל המצבים שהמצבים הקודמים שלהם הם לוח ריק:
Select    *
From    T_Ksharim
Where    IMDp='---------'
כצפוי יש לנו 9 אפשרויות כיצד להתחיל וכולן מובילות לתיקו (תוצאה=2), ואנחנו נבחר ב-X בפינה השמאלית העליונה:
X
כעת תור היריב לשחק, ולמזלנו הרב הוא טירון ונופל לפח:
X
O
אנחנו שואלים בעצת המערכת:
Select    *
From    T_Ksharim
Where    IMDp='X--O-----'
מתברר שמבין 7 אפשרויות התגובה שלנו – 3 יובילו לנצחון (תוצאה = 1) ואנחנו בוחרים באחת מהן:
X
X O
היריב האומלל עוד חושב שיש לו סיכוי ומנסה לחסום את האלכסון:
X
X O
O
אבל אנחנו לא שומטים את הטרף מפינו:
Select    *
From    T_Ksharim
Where    IMDp='X--OX---O'
מתוך 5 האפשרויות הפתוחות בפנינו- 2 מובילות לנצחון ונבחר באחת מהן:
X X
X O
O
וכעת על היריב לבחור אם הוא רוצה לקבל את השלישיה באלכסון השני או בשורה העליונה.. מובן שאם גם לו הייתה גישה לטבלה- הוא לא היה עושה את הטעות בתורו הראשון, והמשחק היה מסתיים בתיקו.