T-SQL: Find Duplicates Without Using JOIN

Filed under: SQL Server — Written by Chrissy on Thursday, May 15th, 2008 @ 12:34 pm

I'd forgotten about this lil trick.

select eUserName from eAttribute GROUP BY eUserName  HAVING COUNT( eUserName) > 1

SQL Server: Change the Owner of All Tables to "dbo"

Filed under: SQL Server — Written by Chrissy on Wednesday, May 7th, 2008 @ 6:32 pm

Reference for me. Reference for you.

sp_MSforeachtable @command1="EXEC sp_changeobjectowner '?','dbo'"

Create a Basic SQL Server 2005 Trigger to Send E-mail Alerts

Filed under: Quick Code, SQL Server — Written by Chrissy on Tuesday, February 5th, 2008 @ 11:07 am

For as many times as I have read about sending e-mails using SQL Server triggers, I've rarely come across actual code samples. After someone asked for a "Triggers for Dummies" example in a Facebook SQL group, I created the following example which uses a trigger to alert a manager that an expensive item has been entered into inventory.

First, if SQL Mail isn't enabled and a profile hasn't been created, we must do so.

Enable SQL Mail/Create Profile

--// First, enable SQL SMail
use master
go
sp_configure 'show advanced options',1
go
reconfigure with override
go
sp_configure 'Database Mail XPs',1
go
reconfigure
go
 
--//Now create the mail profile. CHANGE @email_address,@display_name and @mailserver_name VALUES to support your environment
EXECUTE msdb.dbo.sysmail_add_account_sp
@account_name = 'DBMailAccount',
@email_address = 'sqlserver@domain.com',
@display_name = 'SQL Server Mailer',
@mailserver_name = 'exchangeServer'
 
EXECUTE msdb.dbo.sysmail_add_profile_sp
@profile_name = 'DBMailProfile'
 
EXECUTE msdb.dbo.sysmail_add_profileaccount_sp
@profile_name = 'DBMailProfile',
@account_name = 'DBMailAccount',
@sequence_number = 1 ;

Now that SQL will support sending e-mails, let's create the sample table. This is not a useful or well designed table by any means -- it's just a simple example table:

CREATE TABLE

CREATE TABLE dbo.inventory (
item varchar(50),
price money
)
GO

Now that SQL mail and the table are setup, we will create a trigger that does the following:

  • Creates an AFTER INSERT trigger named expensiveInventoryMailer on the inventory table. This means that the trigger will be executed after the data has been entered.
  • Checks for items being entered that have a price of $1000 or more
  • If there is a match, an email is sent using the SQL Mail profile we used above.

The Actual Trigger

CREATE TRIGGER expensiveInventoryMailer ON dbo.inventory AFTER INSERT AS
 
DECLARE @price money
DECLARE @item varchar(50)
 
SET @price  = (SELECT price FROM inserted)
SET @item = (SELECT item FROM inserted)
 
IF @price >= 1000
  BEGIN
    DECLARE @msg varchar(500)
    SET @msg = 'Expensive item "' + @item + '" entered into inventory at $' + CAST(@price as varchar(10)) + '.'
    --// CHANGE THE VALUE FOR @recipients
    EXEC msdb.dbo.sp_send_dbmail @recipients=N'manager@domain.com', @body= @msg,  @subject = 'SQL Server Trigger Mail', @profile_name = 'DBMailProfile'
  END
GO

The only way to test a trigger is to add actual data, so let's do that here:
insert into inventory (item,price) values ('Vase',100)
insert into inventory (item,price) values ('Oven',1000)

Your email should arrive very quickly. If it doesn't, check the SQL Server mail log in SQL Management Studio by running SELECT * FROM sysmail_allitems.

Have fun!

FIX: SQL Server System Configuration Checker cannot be executed due to WMI configuration (Error:2147500034)

Filed under: SQL Server — Written by Chrissy on Tuesday, January 8th, 2008 @ 1:52 pm

I don't know what on Earth AOL developers do in their AIM 6.x setup but they sure manage to mess things up in Vista. Once AIM is installed, my network detection no longer works and it gives an error that reads: "No network - server execution failed." The problem seems to be some kind of COM error and the fix which has worked for me twice is adding localservice to the Administrator group (Net localgroup Administrators localservice /add).

Today, when attempting to install SQL Server 2005 Express, I kept coming across the following error

The SQL Server System Configuration Checker cannot be executed due to WMI configuration on the machine [computername] Error:2147500034 (0x80004002)

After tons of reboots and WMI resets that didn't work, I found the solution on Chuck Boyer's blog. It was eerily close to the AOL fix: net localgroup Administrators "Network Service" /add.

My SQL install then worked properly right away. I was extremely relieved to find that fix because my experience with faulty SQL Server 2005 installs in the past have not been resolvable AND the reason I was reinstalling SQL and AIM is because I had to format my Vista laptop after a beta Citrix VPN client for Vista completely killed my ability to VPN to any network using any protocol. While I was uninstalling the client, I blue screened and when I came back and tried to establish a VPN connection using Vista's built in client, I was unable to register my computer on the network. It errored out with: "Error 720: No PPP control protocols configured." After a few hours of trying to fix it, I decided to just wipe Vista and start over. It felt kind of good actually. My computer, especially Firefox, runs a lot smoother now that I don't have so many extensions going. Ultimately, though, I was a bit tired and just wanted all of my installs to go well.

Anyway, it took a long time for me to find the solution on Chuck's blog so hopefully this post will help him get some positive PankRanking.

Edit: Already I see a few people have found my site while looking for a solution. Did the above work for you, too?

SQL Server DTS: Dynamically Change Source and Destination in Transform Data Task

Filed under: SQL Server — Written by Chrissy on Friday, August 24th, 2007 @ 6:32 am

I really wish I had more experience with DTS and SSIS (Replication, Clustering, Hardcore HA too), especially since I've been a DBA for 8 years. I bought the Wrox book "Professional SQL Server 2000 DTS" years ago hoping that one day I'd be making 50-step DTS packages but I never had the need. Today, I finished a whopping 5-step package. Creating it took a few hours more than I expected because of an issue with the Data Task not appearing to execute within a loop.

Here's the scenario:

I need to export data from an application database that resides in each of our 6 offices. The query is the same for each city and pretty much the only things that change are the filenames (ex. cityabbr-prepdata.csv) and the names of the database servers (ex. cityabbr-SQL).

At first, I created a package using the Import and Export Wizard and I was saving them to each server, then I felt silly. There had to be a better way. I knew I needed to loop through the package a few times and after searching, I found this super helpful page at SQLDTS.com: Looping, Importing and Archiving.

So I started with the base package created by the Import and Export Wizard.

Then I renamed the two connections. Since the DTS Editor doesn't directly allow renaming of the package, I had to go to the Properties of each connection, select New Connection -> [Enter New Name] -> OK -> OK.

Next, I added three new ActiveX Script Tasks. One set the global variables, one changed the data sources (source sql server, destination text file) and one created the loop. The code for each was as
follows:

Set Globals Script

'**********************************************************************
'  Set Globals Script
'**********************************************************************
Function Main()
  DTSGlobalVariables("strCities").Value = "SD,SF,LA,DM,IR,RS"  'String because I'm not sure if GlobalVariables can do arrays
  arrCity = split(DTSGlobalVariables("strCities").Value,",") ' We will use a split to turn string into array
  DTSGlobalVariables("looptimes").Value =  ubound(arrCity) 'How many times to loop?
 
  DTSGlobalVariables("counter").Value = 0 'Since arrays start at 0, start counter at 0
 
  Main = DTSTaskExecResult_Success
End Function

Set City Script

'**********************************************************************
'  Set City Script
'**********************************************************************
Function Main()
  arrCity = split(DTSGlobalVariables("strCities").Value,",")
  i = DTSGlobalVariables("counter").Value
 
  'Change Source SQL Server
  Set objSQLConn = DTSGlobalVariables.Parent.Connections("Source SQL Server")
  objSQLConn.DataSource = arrCity(i) & "-SQL"
  Set objSQLConn = Nothing
 
  'Change Destination Text File
  Set objTextConn = DTSGlobalVariables.Parent.Connections("Destination Text File")
  objTextConn.DataSource = "C:\sqlexports" & arrCity(i) & "-prepdata.txt"
  Set objTextConn = Nothing
 
  Main = DTSTaskExecResult_Success
End Function

Create Loop (back to City Script)

'**********************************************************************
'  Create Loop (by resetting status of Set City Script)
'  Check name of Set City Script by Right Clicking on Set City Script
'  -> Workflow -> Workflow Properties -> Options -> Name
'**********************************************************************
Function Main()
  strNameOfTask = "DTSStep_DTSActiveScriptTask_2"
 
  If DTSGlobalVariables("counter").Value < DTSGlobalVariables("looptimes").Value Then
    Set objPackage = DTSGlobalVariables.Parent
    objPackage.Steps(strNameOfTask).ExecutionStatus = DTSStepExecStat_Waiting
  End if
 
  DTSGlobalVariables("counter").Value = DTSGlobalVariables("counter").Value + 1
 
  Main = DTSTaskExecResult_Success
End Function

It took me forever to figure out the DTS name of the "Set City" ActiveX Script. I included the instructions within the Create Loop script but feel it's worth repeating. To get the name of a DTS ActiveX script for use with DTSGlobalVariables.Parent, right click on ActiveX Script -> Workflow -> Workflow Properties -> Options -> Name. The description (what we see as the name) won't suffice.

So what problem did I spend hours trying to figure out? Well, when the package was executed, it only wrote C:\sqlexports\sd-prepdata.csv. I debugged it a bit and found that it was definintely looping, and the data sources were changing, and the Transform Data Task was executing (as confirmed by a Msgbox successfully invoked in an ActiveX script within the tasks' workflow) yet only one file was being output(ted).

After poking around for awhile, I tried checking the "Close connection on completion" box under the task's workflow option and sure enough, that's what worked.

Yay, now to treat myself to a piping hot lunch.

Do My SQL Statements Conform to the SQL Standard?

Filed under: SQL Server, Tech Stuff — Written by Chrissy on Tuesday, July 10th, 2007 @ 12:51 pm

While researching for a paper I have to write in class about hierarchical data in relational databases, I was reading Joe Celko's book Trees and Hierarchies in SQL for Smarties. In the intro, he mentions a really useful website for validating SQL-92, SQL-99 and SQL-2003. The site is nice enough but could really benefit from a bit of AJAX. If only I had the code behind the scenes, that'd be a perfect project for learning ASP.NET's AJAX add-on.

T-SQL Equivalent of VBScript's FormatDateTime Function

Filed under: Quick Code, SQL Server, VBScript — Written by Chrissy on Tuesday, July 3rd, 2007 @ 8:17 am

Looking for the T-SQL (somewhat) equivalent to VBScript's FormatDateTime function? I've been too, for years. I finally found it within the CONVERT() function. As stated in SQL Server Books Online:

In CONVERT ( data_type [ ( length ) ] , expression [ , style ] ), style is the style of the date format used to convert datetime or smalldatetime data to character data (nchar, nvarchar, char, varchar, nchar, or nvarchar data types); or the string format used to convert float, real, money, or smallmoney data to character data (nchar, nvarchar, char, varchar, nchar, or nvarchar data types). When style is NULL, the result returned is also NULL.

Manuj Bahl wrote a nice article covering date and time manipulation in SQL Server 2000 and it in, he summarized BOL's table that lists the different style types. It went something like this:

Style ID Style Type
0 or 100 mon dd yyyy hh:miam (or pm)
101 mm/dd/yy
102 yy.mm.dd
103 dd/mm/yy
104 dd.mm.yy
105 dd-mm-yy
106 dd mon yy
107 mon dd, yy
108 hh:mm:ss
9 or 109 mon dd yyyy hh:mi:ss:mmmam (or pm)
110 mm-dd-yy
111 yy/mm/dd
112 yymmdd
13 or 113 dd mon yyyy hh:mm:ss:mmm(24h)
114 hh:mi:ss:mmm(24h)
20 or 120 yyyy-mm-dd hh:mi:ss(24h)
21 or 121 yyyy-mm-dd hh:mi:ss.mmm(24h)
126 yyyy-mm-dd thh:mm:ss.mmm(no spaces)
130 dd mon yyyy hh:mi:ss:mmmam
131 dd/mm/yy hh:mi:ss:mmmam

Try this out with GETDATE() by running the following statement:
SELECT CONVERT(VARCHAR,GETDATE(),7) AS currentdate

Your results should look something like this: Jul 03, 07 Nice! Have fun :)

T-SQL: Convert Seconds to Datetime / Get Total Seconds from Datetime Field

Filed under: Quick Code, SQL Server — Written by Chrissy on Tuesday, July 3rd, 2007 @ 8:06 am

For my final project in my database class at USF, I chose to create a database for a small record label looking to sell albums online as well as provide artist bios, tour information and news. The database, which was scaled down version of a project I worked on in the past, looked something like this:

The professor asked us to compose a few sample queries and, while looking at the songs table, I wondered if there was a way that I could use the information in the length column to determine how long each album ran. The song length column was created as datatype datetime for this very reason. Since SQL Server does't support just a time column, I used the dummy 1900-01-01 date when INSERTING my values. A typical song length would looks like this: 1900-01-01 00:02:11.000.

So I wanted to find the full time length of an album without text parsing to look for ":" and so on. Ultimately, I found this to be possible. Here's the query I came up with:

  SELECT CONVERT(VARCHAR,DATEADD(ss,SUM(DATEPART(HOUR, length)*3600 + DATEPART(MINUTE,length)*60 + DATEPART(SECOND,length)),0),114) AS AlbumLength
  FROM AlbumsTracks Where AlbumID = 1

What you see going on above is the following:
1. Using the DATEPART function to break down the song length into seconds. This is done by grabbing the various parts and multiplying them by 60 for minute or 3600 for hour.
2. Using DATEADD function to add those seconds to the date of "0" (1900-01-01).
3. Using the CONVERT function's ability to change datetime into various formats (kinda like VBScript's FORMATDATETIME), I went with style 114 or hh:mi:ss:mmm (24h))

The above query produced the result: 00:56:02:000. That's a 56 minute and 2 second running time. For more information on CONVERT's ability to manipulate datetime, check out SQL Server Books Online.

VBScript: Forward SQL Server Alerts/Windows Events to (Cingular) SMS

Filed under: SQL Server, VBScript — Written by Chrissy on Friday, May 11th, 2007 @ 6:11 pm

Someone recently asked why I don't use SQL Mail on my SQL Servers (2000 and below). The answer is: I don't want to install Outlook on my SQL Servers. Sure, there are ways around it but I prefer just using "net send" to alert the Operator (moi) then forwarding those messages on to my inbox and phone.

Although my employer offered me a Blackberry, I never really cared for them. The battery life is just too short and I don't like talking into something the size of my hand. So.. I use a regular phone that doesn't receive e-mails easily. It does, however, love to receive texts. Cingular even gave me an email address to go with my phone and this is what I use to Alert me if any of my jobs (Backups, Optimizations, etc) fail.

First, I created a DNS CNAME record to my workstation called DBAWorkstation. Then I created an operator named DBA Workstation. I like to keep things generic in the event that I leave or my workstation name changes. In the event that happens, there's just one change at the DNS level. So next, I set the jobs to alert this operator via net send if any jobs fail. Finally, I set up a script that runs all the time on my workstation which has IIS's SMTP installed locally:

'****************************************************************************
' This script created by Chrissy LeMaire (clemaire@gmail.com)
' Website: http://netnerds.net/
'
' NO WARRANTIES, etc.
'
' This script monitors Windows Events and forwards any Application Popups
' to an email address
'*****************************************************************************
 
' Push Event Viewer Alert
    Set objWMIService = GetObject("winmgmts:{(security)}!root/cimv2")
    Set eventSink = wscript.CreateObject("WbemScripting.SWbemSink", "EVSINK_")
    strWQL = "Select * from __InstanceCreationEvent where TargetInstance isa  'Win32_NTLogEvent' and TargetInstance.SourceName = 'Application Popup' and TargetInstance.EventCode = 26"
    objWMIService.ExecNotificationQueryAsync eventSink,strWQL
 
' Keep it going forever
While (True)
    Wscript.Sleep(1000)
Wend
 
Sub EVSINK_OnObjectReady(objObject, objAsyncContext)
'Here's where I do a whole lot of cleaning. Edit to your liking.
   strCleanedMsg = replace(objObject.TargetInstance.Message,"Application popup: Messenger Service  : ","")
   strCleanedMsg = replace(strCleanedMsg,"Message from ","")
   strCleanedMsg = replace(strCleanedMsg," to " & objObject.TargetInstance.ComputerName,"")
   strCleanedMsg = replace(strCleanedMsg," for DB Maintenance Plan 'DB Maintenance for All DBs'","")
   strCleanedMsg = replace(strCleanedMsg,"JOB RUN:","")
   strCleanedMsg = replace(strCleanedMsg," was run on "," failed around ")
   strCleanedMsg = Left(strCleanedMsg,InStr(strCleanedMsg,"DURATI")-2)
   If Len(strCleanedMsg) > 159 Then strCleanedMsg = Left(strCleanedMsg,159) ' SMS allows max 160 characters. I picked 159 just in case.
 
    Set objCDO = CreateObject("CDO.Message")
    objCDO.From = "SQL Alert <me@myemployer.com>"
    objCDO.To = "0000000000@cingularme.com" ' Phone number goes there. Cingular does the SMS magic
    objCDO.CC = "me@myemployer.com"
    objCDO.TextBody = strCleanedMsg
    objCDO.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 1 'cdoSendUsingPort (1 = local, 2= remote, 3 = Exchange)
    objCDO.Configuration.Fields.Update
    objCDO.Send
    set objCDO = Nothing
End Sub

Run that script as a service and you're set with SMS/E-mail notifications in SQL Server 2000 and below. SQL Server 2005 can do the same thing..but e-mail is much more accessible within the service itself.

MSSQL: Parse Search Engine Querystrings for Search Terms

Filed under: Quick Code, SQL Server — Written by Chrissy on Thursday, February 22nd, 2007 @ 1:15 am

As a compliment to my MaxMind GeoIP Locator scripts and the parse top-level domain from URL function, I've created another function to parse the search term from an URL.

I've heard that T-SQL is a bit slow with parsing strings and wanted to test this myself so I created this in both C# and T-SQL. The difference is very noticeable, indeed: while the original, unoptimized function I wrote in T-SQL took 11 seconds to parse 10,000 referral URLs, the C# version took 4 seconds. The updated, more efficient function takes 6 seconds to run in T-SQL and 3 seconds in C#. Below are two truncated versions of the functions I created. I slimmed them down to make them easier to read on the blog but you can download the full versions here: qscleaner.cs and qscleaner.sql. If you do not have Visual Studio.net, scroll to the bottom of the post -- there I link the qscleaner.dll and give sample CREATE ASSEMBLY syntax.

QSCleaner.cs

/*
Created by Chrissy LeMaire (clemaire@gmail.com)
Website: http://netnerds.net/
 
! - THIS IS THE TRUNCATED VERSION created to keep the blog posting size down. It works but not as
!- precisely. Please download the full version @ http://blog.netnerds.net/code/qscleaner.cs
 
This C# UDF does the following
1. Guesses the search engine's search keyword delimiter
2. Finds the term and extracts it
3. Replaces querystring percentage encoding with its human readable equiv.
 
NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
*/
 
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
 
public partial class UserDefinedFunctions
{
    [Microsoft.SqlServer.Server.SqlFunction]
    public static SqlString QSCleaner(string strURL)
    {
        int ampStart, slashStart, delimiterStart;
        string strQueryString = "";
        string strDelimiter = "";
 
        strURL = strURL.Replace("aq=", "aa=");
        strURL = strURL.Replace("as_q=", "aa=");
        strURL = strURL.Replace("as_oq=", "aa=");
        strURL = strURL.Replace("as_eq=", "aa=");
 
        while (strDelimiter.Length == 0)
        {
 
    // most used search delimiters
         if (strURL.IndexOf("q=") > 0)
        { strDelimiter = "q="; break; }
 
         if (strURL.IndexOf("p=") > 0)
        { strDelimiter = "p="; break; }
 
         if (strURL.IndexOf("query=") > 0)
        { strDelimiter = "="; break; }
 
         if (strURL.IndexOf("search=") > 0)
        { strDelimiter = "search="; break; }
 
         if (strURL.IndexOf("qry=") > 0)
        { strDelimiter = "qry="; break; }
 
         if (strURL.IndexOf("p=") > 0)
        { strDelimiter = "p="; break; }
 
         if (strURL.IndexOf("words=") > 0)
        { strDelimiter = "words="; break; }
 
         if (strURL.IndexOf("word=") > 0)
        { strDelimiter = "word="; break; }
 
         if (strURL.IndexOf("MT=") > 0)
        { strDelimiter = "MT="; break; }
 
         if (strURL.IndexOf("s=") > 0)
        { strDelimiter = "s="; break; }
 
         if (strURL.IndexOf("key=") > 0)
        { strDelimiter = "key="; break; }
        // end most used
 
                  //if by this time it isnt set.. it's not likely a search string
            if (strDelimiter.Length == 0) strDelimiter = "unknown";
        }
 
        if (strURL.IndexOf(strDelimiter) > 0)
        {
            try
            {
                delimiterStart = strURL.IndexOf(strDelimiter) + strDelimiter.Length;
                strQueryString = strURL.Substring(delimiterStart, (strURL.Length - delimiterStart));
                ampStart = strQueryString.IndexOf("&");
                if (ampStart > 0) strQueryString = strQueryString.Substring(0, ampStart);
 
                slashStart = strQueryString.IndexOf("/");
                if (slashStart > 0) strQueryString = strQueryString.Substring(0, slashStart - 1);
 
                // because I can't get System.Web.HttpUtility.UrlDecode to work
                strQueryString = strQueryString.Replace("%20", " ");
                strQueryString = strQueryString.Replace("%21", "!");
                strQueryString = strQueryString.Replace("%22", """);
                strQueryString = strQueryString.Replace("%23", "#");
                strQueryString = strQueryString.Replace("%24", "$");
                strQueryString = strQueryString.Replace("%25", "%");
                strQueryString = strQueryString.Replace("%26", "&");
                strQueryString = strQueryString.Replace("%27", "''");
                strQueryString = strQueryString.Replace("%28", "(");
                strQueryString = strQueryString.Replace("%29", ")");
                strQueryString = strQueryString.Replace("%2A", "*");
                strQueryString = strQueryString.Replace("%2B", "+");
                strQueryString = strQueryString.Replace("%2C", ",");
                strQueryString = strQueryString.Replace("%2D", "-");
                strQueryString = strQueryString.Replace("%2E", ".");
                strQueryString = strQueryString.Replace("%2F", "/");
                strQueryString = strQueryString.Replace("%3A", ":");
                strQueryString = strQueryString.Replace("%3B", ";");
                strQueryString = strQueryString.Replace("%3C", "<");
                strQueryString = strQueryString.Replace("%3D", "=");
                strQueryString = strQueryString.Replace("%3E", ">");
                strQueryString = strQueryString.Replace("%3F", "?");
                strQueryString = strQueryString.Replace("%40", "@");
                strQueryString = strQueryString.Replace("%5B", "[");
                strQueryString = strQueryString.Replace("%5C", "");
                strQueryString = strQueryString.Replace("%5D", "]");
                strQueryString = strQueryString.Replace("%5E", "^");
                strQueryString = strQueryString.Replace("%5F", "_");
                strQueryString = strQueryString.Replace("%60", "`");
                strQueryString = strQueryString.Replace("%7B", "{");
                strQueryString = strQueryString.Replace("%7C", "|");
                strQueryString = strQueryString.Replace("%7D", "}");
                strQueryString = strQueryString.Replace("+", " ");
            }
            catch
            {
                strQueryString = strURL;
            }
        }
        else
            strQueryString = strURL;
 
        return new SqlString(strQueryString);
    }
};

If you don't have access to SQL Server 2005 or SQL 2k5 CLR, you can still use a T-SQL function.

QSCleaner.sql

/*
Created by Chrissy LeMaire (clemaire@gmail.com)
Website: http://netnerds.net/
 
! - THIS IS THE TRUNCATED VERSION created to keep the blog posting size down. It works but not as
!- precisely. Please download the full version @ http://blog.netnerds.net/code/qscleaner.sql
 
This T-SQL Function does the following
1. Guesses the search engine's search keyword delimiter
2. Finds the term and extracts it
3. Replaces querystring percentage encoding with its human readable equiv.
 
NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
*/
 
CREATE FUNCTION [dbo].[QSCleaner] (@strURL varchar(512))
RETURNS varchar(255)
AS
BEGIN
DECLARE @ampStart int
DECLARE @queryStr varchar(512)
DECLARE @slashStart int
DECLARE @strDelimiter varchar(64)
 
--Clean up some stuff that gets in the way of q=
SET @strURL = REPLACE(@strURL, 'aq=', 'aa=')
SET @strURL = REPLACE(@strURL, 'as_q=', 'aa=')
SET @strURL = REPLACE(@strURL, 'as_oq=', 'aa=')
SET @strURL = REPLACE(@strURL, 'as_eq=', 'aa=')
 
--Get Delimiter
WHILE @strDelimiter IS NULL
BEGIN
    -- most used search delimiters
    IF CHARINDEX('q=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'q='
    BREAK
    END
 
    IF CHARINDEX('p=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'p='
    BREAK
    END
 
    IF CHARINDEX('query=',@strURL) > 0
    BEGIN
    SET @strDelimiter = '='
    BREAK
    END
 
    IF CHARINDEX('search=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'search='
    BREAK
    END
 
    IF CHARINDEX('qry=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'qry='
    BREAK
    END
 
    IF CHARINDEX('p=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'p='
    BREAK
    END
 
    IF CHARINDEX('words=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'words='
    BREAK
    END
 
    IF CHARINDEX('word=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'word='
    BREAK
    END
 
    IF CHARINDEX('MT=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'MT='
    BREAK
    END
 
    IF CHARINDEX('s=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 's='
    BREAK
    END
 
    IF CHARINDEX('key=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'key='
    BREAK
    END
    -- end most used
 
IF @strDelimiter IS NULL SET @strDelimiter = ''
END
 
  IF CHARINDEX(@strDelimiter,@strURL) > 0
  BEGIN
  SET  @queryStr = SUBSTRING(@strURL,CHARINDEX(@strDelimiter,@strURL)+LEN(@strDelimiter),LEN(@strURL))
  SET  @ampStart = CHARINDEX('&',@queryStr)
  IF @ampStart > 0 SET @queryStr = LEFT(@queryStr,@ampStart-1)
 
  SET  @slashStart = CHARINDEX('/',@queryStr)
  IF @slashStart > 0 SET @queryStr = LEFT(@queryStr,@slashStart-1)
  SET @queryStr = REPLACE(@queryStr, '%20', ' ')
  SET @queryStr = REPLACE(@queryStr, '%21', '!')
  SET @queryStr = REPLACE(@queryStr, '%22', '"')
  SET @queryStr = REPLACE(@queryStr, '%23', '#')
  SET @queryStr = REPLACE(@queryStr, '%24', '$')
  SET @queryStr = REPLACE(@queryStr, '%25', '%')
  SET @queryStr = REPLACE(@queryStr, '%26', '&')
  SET @queryStr = REPLACE(@queryStr, '%27', '''')
  SET @queryStr = REPLACE(@queryStr, '%28', '(')
  SET @queryStr = REPLACE(@queryStr, '%29', ')')
  SET @queryStr = REPLACE(@queryStr, '%2A', '*')
  SET @queryStr = REPLACE(@queryStr, '%2B', '+')
  SET @queryStr = REPLACE(@queryStr, '%2C', ',')
  SET @queryStr = REPLACE(@queryStr, '%2D', '-')
  SET @queryStr = REPLACE(@queryStr, '%2E', '.')
  SET @queryStr = REPLACE(@queryStr, '%2F', '/')
  SET @queryStr = REPLACE(@queryStr, '%3A', ':')
  SET @queryStr = REPLACE(@queryStr, '%3B', ';')
  SET @queryStr = REPLACE(@queryStr, '%3C', '<')
  SET @queryStr = REPLACE(@queryStr, '%3D', '=')
  SET @queryStr = REPLACE(@queryStr, '%3E', '>')
  SET @queryStr = REPLACE(@queryStr, '%3F', '?')
  SET @queryStr = REPLACE(@queryStr, '%40', '@')
  SET @queryStr = REPLACE(@queryStr, '%5B', '[')
  SET @queryStr = REPLACE(@queryStr, '%5C', '')
  SET @queryStr = REPLACE(@queryStr, '%5D', ']')
  SET @queryStr = REPLACE(@queryStr, '%5E', '^')
  SET @queryStr = REPLACE(@queryStr, '%5F', '_')
  SET @queryStr = REPLACE(@queryStr, '%60', '`')
  SET @queryStr = REPLACE(@queryStr, '%7B', '{')
  SET @queryStr = REPLACE(@queryStr, '%7C', '|')
  SET @queryStr = REPLACE(@queryStr, '%7D', '}')
  SET @queryStr = REPLACE(@queryStr, '+', ' ')
 
  END
if @queryStr is null SET @queryStr = @strURL
RETURN  @queryStr
END

If you do not have VS.NET but still wish to use the C# version of qscleaner, you can download the dll here: qscleaner.dll or you can email me for the SLN file and compile it yourself using msbuild which is included in the .NET 2.0 framework. Anyway, save the dll to the DATA directory that contains your .mdf files (for example: C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data). To "install" the UDF, run the following:

CREATE ASSEMBLY QSCleaner
FROM 'C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data\qscleaner.dll'
WITH PERMISSION_SET = SAFE
GO
 
CREATE FUNCTION dbo.QSCleaner(@strURL nvarchar) RETURNS nvarchar
AS EXTERNAL NAME QSCleaner.UserDefinedFunctions.QSCleaner
GO

To call the function, create a query that's something like this: SELECT TOP 1000 dbo.qscleaner(referer) as keywords FROM hitcounter WHERE referer IS NOT NULL ORDER BY id DESC

Actually, I take it back. The above dll/assembly/function creation doesn't work -- I just receive 1000 h's all the way down. Compiling it via VS.NET works and the T-SQL function works but the manual creation does not. If anyone can shed some light, I'd appreciate it!