By Justin Fidler
Chief Technology Officer
Bantu, Inc.
Introduction
One of the most important new features added in ColdFusion
Server 4.5.1 is the CFQUERYPARAM tag, which enables the use
of bind variables in a query. Bantu.com makes extensive use
of the Oracle 8.1.6 database, and CFQUERYPARAM works perfectly
with it. On the database side, we've seen a performance improvement
of between 100 and 1000 percent, depending on the page.
This document explores how CFQUERYPARAM works, specifically
related to Oracle. Most other major relational databases,
such as Microsoft SQL Server or Informix, also implement
bind variables. Smaller "desktop" databases like Access
may not. Check with your database vendor, as well as the
drivers, to see if bind variables are supported.
In Oracle, ColdFusion's Native Drivers (provided in the
Enterprise version) support bind variables. While I haven't
personally tested the ODBC Oracle drivers for this support,
the bind variable support in the database itself should
work back to at least Oracle 7.3.
The Problem
Let's say you have a query to get a user's name from the users
table. It might look as follows:
<CFQUERY DATASOURCE="DSN_NAME">
SELECT username
FROM users
WHERE user_id=#SESSION.USER_ID#
</CFQUERY>
Based on session variable, ColdFusion will substitute
the current user's ID. (For this example, let's say his
or her ID is 2236.). ColdFusion Server will then send the
following code to the database:
SELECT username FROM users WHERE user_id=2236
The Oracle database parses this statement and determines
the best execution path. An execution path is the process
that Oracle should follow in getting the query data. For
example, it might notice a primary key index on users.user_id
and opt to use that index.
On the other hand, it may notice that while that index
does exist, the table has few enough rows that it's actually
faster to table scan the base data table rather than performing
index reads. Either way, it will then read the data table.
The execution path it finds is almost always optimal, but
it takes time to find it.
As you might imagine, the developers at Oracle realized
this would be a performance hit if it has to parse every
query all the time. So, the database has a cache of queries
and optimal execution paths.
The problem is, for speed and accuracy in lookups of this
cache, the database finds a match (i.e., a cache hit) only
if the statement matches exactly, down to the case of the
fields in the query. This occurs behind the scenes, and
you as a programmer have no control over it.
So, "SELECT count(*) from users" and "select count(*)
from users" are two separate entries in the cache. The case-sensitivity
part of these two statements is not a big concern. At Bantu.com,
we have a standard capitalization procedure for queries
(capitalize keywords, but not field names).
However, the part of the statement that varies (i.e.,
"(*)") can cause signification problems. For the CFQUERY
statement above, substituting the SESSION.USER_ID is going
to result in a lot of queries in the cache. Because each
user has its own user ID, there will be a statement in the
cache for each. This is not good.
To help solve this problem Allaire added the CFQUERYPARAM
tag that passes information to databases, such as Oracle,
as a bind variable.
CFQUERYPARAM
In the example query, ColdFusion Server will send the query
to the Oracle database as:
SELECT username FROM users WHERE user_id=:1
The ":1" is a substitution variable (parameter reference).
The execution path is the same whether the userID is 2236
or 39393. Because there's no use in parsing it more than
once, ColdFusion can use the cached query in the cache and
thus conserve server resources.
To do this, you'd put the CFQUERYPARAM tag inline in the
CFQUERY:
<CFQUERY DATASOURCE="DSN_NAME">
SELECT username
FROM users
WHERE user_id=<CFQUERYPARAM VALUE="#SESSION.USER_ID#" CFSQLTYPE="CF_SQL_NUMERIC">
</CFQUERY>
The CF_SQL_NUMERIC type will work for all integers and
real numbers. (I've tested it with very large integers,
and one place of decimals. On an e-commerce site, where
decimal precision is more important, make sure to test if
you're using decimal numbers with more than one place of
precision).
If it's a character field, you should use CFSQLTYPE=CF_SQL_VARCHAR.
There's something very important to note about character
fields: do not enclose it in single quotes. If you make
this mistake, Oracle will throw an error. For instance,
the query:
<CFSET l_username="justin">
SELECT user_id
FROM users
WHERE logon_id='#l_username#'
would be converted to:
SELECT user_id
FROM users
WHERE logon_id=<CFQUERYPARAM VALUE="#l_username#" CFSQLTYPE="CF_SQL_VARCHAR">
Note there are no longer single quotes surrounding the
username. You should also remove and fix any PreserveSingleQuotes
function calls in your CFQUERY tags if you're converting
them to bind queries.
The ColdFusion documentation makes mention of 20 different
CFSQLTYPEs. None of them are explained very well, and I've
found that CF_SQL_NUMERIC and CF_SQL_VARCHAR work just fine
with Oracle.
You can use multiple CFQUERYPARAMs in the same statement
for deletes, inserts, and updates. Regarding deletes and
updates, examine the code below:
<CFQUERY DATASOURCE="DSN_NAME">
UPDATE users
SET settings_flag=<CFQUERYPARAM VALUE="#l_read_flag#" CFSQLTYPE="CF_SQL_NUMERIC">
WHERE user_id=<CFQUERYPARAM VALUE="#l_user_id#" CFSQLTYPE="CF_SQL_NUMERIC">
</CFQUERY>
Notice that I used a parameter in the actual value section
(the SET statement) as well as in the WHERE clause. The
only time you shouldn't do this is if you're always setting
it to a constant value.
For example, let's say the users.login_flag field is always
set to zero. In that case, you can just hard-code in zero
instead of putting in a CFQUERYPARAM. There are very few cases
where this will be the case. If you're converting existing
CFQUERY code, it probably already has probably already been
inserted.
If you're doing date conversion, you need to force character-date
conversion as a string bind variable. For example:
<CFSET l_In_date="#Now()#">
<CFQUERY name="qry_calendar" datasource="DSN_NAME">
SELECT event_id
FROM calendar
WHERE start_date > TO_DATE(<CFQUERYPARAM VALUE="#DateFormat(l_In_Date,"MMDDYYYY")#"
CFSQLTYPE="CF_SQL_VARCHAR">,'MMDDYYYY')
</cfquery>
The line l_In_Date, which is a ColdFusion date data type,
uses the ColdFusion function DateFormat to convert it to
a specific string with format MMDDYYYY. Next, it tells Oracle
to produce an Oracle data type out of a specified string
format (also MMDDYYY).
Bantu.com doesn't use implicit date conversion because
it's system dependent, and there are bugs in date conversion
in the ColdFusion native database drivers. Since it's no
longer a CF date data type once it's run through the DateFormat
function, notice how the date is passed as a string.
Also note how single quotes are left off when passing
it to TO_DATE. While it's possible to get implicit date
conversion to work reliably and stably, I always use explicit
date conversion. You may move the system to an Oracle database
with a different default date format and that would cause
lots of problems. As far as the performance hit, on the
Oracle side TO_DATE is a very fast function. (It's actually
what occurs behind the scenes anyway during an implicit
date conversion).
Bind Variables
There's one other special case to be considered: null values.
Let's say you want to update a record and set a value to NULL.
You'd do that as follows:
<CFQUERY DATASOURCE="DSN_NAME">
UPDATE users
SET settings_flag=<CFQUERYPARAM VALUE="" NULL="YES" CFSQLTYPE="CF_SQL_NUMERIC">
WHERE user_id=<CFQUERYPARAM VALUE="#l_user_id#" CFSQLTYPE="CF_SQL_NUMERIC">
</CFQUERY>
There are a few things to keep in mind when using null
variables. The VALUE parameter must be an empty string.
By empty string, I mean two sets of double quotes with nothing
in between. If you have anything else in there, even just
a blank space, it can throw an error.
Also, make sure to set NULL="YES" or it won't set it to
NULL properly. Finally, make sure to use the proper CFSQLTYPE
to match the field you're setting to NULL. This may seem
a bit counter-intuitive because NULL has no data type, but
you may run into problems if you don't.
When to Use Bind Variables
Unfortunately, you can't have ColdFusion cache a query
that uses bind variables. Therefore, the CACHEDAFTER and
CACHEDWITHIN parameters of CFQUERY aren't allowed if that
query uses CFQUERYPARAM.
If the query is cache-worthy, it's usually the result of a
query apply across many/all users or sessions. For example,
a query to get a list of countries for a pull-down list applies
to all users and doesn't change much over time. If it's a
user-specific query, like a query to get the username for
the user, use a bind variable query.
Stored Procedures
Before ColdFusion 4.5.1, the workaround to implement bind
variables was to use stored procedures. You could code your
database operation inside a stored procedure and then use
CFSTOREDPROC and CFPROCPARAM. Because it received the information
as parameters, Oracle can preparse the execution path because
stored procedure queries in that format are done as bind
variables. However, this isn't an easy option at all.
While it's nice to have all database operations encapsulated
as stored procedures, this isn't realistic in most circumstances.
Because the results need to be compiles into the database,
it also requires a separate rollout process for stored procedures
in addition to the existing rollout process for rolling
out new CFML pages. On top of that, Oracle makes it much
harder to return a query result set from a stored procedure.
Conclusion
Before Bantu.com started using bind variables, our databases
had serious performance problems. Even though we had a 500MB
SQL cache area in our database, it would use lots of CPU
parsing execution plans, and the SQL cache would be full
of execution plans for queries that were run rarely (per-user
queries).
With CFQUERYPARAM, we can use bind variables so the SQL
cache area is never full, and it contains entries that are
frequently used because it's in bind variable format.
About the Author
As Chief Technology Officer, Justin Fidler oversees all
aspects of technology infrastructure, systems development
and operations at Bantu,
Inc., the leading provider of Web-based instant communication
to businesses, ISPs and Web sites. Founded in 1999, Bantu
operates one of the largest interoperable instant messaging
networks reaching more than 85 million users.
Justin was also CTO at IntraACTIVE, Inc., the predecessor
company to Bantu, Inc., where he was responsible for the
development of SiteKnowledge and InTandem, Web-based groupware
products. Prior to IntraACTIVE, Inc. Justin developed systems
for Oracle, NASA, the Department of Defense, and washingtonpost.com.
Justin holds a bachelor's degree in Information and Decision
Systems from Carnegie Mellon University. He holds certifications
from Allaire, Microsoft, and Novell.