Home InterBase Firebird Community About us Search Help English Russian
Everything about Borland InterBase and Firebird
Get Firebird power with FIBPlus
 
Anonymous Login / Register
Site News
Third-party Product News
Programming
Miscellaneous
Encyclopedia
FAQ


Search the site


Subscribe the site news

Sections:
News
E-mail:


InterBase World is a Full Member of Firebird Foundation



Login


Hits 2621590
1720
Hosts 152021
530
Visitors 771124
720

4



Powered by Bitrix Site Manager - Content Management & Portal Solutions

Home / Article archives / Programming

Emulation of temporary tables in Firebird


05/25/2003  | Views: 17609

Copyrights (c), Vladimir Karataev, 2003. Translated by Marina Novikova


When migrating from file databases or from MS SQL and ORACLE to Firebird, many developers often inquire about temporary tables. They are accustomed to using them while writing stored procedures. Though Firebird lacks temporary tables, a number of its mechanisms successfully emulate them. Below you will read about three methods of such emulation.

The first method. In most cases you can simply use the following structure: FOR SELECT INTO DO . The cursor-based loop FOR SELECT is used to execute operations (DO) on variables (INTO), which value is set equal to those of returned query fields (SELECT). And the Firebird interpreter will:

  1. execute the query creating _a kind of_ a temporary table in RAM (or in a temporary file if the whole table is too big for RAM).

  2. go to the first record of the Select query.

  3. set field values of the first query record to SP variables, mentioned in "into" according to their order: =,=, etc.

  4. execute operations mentioned in the block DO.

  5. go the the second query record (if it exists) and repeat the same operations from step 3.

Full syntax of this operator is available in manuals (see for example "LANGREF.PDF"). To some extent FOR SELECT is a temporary table and all the operations are done on it. Now I will illustrate this with a table:

// personal accounting
CREATE TABLE CASH (
   ID INTEGER, // the record identifier in the table
   NAME VARCHAR(30), // expenses/income
   MONEY INTEGER, // the sum in dollars: with "plus" income (for example, wages), 
   //with "minus" expenses (for example, purchase)*
   RDATE TIMESTAMP // the date of record listing);
 
ID NAME MONEY RDATE
1 wages
3000
10.04.2003
2 public utilities
-500
05.04.2003
3 the Internet
-150
22.04.2003
4 dividends
100
02.04.2003
5 wages
3000
24.04.2003
6 public utilities
-400
29.04.2003

And you need to make an annual report on obtained and gone sums in such a form:

NAME INCOME EXPENSES
wages
3 000
0
public utilities
0
-500
the Internet
0
-150
dividends
100
0
wages
3 000
0
public utilities
0
-400
TOTAL
6 100
-1 050

The point is that you have to divide the column MONEY into two and count the total sum. Roughly you can:

  1. do a query on income: SELECT NAME, MONEY FROM CASH WHERE MONEY>=0.
  2. then do a query on expenses: SELECT NAME, MONEY FROM CASH WHERE MONEY<0.
  3. count total income: SELECT SUM(MONEY) FROM CASH WHERE MONEY>=0.
  4. and total expenses: SELECT SUM(MONEY) FROM CASH WHERE MONEY<0.
  5. join the obtained results of these queries into a table in the client application.

To do this you needed altogether five queries. Temporary queries might be more effective solution. Using Firebird you will have:

CREATE PROCEDURE RESULT (
   NGOD TIMESTAMP,
   EGOD TIMESTAMP)
   RETURNS (
   NAME VARCHAR(30),
   INCOME INTEGER,
   EXPENSES INTEGER)
AS
   DECLARE VARIABLE SINCOME INTEGER;
   DECLARE VARIABLE SEXPENSES INTEGER;
   DECLARE VARIABLE TMONEY INTEGER;
BEGIN
   /* Initialization of variables */
   SINCOME=0;
   SEXPENSES=0;
   /* Selection */
   FOR SELECT NAME, MONEY FROM CASH WHERE RDATE BETWEEN :NGOD AND :EGOD
   INTO :NAME, :TMONEY
 	DO BEGIN
		/* Division of the MONEY field into two columns - INCOME (income)    and EXPENSES (expenses) */
		IF (TMONEY>=0) THEN BEGIN
			INCOME=TMONEY;
			EXPENSES=0;
			/* Calculation of the total income sum */
			SINCOME=SINCOME+TMONEY;
		END
		ELSE BEGIN
			EXPENSES=TMONEY;
			INCOME=0;
			/* Calculation of the total expenses sum */
			SEXPENSES=SEXPENSES+TMONEY;
		END
		SUSPEND;
   END
   /* Showing the total sum */
   NAME='TOTAL';
   INCOME=SINCOME;
   EXPENSES=SEXPENSES;
   SUSPEND;
END

The second method enables you to use FOR SELECT and keep previous values of table fields in SP variables. In the previous example the item "wages" will be shown as many times as it is mentioned in the table CASH. So if the person has got his/her wages 12 times a year, this value will be shown 12 times (and the list won't be sorted). If you group all similar expense items and sum up corresponding columns, our table will be more readable. Then to sum up items you need to have sorting with record accumulation. For this you need to compare each item name in the current record with that of the previous one and keep intermediate results of the item calculation:

NAME INCOME EXPENSES
dividends
100
0
wages
6 000
0
the Internet
0
-150
public utilities
0
-900
TOTAL
6 100
-1 050

 

This will look as:

CREATE PROCEDURE RESULT2 (
   NGOD TIMESTAMP,
   EGOD TIMESTAMP)
   RETURNS (
   NAME VARCHAR(30),
   INCOME INTEGER,
   EXPENSES INTEGER)
AS
   DECLARE VARIABLE SINCOME INTEGER;
   DECLARE VARIABLE SEXPENSES INTEGER;
   DECLARE VARIABLE TMONEY INTEGER;
   DECLARE VARIABLE STDOH INTEGER;
   DECLARE VARIABLE STRAS INTEGER;
   DECLARE VARIABLE STOLD VARCHAR(30) CHARACTER SET WIN1251;
   DECLARE VARIABLE STNEW VARCHAR(30) CHARACTER SET WIN1251;
BEGIN
   /* Initilization of variables */
   SINCOME=0;
   SEXPENSES=0;
   STDOH=0;
   STRAS=0;
   STOLD='';
   STNEW='';
   /* Selection */
   FOR SELECT NAME, MONEY FROM CASH WHERE RDATE BETWEEN :NGOD AND :EGOD ORDER BY    NAME
   INTO :STNEW, :TMONEY
   DO BEGIN
		/* Detection of the beginning of a new item */
		IF (:STOLD<>:STNEW) THEN BEGIN
			/* showing of the result by the previous record */
			NAME=STOLD;
			INCOME=STDOH;
			EXPENSES=STRAS;
			IF (:NAME<>'') THEN SUSPEND;
			/* setting variables to zero to sum up by the next record */
			STOLD=STNEW;
			STDOH=0;
			STRAS=0;
		END
		/* check if the summation is carried on by the same record    */
		IF (:STOLD=:STNEW) THEN BEGIN
			/* division of income and expenses into columns and summing them up */
			IF (:TMONEY>=0) THEN BEGIN
				/* calculation of income in the current record */
				STDOH=STDOH+TMONEY;
				/* calculation of the total income */
				SINCOME=SINCOME+TMONEY;
			END
			ELSE BEGIN
				/* calculation of expenses in the current record */
				STRAS=STRAS+TMONEY;
				/* calculation of the total expenses */
				SEXPENSES=SEXPENSES+TMONEY;
			END
   	END
	END
   /* it is necessary to count the last record of the list */
   NAME=STNEW;
   INCOME=STDOH;
   EXPENSES=STRAS;
   SUSPEND;
   /* showing of the total sum */
   NAME='TOTAL';
   INCOME=SINCOME;
   EXPENSES=SEXPENSES;
   SUSPEND;
END

There is a particular case of the first two methods - an ability to use the structure FOR SELECT INTO DO :

FOR SELECT  INTO  DO
FOR SELECT  INTO  DO 
   
or

FOR SELECT  INTO  DO
BEGIN
   SELECT  INTO 
   
END

Interpreting FOR SELECT as a kind of a "temporary" table, you can use it to work with multiple "temporary" tables. This may be useful if you have another family member's table with the name CASH2 (its structure is similar to that of CASH):

CREATE TABLE CASH2 (
   ID INTEGER,
   NAME VARCHAR(30),
   MONEY INTEGER,
   RDATE TIMESTAMP);
 
ID NAME MONEY RDATE
1 wages 2000 01.04.2003
2 transport -200 30.04.2003

 

To sum up the total result of both members as:

NAME SUM
dividends
100
wages
8 000
the Internet
-150
public utilities
-900
transport
-200
TOTAL
6 850

you can use the procedure:

CREATE PROCEDURE RESULT3 (
   NGOD TIMESTAMP,
   EGOD TIMESTAMP)
   RETURNS (
   NAME VARCHAR(30),
   SUM INTEGER)
AS
   DECLARE VARIABLE S INTEGER;
   DECLARE VARIABLE TMONEY INTEGER;
   DECLARE VARIABLE TMONEY2 INTEGER;
BEGIN
   /* Initialization */
   S=0;
   /* Selection */
   FOR SELECT NAME, SUM(MONEY) FROM CASH WHERE RDATE BETWEEN :NGOD AND :EGOD GROUP    BY NAME
   INTO :NAME, :TMONEY
   DO BEGIN
		/* Selection from the second table */
		TMONEY2=0;
		SELECT SUM(MONEY) FROM CASH2 
		WHERE RDATE BETWEEN :NGOD AND :EGOD AND NAME=:NAME 
		GROUP BY NAME
		INTO :TMONEY2;
   
		/* handling returned NULL records */
		IF (TMONEY IS NULL) THEN TMONEY=0;
		IF (TMONEY2 IS NULL) THEN TMONEY2=0;
   
		/* summing up and showing the results */
		SUM=TMONEY+TMONEY2;
		S=S+SUM;
		SUSPEND;
	END
   
   /* counting records that are present in the second table and lack in the first    */
   FOR SELECT NAME, SUM(MONEY) 
   FROM CASH2 
   WHERE RDATE BETWEEN :NGOD AND :EGOD AND
   NAME NOT IN (SELECT DISTINCT NAME FROM CASH)
   GROUP BY NAME
   INTO :NAME, :SUM
   DO BEGIN
		S=S+SUM;
		SUSPEND;
   END
   /* showing the total sum */
   NAME='TOTAL';
   SUM=S;
   SUSPEND;
END

The third method is used when the task does not allow preliminary selection sorting. First you need to make a pseudo temporary table with necessary columns, fill it out in a procedure and then handle records there. To illustrate this method we will do the previous task using pseudo temporary tables.

/* we need to make a pseudo temporary table */
CREATE TABLE TEMP (
   NAME VARCHAR(30), // a field for the report
   INCOME INTEGER, // a field for the report
   EXPENSES INTEGER, // a field for the report
   TUSER VARCHAR(8), // as there may be multiple user access to    the table, 
   it is necessary to set who a record belongs to. Otherwise 
   you won't distinguish records of different users if the report will be done    in several transactions 
   
   TDATE TIMESTAMP // the date of of record listing. It is necessary    to control old undeleted records
)
   
   /* The procedure of forming of the report */
CREATE PROCEDURE RESULT4 (
   NGOD TIMESTAMP,
   EGOD TIMESTAMP)
   RETURNS (
   NAME VARCHAR(30),
   INCOME INTEGER,
   EXPENSES INTEGER)
AS
   DECLARE VARIABLE TMONEY INTEGER;
   DECLARE VARIABLE ST VARCHAR(30);
   DECLARE VARIABLE SINCOME INTEGER;
   DECLARE VARIABLE SEXPENSES INTEGER;
BEGIN
   /* Initialization of variables */
   SINCOME=0;
   SEXPENSES=0;
   
   /* Deletion of old records, which possibly remained from previous user queries    */
   DELETE FROM TEMP WHERE TUSER=USER;
   
   /* Filling out the pseudo temporary table with a record list */
   INSERT INTO TEMP 
   SELECT DISTINCT NAME, 0, 0, USER, CAST('NOW' AS TIMESTAMP) 
   FROM CASH
   WHERE RDATE BETWEEN :NGOD AND :EGOD;
   
   /* Forming of the report */
   FOR SELECT NAME, MONEY FROM CASH INTO :ST, :TMONEY 
	DO BEGIN
		IF (:TMONEY>=0) THEN
			UPDATE TEMP SET INCOME=INCOME+:TMONEY 
			WHERE NAME=:ST AND TUSER=USER;
		ELSE
   		UPDATE TEMP SET EXPENSES=EXPENSES+:TMONEY 
		  	WHERE NAME=:ST AND TUSER=USER;
	END
	/* Showing of the result */
	FOR SELECT NAME, INCOME, EXPENSES 
	FROM TEMP 
	WHERE TUSER=USER 
	ORDER BY NAME
	INTO :NAME, :INCOME, :EXPENSES
	DO BEGIN
		SINCOME=SINCOME+INCOME;
		SEXPENSES=SEXPENSES+EXPENSES;
		SUSPEND;
	END
	/* Showing of the total sum */
	NAME='TOTAL';
	INCOME=SINCOME;
	EXPENSES=SEXPENSES;
	SUSPEND;
END
The result of the procedure execution: 
NAME INCOME EXPENSES
dividends
100
0
wages
6 000
0
the Internet
0
-150
public utilities
0
-900
TOTAL
6 100
-1 050

As for some reason the table TEMP may have old unnecessary records, you need to clear it regularly (using the fiel d TDATE) in such a way, for example:

 /* A small hint: keep constants used in stored procedures    in separate tables. This will enable you to change their values 
   without recompilation of the procedure */
   
CREATE TABLE SETVARS ( // a table of constants
   NAME VARCHAR(10), // it's better to make index on this field
   SETVAR VARCHAR(50),
   REMARK VARCHAR(50)
   )
 
NAME SETVAR REMARK
OLDTEMP 10 Deletion of old unnecessary records (older than 10 days)
/* Clearing procedure */
CREATE PROCEDURE CLEARTEMP 
AS
   DECLARE VARIABLE T INTEGER;
BEGIN
    /* determining the variable "T" from the table of constants    */
   FOR SELECT CAST(SETVAR AS INTEGER) 
   FROM SETVARS 
   WHERE NAME='OLDTEMP' 
   INTO :T DO
	    /* deleting all old records */
	   DELETE FROM TEMP 
	   WHERE TDATE<(CAST('NOW' AS TIMESTAMP)-:T);
END

The third method is the slowest one because you go through the table CASH twice, insert and update records in a pseudo temporary table and only then show the result. And from time to time it is also necessary to clear the table Temp. The first and second methods are more quick (they will be executed almost at the speed of record selection). I know from experience that indexing the table CASH by the sorting field NAME helps to get first query results in a few seconds even from tables with about 10 million records. And applying the third method you will get the same result time from tables with about 10 thousand records. See below test results of RESULT2 and RESULT4 procedure execution (IBExpert's data)

 

Parameter RESULT2 RESULT4
Query Time
Prepare
0,00 ms
0,00 ms
Execute
15,00 ms
16,00 ms
Avg fetch time
3,00 ms
3,20 ms
Operations
Fetches
15
570

So first it is recommended to use the first (with FOR SELECT) and the second (FOR SELECT with keeping of previous values) methods. As a last resort, you can use the third method of pseudo temporary tables.

Appendix. Illustrations to the article: test database and script (they use the Russian charset)

Back to the news section


 Last articles 
09/24/2007 IBSurgeon would like to inform you about a series of free Firebird technical seminars

09/23/2007 Firebird System Tables (Parts II+III)

09/20/2007 Firebird Maestro 7.9 released

09/19/2007 And the Winner is —

09/19/2007 Unable to access fireruby lib - with firebird 2.0.x and ror

news archive