Tag hackery

Splitting Strings in SQL

Let’s say that you have a column that contains some goofy data and it looks like this:

USA/Ohio
USA/Indiana
USA/Iowa
UK/Scotland
UK/England

So, how would you typically split such a string to only get the part after the ‘/’?

Well, here’s how I did it:

IF OBJECT_ID('tempdb..#tmp') IS NOT NULL
  DROP TABLE #tmp;
GO

CREATE TABLE #tmp
(
  s VARCHAR(255)
);
GO

INSERT INTO #tmp (s) VALUES
('USA/Ohio'),
('USA/Indiana'),
('USA/Iowa'),
('UK/Scotland'),
('UK/England');

SELECT SUBSTRING(s, CHARINDEX('/', s) + 1, LEN(s)) -- this is what does all the work
FROM #tmp;

But, apparently, there’s a simpler way:

SELECT PARSENAME(REPLACE(a, '/', '.'),1)
FROM #tmp;

It’s a bit hacky, but it works.

Basically, it takes advantage of the PARSENAME function to grab specific sections of a period delimited string, as it it were a SQL Server object.

Now you know. Paypal me some money if you find this useful and pass it off as your own work of genius.

Changing SSMS and Visual Studio Default Project Location

Changing the location of the default projects folder in Visual Studio is super easy to do. Just click the Tools menu, choose Options, and then select the Projects and Solutions tab to change the default projects folder location:

Changing default projects is fun!

Changing default projects is fun!

Sadly, this doesn’t change anything for SSMS. Why? I don’t know.

However, I do know how to change this in SSMS. Well, google told me.

  1. Open up your favorite text editor.
  2. Navigate to My Documents\SQL Server Management Studio\Settings
  3. Open up the .vssettings file in there. On my computer it was CurrentSettings-2009-05-24
  4. Search for the text “ProjectsLocation”
  5. You want to find a section that looks like <PropertyValue name="ProjectsLocation">%vsspv_visualstudio_dir%\Projects</PropertyValue> and go ahead and change the text value of that XML element to whatever you want it to be. (I chose C:\Projects!)
  6. ???
  7. Code

Copying Data Should Be Easy

Typically, moving data isn’t that big of a deal. You can either fire up SSMS and write a quick script to copy data from one server to another. Or, if you need to move data from production into development, you can just do a back up and restore.

“Typically” is the key word in the previous paragraph. In my situation, we have a production master that replicates to two separate servers. The data on each subscriber is different and the determination process is driven by business criteria (actually, it’s driven by a number in a TINYINT column). The publisher is mainly a central target for our ETL processes. As such, it doesn’t have a lot of indexing on it. The subscribers, however, do. Refreshing the development environment is a bit trickier than just restoring over the dev database.

Yeah, sure, destroy. Sounds good.

Yeah, sure, destroy. Sounds good.

Initially, I tried using the Import Data task to copy data across from the production server to the dev server. The trick here is that there were still foreign keys on the table. In case you’re unfamiliar with this feature of the Import and Export Wizard, it gives you the option to “Delete” rows in the destination table. In this case, the word “Delete” actually means “Use TRUNCATE TABLE instead and don’t tell anyone about it”. Well, the fun part is that TRUNCATE TABLE will fail noisily if you have foreign keys in place.

Being an enterprising young man, I thought to myself, I know, I’ll just write a script to write a script that will drop and re-create all of my foreign keys. So, instead of writing the script myself, I twittered and Ted Krueger send back a tweet that led me to this little gem. Don’t mind the sloppy syntax, that script works great. So, after modifying it to give me both the drop and creates, I was ready to go, right?

Not so fast! As we know, every row written to a table also has to be written to all of the non-clustered indexes as well: one table plus four indexes = five writes. This is a large database (just large, not a VLDB). As such, there are a lot of indexes. They are also large. Inserting a large number of rows into a large number of indexes will take, as they say, some time.

Being impatient, and enterprising, I decided to write a script to drop and re-create all of the indexes as well. But, rather than write a script, I just went ahead and found one and modified it to suit my purposes. Actually, I tweeted and Adam Machanic and Mladen Prajdic ended up pointing me toward Tibor Karaszi’s sp_indexinfo – enhanced index information procedure which I was able to easily modify to give me the index drop and create info I needed.

Protip: SQL Server Google hint: Add “vyas” or “tibor” to your searches for DDL scripts and you’ll almost always get a good hit… (Thanks, Adam!)

At this point, everything should be done and our story should be over. You, dear reader, would have falsely assumed that this story has a happy ending.

When I fired up the data import task it would eventually and inevitably fail catastrophically with an error along the lines of “TCP Provider: An existing connection was forcibly closed by the remote host.” We started looking into the actual networking issued before we realized that I was running the export data wizard from my local computer, which shares a hub with several other networked devices. Once I remoted into the development server and attempted the process, I got the following error: “TCP Provider: An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full.”

This error comes from an issue when you are using the /3GB switch on a 32-bit server that might be copying large quantities of data from one place to another. The solution is do make sure that you still have AWE and PAE enabled, remove the /3GB switch, and then reboot!

I’m still waiting for my reboot. I think I should have just done a backup and restore and copied the indexes back in from production. So much for trying to be clever and automate the process, eh?

Oh, here’s the script that drops and re-creates all of the indexes in the current database. It should be, at the very least, a curiosity to someone.

DECLARE @newline AS NVARCHAR(2);

SET @newline = NCHAR(13) + NCHAR(10);

IF OBJECT_ID(N'tempdb..#indexes') IS NOT NULL
  DROP TABLE #indexes;

WITH key_columns AS
(
  SELECT c.[object_id],
         c.name AS column_name,
         ic.key_ordinal,
         ic.is_included_column,
         ic.index_id,
         ic.is_descending_key
    FROM sys.columns AS c
         INNER JOIN sys.index_columns AS ic
            ON c.[object_id] = ic.[object_id]
           AND ic.column_id = c.column_id
),
physical_info AS
(
  SELECT p.[object_id],
         p.index_id,
         ds.name AS location,
         SUM(p.rows) AS rows,
         SUM(a.total_pages) AS pages
    FROM sys.partitions AS p
         INNER JOIN sys.allocation_units AS a
            ON p.hobt_id = a.container_id
         INNER JOIN sys.data_spaces AS ds
            ON a.data_space_id = ds.data_space_id
   GROUP BY [object_id], index_id, ds.name
)
SELECT OBJECT_SCHEMA_NAME(i.[object_id]) AS schema_name,
       OBJECT_NAME(i.[object_id]) AS table_name,
       i.name AS index_name,
       CASE i.type WHEN 0 THEN N'heap'
                   WHEN 1 THEN N'CLUSTERED'
                   WHEN 2 THEN N'nc'
                   WHEN 3 THEN N'xml'
                   ELSE CAST(i.type AS VARCHAR(2)) END AS [type],
       i.is_unique,
       CASE WHEN is_primary_key = 0 AND is_unique_constraint = 0 THEN N'IX'
            WHEN is_primary_key = 1 AND is_unique_constraint = 0 THEN N'PK'
            WHEN is_primary_key = 0 AND is_unique_constraint = 1 THEN N'UX'
       END AS cnstr,
       STUFF((SELECT CAST(', ' + kc.column_name
                          + CASE kc.is_descending_key WHEN 0 THEN ''
                                 ELSE ' DESC' END AS VARCHAR(MAX)
                     ) AS [text()]
                FROM key_columns AS kc
               WHERE i.[object_id] = kc.[object_id]
                 AND i.index_id = kc.index_id
                 AND kc.is_included_column = 0
               ORDER BY key_ordinal FOR XML PATH ( '' )), 1, 2, '') AS key_columns,
       STUFF((SELECT CAST(', ' + column_name AS VARCHAR(MAX)) AS [text()]
                FROM key_columns AS kc
               WHERE i.[object_id] = kc.[object_id]
                 AND i.index_id = kc.index_id
                 AND kc.is_included_column = 1
               ORDER BY key_ordinal FOR XML PATH ( '' )), 1, 2, '') AS included_columns,
       p.location,
       p.rows,
       p.pages,
       CAST((p.pages * 8.00) / 1024 AS DECIMAL(9, 2)) AS MB,
       s.user_seeks,
       s.user_scans,
       s.user_lookups,
       s.user_updates
  INTO #indexes
  FROM sys.indexes AS i
        LEFT OUTER JOIN physical_info AS p
          ON i.[object_id] = p.[object_id]
         AND i.index_id = p.index_id
        LEFT OUTER JOIN sys.dm_db_index_usage_stats AS s
          ON s.[object_id] = i.[object_id]
         AND s.index_id = i.index_id
         AND s.database_id = DB_ID()
 WHERE OBJECTPROPERTY(i.[object_id], N'IsMsShipped') = 0
   AND OBJECTPROPERTY(i.[object_id], N'IsTableFunction') = 0
   AND OBJECT_NAME(i.[object_id]) LIKE N'%'
   AND i.[type] >< 0
 ORDER BY table_name, i.[type], index_name;

SELECT N' DROP INDEX '
       + QUOTENAME(i.index_name)
       + N' ON '
       + QUOTENAME(i.[schema_name])
       + N'.'
       + QUOTENAME(i.table_name)
  FROM #indexes AS i
 WHERE i.cnstr >< 'PK'; 

SELECT N'ALTER TABLE '
       + QUOTENAME(i.[schema_name])
       + N'.'
       + QUOTENAME(i.table_name)
       + N' ADD CONSTRAINT '
       + QUOTENAME(i.index_name)
       + N' PRIMARY KEY CLUSTERED ('
       + i.key_columns
       + N') ON '
       + QUOTENAME(location)
  FROM #indexes AS i
 WHERE i.cnstr = 'PK';

SELECT N'CREATE '
       + CASE WHEN is_unique = 1 THEN N' UNIQUE ' ELSE '' END
       + CASE WHEN type = 'CLUSTERED' THEN N' CLUSTERED INDEX '
              ELSE N' NONCLUSTERED INDEX ' END
       + QUOTENAME(i.index_name)
       + N' ON '
       + QUOTENAME(i.[schema_name])
       + N'.'
       + QUOTENAME(i.table_name)
       + N' ('
       + key_columns
       + N')'
       + CASE WHEN included_columns IS NOT NULL THEN N' INCLUDE (' + included_columns + N')'
              ELSE N'' END
       + N' ON '
       + QUOTENAME(i.location)
  FROM #indexes AS i
 WHERE i.cnstr >< 'PK'
   AND i.location IS NOT NULL;

Check for the Existence of a Temp Table

Do you suffer from the pain of temporary tables hanging around long after you need them? Have you ever found yourself frantically googling, trying to remember the syntax to check for the existence of a temporary table before deleting it?

I, too, used to suffer alongside you, dear readers. I, too, know your pain of re-running a script only to find that a temporary table was skulking around in memory somewhere, waiting to ruin my carefully crafted three-value logic.

Well, my pain is gone thanks to my new found understanding of naming things in tempdb! You too can remove the terrible pain and suffering of searching for temp tables in your code and instead can use this one, that’s right only one, simple step to make sure that your temp tables are cleaned up by the time you need them again!

IF OBJECT_ID('tempdb..#magic') IS NOT NULL
  DROP TABLE #magic;

You’re welcome. Please make all checks payable to ‘cash’.

Fun with PATINDEX() and DB_NAME()

I’m going to guess everyone can guess that DB_NAME() will give you the name of the current database when called with no parameters. If not, now you know.

PATINDEX() is a little bit more fun, so before we get into things, here’s how PATINDEX() works:

DECLARE @my_var AS NVARCHAR(30);
SET @my_var = 'fun with patterns';

SELECT PATINDEX('%pattern%', @my_var);
-- returns 10

PATINDEX() returns the position of the pattern in the search expression. It’s 1 indexed for those of you who come from a 0-based string world.

Moving forward!

So far, I haven’t done anything cunning. But what if you have a crazy situation like this:

You have a publisher database. There are two subscriptions set up with bidirectional transactional replication. They are named Subscriber1 and Subscriber2. They are stored on separated physical servers, but they have identical schemas. Funnily enough, and this is where it gets tricky, there’s another database in the picture. One each server there’s a Web database (WebOne and WebTwo). It does reporting and uses tables in the subscriber. It also holds all the stored procedures. Suddenly things get a lot more entertaining because you now have to fully reference table names.

(Is this confusing enough without pictures?)

What to do?!?

Well, for 99% of of your stored procedures, you can use CTRL+H and ignore this blog post. You probably can ignore this blog post for the other 1%. Unless you’re insane, like me, and generate ad hoc SQL from a CLR assembly. If that’s the case, keep reading.

So, I have a CLR assembly which contains a function. That function emits SQL code which is executed via sp_executesql. Rather than maintain two versions of this assembly, one for each server, I decided to use REPLACE, PATINDEX, and DB_NAME to make my life more fun.

DECLARE @condition AS NVARCHAR(10);
SET @condition = '%Two';

IF (SELECT PATINDEX(@condition, DB_NAME())) > 0
BEGIN
  SELECT @magic = REPLACE(@magic, 'Subscriber1', 'Subscriber2');
END

That’s it. It’s pretty simple, really. This is the part where you remark on how smart I am and send me money via PayPal.

IE7, dropdowns, and z-index

I spent most of yesterday and most of this morning battling with a IE7 display bug.

Back story: we are using jQuery’s superfish drop down menus (a jQuery based implement of suckerfish/son of suckerfish) for site navigation. Everything works great in Firefox 2/3 and Safari. I’m going to assume that IE6 looks great as well since suckerfish is known to work well in IE6.

IE7 is a different story.

There is a large image directly beneath the navigation menu. Unfortunately for me, in IE7 the menu was being rendered beneath the image, despite having a z-index of 1001.

After some careful (and careless) digging and googling, I came across a solution.

To summarize, IE7 will not render the menu with the appropriate z-index unless that z-index is present on the top level :hover style. To clarify:


/*** ESSENTIAL STYLES ***/
.sf-menu, .sf-menu * {
margin: 0;
padding: 0;
list-style: none;
}
.sf-menu {
line-height: 1.0;
}
.sf-menu ul {
position: absolute;
top: -999em;
}
.sf-menu li:hover {
z-index: 100; /* This is the important style directive, without it IE7 chokes */
}
.sf-menu * li:hover {
visibility: inherit; /* fixes IE7 'sticky bug' */
font-weight: bold;
}

IE7 Suckerfish Hover CSS

This site is protected with Urban Giraffe's plugin 'HTML Purified' and Edward Z. Yang's Powered by HTML Purifier. 401 items have been purified.