162 CHAPTER 8: HOW TO WRITE STORED PROCEDURES If I can save a few disk fetches, I get a much better return on my efforts than if I write faster executing computations. The seek times have not gotten and are not going to get much better in the foreseeable future. 8.4.3.1 Use CASE Expressions to Replace IF-THEN-ELSE Control Flow Statements As an example of how to do this, consider the problem of updating the prices in a bookstore. This is a version of an exercise in an early Sybase SQL training class to show why we needed cursors. We want to take 10 percent off expensive books ($25 or more) and increase inexpensive books by 10 percent to make up the loss. The following statement is the first impulse of most new SQL programmers, but it does not work. CREATE PROCEDURE IncreasePrices() LANGUAGE SQL DETERMINISTIC BEGIN UPDATE Books SET price = price * 0.90 WHERE price >= 25.00; UPDATE Books SET price = price * 1.10 WHERE price < 25.00; END; A book priced at $25.00 is reduced to $22.50 by the first update. Then it is raised to $24.75 by the second update. Reversing the order of the update statements does not change the problem. The answer given in the course was to use a cursor and to update each book one at a time. This would look something like this: BEGIN DECLARE BookCursor CURSOR FOR SELECT price FROM Books FOR UPDATE; ALLOCATE BookCursor; OPEN BookCursor; FETCH Bookcursor; WHILE FOUND 8.4 Avoid Portability Problems 163 DO IF price >= 25.00 THEN UPDATE Books SET price = price * 0.90 WHERE CURRENT OF BookCursor; ELSE UPDATE Books SET price = price * 1.10 WHERE CURRENT OF BookCursor; END IF; FETCH NEXT Bookcursor; END WHILE; CLOSE BookCursor; DEALLOCATE BookCursor; END; But by using a CASE expression to replace the IF THEN ELSE logic, you can write: UPDATE Books SET price = CASE WHEN price >= 25.00 THEN price * 0.90; ELSE price * 1.10 END; This requires less code and will run faster. The heuristic is to look for nearly identical SQL statements in the branches of an IF statement, then replace them inside one statement with a CASE expression. 8.4.3.2 Use Sequence Tables to Replace Loop Control Flow A sequence table is a single-column table that contains integers from 1 to ( n ), for some values of ( n ) that are large enough to be useful. One way of generating such a table is: CREATE TABLE Sequence (seq INTEGER NOT NULL PRIMARY KEY); CREATE PROCEDURE MakeSequence() LANGUAGE SQL DETERMINISTIC BEGIN 164 CHAPTER 8: HOW TO WRITE STORED PROCEDURES INSERT INTO Sequence (seq) VALUES(1); WHILE (SELECT MAX(seq) FROM Sequence) > 1000 DO INSERT INTO Sequence (seq) SELECT MAX(seq)+1 FROM Sequence; END WHILE; END; However, it is faster to write: CREATE TABLE Sequence (seq INTEGER NOT NULL PRIMARY KEY); CREATE PROCEDURE MakeSequence() LANGUAGE SQL DETERMINISTIC INSERT INTO Sequence (seq) SELECT hundred * 100 + ten * 10 + unit + 1 FROM (VALUES (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) AS Units(unit) CROSS JOIN (VALUES (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) AS Tens(ten) CROSS JOIN (VALUES (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) AS Hundreds(hundred); This use of CROSS JOINs is another example of how to avoid loops. A weird but useful heuristic is to put the phrase “the set of ” in front of the nouns in a sentence that describes the problem you are solving. It is bad grammar, but it can help shift your mindset to thinking in terms of sets. Converting a string with a comma-separated list of values into a proper table with the position and value is done by using a simple WHILE loop that cuts off one substring up to but not including the comma, and then converts the substring to an integer. The code would look like this: CREATE PROCEDURE Parser(IN input_string VARCHAR(255)) DETERMINISTIC LANGUAGE SQL BEGIN DECLARE parm_nbr INTEGER; SET parm_nbr = 0; DECLARE val INTEGER; SET val = CAST(NULL AS INTEGER); SET input_string = TRIM (BOTH input_string); 8.4 Avoid Portability Problems 165 WHILE CHAR_LENGTH(input_string) > 0 DO BEGIN SET parm_nbr = parm_nbr +1 IF POSITION(','IN input_string) > 0 THEN BEGIN SET val = SUBSTRING (input_string FROM 1 FOR POSITION(',' IN input_string)-1); SET input_string = SUBSTRING (input_string FROM CHAR_LENGTH(input_string) - POSITION(',' IN input_string)); END ELSE BEGIN SET val = input_string; SET input_string = '';—empty string END; IF END; INSERT INTO ParmList VALUES (parm_nbr, CAST(val AS INTEGER)); END WHILE; END; However, the same thing can be done with a Sequence table, thus: CREATE PROCEDURE Parser(IN input_string VARCHAR(255)) DETERMINISTIC LANGUAGE SQL BEGIN INSERT INTO ParmList (parm_nbr, parm) SELECT COUNT(S2.seq), CAST (SUBSTRING (',' || input_string || ',' FROM MAX(S1.seq + 1) FOR (S2.seq - MAX(S1.seq + 1))) AS INTEGER) FROM Input_strings AS I1, Sequence AS S1, Sequence AS S2 WHERE SUBSTRING (',' || input_string || ',' FROM S1.seq FOR 1) = ',' AND SUBSTRING (',' || input_string || ',' FROM S2.seq FOR 1) = ',' AND S1.seq < S2.seq AND S2.seq <= CHAR_LENGTH (input_string) + 2 166 CHAPTER 8: HOW TO WRITE STORED PROCEDURES GROUP BY input_string, S2.seq; END; It makes life easier if the lists in the input strings start and end with a comma. You will also need a table called Sequence, which is a set of integers from 1 to ( n ). The S1 and S2 copies of Sequence are used to locate bracketing pairs of commas, and the entire set of substrings located between them is extracted and cast as integers in one nonprocedural step. The trick is to be sure that the left-hand comma of the bracketing pair is the closest one to the second comma. The place column tells you the relative position of the value in the input string. The real advantage of the nonprocedural approach comes from modifying this second procedure to handle an entire table whose rows are CSV strings. CREATE TABLE InputStrings (list_name CHAR(10) NOT NULL PRIMARY KEY, input_string VARCHAR(255) NOT NULL); INSERT INTO InputStrings VALUES ('first', '12,34,567,896'); INSERT INTO InputStrings VALUES ('second', '312,534,997,896'); In fact, the one row at a time procedure can be replaced with a VIEW instead: CREATE VIEW Breakdown (list_name, parm_nbr, param) AS SELECT list_name, COUNT(S2.seq), CAST (SUBSTRING (',' || I1.input_string || ',', MAX(S1.seq + 1), (S2.seq - MAX(S1.seq + 1))) AS INTEGER) FROM InputStrings AS I1, Sequence AS S1, Sequence AS S2 WHERE SUBSTRING (',' || I1.input_string || ',' FROM S1.seq FOR 1) = ',' AND SUBSTRING (',' || I1.input_string || ',' FROM S2.seq FOR 1) = ',' AND S1.seq < S2.seq AND S2.seq <= CHAR_LENGTH (I1.input_string) + 2 GROUP BY I1.list_name, I1.input_string, S2.seq; . following statement is the first impulse of most new SQL programmers, but it does not work. CREATE PROCEDURE IncreasePrices() LANGUAGE SQL DETERMINISTIC BEGIN UPDATE Books SET price = price. problem of updating the prices in a bookstore. This is a version of an exercise in an early Sybase SQL training class to show why we needed cursors. We want to take 10 percent off expensive books. END; This requires less code and will run faster. The heuristic is to look for nearly identical SQL statements in the branches of an IF statement, then replace them inside one statement with