THE SQL Server Blog Spot on the Web

Welcome to SQLblog.com - The SQL Server blog spot on the web Sign in | |
in Search

Denis Gobo

Speed Up Performance And Slash Your Table Size By 90% By Using Bitwise Logic

You have all seen websites where you can pick a bunch of categories by selection a bunch of check boxes. usually what you do is store those in a lookup table and then you create another table where you store all the categories for each customer.
What if I tell you that you can store all that info in 1 row instead of 10 rows if a customer picked 10 categories.
Take a look at this


1 Classic Rock
2 Hard Rock
4 Speed/Trash Metal
You will store a  value of 1 + 2 + 4 = 7(you just sum the values)

Now run this to check, the result will be 7 for a match and some other value otherwise

select 7 |1,
7 |2,
7 |3,
7 |4,
7 |5,
7 |6,
7 |7,
7 |8,
7 | 20

What is this | (pipe symbol)?
From Books on line
The bitwise | operator performs a bitwise logical OR between the two expressions, taking each corresponding bit for both expressions. The bits in the result are set to 1 if either or both bits (for the current bit being resolved) in the input expressions have a value of 1; if neither bit in the input expressions is 1, the bit in the result is set to 0.
The | bitwise operator requires two expressions, and it can be used on expressions of only the integer data type category.


Here is how you would typically use this, first create this table


CREATE TABLE NumbersTable (Num int)
INSERT NumbersTable VALUES(1)
INSERT NumbersTable VALUES(2)
INSERT NumbersTable VALUES(3)
INSERT NumbersTable VALUES(4)
INSERT NumbersTable VALUES(5)
INSERT NumbersTable VALUES(6)
INSERT NumbersTable VALUES(7)
INSERT NumbersTable VALUES(8)
INSERT NumbersTable VALUES(9)
INSERT NumbersTable VALUES(10)
INSERT NumbersTable VALUES(11)
INSERT NumbersTable VALUES(12)
GO

Now run this

SELECT
Num,
CASE 7 | Num WHEN 7 THEN 'Yes' ELSE 'No' END AS COL
FROM NumbersTable

Here is the output


Num COL
---- ---
1 Yes
2 Yes
3 Yes
4 Yes
5 Yes
6 Yes
7 Yes
8 No
9 No
10 No
11 No
12 No


Okay enough theory let's start with some SQL code. First create this table which will hold all the categories


CREATE TABLE MusicChoice (ID INT PRIMARY KEY,
ChoiceDescription VARCHAR(100))


INSERT MusicChoice VALUES(1,'Classic Rock')
INSERT MusicChoice VALUES(2,'Hard Rock')
INSERT MusicChoice VALUES(3,'Speed/Trash Metal')
INSERT MusicChoice VALUES(4,'Classical')
INSERT MusicChoice VALUES(5,'Rap')
INSERT MusicChoice VALUES(6,'Blues')
INSERT MusicChoice VALUES(7,'Jazz')
INSERT MusicChoice VALUES(8,'Alternative Rock')
INSERT MusicChoice VALUES(9,'Easy Listening')
INSERT MusicChoice VALUES(10,'Progressive Rock')
INSERT MusicChoice VALUES(11,'Punk Rock')
INSERT MusicChoice VALUES(12,'Swing')
INSERT MusicChoice VALUES(13,'Techno')
INSERT MusicChoice VALUES(14,'Pop')
INSERT MusicChoice VALUES(15,'Disco')
INSERT MusicChoice VALUES(16,'Big Band')
INSERT MusicChoice VALUES(17,'Gospel')
INSERT MusicChoice VALUES(18,'Heavy Metal')
INSERT MusicChoice VALUES(19,'House')
INSERT MusicChoice VALUES(20,'Celtic')
Now create the Bitwise table

CREATE
TABLE BitwiseMusicChoice (ID INT PRIMARY KEY,
ChoiceDescription VARCHAR(100))

 

We will use the POWER function to create the correct values
run this

SELECT
id,POWER(2,id-1)BitID,ChoiceDescription
FROM MusicChoice


Here is the output
id BitID  ChoiceDescription
1 1 Classic Rock
2 2 Hard Rock
3 4 Speed/Trash Metal
4 8 Classical
5 16 Rap
6 32 Blues
7 64 Jazz
8 128 Alternative Rock
9 256 Easy Listening
10 512 Progressive  Rock
11 1024 Punk Rock
12 2048 Swing
13 4096 Techno
14 8192 Pop
15 16384 Disco
16 32768 Big Band
17 65536 Gospel
18 131072 Heavy Metal
19 262144 House
20 524288 Celtic


Now insert it into the BitwiseMusicChoice table


INSERT BitwiseMusicChoice
SELECT POWER(2,id-1)BitID,ChoiceDescription
FROM MusicChoice


Now create this customer table

 
CREATE
TABLE Customer (CustomerID int identity, CustomerCode uniqueidentifier not null)


Insert these 5 values first, we will use these to compare performance later


INSERT Customer VALUES('1DAB5C03-BC23-4FB5-AC3D-A46489459FE9')
INSERT Customer VALUES('F7DDCDBC-F646-493A-B872-4E2E82EA8E14')
INSERT Customer VALUES('E8A4C3D2-AEB0-4821-A49D-3BF085354448')
INSERT Customer VALUES('52581088-C427-4D2F-A782-250564D44D8C')
INSERT Customer VALUES('1B2622C4-6C17-4E74-99D6-336197FBBCFF')

Now we will insert a total of 10000 customers


SET NOCOUNT ON
BEGIN
TRAN
DECLARE
@LoopCounter INT
SET
@LoopCounter = 6
WHILE @LoopCounter <= 10000
BEGIN
INSERT
Customer VALUES(NEWID())
SET @LoopCounter = @LoopCounter + 1
END
COMMIT
WORK
GO


Now add the primary key


ALTER TABLE Customer ADD CONSTRAINT pk_Customer PRIMARY KEY (CustomerCode)

Create another table to hold the choices

CREATE
TABLE CustomerMusicChoice (id INT identity, MusicChoiceID int, CustomerCode uniqueidentifier)


ALTER TABLE CustomerMusicChoice ADD CONSTRAINT fk_MusicChoice_ID FOREIGN KEY (MusicChoiceID) REFERENCES MusicChoice(ID)


ALTER TABLE CustomerMusicChoice ADD CONSTRAINT fk_CustomerCode FOREIGN KEY (CustomerCode)REFERENCES Customer(CustomerCode)


For each customer insert 10 random choices, this should run less than a minute
 

SET NOCOUNT ON
BEGIN
TRAN
DECLARE
@LoopCounter INT
DECLARE
@CustID uniqueidentifier
SET
@LoopCounter = 1
WHILE @LoopCounter <= 10000
BEGIN
SELECT
@CustID = CustomerCode
FROM Customer
WHERE CustomerID = @LoopCounter
INSERT Customer VALUES(NEWID())
INSERT CustomerMusicChoice(MusicChoiceID,CustomerCode)
SELECT TOP 10 id,@CustID
FROM MusicChoice
ORDER BY NEWID()
SET @LoopCounter = @LoopCounter + 1
END
COMMIT
WORK
GO

Now add these indexes
CREATE INDEX ix_CustomerMusicChoice_Cust On CustomerMusicChoice(CustomerCode)

CREATE INDEX ix_CustomerMusicChoice_ID On CustomerMusicChoice(MusicChoiceID)


Create the BitwiseCustomerMusicChoice which will hold the Bitwise values


CREATE TABLE BitwiseCustomerMusicChoice (id INT identity, MusicChoiceID int, CustomerCode uniqueidentifier not null)


This will populate the BitwiseCustomerMusicChoice table


INSERT INTO BitwiseCustomerMusicChoice
SELECT SUM(POWER(2,MusicChoiceID-1)) as MusicChoiceID,CustomerCode
FROM CustomerMusicChoice
GROUP BY CustomerCode


Add the index and foreign key


ALTER
TABLE BitwiseCustomerMusicChoice ADD CONSTRAINT pk_BitwiseCustomerMusicChoice PRIMARY KEY (CustomerCode)


ALTER TABLE BitwiseCustomerMusicChoice ADD CONSTRAINT fk_BitwiseCustomerCode FOREIGN KEY (CustomerCode)REFERENCES Customer(CustomerCode)

Now let's test performance. Hit CTRL + K (SQL 2000) or CTRL + M (SQL 2005)

These 2 queries will return something like this

 

ID	ChoiceDescription	Picked
8	Alternative Rock	No
16	Big Band		No
6	Blues			No
20	Celtic			No
1	Classic Rock		No
4	Classical		Yes
15	Disco			Yes
9	Easy Listening		Yes
17	Gospel			No
2	Hard Rock		No
18	Heavy Metal		Yes
19	House			Yes
7	Jazz			Yes
14	Pop			Yes
10	Progressive  Rock	Yes
11	Punk Rock		No
5	Rap			No
3	Speed/Trash Metal	Yes
12	Swing			Yes
13	Techno			No


SELECT
mc.ID,ChoiceDescription,CASE WHEN CustomerCode IS NULL THEN 'No' ELSE 'Yes' END Picked
FROM CustomerMusicChoice cmc
RIGHT JOIN MusicChoice mc on cmc.MusicChoiceID = mc.id
AND CustomerCode ='1DAB5C03-BC23-4FB5-AC3D-A46489459FE9'
ORDER BY ChoiceDescription


SELECT bmc.ID,ChoiceDescription,
CASE WHEN bmc.ID | MusicChoiceID =MusicChoiceID THEN 'Yes'
ELSE 'No'
END AS Picked
FROM BitwiseCustomerMusicChoice cmc
CROSS JOIN BitwiseMusicChoice bmc
WHERE CustomerCode ='1DAB5C03-BC23-4FB5-AC3D-A46489459FE9'
ORDER BY ChoiceDescription


Look at the execution plan
67.60% against 32.40% not bad right?

Plan1


Now run this, we will add AND bmc.ID > 0 to both queries. This will change an index scan to an index seek in the bottom query


SELECT mc.ID,ChoiceDescription,CASE WHEN CustomerCode IS NULL THEN 'No' ELSE 'Yes' END Picked
FROM CustomerMusicChoice cmc
RIGHT JOIN MusicChoice mc on cmc.MusicChoiceID = mc.id
AND CustomerCode ='1DAB5C03-BC23-4FB5-AC3D-A46489459FE9'
AND mc.ID > 0
ORDER BY ChoiceDescription


SELECT bmc.ID,ChoiceDescription,
CASE WHEN bmc.ID | MusicChoiceID =MusicChoiceID THEN 'Yes'
ELSE 'No'
END AS Picked
FROM BitwiseCustomerMusicChoice cmc
CROSS JOIN BitwiseMusicChoice bmc
WHERE CustomerCode ='1DAB5C03-BC23-4FB5-AC3D-A46489459FE9'
AND bmc.ID > 0
ORDER BY ChoiceDescription

That improved the performance a little. 82.75% against 17.25%

 

Plan2


Now look at the tables, after running dbcc showcontig you can see that the BitwiseCustomerMusicChoice is about 1/10th the size of the CustomerMusicChoice table which is as expected.


dbcc showcontig ('BitwiseCustomerMusicChoice')
---------------------------------------------------------------------------
DBCC SHOWCONTIG scanning 'BitwiseCustomerMusicChoice' table...
Table: 'BitwiseCustomerMusicChoice' (772197801); index ID: 1, database ID: 26
TABLE level scan performed.
- Pages Scanned................................: 41
- Extents Scanned..............................: 6
- Extent Switches..............................: 5
- Avg. Pages per Extent........................: 6.8
- Scan Density [Best Count:Actual Count].......: 100.00% [6:6]
- Logical Scan Fragmentation ..................: 0.00%
- Extent Scan Fragmentation ...................: 0.00%
- Avg. Bytes Free per Page.....................: 48.0
- Avg. Page Density (full).....................: 99.41%
DBCC execution completed. If DBCC printed error messages, contact your system administrator.


dbcc showcontig ('CustomerMusicChoice')
---------------------------------------------------------------------------
DBCC SHOWCONTIG scanning 'CustomerMusicChoice' table...
Table: 'CustomerMusicChoice' (724197630); index ID: 0, database ID: 26
TABLE level scan performed.
- Pages Scanned................................: 428
- Extents Scanned..............................: 55
- Extent Switches..............................: 54
- Avg. Pages per Extent........................: 7.8
- Scan Density [Best Count:Actual Count].......: 98.18% [54:55]
- Extent Scan Fragmentation ...................: 40.00%
- Avg. Bytes Free per Page.....................: 386.5
- Avg. Page Density (full).....................: 95.22%
DBCC execution completed. If DBCC printed error messages, contact your system administrator.


What happens if you want to get the total count of for example Classical?


SELECT COUNT(*)
FROM CustomerMusicChoice cmc
JOIN MusicChoice mc on cmc.MusicChoiceID = mc.id
WHERE mc.ChoiceDescription ='Classical'
 
SELECT COUNT(*)
FROM BitwiseCustomerMusicChoice cmc
JOIN BitwiseMusicChoice bmc ON bmc.ID | MusicChoiceID =MusicChoiceID
WHERE bmc.ChoiceDescription ='Classical'

Here are execution plans for SQl Server 2000 and 2005

Plan3A

Plan3B 

As you can see SQL Server 2005 has a bigger difference than SQL Server 2000

Now let's look at the overal picture, on a busy system you will have the customer queries running many times an hour/day. The report queries will run maybe a couple a times a day. I think this trade off is perfectly acceptable because overall your system will perform better. Another thing to keep in mind is that instead of 10 inserts you only have to do 1, same with updates, all these little things add up to a lot eventualy.


So as you can see using bitwise logic is a great way to accomplish a couple of things
Reduce table size
Speed up backup and recovery because your table is much smaller
Improve performance


Of course you have to do some testing for yourself because it might not be appropriate for your design. If your system is more of an OLAP than OLTP type of system then don't bother implementing this since it won't help you.
 

Published Tuesday, May 29, 2007 8:16 PM by Denis Gobo

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

Denis Gobo said:

BTW if you run this and turn statistics io on you will get bad performance with the bitwise for reporting only

SELECT COUNT(*)

FROM CustomerMusicChoice cmc

JOIN MusicChoice mc on cmc.MusicChoiceID = mc.id

WHERE mc.ChoiceDescription ='Classical'

SELECT COUNT(*)

FROM BitwiseCustomerMusicChoice cmc

JOIN BitwiseMusicChoice bmc ON bmc.ID | MusicChoiceID =MusicChoiceID

WHERE bmc.ChoiceDescription ='Classical'

Table 'CustomerMusicChoice'. Scan count 1, logical reads 13, physical reads 0, read-ahead reads 0.

Table 'MusicChoice'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0.

Table 'BitwiseMusicChoice'. Scan count 9728, logical reads 19456, physical reads 0, read-ahead reads 0.

Table 'BitwiseCustomerMusicChoice'. Scan count 1, logical reads 42, physical reads 0, read-ahead reads 0.

However if you do this

SELECT COUNT(*)

FROM CustomerMusicChoice cmc

JOIN MusicChoice mc on cmc.MusicChoiceID = mc.id

WHERE mc.ChoiceDescription ='Classical'

--first get the value for classical music

Declare @i int

select @i = id from BitwiseMusicChoice

where ChoiceDescription ='Classical'

--use that id below here

SELECT COUNT(*)

FROM BitwiseCustomerMusicChoice

where @i| MusicChoiceID =MusicChoiceID

and voila much better

Table 'CustomerMusicChoice'. Scan count 1, logical reads 13, physical reads 0, read-ahead reads 0.

Table 'MusicChoice'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0.

Table 'BitwiseMusicChoice'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0.

Table 'BitwiseCustomerMusicChoice'. Scan count 1, logical reads 42, physical reads 0, read-ahead reads 0.

May 30, 2007 1:21 PM
 

Richard Ayotte said:

In MySQL you can use the SET data type to accomplish the same thing in a much simpler way.

mysql> CREATE TABLE myset (col SET('a', 'b', 'c', 'd'));

If you insert the values 'a,d', 'd,a', 'a,d,d', 'a,d,a', and 'd,a,d':

mysql> INSERT INTO myset (col) VALUES

-> ('a,d'), ('d,a'), ('a,d,a'), ('a,d,d'), ('d,a,d');

Query OK, 5 rows affected (0.01 sec)

Records: 5  Duplicates: 0  Warnings: 0

Then all of these values appear as 'a,d' when retrieved:

mysql> SELECT col FROM myset;

+------+

| col  |

+------+

| a,d  |

| a,d  |

| a,d  |

| a,d  |

| a,d  |

+------+

5 rows in set (0.04 sec)

Normally, you search for SET values using the FIND_IN_SET() function or the LIKE operator:

mysql> SELECT * FROM tbl_name WHERE FIND_IN_SET('value',set_col)>0;

mysql> SELECT * FROM tbl_name WHERE set_col LIKE '%value%';

May 31, 2007 6:02 AM
 

Adam Machanic said:

Richard,

Can those sets be indexed?

May 31, 2007 8:50 AM
 

Adam Machanic said:

By the way, is it just me, or do those look more like bags than sets?  {a,d,d} is not a valid set since it has two Ds...

May 31, 2007 8:51 AM
 

Julian Kuiters said:

I like the article; its a great way to have an expandable list of options.

There's no storage advantages over Bit columns.  8 Bit columns are stored in one byte, and multiple columns would reduce complexity.

You'd also be limited by the INT size. You could use a binary column if you want to have more options.

June 1, 2007 7:56 AM
 

Damien Guard said:

Sounds like premature optimization if ever I saw it.

If you have a fixed number of options that won't change then you may as well use bit fields - at least they are less complex to work with.

[)amien

June 1, 2007 9:14 AM
 

Denis Gobo said:

Damian,

If you use bit fields your index selectivity would clearly make the optimizer ignore the index

There are only 2 possible values 1 and 0

June 1, 2007 9:31 AM
 

Robert said:

Seriously, in a team environment, this would cause nothing but confusion, and waste countless amounts of time having to explain how to use bitwise operations in sql server.  Not to mention to the 32 selection limitation.  In a properly indexed database, the difference is negligible and you would never have to do this.

June 1, 2007 10:12 AM
 

Denis Gobo said:

Robert,

Show me one webpage where you have 32 different choices to select?

BTW you can go to 63 if using a bigint ;-)

Also instead of an average of 10 rows returning everytime to the webserver I return one and the webserver does the bitwise calc

This means we save on bandwith and performance since we have many more webservers than database servers

Yes this technique does not make sense for the majority of things out there, I am using it and have improved performance big time over what we had

what we had was a lookup with just 2 columns, nothing changes here really, instead of many rows we now have one row in that table and store the bit value.

Yes reports are slower but those are ran very infrequently

>>waste countless amounts of time having to explain how to use bitwise operations in sql server.

seems very similar to c# to me, how difficult is it to understand that?

result=varA | varB;

June 1, 2007 10:30 AM
 

Adam Machanic said:

While I can't say I love this technique, I do have to take exception to the comment that it would cause "nothing but confusion" in a team environment because you'd have to "explain how to use bitwise operations".  If you have to explain that to your team, they really shouldn't be developing software.

June 1, 2007 11:19 AM
 

Jim Collins said:

Robert said: "Seriously, in a team environment, this would cause nothing but confusion, and waste countless amounts of time having to explain how to use bitwise operations in sql server." He is absolutely correct. This is a maintenance nightmare for a small increase in performance. It is, however, an excellent candidate for the Daily WTF.

June 2, 2007 3:38 PM
 

Ralph D. Wilson II said:

I have often used this technique and there have been mixed results as far as the "nothing but confusion" and "waste countless amounts of time having to explain how to use bitwise operations".  I learned to use this approach when I was working with real-time interrupt driven systems . . . in fact, it is pretty much required that you understand this approach to work in that environment.  I was able to explain this technique to my daughter when she was about 12 and she understood the concept . . . but, then, she was _my_ daughter. ;-)  I have also encountered experienced programmers who persisted in "not getting it".  

Like any tool or hack, this approach is not a universal cure-all nor is it a "silver bulloet"; it is a technique to have in your tool-kit.  I do not use this approach _every_ time I encounter something that _could_ use it . . . however, I usually _do_ at least evaluate whether this approach might be useful.  

As with any tip/trick/hack/technique, always remember, "Your milage may vary." . . . in other words, just because it worked for me or Denis, that doesn't mean that it will be The Solution in the instance you encounter.

June 4, 2007 9:20 AM
 

Keith said:

It seems to me that this is a nice solution mostly when you don't know what all your flags are going to be at the beginning, moreso than with categories, and you don't want to stack up bool columns in your table each time you add another flag to track on each item.  By using an INT, you can just not use most of the bits, and add a new entry in your "bitwiseFlags" table whenever you come up with a new tracking requirement for your system instead of altering your big tables.

For example, we want to track for a bunch (millions) of business events definitively.  Were they posted to accounting?  Were they posted to the customer?  Did the accounting system confirm receipt of each one?  Did the email confirmation get sent? Were they invoiced?  Were they paid?  This bitwise column seems to be a nicer and more flexible solution than having 8-10 bool columns on that large table.

June 23, 2007 9:00 AM
 

Denis Gobo said:

A year in review, The 21 + 1 best blog posts on SQLBlog Best posts according to me, it might have been

December 27, 2007 4:11 PM
 

S. Neumann said:

Hi Denis,

To be honest, 7 years ago, I used bitwise logic to store some kind of a "status" attribute. It wasn't meant to be used in WHERE clause (not even used for JOINs), only to be selected or updated by the application which showed the status in a graphical way.

However, this is clearly a violation of 1NF, just as a PhoneList column would be, or the use of columns such as Phone1, Phone2, Phone3. You'd have to use a check constraint instead of a FK in order to make sure you only use valid values and if you actually try to use it in a predicate, indexes won't be used (might be scanned rather than seeked through)

Regarding your example - with good indexing, the execution of the first couple of queries returns 54%-46% relative costs (adding the quite useless predicate of id>0 doesn't change that). I would also change the RIGHT JOIN to an INNER JOIN (save some bandwidth on redundant data, let the application figure out which checkboxes NOT to set). Anyway, the absolute costs are tiny anyway, (0.008... - 0.006).

On a side note, Winamp lists 148 music genres in ID3 v1 tags. Each mp3 file can only have a single genre (so 1 byte is enough to keep that), but the users in your schema can choose any of the 148 genres together, so you'd require a varbinary (2^148 bits to be percise) for that.

So yes - putting everything in a single row (+single column) means you execute less INSERTs and in this specific case saves disk space (as this is really compression), but I think that you should have a flashing red text (maybe a siren sound effect in the background too) so people won't be tempted to use this, or they will be hunted down by Joe Celko. :)

Happy holidays!

S. Neumann

December 30, 2007 10:31 AM
 

Sohbet Chat Arkadaşlık said:

Thank you...

May 14, 2009 6:05 PM
 

Sohbet said:

hallo i wish you verry  succes operator

June 2, 2009 7:36 PM
 

Bryan Kowalchuk said:

I have successfully used this method in the following example. I used to work for an aircraft company. Every part on the aircraft is controlled by it's effectivity which is a range of serial numbers from 1 to 999. Effectivity was listed something like:

50 to 75, 88, 123, 555 to 558

Storing the effectivity in a bit field was a very efficient, much more so than other methods.  

September 16, 2009 10:45 AM
 

Steve said:

Ever heard of pragmatism?

How about select * from results where id in (1,2,3,4) and userid = 23456

Even in huge data sets (over 100 million rows) this will work very quickly. Don't use clever logic to do tasks SQL was designed to do - supporting it will turn into a nightmare. Trust me. As for size, if youre stroring 100 million rows, your indexes will usually always be larger than your data sizes.

August 20, 2012 4:20 PM
 

Naomi said:

How you can easily translate the number back? Say, what number 42 would mean - which options are selected?

May 29, 2014 6:08 PM
 

Asen said:

42=32+8+2

...so in tis example:

2 Hard Rock

8 Classical

32 Blues

September 18, 2014 8:23 AM

Leave a Comment

(required) 
(required) 
Submit

About Denis Gobo

I was born in Croatia in 1970, when I was one I moved to Amsterdam (and yes Ajax is THE team in Holland) and finally in 1993 I came to the US. I have lived in New York City for a bunch of years and currently live in Princeton, New Jersey with my wife and 3 kids. I work for Dow Jones as a Database architect in the indexes department, one drawback: since our data goes back all the way to May 1896 I cannot use smalldates ;-( I have been working with SQL server since version 6.5 and compared to all the other bloggers here I am a n00b. Some of you might know me from http://sqlservercode.blogspot.com/ or even from some of the newsgroups where I go by the name Denis the SQL Menace If you are a Tek-Tips user then you might know me by the name SQLDenis, I am one of the guys answering SQL Questions in the SQL Programming forum.

This Blog

Syndication

Powered by Community Server (Commercial Edition), by Telligent Systems
  Privacy Statement