Friday, April 25, 2008

Integrated auth in Firebird 2.1

Just a quick sample of integrated windows auth in Firebird 2.1
Logon to my Firebird server as a user with administrative privileges on the machine:

C:\Programmi\Firebird\Firebird_2_1\bin>isql localhost:employee
Database: localhost:employee
SQL>

So I'm logged. let's check who am I for Firebird:

SQL> select current_user from rdb$database;

USER

===============================================================================

SYSDBA


SQL>

That's right, as an administrator on the machine, I'm SYSDBA on the database, now let's try with an unprivileged user I have on this machine:
I'll open another command line client as user postgres (guess what other database is installed on this machine?)

C:\Programmi\Firebird\Firebird_2_1\bin>runas /user:postgres cmd
Immettere la password per postgres:
Tentativo di avvio di cmd come utente "AB-2346789223445\postgres" ...

C:\Programmi\Firebird\Firebird_2_1\bin>

Now I have another command line window open

C:\Programmi\Firebird\Firebird_2_1\bin>isql localhost:employee
Database: localhost:employee
SQL> select current_user from rdb$database;

USER

===============================================================================

AB-2346789223445\POSTGRES


SQL>

Ok, I'm in as postgres, let's try a select

SQL> select * from country;

COUNTRY CURRENCY
=============== ==========
USA Dollar
England Pound
Canada CdnDlr
Switzerland SFranc
Japan Yen
Italy Lira
France FFranc
Germany D-Mark
Australia ADollar
Hong Kong HKDollar
Netherlands Guilder
Belgium BFranc
Austria Schilling
Fiji FDollar

SQL>

This works as expected, table country is available for SYSDBA and for PUBLIC, my current user is in PUBLIC by default.
Let's check a different table, on which PUBLIC has no grants

SQL> select * from tbl_stock_warehouse;
Statement failed, SQLCODE = -551
no permission for read/select access to TABLE TBL_STOCK_WAREHOUSE
SQL>

My request has been rejected as expected, great!
If this is not enough for you a lot of improvements in the user management area are currently in the works for Firebird 2.5 and in derived databases like RedSoft's one which will hopefully be merged in the official Firebird server.

Labels: , , , ,

Saturday, April 19, 2008

Loading data from files

Having already blogged about loading data from flat files to MySQL, it's time to post a similar case for PostgreSQL, as the manual seems to lack a real life example ...

First of all the table to be loaded
CREATE TABLE target
(
code character(3) NOT NULL,
"name" character varying(50) NOT NULL,
amount numeric,
CONSTRAINT pk_1 PRIMARY KEY (code)
)
WITH (OIDS=FALSE);
as you can see one of the column names is a reserved word! Bad practice, but it's here to add some spice.

Now the data
code;name;amount
12A;Pippo;12.5
13B;Topolino;45
23D;Pluto;NULL
as you can see it's delimited by ";", has an header row and contains NULLs, which we want to preserve in our target table.
This file is named 2beloaded.csv and it's placed in c:\

After logging in to PostgreSQL and creating the table with the statement provided above it's time to load it
copy
--target table with columns listed
target(code, "name", amount)
from
--source file
'C:/2beloaded.csv'
with
--list of options
csv
--switches on csv mode
header
--ignores first line as an header line
delimiter ';'
--sets the delimiter
null as 'NULL'
--preserves nulls by telling the database what represents a NULL

Quite nice, isn't it?
Hope this helps.

Labels: , , , ,

Sunday, April 06, 2008

Two basic indexing tips ...

Here are two basic tips for proper indexing ...

  1. Don't mess with datatypes, too often people refer to an attribute defining it as one datatype in a table and as another in different tables, this actually prevents index usage in joins (forget about FKs for this time ;)) See an example here. You could declare a function based index as a workaround, but why don't we all try to make it right?
  2. Put indexes where the database can really use them, if a table is to be fully scanned anyway, it's indexes are unlikely to be used, unless you can compare those index entries with other indexes on tables that won't be fully scanned. Ordering is another game ;). See here for an example.
Easy, isn't it? But these mistakes are still very common ...

Labels: , , , , ,

Sunday, March 30, 2008

PgAgent - Installation and usage

Prompted by the usual Devshed's post, this time about pgAgent, here I am with a small "tutorial" (or maybe just a small collection of screenshots).
PgAgent is a great little tool, much like SQLServer's SQLAgent or the built in scheduling abilities of Oracle and MySQL (which is a much nicer solution IMHO), what's weak is the installation and the docs, I mean, why the pgAdminIII installer does ask for pgAgent installation and doesn't really install it?
All this just to have a few files placed on my Hd?
So, first of all, pgAdmin guys please add to your installer the ability to actually install pgAgent, or even better have the PostgreSQL guys add it to the main server distribution, where it belongs.

Now for the actual installation let's have a look at the docs, the main steps for installation are (I'm working on Windows):
  1. Database setup
    1. Work within the postgres standard database
    2. Check if pl/pgsql is installed
    3. If not, install it
    4. Connect to the postgres database and run a sql script provided with the installation that creates all required objects, that script is named pgagent.sql and it's kept in the [$Program files]\pgAdmin III\[$version]\Scripts folder
    5. After this, if you connect with pgAdminIII you'll see a new branch in the objects tree, named "Job"
  2. Service installation
    1. The service has to be installed from the command line, like this

C:\Documents and Settings\Paolo>"C:\Programmi\pgAdmin III\1.8\pg
Agent" INSTALL pgAgent -u postgres -p postgres hostaddr=127.0.0.1 dbname=pgadmin
user=postgres

C:\Documents and Settings\Paolo>
    1. Start the just installed service
BE AWARE OF THE SECURITY RISKS of this kind of installation

Labels: , ,

Monday, March 24, 2008

Shoot in the foot

I've just finished reading two recent blog posts about new query optimizations in the upcoming MySQL 6.0, it's all fine and dandy but ...
Looking at Correlated semi-join subqueries and PostgreSQL by S. Petrunia we can read something like
Quote:
The first thing we did was to take a look at PostgreSQL as it is easily available and seems to have at least decent subquery handling (or even better than decent, I have not heard much complaints).
Does this mean their benchmark is PostgreSQL? I mean, Oracle and SQLServer are easily available too ... not to mention the comparison any reader can see a few lines below between MySQL's and PostgreSQL's explain plans.

The other amusing read is New optimizer features in MySQL 6.0 again by Sergey where you can find an interesting speed comparison which (might) boil down to

Server version Wallclock time # of reads
MySQL 5.1 12 min 9,001,055
MySQL 5.2 1.8 sec 153,008
MySQL 5.2 no_semijoin 25 sec 7,651,215
PostgreSQL 8.2.5 0.1 sec 2,413

Very interesting!

But anyone interested in query optimization should take the time to check the whole paper, extremely informative (and much of it's content is good for any database too)!!!

Ok, now LET THE FLAME WAR START ;-)

BTW MySQL 5.1 is not in production currently, while PostgreSQL 8.2.5 has just been superseded by the new PostgreSQL 8.3.1

Labels: , , ,

Sunday, March 16, 2008

Fasten your seatbelts ...

... or how to safely run data manipulation statements in your database.
Reading posts on Devshed's forums I sometime notice people doing maintenance work on their data without any safety net apart from occasional ages old backups ;-).
Anyway I think there's no need for a restore if you just issued the wrong update query, I mean, transactions are here for this, it's just a matter of educating people ...
Say you have a database structure like this

  1. CREATE TABLE `test`.`users` (
  2. `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
  3. `username` VARCHAR(45) NOT NULL,
  4. PRIMARY KEY (`id`)
  5. )
  6. ENGINE = InnoDB;

and

  1. CREATE TABLE `test`.`agent` (
  2. `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
  3. `actiontime` DATETIME NOT NULL,
  4. PRIMARY KEY (`id`),
  5. CONSTRAINT `FK_agent_1` FOREIGN KEY `FK_agent_1` (`id`)
  6. REFERENCES `users` (`id`)
  7. ON DELETE CASCADE
  8. ON UPDATE CASCADE
  9. )
  10. ENGINE = InnoDB;

not very nice, it's just an example, but notice that I'm also using real foreign keys (real because these are InnoDB tables).
That's because of the many database engines only InnoDB supports transactions (BDB used to support them but has been discontinued and Falcon is not ready for prime time).

Now let's see how this tables content looks like

mysql> select * from users;
+----+----------+
| id | username |
+----+----------+
| 1 | a |
| 2 | b |
| 3 | f |
+----+----------+
3 rows in set (0.00 sec)

mysql> select * from agent;
+----+---------------------+
| id | actiontime |
+----+---------------------+
| 1 | 2008-03-16 11:21:38 |
| 2 | 2008-03-16 11:21:41 |
| 3 | 2008-03-16 11:21:44 |
+----+---------------------+
3 rows in set (0.00 sec)

Nothing much really, now, say we want to change the actiontime of agent 'f', but we are unshure of the synthax and we don't want to damage our data, that's how we should do:

mysql> set autocommit='OFF';
Query OK, 0 rows affected (0.00 sec)

Actually we have just fastened our seatbelts!!! Autocommit is off and the database will make permanent changes on request only, now on with the query

mysql> update agent a inner join users u on a.id = u.id set a.actiontime=now() where
u.username = 'f';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

Looks ok, but let's check what's really happened

mysql> select * from agent;
+----+---------------------+
| id | actiontime |
+----+---------------------+
| 1 | 2008-03-16 11:21:38 |
| 2 | 2008-03-16 11:21:41 |
| 3 | 2008-03-16 11:27:07 |
+----+---------------------+
3 rows in set (0.00 sec)

Ok, we updated the right row!
At the same time users are querying the database, but, as we haven't committed work, they see the "old" data (note, this depends on transaction isolation level)

mysql> select * from test.agent;
+----+---------------------+
| id | actiontime |
+----+---------------------+
| 1 | 2008-03-16 11:21:38 |
| 2 | 2008-03-16 11:21:41 |
| 3 | 2008-03-16 11:21:44 |
+----+---------------------+
3 rows in set (0.00 sec)

You see? But, as we are satisfied with changes, it's time to commit work, to actually make those changes permanent.

mysql> commit;
Query OK, 0 rows affected (0.03 sec)

At this time users other than the one which actually changed data will start seeing the updated version, see it by yourself

mysql> select * from test.agent;
+----+---------------------+
| id | actiontime |
+----+---------------------+
| 1 | 2008-03-16 11:21:38 |
| 2 | 2008-03-16 11:21:41 |
| 3 | 2008-03-16 11:21:44 |
+----+---------------------+
3 rows in set (0.00 sec)
commit by the other user happened between
these selects

mysql> select * from test.agent;
+----+---------------------+
| id | actiontime |
+----+---------------------+
| 1 | 2008-03-16 11:21:38 |
| 2 | 2008-03-16 11:21:41 |
| 3 | 2008-03-16 11:27:07 |
+----+---------------------+
3 rows in set (0.00 sec)

This was an easy case, but transactions are a safety net because you can rollback changes, see it in action

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from users;
+----+----------+
| id | username |
+----+----------+
| 1 | a |
| 2 | b |
| 3 | f |
+----+----------+
3 rows in set (0.00 sec)

mysql> delete from users;
Query OK, 3 rows affected (0.00 sec)

mysql> select * from users;
Empty set (0.00 sec)

:eek: I just deleted all my precious users!!!

mysql> rollback;
Query OK, 0 rows affected (0.02 sec)

mysql> select * from users;
+----+----------+
| id | username |
+----+----------+
| 1 | a |
| 2 | b |
| 3 | f |
+----+----------+
3 rows in set (0.00 sec)

But I had my safety net :-D

Here you can find the thread that prompted me to write this post.

I seem to hit a bug (severe or not) every time I post, this is no exception, see bug 35318, but again and again MySQL's support team showed it's dedication, solving and closing it in less than 8h!!!

Labels: , , , ,

Sunday, March 09, 2008

Simulating procedural logic

Sometimes I see people having great difficulties in describing how to fetch data for a report.
They are unable to reason by sets and tend to describe things in procedural terms.
Here I'm posting a small example of how you can write a query that reproduces that procedural reasoning and lets the optimizer do the work of translating it into efficient SQL.
Say someone has a table structure like this, a main table named guys holding their id and name and two tables bads_attributes and goods_attributes, if you are a bad guy your attributes will be in the bads_attributes table and vice versa.
Looks ugly? It is, but you'll find it around, sooner or later :-(
And it's not even the worst case scenario, confronted with a similar requirement I've heard that stored procedures where proposed as the ideal solution.
The table structure:

DROP TABLE IF EXISTS `test`.`guys`;
CREATE TABLE `test`.`guys` (
`guy_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`guy_name` varchar(45) NOT NULL,
PRIMARY KEY (`guy_id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `test`.`bads_attributes`;
CREATE TABLE `test`.`bads_attributes` (
`attribute_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`guy_id` int(10) unsigned NOT NULL,
`attribute_name` varchar(45) NOT NULL,
PRIMARY KEY (`attribute_id`,`guy_id`),
KEY `FK_bads_attributes_1` (`guy_id`),
CONSTRAINT `FK_bads_attributes_1` FOREIGN KEY (`guy_id`) REFERENCES `guys` (`guy_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `test`.`goods_attributes`;
CREATE TABLE `test`.`goods_attributes` (
`attribute_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`guy_id` int(10) unsigned NOT NULL,
`attribute_name` varchar(45) NOT NULL,
PRIMARY KEY (`attribute_id`,`guy_id`),
KEY `FK_goods_attributes_1` (`guy_id`),
CONSTRAINT `FK_goods_attributes_1` FOREIGN KEY (`guy_id`) REFERENCES `guys` (`guy_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;



Now let's retrieve those attributes with a query that mimics the logic described above

  1. SELECT
  2. g.guy_id,
  3. g.guy_name,
  4. CASE
  5. WHEN EXISTS (SELECT 0 FROM bads_attributes b WHERE b.guy_id = g.guy_id)
  6. THEN (SELECT group_concat(b.attribute_name separator ', ') FROM bads_attributes b WHERE b.guy_id = g.guy_id GROUP BY b.guy_id)
  7. WHEN EXISTS (SELECT 0 FROM goods_attributes a WHERE a.guy_id = g.guy_id)
  8. THEN (SELECT group_concat(a.attribute_name separator ', ') FROM goods_attributes a WHERE a.guy_id = g.guy_id GROUP BY a.guy_id)
  9. ELSE 'no attributes for this guy'
  10. END right_attributes
  11. FROM
  12. guys g

This goes after the reasoning described above, if you are found in the bads_attributes then your data is retrieved from there, the same for goods_attributes.
Output is

+--------+----------+----------------------------+
| guy_id | guy_name | right_attributes |
+--------+----------+----------------------------+
| 1 | Paolo | Fichissimo |
| 2 | Carlo | Fico |
| 3 | Ciccio | Ugly, Uglier |
| 4 | Bender | Ugliest |
| 5 | New kid | no attributes for this guy |
+--------+----------+----------------------------+
5 rows in set (0.02 sec)

It's explain plan is

+----+--------------------+-------+------+-----------------------+-----------------------+---------+---------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+-------+------+-----------------------+-----------------------+---------+---------------+------+-------------+
| 1 | PRIMARY | g | ALL | NULL | NULL | NULL | NULL | 5 | |
| 5 | DEPENDENT SUBQUERY | a | ref | FK_goods_attributes_1 | FK_goods_attributes_1 | 4 | test.g.guy_id | 1 | Using where |
| 4 | DEPENDENT SUBQUERY | a | ref | FK_goods_attributes_1 | FK_goods_attributes_1 | 4 | test.g.guy_id | 1 | Using index |
| 3 | DEPENDENT SUBQUERY | b | ref | FK_bads_attributes_1 | FK_bads_attributes_1 | 4 | test.g.guy_id | 1 | Using where |
| 2 | DEPENDENT SUBQUERY | b | ref | FK_bads_attributes_1 | FK_bads_attributes_1 | 4 | test.g.guy_id | 1 | Using index |
+----+--------------------+-------+------+-----------------------+-----------------------+---------+---------------+------+-------------+
5 rows in set (0.00 sec)

Well, it could be worse, note that I'm using InnoDB tables and I've declared foreing keys, whose indexes are picked up by the optimizer.

Let's a more SQLish version of this query

  1. SELECT
  2. g.guy_id,
  3. g.guy_name,
  4. COALESCE(
  5. group_concat(b.attribute_name separator ', '),
  6. group_concat(a.attribute_name separator ', ')
  7. ) right_attributes
  8. FROM
  9. guys g LEFT OUTER JOIN bads_attributes b
  10. ON g.guy_id = b.guy_id
  11. LEFT OUTER JOIN goods_attributes a
  12. ON g.guy_id = a.guy_id
  13. GROUP BY
  14. g.guy_id,
  15. g.guy_name

Output is the same

+--------+----------+---------------------------+
| guy_id | guy_name | right_attributes |
+--------+----------+---------------------------+
| 1 | Paolo | Fichissimo |
| 2 | Carlo | Fico |
| 3 | Ciccio | Ugly, Uglier |
| 4 | Bender | Ugliest |
| 5 | New kid | NULL |
+--------+----------+---------------------------+
5 rows in set (0.00 sec)

And the explain plan

+----+-------------+-------+------+-----------------------+-----------------------+---------+---------------+------+----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+-----------------------+-----------------------+---------+---------------+------+----------------+
| 1 | SIMPLE | g | ALL | NULL | NULL | NULL | NULL | 5 | Using filesort |
| 1 | SIMPLE | b | ref | FK_bads_attributes_1 | FK_bads_attributes_1 | 4 | test.g.guy_id | 1 | |
| 1 | SIMPLE | a | ref | FK_goods_attributes_1 | FK_goods_attributes_1 | 4 | test.g.guy_id | 1 | |
+----+-------------+-------+------+-----------------------+-----------------------+---------+---------------+------+----------------+
3 rows in set (0.00 sec)

Well it looks certainly better, but there's a nasty filesort (this is a very small data set) which should be checked against a much larger dataset.

Doing the same on Firebird 2.1 beta 2 (which supports LIST() a function similar to MySQL's group_concat()) leads to:

  1. SELECT
  2. g.guy_id,
  3. g.guy_name,
  4. CASE
  5. WHEN EXISTS (SELECT 0 FROM bads_attributes b WHERE b.guy_id = g.guy_id)
  6. THEN (SELECT CAST(list(b.attribute_name) AS varchar(5000)) FROM bads_attributes b WHERE b.guy_id = g.guy_id
  7. GROUP BY b.guy_id
  8. )
  9. WHEN EXISTS (SELECT 0 FROM goods_attributes a WHERE a.guy_id = g.guy_id)
  10. THEN (SELECT CAST(list(a.attribute_name) AS varchar(5000)) FROM goods_attributes a WHERE a.guy_id = g.guy_id
  11. GROUP BY a.guy_id
  12. )
  13. ELSE 'no attributes for this guy'
  14. END right_attributes
  15. FROM
  16. guys g

and

  1. SELECT
  2. g.guy_id,
  3. g.guy_name,
  4. CAST(
  5. COALESCE(
  6. list(b.attribute_name),
  7. list(a.attribute_name)
  8. ) AS varchar(5000)) right_attributes
  9. FROM
  10. guys g
  11. LEFT OUTER JOIN bads_attributes b
  12. ON g.guy_id = b.guy_id
  13. LEFT OUTER JOIN goods_attributes a
  14. ON g.guy_id = a.guy_id
  15. GROUP BY
  16. g.guy_id,
  17. g.guy_name


Note that both queries need an explicit cast as list's results in Firebird are blobs.
The respective explain plans show that the set oriented one is better.

Prepare time: 00:00:00.
Field #01: GUYS.GUY_ID Alias:GUY_ID Type:INTEGER
Field #02: GUYS.GUY_NAME Alias:GUY_NAME Type:STRING(45)
Field #03: . Alias:RIGHT_ATTRIBUTES Type:STRING(5000)
PLAN (B INDEX (FK_BADS_ATTRIBUTES_1))
PLAN (B ORDER FK_BADS_ATTRIBUTES_1 INDEX (FK_BADS_ATTRIBUTES_1))
PLAN (A INDEX (FK_GOODS_ATTRIBUTES_1))
PLAN (A ORDER FK_GOODS_ATTRIBUTES_1 INDEX (FK_GOODS_ATTRIBUTES_1))
PLAN (G NATURAL)


Executing...
Done.
116 fetches, 0 marks, 0 reads, 0 writes.
0 inserts, 0 updates, 0 deletes, 21 index, 5 seq.
Delta memory: 54852 bytes.
Execute time: 00:00:00.

and

Prepare time: 00:00:00.
Field #01: GUYS.GUY_ID Alias:GUY_ID Type:INTEGER
Field #02: GUYS.GUY_NAME Alias:GUY_NAME Type:STRING(45)
Field #03: . Alias:RIGHT_ATTRIBUTES Type:STRING(5000)
PLAN JOIN (SORT (JOIN (G NATURAL, B INDEX (FK_BADS_ATTRIBUTES_1))), A INDEX (FK_GOODS_ATTRIBUTES_1))


Executing...
Done.
66 fetches, 0 marks, 0 reads, 0 writes.
0 inserts, 0 updates, 0 deletes, 9 index, 5 seq.
Delta memory: 38168 bytes.
Execute time: 00:00:00.

Labels: , , , ,