You are on page 1of 4

Category: Database

Estimated reading time: 10 minutes

=========================================================

Look up values in SQL Server using range joins
by Arthur Fuller
Published: 5/15/06

Takeaway:
Arthur Fuller wants to shake up developers' assumptions that joins are always tests of
equality. He explains that there are lots of ways to join, and demonstrates how you can use
range joins to match a potentially large number of rows.

--------------------------------------------------------------------------------

It's a myth that joins are always tests of equality—they don't have to be. There are uses for
joins based on "less than or equal to" and "greater than or equal to"; I can even think of a
case in which a "not equal to" join would be useful. The point is, there are lots of ways to join,
and range joins is one of them.

SQL Server developers usually use joins to bring together two tables (Listing A) even though
we all know the textbook example of relational multiplication:

SELECT T1.*, T2.ColumnOfInterest

FROM T1, T2

This gives you the number of rows in T1 times the number of rows in T2. It's conceptually
interesting, but how many times have you actually used it?

We tend to think of joins in terms of equality (T1.ColumnName = T2.ColumnName). However,
your tables may include information about such realities as taxation, insurance, and shipping
rates. For instance, if your package weighs less than 1 kilogram, you pay rate x; if it weighs
between 1 and 4 kilograms, then you pay rate y. I call this a range join because it can match a
potentially large number of rows.

You can express the join using the BETWEEN keyword. First, assume two tables (Listing B).
In said database, assume two tables (Listing C). Listing D contains the shipper's rate
schedule. Listing E contains the values of the shipments table. Listing F contains the view
that pulls all of these values together. Listing G contains the values obtained by applying the
view to the tables.

We have a problem, as evinced by the first two rows. A weight of 1 corresponds to two rows
in the ShippingRates table, so we need to refine our boundaries a little. Our mistake was to
define the MinWeight and MaxWeight columns as integers. If we change them to floats and
adjust the MinWeight values slightly (row two becomes 1.01 and row three becomes 4.01),
then the problem is fixed. Listing H contains the new results.

You can use this same technique with almost any data type that might involve a range, e.g.,
salaries for income tax, ages for insurance rates, heights, weights—and use virtually all of
them for demographic analyses.
Listing A
SELECT T1.*, T2.ColumnOfInterest

FROM T1
INNER JOIN T2 ON T1.ColumnName = T2.ColumnName

Listing B
USE [master]
GO
/****** Object: Database [SQL_Tips_Shipping_Rates] Script Date: 05/10/2006 13:37:58 ******/
CREATE DATABASE [SQL_Tips_Shipping_Rates] ON?PRIMARY
(
NAME =N'SQL_Tips_Shipping_Rates', FILENAME
=N'c:\sql\mssql\data\MSSQL\data\SQL_Tips_Shipping_Rates.mdf', SIZE = 1024KB ,
MAXSIZE = UNLIMITED, FILEGROWTH = 10%
)
LOG ON
(
NAME =N'SQL_Tips_Shipping_Rates_log', FILENAME
=N'c:\sql\mssql\data\MSSQL\data\SQL_Tips_Shipping_Rates_log.ldf', SIZE = 1024KB ,
MAXSIZE = UNLIMITED, FILEGROWTH = 10%
)
COLLATE SQL_Latin1_General_CP1_CI_AS
GO

EXEC dbo.sp_dbcmptlevel @dbname=N'SQL_Tips_Shipping_Rates', @new_cmptlevel=80
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET ANSI_NULL_DEFAULT OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET ANSI_NULLS OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET ANSI_PADDING OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET ANSI_WARNINGS OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET ARITHABORT OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET AUTO_CLOSE OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET AUTO_CREATE_STATISTICS ON
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET AUTO_SHRINK OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET AUTO_UPDATE_STATISTICS ON
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET CURSOR_CLOSE_ON_COMMIT OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET CURSOR_DEFAULT?GLOBAL
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET CONCAT_NULL_YIELDS_NULL OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET NUMERIC_ROUNDABORT OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET QUOTED_IDENTIFIER OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET RECURSIVE_TRIGGERS OFF
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET?READ_WRITE
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET RECOVERY FULL
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET?MULTI_USER
GO
ALTER DATABASE [SQL_Tips_Shipping_Rates] SET TORN_PAGE_DETECTION ON

Listing C
USE [SQL_Tips_Shipping_Rates]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[ShippingRates]
(
[RateID] [int] IDENTITY(1,1) NOT NULL,
[Description] [varchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[MinWeight] [int] NOT NULL,
[MaxWeight] [int] NOT NULL,

CONSTRAINT [PK_ShippingRates] PRIMARY KEY CLUSTERED
(
[RateID] ASC
) ON [PRIMARY]
) ON [PRIMARY]

GO
SET ANSI_PADDING OFF

USE [SQL_Tips_Shipping_Rates]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Shipments]
(
[ShipmentID] [int] IDENTITY(1,1) NOT NULL,
[ShipmentDate] [datetime] NOT NULL CONSTRAINT [DF_Shipments_ShipmentDate]
DEFAULT(getdate()),
[Weight] [int] NOT NULL CONSTRAINT [DF_Shipments_Weight] DEFAULT(1)
) ON [PRIMARY]

Listing D
ID Description Min Max
1 Less than 1 Kg 0 1
2 1 to 4 Kgs 1 4
3 4 to 10 Kgs 4 10

Listing E
ID Shipment Date Weight
1 5/9/2006 12:47:03 PM 1
2 5/9/2006 12:47:07 PM 2
3 5/9/2006 12:47:10 PM 3
4 5/9/2006 12:49:11 PM 0

Listing F
SELECT dbo.Shipments.ShipmentID, dbo.Shipments.ShipmentDate, dbo.Shipments.Weight,
dbo.ShippingRates.Description
FROM dbo.Shipments INNER JOIN
dbo.ShippingRates ON dbo.Shipments.Weight BETWEEN
dbo.ShippingRates.MinWeight and ShippingRates.MaxWeight

Listing G
ShipmentID ShipmentDate Weight ShipmentRates.Description
1 5/9/2006 12:47:03 PM 1 Less than 1 Kg
1 5/9/2006 12:47:03 PM 1 1 to 4 Kgs
2 5/9/2006 12:47:07 PM 2 1 to 4 Kgs
3 5/9/2006 12:47:10 PM 3 1 to 4 Kgs
4 5/9/2006 12:49:11 PM 0 Less than 1 Kg

Listing H
ShipmentID ShipmentDate Weight ShipmentRates.Description
1 5/9/2006 12:47:03 PM 1 Less than 1 Kg
2 5/9/2006 12:47:07 PM 2 1 to 4 Kgs
3 5/9/2006 12:47:10 PM 3 1 to 4 Kgs
4 5/9/2006 12:49:11 PM 0 Less than 1 Kg