A blog about SQL Server, SSIS, C# and whatever else I happen to be dealing with in my professional life.

Find ramblings

Showing posts with label SQL Server 2005. Show all posts
Showing posts with label SQL Server 2005. Show all posts

Tuesday, September 20, 2011

Adventures with OUTPUT, MERGE and INSTEAD OF triggers

This StackOverflow question was really interesting for me.
Not because of the users question itself, I thought it was a terrible premise all around to assume that an index violation would lead to the population of an identity value. No, what I found fascinating was my dive into
Excluding the ignore duplicate key, I've used all of these to some extent in my professional career, but never tried to use them all at once.

Highlights of my findings
  • MERGE statement with OUTPUT clause seemed to offer the best fit for what the questioner wanted
  • OUTPUT will not have access to the inserted logical table even though the insert will have consumed an identity value and rolled back
  • You can fake it through an INSTEAD OF trigger but it's an ugly hack and you wouldn't able to take advantage of the OUTPUT clause
    • The inserted virtual table within an INSTEAD OF trigger has identity values populated with 0
    • You cannot update the inserted/deleted logical tables
    • The OUTPUT clause on a table with an instead of trigger operates on the virtual tables


-- This script demonstrates the usage of 
-- IGNORE_DUP_KEY option for an index
-- http://msdn.microsoft.com/en-us/library/ms186869.aspx
-- Why you'd want this behaviour is left as 
-- an excercise to the reader
--

SET NOCOUNT ON

IF EXISTS(SELECT 1 FROM sys.tables T WHERE T.name = 'DupesOk' AND T.schema_id = schema_id('dbo'))
BEGIN
    DROP TABLE dbo.DupesOk
END
GO

CREATE TABLE 
    dbo.DupesOk
(
    dupe_id int identity(1,1) NOT NULL PRIMARY KEY
,   name varchar(50) NOT NULL
)

-- Create an index that is unique but
-- violation of the unique constraint is
-- merely discarded with warning instead of
-- blowing up
CREATE UNIQUE INDEX 
    uq_dupes_name
ON dbo.DupesOk
(
    name
)
WITH IGNORE_DUP_KEY 

-- Add a name and emit the identity value
-- from the inserted virtual table
INSERT INTO
    dbo.DupesOk
OUTPUT
    inserted.dupe_id
,   inserted.name
SELECT
    'Peter Parker'

-- Old-school means of showing the identity
-- values, 1's across the board
-- See earlier posting
-- http://billfellows.blogspot.com/2009/10/scope-and-identity.html
SELECT 
    @@IDENTITY AS six_of_one
,   SCOPE_IDENTITY() half_dozen_of_other
,   IDENT_CURRENT('dbo.DupesOk') AS current_identity_value


-- Add a new name and emit the identity value
-- from the inserted virtual table
INSERT INTO
    dbo.DupesOk
OUTPUT
    inserted.dupe_id
,   inserted.name
SELECT
    'Spider man'

-- Same as above, 2s across the board
SELECT 
    @@IDENTITY AS six_of_one
,   SCOPE_IDENTITY() half_dozen_of_other
,   IDENT_CURRENT('dbo.DupesOk') AS current_identity_value

-- Insert a duplicate value for the unique index
-- watch it not explode with a output message of 
-- 'Duplicate key was ignored.'
INSERT INTO
    dbo.DupesOk
OUTPUT
    -- This won't show anything as there is nothing to show
    inserted.dupe_id
,   inserted.name
SELECT
    'Peter Parker'

-- The first two remain 2's as they belong to the successful
-- insert of Spider man. ident_current shows that the value was
-- incremented. The calling code did not do a lookup to  
SELECT 
    @@IDENTITY AS this_identity_belongs_to_spider_man
,   SCOPE_IDENTITY() this_identity_also_belongs_to_spider_man
    -- As expected, the value is now 3, it got incremented 
,   IDENT_CURRENT('dbo.DupesOk') AS current_identity_value

;
MERGE
    -- target table
    dbo.DupesOk AS T
USING
(
    -- source system
    SELECT 'Hal Jordan' AS name
) AS S
ON S.name = T.name
WHEN
    MATCHED THEN
    UPDATE
    SET 
        T.name = S.name
WHEN
    NOT MATCHED THEN
    INSERT
    (
        [name]
    )
    VALUES
    (
        [name]
    )
-- 4 | Hal Jordan | INSERT
OUTPUT
    inserted.dupe_id
,   inserted.name
,   $action
;


-- 4's as expected
SELECT 
    @@IDENTITY AS hal_jordan
,   SCOPE_IDENTITY() still_hal_jordan
,   IDENT_CURRENT('dbo.DupesOk') AS current_identity_value


-- Add someone else just to get the ids to flip
INSERT INTO
    dbo.DupesOk
OUTPUT
    inserted.dupe_id
,   inserted.name
SELECT
    'Tony Stark'

-- 5's
SELECT 
    @@IDENTITY AS tony_stark
,   SCOPE_IDENTITY() still_tony_stark
,   IDENT_CURRENT('dbo.DupesOk') AS current_identity_value

;
-- Try inserting an existing id 
MERGE
    -- target table
    dbo.DupesOk AS T
USING
(
    -- source system
    SELECT 'Hal Jordan' AS name
) AS S
ON S.name = T.name
WHEN
    MATCHED THEN
    UPDATE
    SET 
        T.name = S.name
WHEN
    NOT MATCHED THEN
    INSERT
    (
        [name]
    )
    VALUES
    (
        [name]
    )
-- 4 | Hal Jordan | UPDATE
OUTPUT
    inserted.dupe_id
,   inserted.name
,   $action
;

-- Still 5's
SELECT 
    @@IDENTITY AS tony_stark
,   SCOPE_IDENTITY() still_tony_stark
,   IDENT_CURRENT('dbo.DupesOk') AS current_identity_value


GO
-- What if we try a trigger?
-- It would need to be an instead of trigger
-- as the value will have already been 
-- http://msdn.microsoft.com/en-us/library/ms189799.aspx
-- http://msdn.microsoft.com/en-us/library/ms175089.aspx
CREATE TRIGGER tr_dupes_insert
ON dbo.DupesOk
INSTEAD OF INSERT
AS
BEGIN
    SET NOCOUNT ON
    -- variety of different approaches here but
    -- I'll attempt the insert and if no rows
    -- are affected, then we know it's an existing
    -- row and lookup the identity
    DECLARE
        @ident TABLE
    (
        dupe_id int NOT NULL
    ,   name varchar(50) NOT NULL
    )

    -- Only n00bs code triggers for single rows
    INSERT INTO
        dbo.DupesOk
    (
        name
    )
    -- output clause
    -- http://msdn.microsoft.com/en-us/library/ms177564.aspx
    OUTPUT
        -- the output's virtual table
        -- recursion is deep, yo
        inserted.dupe_id
    ,   inserted.name
    INTO
        @ident
    SELECT
        I.name
    FROM
        -- the trigger's virtual table
        -- fascinatingly enough, the value for
        -- an identity field pre-insert on an
        -- instead of trigger is 0 and not NULL
        -- as one would assume
        inserted I

    -- Now we need to add anyone into the 
    -- table variable that didn't get inserted
    -- into @ident in the previous statement
    INSERT INTO
        @ident
    SELECT
        D.dupe_id
    ,   D.name
    FROM
        inserted I
        INNER JOIN
            dbo.DupesOk D
            ON D.name = I.name
        LEFT OUTER JOIN
            @ident tv
            -- can't match on ids here
            -- as they all come in as zero
            ON tv.name = I.name
    WHERE
        tv.dupe_id IS NULL

    SELECT
        I.dupe_id
    ,   I.name
    FROM
        @ident I

    -- To make OUTPUT work correctly, we'd need to
    -- "fix" the values in the inserted virtual tables
    -- but uncommenting this will result in a 
    -- trigger creation error of
    -- "The logical tables INSERTED and DELETED cannot be updated"
    --UPDATE 
    --    I
    --SET
    --    dupe_id = -1
    --FROM
    --    inserted i
    --    INNER JOIN
    --        @ident TV
    --        ON TV.name = i.name
END
GO

DECLARE
    @idents TABLE
(
    dupe_id int 
,   name varchar(50)
)


-- We should see
-- 1 | Peter Parker
-- 7 | Barry Allen
--
-- 6 was consumed by the double pump of Hal Jordan

-- results were surprising, to me at least


INSERT INTO
    dbo.DupesOk
-- this will generate an error
-- The target table 'dbo.DupesOk' of the DML statement cannot have any enabled triggers if the statement contains an OUTPUT clause without INTO clause.
-- unless we dump output results into a table
OUTPUT
    inserted.dupe_id
,   inserted.name
INTO
    @idents
SELECT
    'Peter Parker'
UNION ALL
SELECT
    'Barry Allen'

-- The above statement's trigger correctly spits out the rows we emit in the final
-- step of the trigger
-- dupe_id  name
-- 7        Barry Allen
-- 1        Peter Parker

-- Look at this, it's the inserted virtual table
-- from the trigger in pristine condition
-- and there's no way to unbugger it
SELECT * FROM @idents I

Results
dupe_id     name
----------- --------------------------------------------------
1           Peter Parker

six_of_one                              half_dozen_of_other                     current_identity_value
--------------------------------------- --------------------------------------- ---------------------------------------
1                                       1                                       1

dupe_id     name
----------- --------------------------------------------------
2           Spider man

six_of_one                              half_dozen_of_other                     current_identity_value
--------------------------------------- --------------------------------------- ---------------------------------------
2                                       2                                       2

dupe_id     name
----------- --------------------------------------------------
Duplicate key was ignored.

this_identity_belongs_to_spider_man     this_identity_also_belongs_to_spider_man current_identity_value
--------------------------------------- ---------------------------------------- ---------------------------------------
2                                       2                                        3

dupe_id     name                                               $action
----------- -------------------------------------------------- ----------
4           Hal Jordan                                         INSERT

hal_jordan                              still_hal_jordan                        current_identity_value
--------------------------------------- --------------------------------------- ---------------------------------------
4                                       4                                       4

dupe_id     name
----------- --------------------------------------------------
5           Tony Stark

tony_stark                              still_tony_stark                        current_identity_value
--------------------------------------- --------------------------------------- ---------------------------------------
5                                       5                                       5

dupe_id     name                                               $action
----------- -------------------------------------------------- ----------
4           Hal Jordan                                         UPDATE

tony_stark                              still_tony_stark                        current_identity_value
--------------------------------------- --------------------------------------- ---------------------------------------
5                                       5                                       5

Duplicate key was ignored.
dupe_id     name
----------- --------------------------------------------------
7           Barry Allen
1           Peter Parker

dupe_id     name
----------- --------------------------------------------------
0           Peter Parker
0           Barry Allen

Friday, June 24, 2011

Making dates out of SQL Agent history

I started this post to simply have a reference piece of code on how to combine the run_date and run_time values from the msdb.dbo.sysjobhistory. I could have saved a great deal of time had I known to look at Working With SQL Server Agent Dates/Times (but of course there wouldn't have been the satisfaction of having written this code) The problem is that SQL Server stores dates and times as integer values (YYYYMMDD and HHMMSS) respectively. There isn't a method for concatenating those values into something and casting/converting it into a datetime value. Except there is and it's called msdb.dbo.agent_datetime(@date, @time). My solution was to create a jobhistory view with my code inline while MS provided a scalar function to do the same. An inline TVF might provide better performance but really with the tsql string mashing performance isn't going to be good either way.

CREATE VIEW 
    dbo.SysJobHistory2
AS

SELECT
    JH.*
,   CAST((CAST(JH.run_date / 10000 AS CHAR(4)) + '-' 
    + RIGHT('0' + CAST(((JH.run_date/100) % 100) AS varchar(2)), 2) + '-'
    + RIGHT('0' + CAST(JH.run_date % 100 AS varchar(2)), 2) + 'T'
    + RIGHT('0' + CAST(JH.run_time/10000 AS varchar(2)), 2) + ':' 
    + RIGHT('0' + CAST(((JH.run_time/100) % 100) AS varchar(2)), 2) + ':'
    + RIGHT('0' + CAST(JH.run_time % 100 AS varchar(2)), 2)) AS datetime) AS run_datetime
FROM
    dbo.sysjobhistory JH

Sunday, August 22, 2010

SQL Server 2005/2008 what's new, part 7

SQL Saturday 53, takes place in 41 days. I will presenting 45 new TSQL features in 45 minutes which will be a purely demo driven presentation designed to give the audience a taste of what's out there and serve a springboard for them to explore on their own. Sign up now for this great opportunity for free SQL Server training in Kansas City, MO.

TableSample

Tablesample is a hint to SQL Server to just grab some rows. Which rows? If you're asking "which rows", tablesample isn't for you. No, tablesample is a hint to SQL Server that you're in a lascivious mood and you just want some data now. It's clean data, no dirty reads but you might get some really odd, inconsistent results if you try to do things like apply filtering. Best use I've found so far is quasi-psuedo-random sampling of data. It's not random enough for true random so don't treat it as such but if you just want a drink of the data in the table, it's probably "good enough," assuming you remember the syntax.

The TABLESAMPLE clause is a different sort of animal. Conceptually, one may think it's nothing more than an ugly synonym for TOP or LIMIT, if you're using a different RDBMS vendor. However, from a query processing perspective, TOP can't run until after the query has been run through the wringer which for large tables might negate any performance savings. At least based on my chapter 1 understanding of logical query processing. It's an odd duck because it either includes a page of information or it doesn't. The query plan will always indicate it's a table scan but it will only read from disk if the selected pages, something something. At this point, read the BOL article above because we are unfortunately way beyond my SQL Server knowledge zone.

Calls to tablesample can either be deterministic or non-deterministic based on whether the REPEATABLE option is used. Activity in the database that affects data pages may also affect the deterministic-ness of your REPEATABLE option. These activities include "inserting, updating, deleting, index rebuilding, index defragmenting, restoring a database, and attaching a database" Consult your DBA if you experience eye watering, mouth watering, flower watering or if your queries last for more than 4 hours.

Syntax

TABLESAMPLE is a clause in your FROM statement. From BOL, the syntax is TABLESAMPLE [SYSTEM] (sample_number [ PERCENT | ROWS ])[REPEATABLE (repeat_seed)]. As tablesample requires real tables, none of the jiggered up examples I have used thus far will work. Rather than forcing you to have adventureworks installed, I figured querying against master.dbo.spt_values seemed a safe table to assume will exist on your machine. What I wasn't ready for, was seeing the caveat about pages of data and maybe you won't get anything back. The other surprise was just how variable "10 rows" can be in result sets. In the handful of times I reran, I saw sets ranging from empty, to 47 to 149. Changing it from rows to percent didn't really dial back the variability like I'd have thought based on this statement in BOL "When a number of rows is specified, instead of a percentage based on the total number of rows in the table, that number is converted into a percentage of the rows and, therefore, pages that should be returned. The TABLESAMPLE operation is then performed with that computed percentage." I still saw some pretty big swings from 0 to 61 to 358 rows.
SELECT COUNT(1) rc FROM master.dbo.spt_values T
rc
2508
SELECT * FROM master.dbo.spt_values T tablesample (10 rows)

Invocation 1

namenumbertypelowhighstatus

Invocation 2

namenumbertypelowhighstatus
ROUTE21586EOBNULLNULL0
STATISTICS21587EOBNULLNULL0
ASYMMETRIC KEY USER21825EOBNULLNULL0
CERTIFICATE USER21827EOBNULLNULL0
GROUP USER21831EOBNULLNULL0
SQL USER21843EOBNULLNULL0
WINDOWS USER21847EOBNULLNULL0
SERVICE22099EOBNULLNULL0
INDEX22601EOBNULLNULL0
LOGIN22604EOBNULLNULL0
XML SCHEMA COLLECTION22611EOBNULLNULL0
TYPE22868EOBNULLNULL0
SERVER AUDIT8257EODNULLNULL0
CHECK CONSTRAINT8259EODNULLNULL0
DEFAULT8260EODNULLNULL0
FOREIGN KEY CONSTRAINT8262EODNULLNULL0
STORED PROCEDURE8272EODNULLNULL0
RULE8274EODNULLNULL0
TABLE SYSTEM8275EODNULLNULL0
TRIGGER SERVER8276EODNULLNULL0
TABLE8277EODNULLNULL0
VIEW8278EODNULLNULL0
STORED PROCEDURE EXTENDED8280EODNULLNULL0
DATABASE AUDIT SPECIFICATION16708EODNULLNULL0
SERVER AUDIT SPECIFICATION16723EODNULLNULL0
TRIGGER ASSEMBLY16724EODNULLNULL0
DATABASE16964EODNULLNULL0
OBJECT16975EODNULLNULL0
FULLTEXT CATALOG17222EODNULLNULL0
STORED PROCEDURE ASSEMBLY17232EODNULLNULL0
SCHEMA17235EODNULLNULL0
CREDENTIAL17475EODNULLNULL0
EVENT NOTIFICATION SERVER17491EODNULLNULL0
EVENT SESSION17747EODNULLNULL0
AGGREGATE17985EODNULLNULL0
FUNCTION TABLE-VALUED INLINE SQL17993EODNULLNULL0
PARTITION FUNCTION18000EODNULLNULL0
STORED PROCEDURE REPLICATION FILTER18002EODNULLNULL0
FUNCTION TABLE-VALUED SQL18004EODNULLNULL0
RESOURCE GOVERNOR18258EODNULLNULL0
SERVER ROLE18259EODNULLNULL0
WINDOWS GROUP18263EODNULLNULL0
ASYMMETRIC KEY19265EODNULLNULL0
DATABASE ENCRYPTION KEY19268EODNULLNULL0
MASTER KEY19277EODNULLNULL0
PRIMARY KEY19280EODNULLNULL0
SYMMETRIC KEY19283EODNULLNULL0

What to notice

The rows or percentage supplied to TABLESAMPLE has some fairly wild variability based on table width and how it physically got laid onto disk. If you need to provide an upper bound to that variability, use tablesample with TOP. Don't trust REPEATABLE unless you are working against a read-only database.

Availability

SQL Server 2005+

Saturday, August 21, 2010

SQL Server 2005/2008 what's new, part 6

SQL Saturday 53, takes place in 42 days. I will presenting 45 new TSQL features in 45 minutes which will be a purely demo driven presentation designed to give the audience a taste of what's out there and serve a springboard for them to explore on their own. Sign up now for this great opportunity for free SQL Server training in Kansas City, MO.

NTILE()

The NTILE function is the fourth of four windowing functions introduced in SQL Server 2005. NTILE takes a different approach to paritioning data. ROW_NUMBER, RANK and DENSE_RANK will generate variable sized buckets of data based on the partition key(s). NTILE attempts to split the data into equal, fixed size buckets. BOL has a comprehensive page comparing the ranking functions if you want a quick visual reference on their effects.

Syntax

The syntax for NTILE differs slightly from the other window functions. It's NTILE(@BUCKET_COUNT) OVER ([PARTITION BY _] ORDER BY _) , where @BUCKET_COUNT is a positive integer or bigint value. Using a fast number generator calendar, I added a call to NTILE() based on event date to introduce a rank column.
-- Example of using NTILE to split 30 numbers
-- into 10 and 4 equal sized buckets
SELECT
    GN.number
,   NTILE(10) OVER (ORDER BY GN.number) AS thirds
,   NTILE(4) OVER (ORDER BY GN.number) AS quartile
FROM 
    dbo.GenerateNumbers(30) GN
numberthirdsquartile
111
211
311
421
521
621
731
831
932
1042
1142
1242
1352
1452
1552
1662
1763
1863
1973
2073
2173
2283
2383
2484
2594
2694
2794
28104
29104
30104

What to notice

The, backwardsly named, thirds column was evenly partitioned with the NTILE function. As expected, ten buckets were created and each hold 3 sets of numbers (3 * 10 = 30). Quartile column as expected was 4 buckets of 8, or is it 4 buckets of 7? 4 * 8 = 32; 4 * 7 = 28. Neither is the correct answer. In cases where the rows can't be broken out into even buckets, NTILE will fill from the top down so there is at most a 1 count difference between the first and last bucket.

Availability

SQL Server 2005+

Friday, August 20, 2010

SQL Server 2005/2008 what's new, part 5

SQL Saturday 53, takes place in 43 days. I will presenting 45 new TSQL features in 45 minutes which will be a purely demo driven presentation designed to give the audience a taste of what's out there and serve a springboard for them to explore on their own. Sign up now for this great opportunity for free SQL Server training in Kansas City, MO.

DENSE_RANK()

The DENSE_RANK function is the third of four windowing functions introduced in SQL Server 2005. DENSE_RANK is nearly identical to RANK except that in the case of a tie or duplicate values, DENSE_RANK will leave no gaps in numbering.

Syntax

The syntax for DENSE_RANK is identical to RANK(). It's DENSE_RANK() OVER (ORDER BY _) , parenthesis required. Using the upcoming SQL Saturday calendar, I added a call to DENSE_RANK() based on event date to introduce a rank column.
-- This query demonstrates the use of the DENSE_RANK function
-- Notice the ranks for rows with same key (event_date) are 
-- ranked equally and the subsequent rank number leaves
-- no gaps in the numbers.
;
WITH SQL_SATURDAY (event_date, event_name, event_location) AS
(
SELECT CAST('Aug 21, 2010' as datetime), 'SQLSaturday #51', 'Nashville 2010'
UNION ALL SELECT 'Sep 18, 2010', 'SQLSaturday #46', 'Raleigh 2010'
UNION ALL SELECT 'Sep 18, 2010', 'SQLSaturday #50', 'East Iowa 2010'
UNION ALL SELECT 'Sep 18, 2010', 'SQLSaturday #55', 'San Diego 2010'
UNION ALL SELECT 'Sep 25, 2010', 'SQLSaturday #52', 'Colorado 2010'
UNION ALL SELECT 'Oct 2, 2010', 'SQLSaturday #53', 'Kansas City 2010'
UNION ALL SELECT 'Oct 2, 2010', 'SQLSaturday #48', 'Columbia 2010'
UNION ALL SELECT 'Oct 16, 2010', 'SQLSaturday #49', 'Orlando 2010'
UNION ALL SELECT 'Oct 23, 2010', 'SQLSaturday #54', 'Salt Lake City 2010'
UNION ALL SELECT 'Oct 23, 2010', 'SQLSaturday #56', 'Dallas (BI Edition) 2010'
UNION ALL SELECT 'Oct 29, 2010', 'SQLSaturday #58', 'Minnesota 2010'
UNION ALL SELECT 'Nov 20, 2010', 'SQLSaturday #59', 'New York City 2010'
UNION ALL SELECT 'Jan 22, 2011', 'SQLSaturday #45', 'Louisville 2011'
UNION ALL SELECT 'Jan 29, 2011', 'SQLSaturday #57', 'Houston 2011'
UNION ALL SELECT 'Feb 5, 2011', 'SQLSaturday #47', 'Phoenix 2011'
)
SELECT
    SS.* 
,   DENSE_RANK() OVER (ORDER BY SS.event_date ASC) AS zee_dense_rank
FROM 
    SQL_SATURDAY SS
event_dateevent_nameevent_locationzee_dense_rank
2010-08-21SQLSaturday #51Nashville 20101
2010-09-18SQLSaturday #50East Iowa 20102
2010-09-18SQLSaturday #55San Diego 20102
2010-09-18SQLSaturday #46Raleigh 20102
2010-09-25SQLSaturday #52Colorado 20103
2010-10-02SQLSaturday #48Columbia 20104
2010-10-02SQLSaturday #53Kansas City 20104
2010-10-16SQLSaturday #49Orlando 20105
2010-10-23SQLSaturday #56Dallas (BI Edition) 20106
2010-10-23SQLSaturday #54Salt Lake City 20106
2010-10-29SQLSaturday #58Minnesota 20107
2010-11-20SQLSaturday #59New York City 20108
2011-01-22SQLSaturday #45Louisville 20119
2011-01-29SQLSaturday #57Houston 201110
2011-02-05SQLSaturday #47Phoenix 201111

What to notice

I created a new column called "zee_dense_rank" with the invocation of the DENSE_RANK function. Because I ordered it by dates, and we have 3 events all on 2010-09-18, the rank starts at 1 for Nashville represent! East Iowa, San Diego and Raleigh battled it out for supremacy on 2010-09-18 and it was a three way tie at 2 which resulted in Colorado being ranked 3rd versus 5th if RANK had been used.

Availability

SQL Server 2005+

Thursday, August 19, 2010

SQL Server 2005/2008 what's new, part 4

SQL Saturday 53, takes place in 44 days. I will presenting 45 new TSQL features in 45 minutes which will be a purely demo driven presentation designed to give the audience a taste of what's out there and serve a springboard for them to explore on their own. Sign up now for this great opportunity for free SQL Server training in Kansas City, MO.

RANK()

The RANK function is the second of four windowing functions introduced in SQL Server 2005. RANK is nearly identical to ROW_NUMBER except that in the case of a tie or duplicate values, ROW_NUMBER will select one as coming first. RANK is a New Age, granola loving function that says "you're all winners in my book" and gives all the matches the same value. RANK keeps track of how many ties for Nth place occur and adjusts the next number accordingly.

Syntax

The syntax for RANK is identical to ROW_NUMBER(). It's RANK() OVER (ORDER BY _) , parenthesis required. Using the upcoming SQL Saturday calendar, I added a call to RANK() based on event date to introduce a rank column.
-- This query demonstrates the use of the RANK function
-- Notice the ranks for rows with same key (event_date) are 
-- ranked equally and the subsequent rank number behaves
-- like an identity insert that was rolled back, i.e. gaps
-- in the meaningless identifiers that someone (QA) will question
;
WITH SQL_SATURDAY (event_date, event_name, event_location) AS
(
SELECT CAST('Aug 21, 2010' as datetime), 'SQLSaturday #51', 'Nashville 2010'
UNION ALL SELECT 'Sep 18, 2010', 'SQLSaturday #46', 'Raleigh 2010'
UNION ALL SELECT 'Sep 18, 2010', 'SQLSaturday #50', 'East Iowa 2010'
UNION ALL SELECT 'Sep 18, 2010', 'SQLSaturday #55', 'San Diego 2010'
UNION ALL SELECT 'Sep 25, 2010', 'SQLSaturday #52', 'Colorado 2010'
UNION ALL SELECT 'Oct 2, 2010', 'SQLSaturday #53', 'Kansas City 2010'
UNION ALL SELECT 'Oct 2, 2010', 'SQLSaturday #48', 'Columbia 2010'
UNION ALL SELECT 'Oct 16, 2010', 'SQLSaturday #49', 'Orlando 2010'
UNION ALL SELECT 'Oct 23, 2010', 'SQLSaturday #54', 'Salt Lake City 2010'
UNION ALL SELECT 'Oct 23, 2010', 'SQLSaturday #56', 'Dallas (BI Edition) 2010'
UNION ALL SELECT 'Oct 29, 2010', 'SQLSaturday #58', 'Minnesota 2010'
UNION ALL SELECT 'Nov 20, 2010', 'SQLSaturday #59', 'New York City 2010'
UNION ALL SELECT 'Jan 22, 2011', 'SQLSaturday #45', 'Louisville 2011'
UNION ALL SELECT 'Jan 29, 2011', 'SQLSaturday #57', 'Houston 2011'
UNION ALL SELECT 'Feb 5, 2011', 'SQLSaturday #47', 'Phoenix 2011'
)
SELECT
    SS.* 
,   RANK() OVER (ORDER BY SS.event_date ASC) AS zee_rank
FROM 
    SQL_SATURDAY SS
event_dateevent_nameevent_locationzee_rank
2010-08-21SQLSaturday #51Nashville 20101
2010-09-18SQLSaturday #50East Iowa 20102
2010-09-18SQLSaturday #55San Diego 20102
2010-09-18SQLSaturday #46Raleigh 20102
2010-09-25SQLSaturday #52Colorado 20105
2010-10-02SQLSaturday #48Columbia 20106
2010-10-02SQLSaturday #53Kansas City 20106
2010-10-16SQLSaturday #49Orlando 20108
2010-10-23SQLSaturday #56Dallas (BI Edition) 20109
2010-10-23SQLSaturday #54Salt Lake City 20109
2010-10-29SQLSaturday #58Minnesota 201011
2010-11-20SQLSaturday #59New York City 201012
2011-01-22SQLSaturday #45Louisville 201113
2011-01-29SQLSaturday #57Houston 201114
2011-02-05SQLSaturday #47Phoenix 201115

What to notice

I created a new column called "zee_rank" with the invocation of the RANK function. Because I ordered it by dates, and we have 3 events all on 2010-09-18, the rank starts at 1 for Nashville represent! East Iowa, San Diego and Raleigh battled it out for supremacy on 2010-09-18 and it was a three way tie at 2 which resulted in Colorado being ranked 5th.

Availability

SQL Server 2005+

Wednesday, August 18, 2010

SQL Server 2005/2008 what's new, part 3

SQL Saturday 53, takes place in 45 days. I will presenting 45 new TSQL features in 45 minutes which will be a purely demo driven presentation designed to give the audience a taste of what's out there and serve a springboard for them to explore on their own. Sign up now for this great opportunity for free SQL Server training in Kansas City, MO.

ROW_NUMBER()

The ROW_NUMBER function is one of four windowing functions introduced in SQL Server 2005. ROW_NUMBER() is a monotomically increasing function for each partition within a query. That's a fancy way of saying starting at 1, add 1 for every row you encounter. The partition is simply the signal to start counting over.

Syntax

The syntax simple, it's ROW_NUMBER() OVER (ORDER BY _) , parenthesis required. Using the query from the CTE introduction, I added a call to row_number() to introduce a sequential row number column.
-- This query demonstrates the ROW_NUMBER function 
-- along with partitioning
;
WITH BORDER_STATES (state_name, abbreviation) AS
(
    SELECT 'IOWA', 'IA'
    UNION ALL SELECT 'NEBRASKA', 'NE'
    UNION ALL SELECT 'OKLAHOMA', 'OK'
    UNION ALL SELECT 'KANSAS', 'KS'
    UNION ALL SELECT 'ILLINOIS', 'IL'
    UNION ALL SELECT 'KENTUCKY', 'KY'
    UNION ALL SELECT 'TENNESSEE', 'TN'
)
, BEST_STATE AS
(
    SELECT 'MISSOURI' AS state_name, 'MO' AS state_abbreviation
)
, JOINED AS
(
    SELECT M.*, 1 AS state_rank FROM BEST_STATE M
    UNION
    SELECT BS.*, 2 AS state_rank FROM BORDER_STATES BS
)
SELECT J.*, ROW_NUMBER() OVER (PARTITION BY J.state_rank ORDER BY J.state_rank) AS zee_row_number
FROM JOINED J
state_namestate_abbreviationstate_rankzee_row_number
MISSOURIMO11
IOWAIA21
NEBRASKANE22
OKLAHOMAOK23
KANSASKS24
ILLINOISIL25
KENTUCKYKY26
TENNESSEETN27

What to notice

I created a new column called "zee_row_number" with the invocation of the ROW_NUMBER function. I partitioned my results on the state_rank which lead to my data being segmented into two sets, state_rank 1 and 2. Missouri being the only element in its set is assigned zee_row_number of 1. Within the second set, Iowa was selected as the first element of that set. Your mileage may vary. After that, the other 6 rows had their zee_row_number incremented by 1.

Availability

SQL Server 2005+

Tuesday, August 17, 2010

SQL Server 2005/2008 what's new part 2

SQL Saturday 53, takes place in 46 days. I will presenting 45 new TSQL features in 45 minutes which will be a purely demo driven presentation designed to give the audience a taste of what's out there and serve a springboard for them to explore on their own. Sign up now for this great opportunity for free SQL Server training in Kansas City, MO.

Recursive Common Table Expressions

Recursive CTEs are like a normal Common Table Expressions, just with a little something extra to them. By default, you can have 100 levels of recursion before it chokes. Vast improvement over the 32 levels of default recursion allowed with procedure, function, trigger or nested views.

Syntax

The syntax is quite simple, see books online for the BNF. The following contrived queries count down from 100 and 101 to 0, endpoints inclusive.
-- This query demonstrates recursion by counting down from 100 to 0
;
WITH BASE AS
(
    -- Anchor query
    SELECT 100 AS anchor
    
    -- recursive query
    -- notice that I can reference BASE
    UNION ALL
    SELECT B.anchor -1 FROM BASE B WHERE B.anchor > 0
    
)
SELECT B.* FROM BASE B

-- This query demonstrates recursion by counting down from 101 to 0
-- The MAXRECURSION hint allows us to override the default for good or ill
;
WITH BASE AS
(
    -- Anchor query
    SELECT 101 AS anchor
    
    -- recursive query
    -- notice that I can reference BASE
    UNION ALL
    SELECT B.anchor -1 FROM BASE B WHERE B.anchor > 0
    
)
SELECT B.* 
FROM BASE B 
OPTION (MAXRECURSION 101)

What to notice

Recursive queries can be split into their non-recursive or anchor portion and the recursive portion. The anchor query which can be as complex as need be but it must is evaluated before the recursive portion begins. The recursive will execute until it either overflows the stack or returns an empty set a.k.a. meets the terminal condition. The default maximum recursion level for a CTE is 100 frames. The absolute maximum level of CTE recursion is 32,767 . If you're hitting this limit, congratulations. Now, rethink your query.

Availability

SQL Server 2005+

Monday, August 16, 2010

SQL Server 2005/2008 what's new, part 1

SQL Saturday 53, takes place in 47 days. I will be presenting 45 new TSQL features in 45 minutes which will be a purely demo driven presentation designed to give the audience a taste of what's out there and serve a springboard for them to explore on their own. Sign up now for this great opportunity for free SQL Server training in Kansas City, MO.

Common Table Expressions

I like to come out swinging and Common Table Expressions, CTE, introduced with SQL Server 2005 are worth the price of admission! At their simplest, they're nothing more than syntactic sugar but don't dismiss them as such. There is very little you can do with CTEs that you couldn't do with temporary tables, derived tables, table variables and what have you. The biggest difference developing queries using CTEs versus the Tumbling data anti-pattern is that with a Common Table Expression the compiler can have a chance to optimize your query despite being separated into logic sub tables. Think of them as single use tables with no explicit cleanup required.

Syntax

The syntax is quite simple, see books online for the BNF. The following contrived query builds up data to define the states that border Missouri.
;
WITH BORDER_STATES (state_name, abbreviation) AS
(
    SELECT 'IOWA', 'IA'
    UNION ALL SELECT 'NEBRASKA', 'NE'
    UNION ALL SELECT 'OKLAHOMA', 'OK'
    UNION ALL SELECT 'KANSAS', 'KS'
    UNION ALL SELECT 'ILLINOIS', 'IL'
    UNION ALL SELECT 'KENTUCKY', 'KY'
    UNION ALL SELECT 'TENNESSEE', 'TN'
)
, BEST_STATE AS
(
    SELECT 'MISSOURI' AS state_name, 'MO' AS state_abbreviation
)
, JOINED AS
(
    SELECT M.*, 1 AS state_rank FROM BEST_STATE M
    UNION
    SELECT BS.*, 2 AS state_rank FROM BORDER_STATES BS
)
SELECT * FROM JOINED

What to notice

As a helpful suggestion, preface all your CTEs with a semi-colon on the preceding line. If they are the only statement in a batch, then it doesn't matter. Otherwise, you'll encounter "Incorrect syntax near the keyword 'with'. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon." BORDER_STATES is an in memory table containing the 8 states that directly border Missouri. Notice that I explicitly specify the column names when I defined the table. BEST_STATE is also an in memory table but here I chose to define the column names within the query. JOINED shows I can reference the tables immediately after defining them. Finally, I reference JOINED from outside the CTE by grabbing all of the data.

What's better, it depends. Besides that hoary chestnut, I find myself defining columns if I am building up some static data values like I do in BORDER_STATES. Otherwise, I typically just alias my columns as I query them like I do with BEST_STATE.

Finally, you should notice that the use of SELECT * leads to puppies being slaughtered. I read it on a blog so it must be true.

Availability

SQL Server 2005+

Monday, April 26, 2010

SSIS Derived Column Transformation Editor, a turn for the worse in 2008

This year my company is finally moving forward with SQL Server 2008 and my current project is the first to use the 2008 SSIS engine.  In general, I haven't had that much of a geekgasm over it.  I made my peace with VB in the scripting components a long time ago so C# wasn't that big of an upsell.  This post however, is about a deficit in the SSIS Derived Column Transformation Editor in the 2008 world.  I might have also stumbled onto a bug which is exciting.  

Given the following expression, 
0 == 0 ? (DT_STR,3,1252)"pre" : (DT_STR,3,1252)"pst"
one would expect that to have a data type of non-unicode string (DT_STR) due to the explicit cast.  However, that is not the case, plug that expression into either 2005 or 2008 and it'll set the data type as Unicode string (DT_WSTR).  In 2005, you still had the option of "fixing" data types but that feature has been removed in 2008.  See attached screen shots.
The solution with much thanks to @VidasM is to cast the results of the ternary operator to string. I had tried that but operator precedence threw me off.
(DT_STR,3,1252)(0 == 0 ? "pre" : "pst"))

Thursday, January 7, 2010

Instead Of

I learned something nifty today about triggers. @dyfhid had a scenario where they wanted to make a unique constraint for only a subset of the data. Not running SQL Server 2008 on the VM I already had spun up, I didn't get to play with the nifty filtered index solutions others proposed. Below is my quick and dirty solution to allow the text to be as duplicated as they want but to keep the serialnum column unique for numeric values.



create table dbo.production3
(
    production3_id int identity(1,1) NOT NULL PRIMARY KEY
,   serialnum varchar(50) NOT NULL
)

GO

INSERT INTO
    dbo.production3
SELECT
    'Hi mom Pack with NO SN' AS serialnum
UNION ALL
SELECT
    'Cactus Pack with NO SN' AS serialnum
UNION ALL
SELECT
    'Cactus Pack with NO SN' AS serialnum
UNION ALL
SELECT
    '01234567890' AS serialnum
UNION ALL
SELECT
    '12345678901' AS serialnum
UNION ALL
SELECT
    'Cactus Pack with NO SN' AS serialnum

GO
-- Return 1 if the item is unique enough
-- return 0 if it's not unique enough (numbers only)
CREATE FUNCTION dbo.MostlyUnique
(
    @serialNum varchar(50)
)
RETURNS bit
BEGIN
    DECLARE @retval bit
    SET @retval = 1
    -- We only care if it looks like a number
    if isnumeric(@serialNum) = 1 AND EXISTS (select serialnum from production3 where SerialNum = @serialnum)
    BEGIN
        SET @retval = 0
    END

    RETURN @retval
END

GO


CREATE TRIGGER DBO.MostlyUniqueTrigger
ON dbo.production3
INSTEAD OF INSERT
AS
BEGIN
    SET NOCOUNT ON
    INSERT INTO
        dbo.production3
    SELECT
        I.serialnum
    FROM
        INSERTED I
    WHERE
        -- this probably sucks for performance
        -- but it keeps the dupes out
        dbo.MostlyUnique(I.serialnum) = 1
END

GO


-- readd the original set, should only see the NO SN stuff

INSERT INTO
    dbo.production3
SELECT
    'Hi mom Pack with NO SN' AS serialnum
UNION ALL
SELECT
    'Cactus Pack with NO SN' AS serialnum
UNION ALL
SELECT
    'Cactus Pack with NO SN' AS serialnum
UNION ALL
SELECT
    '01234567890' AS serialnum
UNION ALL
SELECT
    '12345678901' AS serialnum
UNION ALL
SELECT
    'Cactus Pack with NO SN' AS serialnum
UNION ALL
SELECT
    '23456789012' AS serialnum


SELECT * FROM production3


--production3_id  serialnum
--1   Hi mom Pack with NO SN
--2   Cactus Pack with NO SN
--3   Cactus Pack with NO SN
--4   01234567890
--5   12345678901
--6   Cactus Pack with NO SN
--7   Hi mom Pack with NO SN
--8   Cactus Pack with NO SN
--9   Cactus Pack with NO SN
--10  Cactus Pack with NO SN
--11  23456789012

Alternate solution (there are many):
http://markvsql.com/index.php/2009/05/enforcing-uniqueness-on-a-nullable-column/

References:
http://msdn.microsoft.com/en-us/library/ms189799.aspx

Thursday, November 5, 2009

Fast number generator

Code via Itzik Ben-Gan. This is a very fast function/query to generate sequential numbers in T-SQL/SQL Server 2005+. How fast? Screaming cheetah wheelies fast. Yeah, that fast!

-- Generate sequential numbers super fast
-- Code via Itzik Ben-Gan
-- Reset to jive with my syntax preference
CREATE FUNCTION
    dbo.GenerateNumbers
(
    @n as bigint
)
RETURNS TABLE
RETURN
    WITH L0 AS
    (
        SELECT
            0 AS C
        UNION ALL
        SELECT
            0
    )
    , L1 AS
    (
        SELECT
            0 AS c
        FROM
            L0 AS A
            CROSS JOIN L0 AS B
    )
    , L2 AS
    (
        SELECT
            0 AS c
        FROM
            L1 AS A
            CROSS JOIN L1 AS B
    )
    , L3 AS
    (
        SELECT
            0 AS c
        FROM
            L2 AS A
            CROSS JOIN L2 AS B
    )
    , L4 AS
    (
        SELECT
            0 AS c
        FROM
            L3 AS A
            CROSS JOIN L3 AS B
    )
    , L5 AS
    (
        SELECT
            0 AS c
        FROM
            L4 AS A
            CROSS JOIN L4 AS B
    )
    , NUMS AS
    (
        SELECT
            ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS number
        FROM
            L5
    )
    SELECT top (@n)
        number
    FROM
        NUMS
    ORDER BY
        number
GO

Thursday, October 8, 2009

You got XML in my database

You got your database in my XML! And if you haven't tried the dark chocolate Reese's peanut butter cups, I highly suggest you try them. They're delicious.

In SQL Server 2005, Microsoft introduced the XML datatype and while I'm still unconvinced that it's a needed datatype, it does present some interesting possibilities. I have a Post It note on my work computer that simply says "FOR XML 2009-04-24" We had to provide an XML file to a third party that contained a list of all of our advisors and their reporting hierarchy. I knew there was something in SQL server for handling that type of work and my quick look at BOL turned up the FOR clause you can add to a query to transform the resultset into an XML format. It's really that easy. Take your favorite SELECT statement and at the end, add "FOR XML AUTO" and run it. My query generated
<defaults col1="Hello" last_user="tom thumb"/>
<defaults col1="Hola" last_user=""/>
<defaults col1="Hai" last_user="ANGBANDVM\bfellows"/>

That may or may not be what you are interested in. It was not, the vendor had some rules on how the data needed to be formatted and my coworker wasn't interested in exploring this fun new feature. I am slowly getting around to tearing that Post It down and as I work through the power of FOR and OPENXML, I"ll be writing posts on it. Another coworker has a project where they'll be importing hierarchical-type data (NSCC) and it seems like this might be a good project to work with the XML possibilities of SQL Server as SSIS isn't going to be very practical due to the input file being in a painful format.

Monday, September 21, 2009

Database Snapshots

This post is a work in progress. I will edit once I'm home and not
sending these in via email

In short, I think using database snapshots coupled with unit testing
could help alleviate my challenge with database testing in that it
changes the state of the sucker as you're testing. Transactions are
limited in that the changes only exist within the context of the
current thread. Snapshots seem like they'll allow for a persisted
transaction. Once everything is validated, I can roll it back and
have not altered the state of the database. That's my theory at
least. TODO: see whether identities remain incremented ala
transaction or if it really gets rolled back.

I love TSQL but my coworkers don't share my zeal so I want to simplify
the creation of a database snapshot. I use the database created in an
earlier post, SCHEMAD in this example. If I print out the sql
statement I build up, it looks right. If I copy the output and run
it, it works fine. However, if I have the script execute it, it blows
chunks with the error about not locating it in sysdatabases

-- http://msdn.microsoft.com/en-us/library/ms175876.aspx
DECLARE @sql nvarchar(max)
SELECT
@sql = 'CREATE DATABASE ' + DB.name + '_snapshot ON ' + char(10)
+ '( NAME = ' + MF.name + char(10) + ', FILENAME = ''' +
replace(MF.physical_name, '.mdf', '.snapshot')
+ '''' + char(10)+ ')' + char(10) + ' AS SNAPSHOT OF ' + DB.name
FROM
sys.master_files MF
INNER JOIN
sys.databases DB
ON DB.database_id = MF.database_id
WHERE
MF.data_space_id = 1
AND MF.type_desc = 'ROWS'
-- create filter here to identify what database you want a snapshot of
AND DB.name = 'SCHEMAD'

PRINT @sql

EXECUTE @sql


/*
CREATE DATABASE SCHEMAD_snapshot ON
( NAME = SCHEMAD
, FILENAME = 'C:\Program Files\Microsoft SQL
Server\MSSQL.2\MSSQL\DATA\SCHEMAD.snapshot'
)
AS SNAPSHOT OF SCHEMAD
Msg 911, Level 16, State 1, Line 19
Could not locate entry in sysdatabases for database 'CREATE DATABASE
SCHEMAD_snapshot ON
( NAME = SCHEMAD
, FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL'. No entry
found with that name. Make sure that the name is entered correctly.
*/

Dude, where's my database?

Ultra-brief post today.

If you need to know what physical file(s) are associated with what
databases, then sys.master_files is the view for you.
http://msdn.microsoft.com/en-us/library/ms186782.aspx Plenty of handy
information there but physical_name is where you can determine what
mdf file(s) a database is using and where they are located on the file
system.

Wednesday, September 16, 2009

Database schema

Brief post as I'm tired this evening. In an upcoming project, the users have requested that we stage their edits. Lead dev and myself have had similar experiences where users request to stage their data, wonderfully complex workflow engines are built and after a short period in production, they ask for the big red button to mass approve all changes.


The purpose of tonight's post is to address one of the concepts they are kicking around. Instead of having staged and live data in the same tables with an indicator (flag, publish date, whatever) they were considering having two separate databases with identical schemas to support this. They would create different user accounts for accessing the data to make sure only blessed content is served to the public. Without knowing the details, I can see why separate databases would be considered but I also mentioned that it might be an opportunity to use schemas to logically partition the database. They could still create the different accounts for access but for database backups and restores, especially as developers roll on and off the project, I thought there might be some merit to the schema approach.


The following code will create a sample database. Within that database, it will create a live and stage schema and the same table within each (works). I will load sample data into the stage version and use a stored procedure to simulate the approval of content for the productional site. TSQL is unpolished but it gets the point across.

What's not shown is the creation of user accounts and assignment of accounts/roles to schemas. It's late and unfortunately for me, I am not fluent enough in the security aspects of SQL Server to generate the TSQL without digging in to BOL.


CREATE DATABASE SCHEMAD
go
USE schemad
go
create schema stage
GO
create schema live
GO

CREATE TABLE stage.WORKS
(
works_id int identity(1, 1) NOT NULL PRIMARY KEY
, message_text varchar(max) NOT NULL
, name_last varchar(50) NULL
)
GO
CREATE TABLE live.WORKS
(
works_id int identity(1, 1) NOT NULL PRIMARY KEY
, message_text varchar(max) NOT NULL
, name_last varchar(50) NULL
)
GO
CREATE PROCEDURE
stage.Publish
(
@works_id int
)
AS
BEGIN
SET NOCOUNT ON

INSERT INTO
live.WORKS
(
message_text
, name_last
)
SELECT
W.message_text
, name_last
FROM
stage.WORKS W
WHERE
W.works_id = @works_id

END
GO
-- load up some staging data
INSERT INTO
stage.WORKS
(
message_text
, name_last
)
OUTPUT INSERTED.*
SELECT
'My fingers emit sparks of fire with expectations of my future labors' AS message_text
, 'blake' AS name_last
UNION ALL
SELECT
'The mind can make a heaven out of hell and a hell out of heaven' AS message_text
, 'milton' AS name_last
UNION ALL
SELECT
'If the fool would persist in his folly he would become wise' AS message_text
, 'blake' AS name_last
UNION ALL
SELECT
'Excess of sorrow laughs. Excess of joy weeps' AS message_text
, 'blake' AS name_last

GO
-- publish some blake
EXECUTE stage.publish 1
EXECUTE stage.publish 3

GO
SELECT * FROM stage.works
SELECT * FROM live.works

GO
/*
-- cleanup
drop table live.works
drop table stage.works
drop procedure stage.publish
drop schema stage
drop schema live


*/

Tuesday, August 25, 2009

EXECUTE AS

I ran into a situation yesterday where a third party is developing TSQL objects for the rewrite of our external site.  The DBAs were attempting to provide a minimum of rights for that user in the database.  They could create stored procs but couldn't assign permissions to it or even execute themselves.  We have a ticket open with the DBAs for them to correct that permission issue but until that gets queued up, it was looking like I'd be the one granting them permissions each time they recreated the proc.  This vendor loves to play the "I'd have met this milestone, but you didn't provide me with the appropriate resource."  DBAs are slammed so I became the one to grant permissions every time they sent an email and three times was enough for me to automate it.  The easy part was the cursor to iterate through the procs and assign permissions to both the contractor and the SQL Server login that will connect from IIS.  The equally easy part was creating the proc so that it didn't run with the invoker's credentials.  The EXECUTE AS clause saves the day there http://msdn.microsoft.com/en-us/library/ms188354.aspx
 
The code itself is trivial but the idea of writing methods that run under a different context in a SQL Server world merits some investigation.  TSQL below so in case you were curious.
 


CREATE PROCEDURE dbo.PermissionReset
WITH EXECUTE AS owner
AS
BEGIN
SET NOCOUNT ON

DECLARE Csr CURSOR
FOR
SELECT
T.name
FROM
sys.objects T
WHERE
T.type = 'P'

DECLARE
@name varchar(500)
, @tsql varchar(max)

SET NOCOUNT ON
OPEN Csr
FETCH NEXT FROM Csr INTO
@name
WHILE (@@fetch_status <> -1)
BEGIN
IF (@@fetch_status <> -2)
BEGIN
SELECT @tsql = 'GRANT EXECUTE ON dbo.' + @name + ' TO [DomainWebUser]'
EXEC(@tsql)
SELECT @tsql = 'GRANT EXECUTE ON dbo.' + @name + ' TO [Domain\aUser]'
EXEC(@tsql)
END
FETCH NEXT FROM Csr INTO
@name
END
CLOSE Csr
DEALLOCATE Csr
END
GO
GRANT EXECUTE ON dbo.PermissionReset TO [Domain\aUser]
GO

Wednesday, August 27, 2008

Query to build SSIS qualified path

I had been hemming and hawing on how to best deal with the recursive nature of paths in SQL Server. This query is going to be what I need to build out the path correctly and being a good geek, I share.

TODO: add pictures so people have an idea of what it would look like


;
WITH FOLDER_STRUCTURE AS
(
SELECT
F.folderid
, cast('MSDB/' as nvarchar(max)) As folder_path
FROM
msdb.dbo.sysdtspackagefolders90 F
WHERE
folderid = '00000000-0000-0000-0000-000000000000'
UNION ALL
SELECT
F.folderid
, R.folder_path + F.foldername + '/' As folder_path
FROM
FOLDER_STRUCTURE R
INNER JOIN
msdb.dbo.sysdtspackagefolders90 F
ON F.parentfolderid = R.folderid
)
, PACKAGES AS
(
SELECT
P.name AS package_name
, P.folderid
, P.id as package_id
, P.createdate as package_createdate
, P.vermajor
, P.verminor
, P.verbuild
FROM
msdb.dbo.sysdtspackages90 P
)
SELECT
FS.folder_path + P.package_name
FROM
PACKAGES P
INNER JOIN
FOLDER_STRUCTURE FS
ON FS.folderid = p.folderid

Monday, July 7, 2008

To hook or not to hook

And of course the response is "Hookers are insane, I'll gladly stay in the second row"

I thought SMO was going to hold the answer to how to save objects to SQL Server but that didn't turn out to be the case so all the work I've been doing on the SSISUploader has been based on me hooking the command line thinger and building an object model on top of that. It's been going well, I've gotten in and messed with some stuff I had not touched before in the .NET world. I've got a working concept out there and it seems to be getting some download activity. I've started getting a testing harness wrapped around it so I'll know when I break something trying to build a more coherent pattern around it.

I thought I'd take a night or two off from it and try to scratch a different itch---COBOL copy books. We consume a good deal of data from a mainframe (oh how deliciously wrong it is to be using cutting edge ETL tools on data from a 40+ year old system) and that means I have to spend a good deal of time dealing with fixed-width flat files. Great big pain in my ass when I have a 2000 byte wide file with 300+ columns in it and no headers. I'd built one tool to dummy up data so visually I could see where the columns change over thus


C0 C1 C2 C3
AAAAABBBBBCCCCCDDDDD


It works well enough for what it's doing but it still doesn't spare me the pain of having to transcribe a few hundred field names into the click happy connection manager interface. So I dive into BOL and find out about creating a connection manager dynamically, loading my package from XML which will be nice as I can just read in an existing package and slap a new connection manager into. And then I see some methods I was looking for back before I started hooking dtutil. Look at the object model on Microsoft.SqlServer.Dts.Runtime.Application


Microsoft.SqlServer.Dts.Runtime.Application application = null;
application = new Microsoft.SqlServer.Dts.Runtime.Application();
application.CreateFolderOnDtsServer("\\MSDB", "HeyLookAtMe", "localhost");


I think I'm going to turn in early. Maybe I'll get up and get some miles in before work, less than 60 days 'til the MS 150. Oh well, if I end up gutting the dtutil hook, it will still have provided a functionality that the object model doesn't provide and that is the simulate portion. People would still be able to use my tool to figure out what the commands they can pass in to dtutil from a scripting perspective.

Monday, June 30, 2008

SSISUploader

In the immortal words ofDr. Frankenstein, "It is alive!" My first open source contribution, beyond just monitoring assorted mailing lists---ant, I'm looking at you, was published this evening. SSISUploader is an attempt to take the dtutil tool and make it more command line friendly, at least for SQL Server deploys. It's after midnight and the past week has been like this coding on it so I best turn in but I wanted to let google start linking it in case it'll be of use to someone else.