0370 Temporary Tables in MonetDB

Sample Table

We will login as adminstrators, but we will set VOC shema as default. Password is "monetdb".mclient -u monetdb -d voc;
SET SCHEMA voc;

CREATE TABLE permTable ( Number INTEGER );

INSERT INTO permTable ( Number ) VALUES ( 1 ), ( 2 );

Temporary Tables

After the execution of the query, if the result is not saved in a table or sent to an application, the result of a query will be discarded. Queries are transient, but tables are permanent. Tables will permanently save data stored in them. Between queries and tables, we have temporary data structures called temporary tables. These structures are used to store session-specific data that only needs to exist for a short duration.

Creation of a Local Temporary Table

We will create a temporary table that will exist only during one session. Such temporary tables are called LOCAL temporary tables. Default behavior of temporary tables is to lose their content at the end of transaction. We can prevent that with option on commit PRESERVE ROWS. We don't want the temporary table to be emptied at the end of the transaction because we want to observe the behavior of the table.

CREATE LOCAL TEMPORARY TABLE tempTable ( Number INTEGER PRIMARY KEY  ) ON COMMIT PRESERVE ROWS;
INSERT INTO tempTable ( Number ) VALUES ( 3 ), ( 4 );
SELECT * FROM tempTable;

Temporary tables are created in "tmp" schema. We don't have to prefix their names with "tmp".
Both statements bellow will work the same
SELECT * FROM tempTable;
SELECT * FROM tmp.tempTable;

We can not create temporary table in some schema other than "tmp".
CREATE LOCAL TEMPORARY TABLE voc.tempTable2
( Number INTEGER PRIMARY KEY );

Permanent table and temporary table can have the same name because they are in different schemas. I will create one temporary table with the name "permTable".
CREATE LOCAL TEMPORARY TABLE permTable ( Number INTEGER );
If we read from the "permTable" without specifying schema, we will get the temporary table.
SELECT * FROM permTable;
For reading from the permanent table with the same name, we have to use fully qualified name.
SELECT * FROM voc.permTable;
So, temporary table has priority, although our current schema is "voc" (and not "tmp").


We can not create permanent objects in "tmp".
SET SCHEMA tmp;
CREATE TABLE ZZZ ( Number INTEGER );
SET SCHEMA voc;

 Usage of a Temporary Table

It is possible to create queries that combine temporary and normal tables.  
SELECT * FROM voc.permTable JOIN tempTable ON permTable.Number <= tempTable.Number;

We can have constraints on the temporary table. In this case we have PK constraint.
INSERT INTO tempTable ( Number ) VALUES ( 3 );

It is possible to export data from temporary table into CSV.
COPY SELECT * FROM tempTable INTO '/home/fffovde/Desktop/tblCSV';
It is possible to import data from a CSV file into temporary table.
COPY INTO tempTable ( Number ) FROM '/home/fffovde/Desktop/tblCSV'( Number );

It is possible to use UPDATE and DELETE on temporary tables.

UPDATE tempTable SET Number = 6 WHERE Number = 5;
DELETE FROM tempTable WHERE Number = 6;

We can NOT alter our temporary table.
ALTER TABLE tempTable ADD COLUMN Letter VARCHAR(10);
ALTER TABLE tempTable DROP CONSTRAINT temptable_number_pk;

It is possible to create a view on a temporary table.
CREATE VIEW voc.View1 ( Number ) AS         
              SELECT * FROM tmp.tempTable;
SELECT * FROM View1;

It is not possible to create foreign key constraint on the permTable if it references tempTable.
ALTER TABLE voc.permTable ADD CONSTRAINT FromTempTableConstraint FOREIGN KEY ( Number ) REFERENCES tmp.tempTable ( Number );

Info About Temporary Tables

We can NOT get statistics about our temporary table.
SELECT * FROM sys.statistics( 'tmp','temptable');

We can find our temporary table in the system catalog.
SELECT * FROM sys.tables WHERE Name = 'temptable';

Visibility of Local Temporary Table

I will log in to MonetDB from another instance of mclient, as "voc" user (password "voc").
mclient -u voc -d voc 
I will try to read local table "tempTable".
SELECT * FROM tempTable;
This will not work, because local temporary table are visible only in the session where they are created. Other users can not see these tables.

As the user "voc" I will create temporary table with the same name -"tempTable". This will be successful. Each user can have its own local temp table.
CREATE LOCAL TEMPORARY TABLE tempTable ( Number INTEGER );

Even if we don't explicitly drop the table, our table will disappear after we log out of the current session. I'll log out of the session (monetdb user session), and after I log back in, "tempTable" will no longer exist.
quit
mclient -u monetdb -d voc
SELECT * FROM tempTable;

We can terminate our "tempTable" by dropping it explicitly, even before the end of the session.DROP TABLE tempTable;

 ON COMMIT DELETE ROWS

"ON COMMIT DELETE ROWS" subclause means that after each transaction, data will be deleted. This is default behavior.

CREATE LOCAL TEMPORARY TABLE tempTable ( Number INTEGER PRIMARY KEY  ) ON COMMIT DELETE ROWS;
INSERT INTO tempTable ( Number ) VALUES ( 3 ), ( 4 );
SELECT * FROM tempTable;

In this case, temporary table is useful only inside of transaction.
START TRANSACTION;
INSERT INTO tempTable ( Number ) VALUES ( 3 ), ( 4 );
SELECT * FROM tempTable;

COMMIT;

AS SELECT

We can create temporary table based on some other table. Base table can be temporary or normal table.

CREATE LOCAL TEMPORARY TABLE tempTable2 ( Number )
AS ( SELECT Number FROM tempTable );
CREATE LOCAL TEMPORARY TABLE tempTable3 ( Number )
AS (SELECT Number FROM voc.permTable ) ON COMMIT PRESERVE ROWS;

By defult we will use "WITH DATA". If try to read from tempTable3, we'll see the values.
SELECT * FROM tempTable3;

The "WITH NO DATA" subclause will make us not to inherit data.
CREATE LOCAL TEMPORARY TABLE tempTable4 ( Number )
AS ( SELECT Number FROM voc.permTable ) WITH NO DATA ON COMMIT PRESERVE ROWS;
SELECT * FROM tempTable4;

Global Temporary Tables

Global temporary tables are somewhere between normal tables and local tables. Their definition ( columns and data types ) is permanent. Name of the global table has to be unique in the "tmp" schema. Only users with authorization over "tmp" schema can create global temporary tables. In our example, administrator "monetdb" can create global temporary tables, but "voc" user can not.

The thing that makes these tables temporary is their data. All the rows of the global temporary tables will be deleted after each transaction (for ON COMMIT PRESERVE ROWS) or after the session (ON COMMIT DELETE ROWS).

While definition of the global temporary tables is shared, data is not. Data placed in the global table by one user can not be seen by another user. So, global temporary table is a playground where each user can play with his own data.

Global temporary tables have similar characteristics as local temporary tables. We can use SELECT, DELETE, UPDATE. We can export them to CSV file. We can NOT alter global tables. We can create views on them. So, everything is the same as for local temporary tables.

It is possible to get statistics about global tables.
SELECT * FROM
sys.statistics( 'tmp','globtable');

Creation of the Global Temporary Table

We create global temporary table with similar statement as for the local temporary tables.

CREATE GLOBAL TEMPORARY TABLE globTable ( Number INTEGER PRIMARY KEY ) ON COMMIT PRESERVE ROWS;
This will fail for the "voc" user who doesn't have enough privileges over "tmp" schema.
Privileged users can successfully create global temporary table, but not if the table with such name already exist. It is not possible for two users to create global tables with the same names.

Visibility of Global Temporary Table

We will insert some data in our global temporary table.
INSERT INTO globTable ( Number ) VALUES ( 5 ), ( 6 );
SELECT * FROM globTable;

If we try to read our table from the session of the "voc" user, we will see empty table. This show us that definition of a table is shared, but data is not shared.
SELECT * FROM globTable;

Although "voc" user can not create global table, it can use tables created by others. "Voc" user can play with his own data.
INSERT INTO globTable ( Number ) VALUES ( 7 ), ( 8 );
SELECT * FROM globTable;

When to Use Temporary Tables

  1. You can create an excerpt from some big table. After that, you can run you queries on that smaller table, instead of the big one.
  2. Because temporary tables are volatile and data is isolated between users, so temporary tables are great for experiments.
  3. Temporary tables should not be used as an intermediate step in queries. In that case, it is much wiser to use CTE.

0360 Loader Functions in MonetDB

Loader functions are UDFs (user defined functions), that are used to insert data from various data sources. Currently, we can only make these functions with python programming language.

Idea is, to be able to read from the different data sources by using the power of python.  

Monetdb-Python3 Integration Package

Previously, we have installed MonetDB with two packages. We have installed monetdb5-sql and monetdb-client.

For python, we will need one more package. Monetdb-Python3 is integration package that allows MonetDB to interact with python.
sudo apt install monetdb5-sql monetdb-client


sudo apt install monetdb-python3

Command, sudo apt -a list monetdb-python3, will show us that we have 8 different versions of this package in the repository.
Command sudo apt -a show monetdb-python3, will claim that for MonetDB version 11.51.7, we should install version 11.51.7 of monetdb-python3. We should always match versions if we can.

I have the version 11.51.7 of MonetDB server.
monetdb --version
I can install the last version of monetdb-python3:
sudo apt install monetdb-python3
Or, I can install specific version of monetdb-python3:
sudo apt install monetdb-python3=11.51.7

Enabling Embedded Python

I will first start monetdb daemon:

monetdbd start /home/fffovde/DBfarm1
We have to enable python integration package for each database. By typing monetdb get all voc, we can list properties of the voc database. We can see that for "embedpy3" setting is NO. We will change that.
name   prop               source     value
voc    name               -          voc
voc    type               default    database
voc    shared             default    yes
voc    nthreads           default    4
voc    ncopyintothreads   default    4
voc    optpipe            default    default_pipe
voc    readonly           default    no
voc    embedr             default    no
voc    embedpy3           local      no
voc    embedc             default    no
voc    listenaddr         default    <unset>
voc    nclients           default    64
voc    dbextra            default    <unset>
voc    memmaxsize         default    <unset>
voc    vmmaxsize          default    <unset>
voc    raw_strings        default    <unset>
voc    loadmodules        default    <unset>

We will stop the database if it is running. Then we will change the setting, and after that we will run our database again.
monetdb stop voc
monetdb set embedpy3=true
monetdb start voc

We have changed embedpy3 property to yes.

Now we can login to our database. I will login as an administrator, although that is not needed, any user has ability to create LOADER function.mclient -u monetdb -d voc
Password: monetdb

Python LOADER Function

CREATE LOADER myloader() LANGUAGE PYTHON {
     _emit.emit( { 'Col1': [ "A", "B" ], 'Col2': [ 1, 2 ] } )      _emit.emit( { 'Col1': [ "C", "D" ], 'Col2': [ 3, 4 ] } ) };
This statement will create LOADER function. Columns are defined
as python lists. Each list, together with the name of a column, is
placed inside of the python dictionary.
We are using function "_emit.emit" do divide our inserts into chunks.
In this way we can preserve memory. After inserting the first chunk, (A1,B2), we can delete it from the memory, and we can continue inserting the second chunk (C3,D4).

Instead of the python lists, we can also use NumPy arrays. Instead of [1, 2, 3, 4, 5], we can use np.array( [1, 2, 3, 4, 5] ). NumPy arrays are faster.

LOADER functions are of the type 7, so we can read them as "SELECT * FROM sys.functions WHERE type = 7;". We can also notice that our function belongs to schema 2000 (schema sys), because that is the default schema for administrators (I am logged in as an administrator). Creation of LOADER functions is not limited to administrators, every user can create LOADER function.

Using LOADER Function

We can create a table from our LOADER function. Columns and data types will be deduced automatically.
CREATE TABLE myLoaderTable FROM LOADER myloader();
SELECT * FROM myLoaderTable;

It is also possible to add data to an existing table. I will first truncate myLoaderTable and then I will append new data to an existing table.
TRUNCATE myLoaderTable;
COPY LOADER INTO myLoaderTable FROM myloader();

Using a Parameter in a LOADER function

With python we can pull data from anywhere, from any database or file. Here is an example where we will read data from a JSON file.

{ "Col1": ["A","B","C","D"] , "Col2": [1,2,3,4] } We have a JSON file, with the name "File.json".

CREATE LOADER json_loader(filename STRING) LANGUAGE PYTHON {     
import json
    f = open(filename)
   _emit.emit(json.load(f))
   f.close()
};
This is how we can create LOADER function, that will read from our JSON file. This time we are using an argument for our function. This argument is of the STRING data type. STRING is an alias for the CLOB data type in MonetDB.

json module is builtin Python3 module.

We can truncate previous results and we can import from the json file.  

TRUNCATE myLoaderTable;

COPY LOADER INTO myLoaderTable FROM json_loader('/home/fffovde/Desktop/File.json');
SELECT * FROM myLoaderTable;

Missing Columns

TRUNCATE myLoaderTable;

During the import missing columns will be filled with nulls.
CREATE LOADER myloader2() LANGUAGE PYTHON {      
 
         _emit.emit( { 'Col1': [ "A", "B", "C", "D" ] } )
};
COPY LOADER INTO myLoaderTable FROM myloader2();
SELECT * FROM myLoaderTable;

Delete LOADER function

DROP LOADER FUNCTION sys.myloader2;We can always delete this function with DROP LOADER FUNCTION statement.

0350 Exporting Data and Binary Files in MonetDB

Sample Table

CREATE TABLE tblCSV ( Letter VARCHAR(10), Number REAL, Calendar DATE );  

INSERT INTO tblCSV (Letter, Number, Calendar)    
        VALUES ('A', 3.82, null), ('B', 4.83, '2025-01-02'), ('C', 5.64, '2025-01-03');

Exporting Data from MonetDB to CSV

We saw how to import data from CSV file. For exporting to CSV file, we will again use COPY INTO statement.

COPY SELECT * FROM tblCSV INTO '/home/fffovde/Desktop/tblCSV';

By default, we'll get textual file, in UTF-8 encoding (1). Line endings are going to be LF (2). Column separators are pipes (3). Default string wrappers will be double quotes (4). Nulls will be represented as a >null< (5).
Notice that I have omitted "ON SERVER" subclause. Subclause "ON SERVER" is the default.

Default string wrappers are not the same when writing and reading strings. They are double quotes when writing, and empty strings when reading.

We can change default settings, like in this example. Column separator is TAB sign (1). String wrapper is a star (2). NULLs are represented with >Nil< (3).
COPY SELECT * FROM tblCSV INTO '/home/fffovde/Desktop/tblCSV' ON SERVER
USING DELIMITERS E'\t', E'\n', '*'
NULL AS 'Nil';

We can not overwrite  the existing file with the same name. That would return the error.

Exporting Compressed CSV File

If our file name has extensions  .xz  .bz2  .gz  .lz4, then the result file will be compressed. Compression level is the best when using .xz ( .xz > .bz2 > .gz > .lz4 ). For better compression, the more time is needed.

Let's use extreme zip compression (.xz).
COPY SELECT * FROM tblCSV INTO '/home/fffovde/Desktop/tblCSV.xz';  

Our file is reduced from 648 to 108 bytes.

Exporting CSV File on the Client

I will demonstrate exporting of a CSV file, on the client, by using python. We will use the code bellow. This is the same code used for uploading files, with two distinctions. Instead "set_uploader" we are using "set_downloader". COPY INTO statement is also different.

import pymonetdb 
connection = pymonetdb.connect(username="monetdb", password="monetdb", hostname="localhost", database="voc")   

handler = pymonetdb.SafeDirectoryHandler("/home/fffovde/Desktop")
connection.set_downloader(handler)                                   

cursor = connection.cursor() 
cursor.execute("COPY SELECT * FROM tblCSV INTO '/home/fffovde/Desktop/tblCSV.bz2' ON CLIENT;")
connection.commit()   

cursor.close()
connection.close()
Our file will
be saved in
compressed state:

Binary Files

MonetDB can import/export data even faster than from/to CSV files. For that we can use binary files.

Little Endian vs Big Endian

Let's say that we have hexadecimal number 0x12345678. Every two figures represent one byte:

12 => 0000110034 => 0010001056 => 0011100078 => 01001110

When we write our number to memory, we can write it in two ways.
– We can write it in order 12345678. This is called Big Endian.
– We can write it in the reverse order 78563412. This is called Little Endian.  

This distinction is important when writing binary files.

There is also a term Native Endian. That is the preferred byte order of the system MonetDB is running on. If your system is using AMD, ARM, or Intel CPU, then your system is using Little Endian.

We can check the Endianness of our system.
lscpu | grep "Byte Order"

Exporting Binary Files

MonetDB can export data into MonetDB custom binary files. Each table column will become a separate file.COPY SELECT * FROM tblCSV INTO LITTLE ENDIAN BINARY
      '/home/fffovde/Desktop/Letter',
      '/home/fffovde/Desktop/Number',       '/home/fffovde/Desktop/Calendar' ON SERVER;

As a result, we'll get three binary files. One file for each column.

Instead of "Little Endian" we can use "Big Endian" or "Native Endian". Instead of "ON SERVER", we can use "ON CLIENT". For exporting data on the client, we can reuse the same python script shown above.

Loading Binary Files to MonetDB

Exported binary files can be imported in any MonetDB database. Before import, we can empty database table "TRUNCATE tblCSV;".

COPY LITTLE ENDIAN BINARY INTO tblCSV( Letter, Number, Calendar ) FROM    '/home/fffovde/Desktop/Letter',
   '/home/fffovde/Desktop/Number',
   '/home/fffovde/Desktop/Calendar' ON SERVER;

TRUNCATE tblCSV;

It is possible to load less then three columns. We can see that "Calendar" column is empty.
COPY LITTLE ENDIAN BINARY INTO tblCSV( Letter, Number ) FROM
    '/home/fffovde/Desktop/Letter',
    '/home/fffovde/Desktop/Number' ON SERVER;

TRUNCATE tblCSV;

COPY LITTLE ENDIAN BINARY INTO tblCSV FROM
     '/home/fffovde/Desktop/Letter',
     '/home/fffovde/Desktop/Number',
     '/home/fffovde/Desktop/Calendar' ON SERVER;           
We don't have to declare columns of the database table. In that case,
we just have to make sure that order and number of files is the same
as the order and number of the columns in the table.

Loading Binary Files to MonetDB on Client

TRUNCATE tblCSV;

import pymonetdb 
connection = pymonetdb.connect(username="monetdb", password="monetdb", hostname="localhost", database="voc")   

handler = pymonetdb.SafeDirectoryHandler("/home/fffovde/Desktop") connection.set_uploader(handler)                                   

cursor = connection.cursor() 
cursor.execute("COPY LITTLE ENDIAN BINARY INTO tblCSV FROM '/home/fffovde/Desktop/Letter', '/home/fffovde/Desktop/Number', '/home/fffovde/Desktop/Calendar' ON CLIENT;")
connection.commit()   

cursor.close()
connection.close()
For uploading binary files to MonetDB, from the client, we are using the similar python script that we have used for CSV files.

0340 Loading from CSV files into MonetDB

Importing data from CSV files into MonetDB is much faster than using "INSERT INTO". The reason is, CSV can be loaded as is, no need for parsing like for "INSERT INTO". The other reason is because CSV file can be imported by using several CPU cores at once, while "INSERT INTO" can only use one thread.

Sample table and CSV file

To use COPY INTO statement, we have to log in as an administrator (password is monetdb):
mclient -u monetdb -d voc;

In order to import data, first we need to have a table where we will place data.
CREATE TABLE tblCSV ( Letter VARCHAR(10) DEFAULT 'Z', Number REAL, Calendar DATE );

We'll start with this CSV file. Line ending can be \n (Linux) (1) or \r\n (Windows), it doesn't matter. File has to be encoded as UTF-8 (2).  In our example, separator between columns is comma (3). Strings are wrapped inside double quotes (4).
Letter,Number,Calendar
"A",3.82,2025-01-01
"B",4.83,2025-01-02
"C",5.64,2025-01-03

Reading from CSV File to MonetDB table

We will read our CSV file into tblCSV table by this statement bellow. User executing this statement must have privilege to read the file.

• OFFSET 2 will skip the first row. In the first row we have the header. Header already exists in our MonetDB table, so we do not import it.
• tblCSV( Letter, Number, Calendar ) – this part is the table and the columns in which we are importing data.
• '/home/fffovde/Desktop/tblCSV.csv'( Letter, Number, Calendar ) ON SERVER – location of our file.
',' – this is separator between columns.
• E'\n' – this is delimiter between rows. '\n' is a symbol for LF (line feed).
'"' – strings are wrapped with double quotes.

COPY OFFSET 2 INTO tblCSV( Letter, Number, Calendar )
FROM '/home/fffovde/Desktop/tblCSV.csv'( Letter, Number, Calendar )
ON SERVER
USING DELIMITERS ',', E'\n', '"';

SELECT * FROM tblCSV;
This is now our table.

Number of Records and Default Options

Before each insert, I will delete all the existing rows. Deletion of all of the rows can be done with TRUNCATE statement:
TRUNCATE tblCSV;

• 2 RECORDS – we can limit our import to only two rows (after the header). If we know in advance how many rows we will import, that will allow MonetDB to reserve space on disk in advance, so it will increase performance.

We can also notice, that we do not have to provide column names. In that case, we have to make sure that columns have the same order in the file, as in the database table.COPY 2 RECORDS OFFSET 2 INTO tblCSV
FROM '/home/fffovde/Desktop/tblCSV.csv' ON SERVER USING DELIMITERS ',';

There is also no need to always define columns separator and string wrapper. MonetDB will assume that columns separator is either \n or \r\n. It will also assume that nothing is used as a string wrapper. On the image above, double quotes are now considered as a part of a string.

Column Names

TRUNCATE tblCSV;

If we are providing column names of the CSV file, we always have to provide all of the column names, but we can give them fake names. In the example bellow we are importing only the column "Number". Other two columns will not be imported. Because "Letter" column has a default value "Z", all the values in that column will be "Z". "Calendar" column will be filled with nulls.

COPY OFFSET 2 INTO tblCSV( Number )
FROM '/home/fffovde/Desktop/tblCSV.csv'( Dummy, Number, Dummy2 )

ON SERVER
USING DELIMITERS ',';

TRUNCATE tblCSV;

For next example I will add one more column to my CSV file ("Color"), and I will place Number column before Letter column. I will change columns separator to semicolon, and record delimiters to CRLF. I will remove string wrapers.

Colors, Number, Letter, Calendar

If we provide column names, we can use that fact to reorder the columns in correct order.
COPY OFFSET 2 INTO tblCSV( Letter, Number, Calendar )
FROM '/home/fffovde/Desktop/tblCSV.csv'( Color, Number, Letter, Calendar )
ON SERVER
USING DELIMITERS ';', E'\n';

– This time, we are using semicolon as a separator between columns.
– We have not provided string wrappers, so MonetDB will take data as is.
– Column delimiter is defined as LF, it doesn't matter that our file is using CRLF delimiter. Symbol "\n" will work in both cases.
SELECT * FROM tblCSV;

TRUNCATE tblCSV;

If we don't provide column names for the CSV file, MonetDB will assume that CSV file has the same number of columns as the target table. In the example bellow, MonetDB assumes that our CSV file has three columns. MonetDB will raise an error that one column is leftover.

COPY OFFSET 2 INTO tblCSV( Letter, Number )
FROM '/home/fffovde/Desktop/tblCSV.csv' --x3
ON SERVER
USING DELIMITERS ';';

Column Separators and NULL-s

TRUNCATE tblCSV;

This time we will remove header, we will use pipe "|" as columns separator.
In the first row, I will place one surplus pipe at the end of the record (1).

We will take only two records. We don't have to skip header, because there is no header in the CSV file. Our separators are pipes. They are default, so we can omit "USING" clause altogether.
COPY 2 RECORDS INTO tblCSV( Letter, Number, Calendar )
FROM '/home/fffovde/Desktop/tblCSV.csv'( Color, Number, Letter, Calendar )
ON SERVER;

The lone pipe (1), from the picture above, won't cause us any problems.
SELECT * FROM tblCSV;

TRUNCATE tblCSV;

We will replace column delimiters with dots. Dot is also used as a decimal point. When we use a dot as a decimal point, we have to place an ESCAPE sign before it. ESCAPE SIGN is backslash (1).

COPY 2 RECORDS INTO tblCSV( Letter, Number, Calendar )
FROM '/home/fffovde/Desktop/tblCSV.csv'( Color, Number, Letter, Calendar )
ON SERVER
USING DELIMITERS '.';

We can see that the escape characters did their job of distinguishing decimal points.
SELECT * FROM tblCSV;

TRUNCATE tblCSV;

It is also possible to have string wrapper sign inside of a string. We can escape such sign by doubling it (1), or by placing an escape sign before it (2).

Notice, also, that I have placed the word NULL in the third record (3). By default this will be considered as a "null" in MonetDB

COPY OFFSET 0 INTO tblCSV( Letter, Number, Calendar )
FROM '/home/fffovde/Desktop/tblCSV.csv'( Color, Number, Letter, Calendar )
ON SERVER
USING DELIMITERS ',', E'\n', '"';

If we offset zero or 1 row, that means that we are reading data from the start.

TRUNCATE tblCSV;

Sometimes we want to have backslash inside of a string as a literal. In that case we use "NO ESCAPE" subclause (1).

NULL is representing null value by default. It is possible to use any other representation (2).

COPY OFFSET 0 INTO tblCSV( Letter, Number, Calendar )
FROM '/home/fffovde/Desktop/tblCSV.csv'( Color, Number, Letter, Calendar )
ON SERVER
USING DELIMITERS ',', E'\n', '"'
NO ESCAPE       --backslash is considered literal (1)
NULL AS '';     --NULL is represented by empty string (2)

Fixed Width Columns

TRUNCATE tblCSV;

Beside using comma separated format, we can also use fixed width format. In this CSV file we have three columns – Letter, Number, Calendar, but each column has the fixed width. Letter is 4 characters wide, Number is 7 characters wide, Calendar is 10 characters wide.

COPY INTO tblCSV
FROM '/home/fffovde/Desktop/tblCSV.csv' ON SERVER
FWF ( 4, 7, 10 );

We are using FWF subclause to inform MonetDB about columns width.
Spaces in the columns will be stripped and only values will be imported.

Error Handling

TRUNCATE tblCSV;

This CSV file has an error that will prevent import. There is a letter in a number column (W), in the second row.

COPY INTO tblCSV
FROM '/home/fffovde/Desktop/tblCSV.csv'

ON SERVER
FWF ( 4, 7, 10 );
We will get an error, for this statement, and we will not import any rows.

COPY INTO tblCSV
FROM '/home/fffovde/Desktop/tblCSV.csv'

ON SERVER
BEST EFFORT

FWF ( 4, 7, 10 );
Solution is to use subclause "BEST EFFORT".
Only first and third rows are now imported.
Second row is not imported.

We can find the rows that are not imported by reading from the table:
SELECT * FROM sys.rejects;
This table will preserve its content until the end of a session, or until procedure statement "CALL sys.clearrejects();" is executed.

Loading Several CSV Files at Once

TRUNCATE tblCSV;

We will import three CSV files at once. We will offset the first record in each file, and then we will read only one next row. From each file we will import only one record, so in total we will insert three records.

COPY 1 RECORDS OFFSET 2 INTO tblCSV
FROM ( '/home/fffovde/Desktop/tblCSV1.csv'      

          , '/home/fffovde/Desktop/tblCSV2.csv'      
          , '/home/fffovde/Desktop/tblCSV3.csv' ) ON SERVER;

Loading Data on the Client Side

TRUNCATE tblCSV;

It is also possible to upload CSV file from the client. We will see how to do this by using python.

import pymonetdb  
connection = pymonetdb.connect(username="monetdb", password="monetdb", hostname="localhost", database="voc")  

handler = pymonetdb.SafeDirectoryHandler("/home/fffovde/Desktop") –this will designate this directory as safe connection.set_uploader(handler)                                  –we can place our files into this folder to upload them

cursor = connection.cursor()  
cursor.execute("COPY INTO tblCSV FROM '/home/fffovde/Desktop/tblCSV.csv' ON CLIENT BEST EFFORT FWF ( 4, 7, 10 );")
connection.commit()  

cursor.close()
connection.close()

Important thing is to register some folder from the client computer as the safe folder:
handler = pymonetdb.SafeDirectoryHandler("/home/fffovde/Desktop")
After that, we assign that folder as the folder from which we can upload files.
connection.set_uploader(handler)
Then we just execute our statement.
This is the result in our database table:


You can download CSV files from here:

0330 Loading Data Using  SQL in MonetDB and Timing

Sample Table

We will start mclient with the timer turned on. The timer will measure the time to execute the query.

mclient --timer="clock" -u voc -d voc

Then we will create the sample table:

CREATE TABLE tblSample ( Letter VARCHAR(10) UNIQUE
                       , Number INT PRIMARY KEY
                       , Calendar DATE NOT NULL );  


INSERT INTO tblSample ( Letter, Number, Calendar )                  
               VALUES ( 'A', 1, '2025-02-01' )                       
                    , ( 'B', 2, '2025-02-02' )                       
                    , ( 'C', 3, '2025-02-03' );  


SELECT * FROM tblSample;   We can see that the query timing is 0.421 milliseconds.

Inserting Data with INSERT INTO

We can insert data through some application to MonetDB, by sending INSERT INTO statements. This will work great if we don' t load a lot of rows. If we are USING a lot of consecutive INSERT statements then we can have performance issues.

INSERT INTO tblSample ( Letter, Number, Calendar ) VALUES ( 'D', 4, '2024-02-04' );
··· 1.000.000 X ···
INSERT INTO tblSample ( Letter, Number, Calendar ) VALUES ( 'ZZZZZZZ', 1000004, '2240-02-04' );

We know that in MonetDB, we can insert several rows with the one INSERT statement. This will not save us, because we shouldn't use more than 20 rows per one INSERT STATEMENT. If we use more than that, we will decrease performance.

INSERT INTO tblSample ( Letter, Number, Calendar ) VALUES ( 'D', 4, '2024-02-05' )
                                                        , ( 'E', 5, '2024-02-06' )
                                                        ··· 17 X ··· 
                                                        , ( 'F', 6, '2024-02-07' );    --20 rows at most

We can improve performance by following these 5 advice:

  1. Disable autocommit.  Autocommit will commit the data after each INSERT statement. If we can avoid that, we can speed up things.
  2. We should prepare our statement. That means that our statement will be parsed and cached once. After that, each consecutive INSERT query will use the same statement, just with another parameters.
  3. Use batch processing. Instead of sending a million INSERT statements, we can sent 100 batches of 10.000 INSERT statements. This will reduce communication latency between application and MonetDB, it will reduce memory usage and will minimize locking of a table.
  4. We should disable optimizer. Optimizer can speed up more complex statements, but there is nothing that can be improved for the simple INSERT statement.
  5. We can temporarily disable table constraints like primary key, foreign key or unique. We can restore those constraints after the import.

SQL benchmark

We'll insert one row, with one INSERT INTO statement. Then we'll see if we can noticeably increase the speed by following the tips above.
INSERT INTO tblSample ( Letter, Number, Calendar ) VALUES ( 'D', 4, '2024-02-05' );

First, we will disable our optimizer.
SET sys.optimizer = 'minimal_pipe';

In order to delete constraints, we have to found out their names. We can do that from the system tables.

SELECT T.id, T.name, C.id, C.name, K.key_id, K.key_name
FROM sys.tables  T INNER JOIN sys.columns C      
      ON T.id  = C.table_id
INNER JOIN dependency_columns_on_keys K      
      ON C.id = K.column_id AND C.table_id = K.table_id
WHERE T.name IN ( 'tblsample' );

Our constraints will be temporarily removed.
ALTER TABLE tblSample DROP CONSTRAINT tblsample_number_pkey;
ALTER TABLE tblSample DROP CONSTRAINT tblsample_letter_unique;

We will not remove constraint "NOT NULL", because that constraint will not restrain performance.

START TRANSACTION;Now, we will start a transaction to disable autocommit.

Then we will prepare our statement. Zero is an ID of a prepared statement.
PREPARE INSERT INTO tblSample ( Letter, Number, Calendar ) VALUES ( ?, ?, ? );

After all this, we will again check the timing of our INSERT statement. We are now faster.
EXECUTE 0( 'E', 5, '2024-02-06');

The last thing is that we have to change
everything the way it was.
COMMIT;                                          -- finish transaction
DEALLOCATE PREPARE ALL;                          -- delete prepared statement
SET sys.optimizer = 'default_pipe';              -- turn on optimizer
ALTER TABLE tblSample ADD UNIQUE ( Letter );     -- bring back unique constraint
ALTER TABLE tblSample ADD PRIMARY KEY ( Number );-- bring back primary key constraint

Python Benchmark

We will now try INSERT with python script. In the blog post  "Connect to MonetDB from Python" we have already saw how to use python with MonetDB. Bellow is the script we will use now. This time we will insert 10 rows of data.

import pymonetdb
import time

connection = pymonetdb.connect(username="voc", password="voc", hostname="localhost", database="voc")
cursor = connection.cursor()
insert_statements = [
        "INSERT INTO tblSample (Letter, Number, Calendar) VALUES ('F', 6, '2024-02-06');",
        "INSERT INTO tblSample (Letter, Number, Calendar) VALUES ('G', 7, '2024-02-07');",
        "INSERT INTO tblSample (Letter, Number, Calendar) VALUES ('H', 8, '2024-02-08');",
        "INSERT INTO tblSample (Letter, Number, Calendar) VALUES ('I', 9, '2024-02-09');",
        "INSERT INTO tblSample (Letter, Number, Calendar) VALUES ('J', 10, '2024-02-10');",
        "INSERT INTO tblSample (Letter, Number, Calendar) VALUES ('K', 11, '2024-02-11');",
        "INSERT INTO tblSample (Letter, Number, Calendar) VALUES ('L', 12, '2024-02-12');",
        "INSERT INTO tblSample (Letter, Number, Calendar) VALUES ('M', 13, '2024-02-13');",
        "INSERT INTO tblSample (Letter, Number, Calendar) VALUES ('N', 14, '2024-02-14');",
        "INSERT INTO tblSample (Letter, Number, Calendar) VALUES ('O', 15, '2024-02-15');",
    ]
overall_start_time = time.time()
for i, sql in enumerate(insert_statements, start=1):
       cursor.execute(sql)
connection.commit()
overall_end_time = time.time()
total_time = overall_end_time - overall_start_time
print(f"\n⏳ Total execution time for all inserts: {total_time:.6f} seconds")
cursor.close()
connection.close()
Total execution time is 0.008114 seconds.

Python Benchmark With Accelerations

We can speed up our Python script by using all of the advice mentioned in the text above. This is how our python procedure now looks like:

import pymonetdb
import time

connection = pymonetdb.connect(username="voc", password="voc", hostname="localhost", database="voc")
cursor = connection.cursor()
overall_start_time = time.time()
cursor.execute("SET sys.optimizer = 'default_pipe';")
cursor.execute("ALTER TABLE tblSample DROP CONSTRAINT tblsample_number_pkey;")
cursor.execute("ALTER TABLE tblSample DROP CONSTRAINT tblsample_letter_unique;")
sql = "INSERT INTO tblSample (Letter, Number, Calendar) VALUES (%s, %s, %s);"
data = [    ('P', 16, '2024-02-16'),
            ('Q', 17, '2024-02-17'),
            ('R', 18, '2024-02-18'),
            ('S', 19, '2024-02-19'),
            ('T', 20, '2024-02-20'),
            ('U', 21, '2024-02-21'),
            ('V', 22, '2024-02-22'),
            ('W', 23, '2024-02-23'),
            ('X', 24, '2024-02-24'),
            ('Y', 25, '2024-02-25')
        ]
overall_start_time = time.time()
cursor.executemany(sql, data)
connection.commit()
overall_end_time = time.time()
cursor.execute("DEALLOCATE PREPARE ALL;")
cursor.execute("SET sys.optimizer = 'default_pipe';")
cursor.execute("ALTER TABLE tblSample ADD PRIMARY KEY ( Number )")
cursor.execute("ALTER TABLE tblSample ADD UNIQUE ( Letter );")
connection.commit()
total_time = overall_end_time - overall_start_time
print(f"\n⏳ Total execution time for all inserts: {total_time:.6f} seconds")
cursor.close()
connection.close()

We don't have to explicitly start a new transaction, pymonetdb will do that automatically.

Now our timing is 0.004584 seconds.

Timing

How to Measure Time of the Query Execution

mclient --timer="clock" -u voc -d vocTimer can be "none" (default), "clock" or "performance".

Bellow we can results for these three modes of the timer switch.

mclient --timer="none" -u voc -d voc mclient --timer="clock" -u voc -d voc mclient --timer="performance" -u voc -d voc

When we use "performance", we get 4 results. "SQL" is time used for parsing. "Opt" is time used for optimizing statement. "Run" is time used for running the statement. "Clk" is total time used.

Query History

Data about executed statements is kept in the two tables. Those two tables can be returned with functions " sys.querylog_catalog()" and "sys.querylog_calls()". In order to work with those tables we have to login as administrator.


mclient -u monetdb -d voc
Password is "monetdb".

Data saved in these two tables is persistent between sessions. We can use procedure "sys.querylog_empty()" to clear content from those two tables.

CALL sys.querylog_empty(); –procedures are started with the "call" keyword

In the current session we can start logging with the procedure "querylog_enable()".

CALL querylog_enable();

After that, I will run statement "SELECT * FROM voc.tblSample;" three times.

This will be, now, the content of the "querylog_catalog()" table.
SELECT * FROM querylog_catalog();

"Owner" is the user who started the query at "defined" time.

We can also read from the "querylog_calls()" table.

SELECT * FROM querylog_calls();

In this table we can see the "start" time and the "end" time of our query.  

We can stop logging or queries before the end of the session with:

CALL querylog_disable();

Threshold

Each time we enable logging, our logging tables will become bigger and bigger. This can make the search for a query troublesome. In order to control amount od statements that will be logged, we can use "threshold" argument.

CALL  querylog_enable(5000);

The threshold will limit the logged statements to only those whose execution time is longer than 5000 milliseconds. This allows us to perform profiling, to find the queries that are sucking up our resources the most.