ptg 1574 CHAPTER 42 What’s New for Transact-SQL in SQL Server 2008 @time3 as ‘time3’ select @datetime2 as ‘datetime2’, @datetimeoffset as ‘datetimeoffset’, @utcdatetime as ‘utcdatetime’ select SYSDATETIMEOFFSET() as sysdatetimeoffset, SYSDATETIME() as sysdatetime go datetime date time time3 2010-03-28 23:18:30.490 2010-03-28 23:18:30.4904294 23:18:30.492 datetime2 datetimeoffset utcdatetime 2010-03-28 23:18:30.49 2010-03-28 23:18:30.4924295 -04:00 2010-03-29 03:18:30.49 sysdatetimeoffset sysdatetime 2010-03-28 23:24:10.7485902 -04:00 2010-03-28 23:24:10.74 Be aware that retrieving the value from getdate() or sysdatetime() into a datetimeoffset variable or column does not capture the offset from UTC, even if you store the returned value in a column or variable defined with the datetimeoffset data type. To do so, you need to use the SYSDATETIMEOFFSET() function: declare @datetimeoffset1 datetimeoffset, @datetimeoffset2 datetimeoffset select @datetimeoffset1 = SYSDATETIME(), @datetimeoffset2 = SYSDATETIMEOFFSET() select @datetimeoffset1, @datetimeoffset2 go 2010-03-28 23:36:39.7271831 +00:00 2010-03-28 23:36:39.7271831 -04:00 Note that in the output, SQL Server Management Studio (SSMS) trims the time values down to two decimal places when it displays the results in the Text Results tab. However, this is just for display purposes (and applies only with text results; grid results display the full decimal precision). The actual value does store the precision down to the specified number of decimal places, which can be seen if you convert the datetime2 value to a string format that displays all the decimal places: ptg 1575 New date and time Data Types and Functions 42 select SYSDATETIME() as datetime2_trim, convert(varchar(30), SYSDATETIME(), 121) as datetime2_full go datetime2_trim datetime2_full 2010-03-30 23:52:30.68 2010-03-30 23:52:30.6851262 The SWITCHOFFSET() function can be used to convert a datetimeoffset value into a differ- ent time zone offset value: select SYSDATETIMEOFFSET(), SWITCHOFFSET ( SYSDATETIMEOFFSET(), ‘-07:00’ ) go 2010-03-29 00:07:21.1335738 -04:00 2010-03-28 21:07:21.1335738 -07:00 When you are specifying a time zone value for the SWITCHOFFSET or TODATETIMEOFFSET offset functions, the value can be specified as an integer value representing the number of minutes of offset or as a time value in hh:mm format. The range of allowed values is +14 hours to -13 hours. select TODATETIMEOFFSET ( SYSDATETIME(), -300 ) select TODATETIMEOFFSET ( SYSDATETIME(), ‘-05:00’ ) go 2010-03-29 00:23:05.5773288 -05:00 2010-03-29 00:23:05.5773288 -05:00 Date and Time Conversions If an existing CONVERT style includes the time part, and the conversion is from datetimeoffset to a string, the time zone offset (except for style 127) is included. If you do not want the time zone offset, you need to use cast or convert the datetimeoffset value to datetime2 first and then to a string: select convert(varchar(35), SYSDATETIMEOFFSET(), 121) as datetime_offset, CONVERT(varchar(30), cast(SYSDATETIMEOFFSET() as datetime2),121) as datetime2 go datetime_offset datetime2 2010-03-30 23:57:36.1015950 -04:00 2010-03-30 23:57:36.1015950 ptg 1576 CHAPTER 42 What’s New for Transact-SQL in SQL Server 2008 When you convert from datetime2 or datetimeoffset to date, there is no rounding and the date part is extracted explicitly. For any implicit conversion from datetimeoffset to date, time, datetime2, datetime, or smalldatetime, conversion is based on the local date and time value (to the persistent time zone offset). For example, when the datetimeoffset(3) value, 2006-10-21 12:20:20.999 -8:00, is converted to time(3), the result is 12:20:20.999, not 20:20:20.999(UTC). If you convert from a higher-precision time value to a lower-precision value, the conversion is permitted, and the higher-precision values are truncated to fit the lower precision type. If you are converting a time(n), datetime2(n), or datetimeoffset(n) value to a string, the number of digits depends on the type specification. If you want a specific precision in the resulting string, convert to a data type with the appropriate precision first and then to a string, as follows: select convert(varchar(35), sysdatetime(), 121) as datetime_offset, CONVERT(varchar(30), cast(sysdatetime() as datetime2(3)), 121) as datetime2 go datetime_offset datetime2 2010-03-31 00:04:37.3306880 2010-03-31 00:04:37.331 If you attempt to cast a string literal with a fractional seconds precision that is more than that allowed for smalldatetime or datetime, Error 241 is raised: declare @datetime datetime select @datetime = ‘2010-03-31 00:04:37.3306880’ go Msg 241, Level 16, State 1, Line 2 Conversion failed when converting date and/or time from character string. Table-Valued Parameters In previous versions of SQL Server, it was not possible to share the contents of table vari- ables between stored procedures. SQL Server 2008 changes that with the introduction of table-valued parameters, which allow you to pass table variables to stored procedures as input parameters. Table-valued parameters provide more flexibility and, in many cases, better performance than temporary tables as a means to pass result sets between stored procedures. ptg 1577 Table-Valued Parameters 42 To create and use table-valued parameters, you must first create a user-defined table type as a TABLE data type and define the table structure. This is done using the CREATE TYPE command, as shown in Listing 42.10. LISTING 42.10 Defining a User-Defined Table Type if exists (select * from sys.systypes t where t.name = ‘ytdsales_tabletype’ and t.uid = USER_ID(‘dbo’)) drop type ytdsales_tabletype go CREATE TYPE ytdsales_tabletype AS TABLE (title_id char(6), title varchar(50), pubdate date, ytd_sales int) go After creating the user-defined table data type, you can use it for declaring local table vari- ables and for stored procedure parameters. To use the table-valued parameter in a proce- dure, you create a procedure to receive and access data through a table-valued parameter, as shown in Listing 42.11. LISTING 42.11 Defining a Stored Procedure with a Table-Valued Parameter /* Create a procedure to receive data for the table-valued parameter. */ if OBJECT_ID(‘tab_parm_test’) is not null drop proc tab_parm_test go create proc tab_parm_test @pubdate datetime = null, @sales_minimum int = 0, @ytd_sales_tab ytdsales_tabletype READONLY as set nocount on if @pubdate is null if no date is specified, set date to last year set @pubdate = dateadd(month, -12, getdate()) select * from @ytd_sales_tab where pubdate > @pubdate and ytd_sales >= @sales_minimum return go ptg 1578 CHAPTER 42 What’s New for Transact-SQL in SQL Server 2008 Then, when calling that stored procedure, you declare a local table variable using the table data type defined previously, populate the table variable with data, and then pass the table variable to the stored procedure (see Listing 42.12). LISTING 42.12 Executing a Stored Procedure with a Table-Valued Parameter /* Declare a variable that references the table type. */ declare @ytd_sales_tab ytdsales_tabletype /* Add data to the table variable. */ insert @ytd_sales_tab select title_id, convert(varchar(50), title), pubdate, ytd_sales from titles /* Pass the table variable populated with data to a stored procedure. */ exec tab_parm_test ‘6/1/2001’, 10000, @ytd_sales_tab go title_id title ytd_sales BU2075 You Can Combat Computer Stress! 18722 MC3021 The Gourmet Microwave 22246 TC4203 Fifty Years in Buckingham Palace Kitchens 15096 The scope of a table-valued parameter is limited to only the stored procedure to which it is passed. To access the contents of a table-valued parameter in a procedure called by another procedure that contains a table-valued parameter, you need to pass the table- valued parameter to the subprocedure. Listing 42.13 provides an example of a subproce- dure and alters the procedure created in Listing 42.6 to call the subprocedure. LISTING 42.13 Passing a Table-Valued Parameter to a Subprocedure /* Create the sub-procedure */ create proc tab_parm_subproc @pubdate datetime = null, @sales_minimum int = 0, @ytd_sales_tab ytdsales_tabletype READONLY as select * from @ytd_sales_tab where ytd_sales <= @sales_minimum and ytd_sales <> 0 ptg 1579 Table-Valued Parameters 42 go /* modify the tab_part_test proc to call the sub-procedure */ alter proc tab_parm_test @pubdate datetime = null, @sales_minimum int = 0, @ytd_sales_tab ytdsales_tabletype READONLY as set nocount on if @pubdate is null if no date is specified, set date to last year set @pubdate = dateadd(month, -12, getdate()) select * from @ytd_sales_tab where pubdate > @pubdate and ytd_sales >= @sales_minimum exec tab_parm_subproc @pubdate, @sales_minimum, @ytd_sales_tab return go /* Declare a variable that references the type. */ declare @ytd_sales_tab ytdsales_tabletype /* Add data to the table variable. */ insert @ytd_sales_tab select title_id, convert(varchar(50), title), pubdate, ytd_sales from titles where type = ‘business’ /* Pass the table variable populated with data to a stored procedure. */ exec tab_parm_test ‘6/1/2001’, 10000, @ytd_sales_tab go title_id title pubdate ytd_sales BU2075 You Can Combat Computer Stress! 2004-06-30 18722 title_id title pubdate ytd_sales BU1032 The Busy Executive’s Database Guide 2004-06-12 4095 ptg 1580 CHAPTER 42 What’s New for Transact-SQL in SQL Server 2008 BU1111 Cooking with Computers: Surreptitious Balance Shee 2004-06-09 3876 BU7832 Straight Talk About Computers 2004-06-22 4095 Table-Valued Parameters Versus Temporary Tables Table-valued parameters offer more flexibility and in some cases better performance than temporary tables or other ways to pass a list of values to a stored procedure. One benefit is table-valued parameters do not acquire locks for the initial population of data from a client. Also, table-valued parameters are memory resident and do not incur physical I/O unless they grow too large to remain in cache memory. However, table-valued parameters do have some restrictions: . SQL Server does not create or maintain statistics on columns of table-valued parame- ters. . Table-valued parameters can be passed only as READONLY input parameters to T-SQL routines. You cannot perform UPDATE, DELETE, or INSERT operations on a table-valued parameter within the body of the stored procedure to which it is passed. . Like table variables, a table-valued parameter cannot be specified as the target of a SELECT INTO or INSERT EXEC statement. They can only be populated using an INSERT statement. Hierarchyid Data Type The Hierarchyid data type introduced in SQL Server 2008 is actually a system-supplied common language runtime (CLR) user-defined type (UDT) that can be used for storing and manipulating hierarchical structures (for example, parent-child relationships) in a rela- tional database. The Hierarchyid type is stored as a varbinary value that represents the position of the current node in the hierarchy (both in terms of parent-child position and position among siblings). You can perform manipulations on the type in Transact-SQL by invoking methods exposed by the type. Creating a Hierarchy First, let’s define a hierarchy in a table using the Hierarchyid data type. For example, this section uses the Parts table example used in Chapter 28, “Creating and Managing Stored Procedures,” to demonstrate how a stored procedure could be used to traverse a hierarchy stored in a table. There is also an example in Chapter 52 using a recursive common table expression (CTE) to perform a similar action. Let’s see how to implement an alternative solution by adding a Hierarchyid column to the Parts table. First, you create a version of the Parts table using the Hierarchyid data type (see Listing 42.14). ptg 1581 Hierarchyid Data Type 42 LISTING 42.14 Creating the Parts Table with a Hierarchyid Data Type Use bigpubs2008 Go CREATE TABLE PARTS_hierarchy( partid int NOT NULL, hid hierarchyid not null, lvl as hid.GetLevel() persisted, partname varchar(30) NOT NULL, PRIMARY KEY NONCLUSTERED (partid), UNIQUE NONCLUSTERED (partname) ) Note the hid column defined with the Hierarchyid data type. Notice also how the lvl column is defined as a compute column using the GetLevel method of the hid column to define the persisted computed column level. The GetLevel method returns the level of the current node in the hierarchy. The Hierarchyid data type provides topological sorting, meaning that a child’s sort value is guaranteed to be greater than the parent’s sort value. This guarantees that a node’s sort value will be higher than all its ancestors. You can take advantage of this feature by creating an index on the Hierarchyid column because the index will sort the data in a depth-first manner. This ensures that all members of the same subtree are close to each other in the leaf level of the index, which makes the index useful as an efficient mechanism for returning all descendents of a node. To take advantage of this, you can create a clustered index on the hid column: CREATE UNIQUE CLUSTERED INDEX idx_hid_first ON Parts_hierarchy (hid); You can also use another indexing strategy called breadth-first, in which you organize all nodes from the same level close to each other in the leaf level of the index. This is done by building the index such that the leading column is level in the hierarchy. Queries that need to get all nodes from the same level in the hierarchy can benefit from this type of index: CREATE UNIQUE INDEX idx_lvl_first ON Parts_hierarchy(lvl, hid); Populating the Hierarchy Now that you’ve created the hierarchy table, the next step is to populate it. To insert a new node into the hierarchy, you must first produce a new Hierarchyid value that repre- sents the correct position in the hierarchy. There are two methods available with the Hierarchyid data type to do this: the HIERARCHYID::GetRoot() method and GetDescendant method. You use the HIERARCHYID::GetRoot() method to produce the value for the root node of the hierarchy. This method simply produces a Hierarchyid value that is internally an empty binary string representing the root of the tree. You can use the GetDescendant method to produce a value below a given parent. The GetDescendant method accepts two optional Hierarchyid input values that represent the two nodes between which you want to position the new node. If both values are not NULL, the method produces a new value positioned between the two nodes. If the first parameter ptg 1582 CHAPTER 42 What’s New for Transact-SQL in SQL Server 2008 is not NULL and the second parameter is NULL, the method produces a value greater than the first parameter. Finally, if the first parameter is NULL and the second parameter is not NULL, the method produces a value smaller than the second parameter. If both parameters are NULL, the method produces a value simply below the given parent. NOTE The GetDescendant method does not guarantee that Hierarchyid values are unique. To enforce uniqueness, you must define either a primary key, unique constraint, or unique index on the Hierarchyid column. The code in Listing 42.15 uses a cursor to loop through the rows currently in the Parts table and populates the Parts_hierarchy table. If the part is the first node in the hierar- chy, the procedure uses the HIERARCHYID::GetRoot() method to assign the hid value for the root node of the hierarchy. Otherwise, the code in the cursor looks for the last child hid value of the new part’s parent part and uses the GetDescendant method to produce a value that positions the new node after the last child of that parent part. NOTE Listing 42.15 also makes use of a recursive common table expression to traverse the existing Parts table in hierarchical order to add in the rows at the proper level, starting with the top-most parent part. If you are unfamiliar with CTEs (which were introduced in SQL Server 2005), you may want to review the “In Case you Missed it…” section in Chapter 43. LISTING 42.15 Populating the Parts_hierarchy Table DECLARE @hid AS HIERARCHYID, @parent_hid AS HIERARCHYID, @last_child_hid AS HIERARCHYID, @partid int, @partname varchar(30), @parentpartid int declare parts_cur cursor for WITH PartsCTE(partid, partname, parentpartid, lvl) AS ( SELECT partid, partname, parentpartid, 0 FROM PARTS WHERE parentpartid is null ptg 1583 Hierarchyid Data Type 42 UNION ALL SELECT P.partid, P.partname, P.parentpartid, PP.lvl+1 FROM Parts as P JOIN PartsCTE as PP ON P.parentpartid = PP.Partid ) SELECT PartID, Partname, ParentPartid FROM PartsCTE order by lvl open parts_cur fetch parts_cur into @partid, @partname, @parentpartid while @@FETCH_STATUS = 0 begin if @parentpartid is null set @hid = HIERARCHYID::GetRoot() else begin select @parent_hid = hid from PARTS_hierarchy where partid = @parentpartid select @last_child_hid = MAX(hid) from PARTS_hierarchy where hid.GetAncestor(1) = @parent_hid select @hid = @parent_hid.GetDescendant(@last_child_hid, NULL) end insert PARTS_hierarchy (partid, hid, partname) values (@partid, @hid, @partname) fetch parts_cur into @partid, @partname, @parentpartid end close parts_cur deallocate parts_cur go Querying the Hierarchy Now that you’ve populated the hierarchy, you should query it to view the data and verify the hierarchy was populated correctly. However, If you query the hid value directly, you see only its binary representation, which is not very meaningful. To view the Hierarchyid value in a more useful manner, you can use the ToString method, which returns a logical string representation of the Hierarchyid. This string representation is shown as a path . string. Table-Valued Parameters In previous versions of SQL Server, it was not possible to share the contents of table vari- ables between stored procedures. SQL Server 2008 changes that with the introduction. 23:57:36.1015950 -04:00 2010-03-30 23:57:36.1015950 ptg 1576 CHAPTER 42 What’s New for Transact -SQL in SQL Server 2008 When you convert from datetime2 or datetimeoffset to date, there is no rounding and the. ptg 1574 CHAPTER 42 What’s New for Transact -SQL in SQL Server 2008 @time3 as ‘time3’ select @datetime2 as ‘datetime2’, @datetimeoffset as ‘datetimeoffset’, @utcdatetime