Your users probably don't know a thing about SQL, but you know its value. It's everywhere, it's easy to learn, and SQL solutions are simple to implement. Whether you use SQL a lot or sparingly, working smarter with SQL will help you avoid errors and improve performance.
Many SQLs are vendor-specific. The following tips apply to Jet and Transact-SQL, but most SQL products are similar.
Access users create SQL statements every time they create a query; they just don't know it. They're using the Query Design window, a graphic representation of the actual SQL. Most users can't create the appropriate SQL statements they need, so the graphical interface is helpful, but it can help you too. If you can't create error-less SQL code off the top of your head, let the SQL window help out:
You can't always copy a SQL statement straight from the module to the SQL window. If the statement includes concatenation or variables, add a Debug.Print statement right after the definition statement. Then, if you need to debug the statement, copy the evaluated statement from the Immediate window. For instance, the following statement in code won't run in the SQL window because of the variables and concatenation:
"SELECT * FROM " & tbl & " ORDER BY " & fld
However, if you follow the statement with a Debug.Print statement, you can copy the evaluated results, which follow, from the Immediate window:
SELECT * FROM Employees ORDER BY HireDate
SQL reserves several words, such as keywords. Avoid using these words to name columns, tables, variables, or any objects. Their use outside of the context in which the engine expects them confuses the engine and generates errors, or worse -- incorrect results.
SQL's SELECT statement supports three predicates: ALL, DISTINCT and DISTINCTROW. ALL is the default and returns all records that fulfil any specified conditions. DISTINCT limits the results to unique values in a specific column or columns. For instance, the following statement would return only one record for each LastName value:
SELECT DISTINCT LastName
In other words, if you have a record for both John and Mary Smith, this statement returns only one record for Smith. However, DISTINCT works with all the columns, not just the column that immediately follows. That means, this statement will return a record for both John and Mary because the combined columns don't produce a duplicate record:
SELECT DISTINCT LastName, FirstName
If the SELECT clause references more than one column, the combination of values from all the selected columns must be unique for a given record.
DISTINCT has a few quirks you should know about but might not expect:
While DISTINCT works with columns, DISTINCTROW works with records. (Transact-SQL doesn't support DISTINCTROW.) This predicate has a few limitations of its own, which can make an error hard to troubleshoot:
It's tempting to use the asterisk character (*) when retrieving data via a SELECT clause, but don't, unless you really need to retrieve all columns. The more data you retrieve, the slower your application will perform. For optimum performance, retrieve only the columns you need.
Both SQL and VBA support aggregate functions, but not the same aggregates. Although they aren't interchangeable, you can often use one or the other. Table A compares the two types within the context of Jet and Transact-SQL.
T-SQL | Jet | VBA | Explanation | Considers Null |
Avg | Avg | DAvg | Returns the mean or average of the specified domain | No |
Count | Count | DCount | Counts the number of non-Null values in the specified column | No |
Count(*) | Count(*) | DCount(*) | Counts the number of rows | Yes |
Sum | Sum | DSum | Totals the values in the specified column | No |
Min | Min | DMin | Returns the smallest value | No |
Max | Max | DMax | Returns the largest value | No |
First | Returns the value from the first row in the specified column | Yes | ||
Last | Returns the value from the last row in the specified column | Yes | ||
StDev | StDev | DStDev | Returns sample standard deviation | No |
StDevP | StDevP | DStDevP | Returns population standard deviation | No |
Var | Var | DVar | Returns sample variance | No |
VarP | VarP | DVarP | Returns population deviation | No |
Don't use VBA's domain aggregates when a SQL aggregate will do. When you must use VBA's domain aggregates, apply an index to the underlying column for best performance. Keep in mind that although SQL's GROUP BY doesn't eliminate null values, most SQL aggregates don't evaluate them. If you want null values considered, you must accommodate them in the expression.
SQL's GROUP BY clause defines subsets of data. The most important thing to remember when including a GROUP BY clause is to include only those columns that define the subset or summarise data for the subset. In other words, a GROUP BY can't include extraneous data. For instance, to learn the number of orders placed on a specific date, you'd use a statement similar to the following:
SELECT OrderDate, Count(OrderID)
FROM Orders
GROUP BY OrderDate
This query would return one record for each date. Each record would display the date and the number of orders for that date. You can't include any other columns.
GROUP BY is versatile. You don't need to specify a column in the SELECT clause to group by it. For instance, you could omit OrderDate from the above query and return just the count for each date (although the results wouldn't make much sense). As long as the GROUP BY column is in the source, SQL doesn't require it in the SELECT clause. On the other hand, if you refer to a column in the SELECT clause, you must also include it in the GROUP BY clause or in an aggregate function. For instance, the following statement doesn't work because the Freight column isn't part of an aggregate or the GROUP BY clause:
SELECT OrderDate, Count(OrderID) AS TotalForDate, Freight
FROM Orders
GROUP BY OrderDate
In truth, it doesn't really make sense to try to include a column in this way. If you want the Freight data within the context of a GROUP BY query, you probably want a summary of the freight values in the group, as follows:
SELECT OrderDate, Count(OrderID) Max(Freight)
FROM Orders
GROUP BY OrderDate
Jet can't group a Memo or OLE Object column. In addition, you can't include a GROUP BY clause in an UPDATE statement, which makes sense. SQL would have no way of knowing which record to update.
There are several SQL statements for altering the design of a table. Always approach such a task with care because you could destroy data. For instance, if you change a column's data type, the engine might truncate or delete existing data to accommodate the new column's data type. Keep the following limitations in mind when altering tables:
On Error Resume Next
DoCmd.Close acTable, table
On Error GoTo 0
SQL's INSERT INTO statement lets you add new data to an existing table. Used correctly, INSERT INTO works great, but you must remember one critical thing: although the table designs don't have to match, the specific columns identified on both sides of the task must match. In short, the table that receives the new data must contain the same columns as the incoming data.
You can also explicitly identify each column using the VALUES clause as follows:
INSERT INTO target (col1, col2, col3, ...)
VALUES(value1, value2, value3, ...)
However, this syntax adds only a single row at a time, so it will have limited use. You can omit the column references only if you supply a value for every column in target. When including the column references, their order must match the order in the table definition. You must include the primary key column, unless that key is an auto-numbering column -- it isn't necessary to include an auto-numbering column.
SQL's DELETE statement deletes entire records. The statement won't return an error if you specify a list of columns, but it will ignore the list. For instance, the following statements would delete all the data in a table named Employees:
DELETE
FROM Employees
DELETE Employees.*
FROM Employees
DELETE Employees.Salary
FROM Employees
Even though the last syntax specifies Salary, SQL will delete all the data, not just the Salary values. To delete specific values, use SQL's UPDATE in the form:
UPDATE source
SET source.column = Null
[WHERE condition]
However, even this will return an error if column properties are in conflict. For instance, if a column requires data, it will reject Null.
SQL's SELECT INTO creates a new table by copying rows from an existing table using the following form:
SELECT list|* INTO newtable
FROM sourcetable
[WHERE condition]
However, this statement doesn't copy the table exactly. It won't copy the original table's primary key, indexes, or column and table properties (beyond the default settings). In addition, it ignores Caption settings and uses the original column names.
When including a SELECT INTO statement, keep in mind that this statement will replace an existing table if one of the same name (newtable) exists within its scope. Fortunately, SQL will warn you first. In some cases, the engine will delete the existing table before it actually creates the new one, so if something goes wrong, you can't rely on the original table because it'll be gone. Before running a SELECT INTO, it's a good idea to make a copy of the existing newtable, if one exists. In addition, if newtable is open, SQL will return an error.
You don't have to copy data to the new table. You can create a new empty table by adding a WHERE clause as follows:
SELECT * INTO newtable
FROM source
WHERE FALSE
SQL will create newtable but copy no data to it because no record can satisfy the FALSE condition.
The WHERE and HAVING clauses perform similar functions but they aren't interchangeable. WHERE limits the data returned by the SELECT clause; therefore, a GROUP BY is inconsequential. The engine compares data and eliminates records that don't satisfy the WHERE clause before it groups the records. On the other hand, the HAVING clause eliminates data that doesn't satisfy the grouping criteria.
If you have trouble remembering which clause to use, remember that the WHERE clause is positioned before the GROUP BY clause and the engine applies the WHERE clause before grouping the records.
SQL's UNION operator lets you combine records from different sources using the following form:
SELECT1 list|*
UNION
SELECT2 list|*
The important thing to remember with a UNION is that the column order in both SELECT statements must match. The column names don't have to match, but each list must contain the same number of columns and their data types must be compatible. If the data types don't match, the engine sometimes chooses the most compatible for you. The results might work, but then again, they might not.
By default, UNION sorts records by the values in the first column because UNION uses an implicit DISTINCT predicate to omit duplicate records. To include all records, including duplicates, use UNION ALL, which eliminates the implicit sort. If you know there are no duplicate records, but there are a lot of records, you can use UNION ALL to improve performance because the engine will skip the comparison that's necessary to sort (to find duplicates).