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

Thursday, 9 July 2009

A faster TSQL random length random string generator

The following function function will return you a random string of the specified characters, for a length of between @StrLenLo and @StrLenHi.  The only ‘Oddity’ with using this function is that if the parameters are not dependant upon data within a table SQLServer will create a hash join which will cause the same value to be returned.  This is the reason for the bizzare looking  ‘case when Num>=0 then 8 else 8 end’

First off generate a ‘numbers’ table

CREATE TABLE dbo.Numbers (Num INT NOT NULL PRIMARY KEY CLUSTERED);
GO
DECLARE
@i INT;
SELECT @i = 1;
WHILE @i <= 1000
BEGIN
INSERT INTO
dbo.Numbers(Num) VALUES (@i);
SELECT @i = @i + 1;
END;
go
update statistics
Numbers with fullscan
go


Then , as SqlServer does not allow the use of newid() within functions create a small view that will return a random integer.



drop View VwNewCheck
go
Create View
VwNewCheck
with schemabinding
as
Select
abs(checksum(NewId())) as New_Id
go


Next up the actual function



Drop Function GetVariableLengthRandomCode
go

Create Function
GetVariableLengthRandomCode(@StrLenLo integer,@StrLenHi integer,@CharsNeeded char(62))
returns table
with schemabinding
as
return
(
with cteRandomLength(StrLen)
as
(
Select @StrLenLo + VwNewCheck.new_id%((@StrLenHi+1)-@StrLenLo)
from dbo.VwNewCheck
),
cteRandomChars(num,c)
as
(
Select Num,substring(@CharsNeeded,(Select VwNewCheck.new_id%(len(@CharsNeeded)-1)+1 from dbo.VwNewCheck where num = num ),1)
from dbo.numbers
where Num <= (Select StrLen from cteRandomLength)

)
select (
select c as [text()]
from cteRandomChars
for xml path('')) as random
)
go




And you are good to go.



select * from GetVariableLengthRandomCode(8,16,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')



As mentioned above if you execute



select  Random from numbers cross apply GetVariableLengthRandomCode(8,16,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')


Then the same value will be returned a thousand times,  so fool the optimizer by



select  Random 
from numbers
cross apply GetVariableLengthRandomCode(case when Num>=0 then 8 else 8 end,16,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')


and you will get a thousand random strings between 8 and 16 characters long.

Friday, 29 May 2009

Optimal Query Plans With Unbalanced Data Loads

Sql Server is great at producing and caching efficient query plans.  However these plans may not be the most efficient based upon the parameters passed into subsequent calls off a stored procedure.

In this article I am assuming that you have read both of Erland Sommarskog’s articles The Curse and Blessings of Dynamic SQL and Dynamic Search Conditions in T-SQL.

First off,  in AdventureWorks we need to create a few indexes help help prove the point.

Create Index IdxLastName on person.Contact(LastName)
Create Index IdxContactId on sales.SalesOrderHeader(ContactID)
Create Index IdxOrderDate on sales.SalesOrderHeader(OrderDate)

Then if you execute :


Select * from sales.SalesOrderHeader  SOH ,
person.Contact CON
where SOH.ContactID = CON.ContactId
and Con.LastName = 'Smith'
and SOH.OrderDate between '2002-09-01' and '2002-09-02'

and


Select * from sales.SalesOrderHeader  SOH ,
person.Contact CON
where SOH.ContactID = CON.ContactId
and Con.LastName = 'Smith'
and SOH.OrderDate between '2002-09-01' and '2005-09-02'

You will notice the stark, and unsurprising,  difference in query plans.  In the first query its quicker to find all those order made on the 1st or 2nd of September  2002 and filter that list for those made by ‘Smith’s.  In the second it has found the orders made by ‘Smiths’ and filter those order for those made between 01 September 2002 and 02 September 2005.


Naturally if we did :


declare @LastName nvarchar(50),
@OrderDateLo smalldateTime,
@OrderDateHi smalldatetime

select
@LastName = 'Smith'
select @OrderDateLo = '2002-09-01'
select @OrderDateHi = '2002-09-01'
Select * from sales.SalesOrderHeader SOH ,
person.Contact CON
where SOH.ContactID = CON.ContactId
and Con.LastName = @LastName
and SOH.OrderDate between @OrderDateLo and @OrderDateHi

followed by


declare @LastName nvarchar(50),
@OrderDateLo smalldateTime,
@OrderDateHi smalldatetime

select
@LastName = 'Smith'
select @OrderDateLo = '2002-09-01'
select @OrderDateHi = '2005-09-01'
Select * from sales.SalesOrderHeader SOH ,
person.Contact CON
where SOH.ContactID = CON.ContactId
and Con.LastName = @LastName
and SOH.OrderDate between @OrderDateLo and @OrderDateHi

Then SQL Server will use the same (non optimal in the second case) cached query plan.


This could have quite an impact on your system.  We can , although not completely, resolve this inefficiency with one or two little tricks.   When deciding if to re-use a cached plan or create a new one SqlServer compares the entire SQL Statement , comments and all.  This means that :


Select * from person.Contact CON Where Con.LastName = 'Smith' /* 1 */

and


Select * from person.Contact CON Where Con.LastName = 'Smith' /* 2 */

will have different cache plans. Verify this yourself by checking sys.dm_exec_cached_plans.  We can use this to our advantage by generating comments based upon the input data to create different query plans.


Using a simplified search_orders procedure where we are only interested in search on last name and an order date range such as this :


Create Procedure search_orders @LastName    nvarchar(50) = NULL,
@OrderDateLo smalldateTime =NULL,
@OrderDateHi smalldatetime =NULL
as
Declare
@Sql nvarchar(max)

Select @Sql = 'Select * from sales.SalesOrderHeader SOH , person.Contact CON where SOH.ContactID = CON.ContactId '
if(@LastName is not null) Select @Sql = @Sql +' and CON.LastName = @LastName '
if(@OrderDateLo is not null)Select @Sql = @Sql +' and SOH.OrderDate >= @OrderDateLo '
if(@OrderDateHi is not null) Select @Sql = @Sql +' and SOH.OrderDate <= @OrderDateHi '
declare @DaysDiff integer
Select
@DaysDiff = datediff(dd,coalesce(@OrderDateLo,'2001-07-01 00:00:00.000'),
coalesce(@OrderDateHi,getdate()))
Select @Sql = @Sql + case when @DaysDiff <=2 then ' /* DR:1 */'
when @DaysDiff <=7 then ' /* DR:2 */'
when @DaysDiff <=30 then ' /* DR:3 */'
else ' /* DR:4 */' end

exec
sp_executesql @stmt = @Sql,
@params = N'@LastName nvarchar(50),@OrderDateLo smalldatetime,@OrderDateHi smalldatetime',
@LastName = @LastName,
@OrderDateLo = @OrderDateLo,
@OrderDateHi = @OrderDateHi

Note how a comment is added to the dynamic sql string depending on how many days are to be searched.


If we execute search_orders with varying parameters,  the maximum number of query plans we will end up with is 4.  However they will be optimized(ish) to account for possible date range sizes.    Again check sys.dm_exec_cached_plans to confirm this for yourself.


Taking this one step further if you are querying multiple tables for multiple wide or narrow ranges,  you could use a ratio of the count of resolved rows in each table as the comment.  The con is you would obviously have the extra processing of the counting the rows,  but at least you would not end up using an appalling query plan.

Thursday, 12 March 2009

Direct EMailing of SSRS Reports via SQLCLR

This is something that I was quite surprised that wasn't supported out of the tin with SSRS. When you schedule an email delivery, the report is always sent as an attachment. What would be great, if when you specified the render format as "MHTML", you had the option that the report forms the body of the email. Taking that one step on, how about calling that directly from SQLServer ?

Some of which is to follow has been shamelessly ripped from here

Source Code here
Useage :
exec SSRSMail @retval output,   --1 On Success , 99 on Fail
@ExceptionString output, -- ErrorMessage
@SSRSReport, -- Report name including forward slashes '/Marketing/SalesFigures'
@Params, -- Comma Delimited list of parameters to pass to the report
-- Case Sensitive ie 'SalesMonth = 3 , SalesYear = 2009 , SalesManager = John'
@RecipientList -- Comma Delimited list of emailRecipients ,
@SenderEmail, -- The Sender address. Supports a friendly name ie '<Company Reports>Reports@YourServer.com'
@Subject , -- Email Subject
@CCList, -- Comma Delimited CCList
@BCCList -- Comma Delimited BCCList




To use this create a new "SQL Server Project" within visual studio , and add a web reference to your SSRS Server http://yourserver/ReportServer/ReportExecution2005.asmx?wsdl naming it "ReportExecution".

Inside the source code rename "MailServer" to your mailserver. Please note that as i dont logon to that server that it needs SMTP-relaying enabled. The reason i dont use the system.net.mail namespace is that this routine was originally used within SSIS, but within that you cant specify the sender address. If you dissaprove, feel free to change to using that.

So the routine can consume the webservice you need to serialize the dll using the "sgen.exe" utility. More info on that here

We've been running live with this now for a few weeks , sending out a good few hundred emails a day to our clients , so stability seems good :). Ive had no complaints either about incorrect formatting. If you get any problems with that then let me know.