Archive for the 'mysql' Category

Reusing an existing database connection with MDB2

Thursday, January 4th, 2007

This is a follow up to a question posted by Sam in my DB-2-MDB2 post. The question was if you can reuse an exisitng database connection you've already established and not have MDB2 creating a second connection.

When using a non-persistent connection

No worries in this case. No new connection will be established. As the PHP manual states:

If a second call is made to mysql_connect() with the same arguments, no new link will be established, but instead, the link identifier of the already opened link will be returned.

That is, if you don't set the fourth parameter to mysql_connect() to true. This parameter forces a new connection. BTW, in MDB2 if you do want to force a new connection, you have to set new_link in the DSN string to true

Bottom line, if you don't do anything special, the existing connection will be reused by MDB2. You can always verify that this is the case by calling phpinfo(INFO_MODULES); and looking in the "mysql" section.

When using a persistent connection

When using a persistent connection you have to do some additional steps to ensure that the same persistent connection is used by MDB2.

  • Tell MDB2 that you want a persistent connection - $mdb2->setOption('persistent', true);
  • Tell MDB2 which connection you want to use - $mdb2->connection = $link;, where $link is your existing connection
  • Set $mdb2->opened_persistent = true;

Here's an example:

<?php
// somewhere you've established a connection
$link = mysql_pconnect('localhost', 'root', '');
mysql_select_db('test', $link);
echo $link; // e.g. Resource id #5

// Create MDB2 object
require_once 'MDB2.php';
$dsn = 'mysql://root@localhost/test';
$mdb2 =& MDB2::factory($dsn);

// reuse your connection
$mdb2->setOption('persistent', true);
$mdb2->opened_persistent = true;
$mdb2->connection = $link;

// connect
$mdb2->connect();
echo $mdb2->connection; // Resource id #5

// check the "mysql" part to be sure
phpinfo(INFO_MODULES);
?>
 

MySQL Events scenario (and a funky subquery)

Friday, December 15th, 2006

Coming back to those MySQL events with a sort of more practical example. In case you've missed the previous post, events in brief: turns out that in MySQL starting from 5.1.6. you can setup and schedule cronjobs in the DB server itself. These are called events. An intro article and MySQL manual entry.

What these can be used for? OK, so you have a blog, which gets dugg and slashdotted often and is in the top 10 Google results for "Paris Hilton", in other words you get tons of visitors. You want it to load fast and you want visitor stats as real-time as possible, so that you can display "This posts was viewed 12345678 times". It would be best if you can get all data about a post with a simple super fast SELECT … WHERE id=123.

To keep track of the stats you have a table `post_stats` with two fields - post ID and timestamp of the visit. This way later you can run all sorts of reports by date, grouped my hour, week, etc. For performance reasons every visit to a post results in one quick SELECT from the `posts` table and one INSERT into the stats, that's it. Doing a JOIN and COUNT()-ing the huuuge `post_stats` table when selecting from `posts` is unthinkable for your busy blog. So you add a field to the `posts` table called `hits` that has the count, this way a simple select is enough. OK, but how do you update the `hits`. Doing an
UPDATE posts SET hits = hits + 1
is not a good idea, because you lock the table to do the update and doing it for every hit will put the other visitors on hold.

The solution would be to have a cron job that counts `post_stats` and updates the `hits` value in `posts`. And now you can do this within MySQL, no need to setup a Unix cronjob and to write a PHP script for this simple task. Let's see.

Connecting to our `blog` DB.

C:\Development\Apache\MySQL\bin>mysql -u root
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 1
Server version: 5.1.14-beta-community-nt MySQL Community Server (GPL)

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> \u blog
Database changed

Hmm, what do we have here?

mysql> SHOW TABLES;
+----------------+
| Tables_in_blog |
+----------------+
| post_stats     |
| posts          |
+----------------+
2 rows in set (0.00 sec)

Posts? What's in there?

mysql> SELECT * FROM posts;
+----+---------+--------+------+
| id | title   | body   | hits |
+----+---------+--------+------+
|  1 | title 1 | body 1 |    0 |
|  2 | title 2 | body 2 |    0 |
|  3 | title 3 | body 3 |    0 |
|  4 | title 4 | body 4 |    0 |
|  5 | title 5 | body 5 |    0 |
|  6 | title 6 | body 6 |    0 |
+----+---------+--------+------+
6 rows in set (0.04 sec)

The field `hits` was just added and it has all zeros.

What's in `post_stats`?

mysql> SHOW FIELDS FROM post_stats;
+---------+----------+------+-----+---------+-------+
| Field   | Type     | Null | Key | Default | Extra |
+---------+----------+------+-----+---------+-------+
| post_id | int(11)  | NO   | MUL |         |       |
| ts      | datetime | NO   | MUL |         |       |
+---------+----------+------+-----+---------+-------+
2 rows in set (0.00 sec)

Good, let's pretend we have visitors that cause INSERTs into the stats table.

mysql> INSERT INTO post_stats VALUES
    ->  (1,NOW()), (1,NOW()), (1,NOW());
Query OK, 3 rows affected (0.03 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> INSERT INTO post_stats VALUES
    ->  (2,NOW()), (2,NOW()), (2,NOW())
    -> , (2,NOW()), (2,NOW()), (2,NOW());
Query OK, 6 rows affected (0.02 sec)
Records: 6  Duplicates: 0  Warnings: 0

mysql> INSERT INTO post_stats VALUES
    ->  (3,NOW()), (3,NOW());
Query OK, 2 rows affected (0.03 sec)
Records: 2  Duplicates: 0  Warnings: 0

How many hits per post?

mysql> SELECT post_id, COUNT(*) FROM post_stats
    -> GROUP BY post_id;
+---------+----------+
| post_id | COUNT(*) |
+---------+----------+
|       1 |        3 |
|       2 |        6 |
|       3 |        2 |
+---------+----------+
3 rows in set (0.01 sec)

Now let's write up an UPDATE that updates the hits in `posts`. Hold your breath, this is fancy stuff - a subquery. An UPDATE that uses an aggregate SELECT that COUNTs. This way we update all posts in one shot.

mysql> UPDATE LOW_PRIORITY posts
    -> SET hits = (
    ->  SELECT COUNT(*)
    ->  FROM post_stats
    ->  WHERE posts.id = post_stats.post_id
    -> );
Query OK, 6 rows affected (0.02 sec)
Rows matched: 6  Changed: 6  Warnings: 0

LOW_PRIORITY because this an unobtrusive statement that is as polite as it is powerful and waits for everybody to finish with their SELECTs.

Does it work?

mysql> SELECT * FROM posts;
+----+---------+--------+------+
| id | title   | body   | hits |
+----+---------+--------+------+
|  1 | title 1 | body 1 |    3 |
|  2 | title 2 | body 2 |    6 |
|  3 | title 3 | body 3 |    2 |
|  4 | title 4 | body 4 |    0 |
|  5 | title 5 | body 5 |    0 |
|  6 | title 6 | body 6 |    0 |
+----+---------+--------+------+
6 rows in set (0.00 sec)

Kool! Let's reset the hits.

mysql> UPDATE posts SET hits = 0;
Query OK, 6 rows affected (0.03 sec)
Rows matched: 6  Changed: 6  Warnings: 0

Now let's create the event already. It's scheduled to start right away, to run once an hour (is that close enough to real-time? No? We can alter it after that) and to DO that funky update query every time it fires.

mysql> CREATE EVENT stats_update
    ->     ON SCHEDULE EVERY 1 HOUR
    ->     STARTS CURRENT_TIMESTAMP
    ->     DO
    ->       UPDATE LOW_PRIORITY posts
    ->       SET hits = (
    ->         SELECT COUNT(*)
    ->         FROM post_stats
    ->         WHERE posts.id = post_stats.post_id
    ->       );
Query OK, 0 rows affected (0.00 sec)

Let's see what we did.

mysql> SHOW EVENTS\G;
*************************** 1. row *********
            Db: blog
          Name: stats_update
       Definer: root@localhost
          Type: RECURRING
    Execute at: NULL
Interval value: 1
Interval field: HOUR
        Starts: 2006-12-14 20:29:17
          Ends: NULL
        Status: ENABLED
1 row in set (0.00 sec)

Now let's ALTER the event a little, to ease the testing.

mysql> ALTER EVENT stats_update
    ->   ON SCHEDULE every 1 MINUTE;
Query OK, 0 rows affected (0.00 sec)

If event_scheduler is not already ON by configuration, let's turn it ON.

mysql> SET GLOBAL event_scheduler = "ON";
Query OK, 0 rows affected (0.00 sec)

Initial state:

mysql> SELECT * FROM posts;
+----+---------+--------+------+
| id | title   | body   | hits |
+----+---------+--------+------+
|  1 | title 1 | body 1 |    0 |
|  2 | title 2 | body 2 |    0 |
|  3 | title 3 | body 3 |    0 |
|  4 | title 4 | body 4 |    0 |
|  5 | title 5 | body 5 |    0 |
|  6 | title 6 | body 6 |    0 |
+----+---------+--------+------+
6 rows in set (0.00 sec)

After a minute:

mysql> SELECT * FROM posts;
+----+---------+--------+------+
| id | title   | body   | hits |
+----+---------+--------+------+
|  1 | title 1 | body 1 |    3 |
|  2 | title 2 | body 2 |    6 |
|  3 | title 3 | body 3 |    2 |
|  4 | title 4 | body 4 |    0 |
|  5 | title 5 | body 5 |    0 |
|  6 | title 6 | body 6 |    0 |
+----+---------+--------+------+
6 rows in set (0.00 sec)

Let's insert some more stats data.

mysql> INSERT INTO post_stats VALUES
    ->  (4,NOW()), (4,NOW()), (4,NOW());
Query OK, 3 rows affected (0.05 sec)
Records: 3  Duplicates: 0  Warnings: 0

… and check after a while.

mysql> SELECT * FROM posts;
+----+---------+--------+------+
| id | title   | body   | hits |
+----+---------+--------+------+
|  1 | title 1 | body 1 |    3 |
|  2 | title 2 | body 2 |    6 |
|  3 | title 3 | body 3 |    2 |
|  4 | title 4 | body 4 |    3 |
|  5 | title 5 | body 5 |    0 |
|  6 | title 6 | body 6 |    0 |
+----+---------+--------+------+
6 rows in set (0.00 sec)

mysql> wicked! awesome!
    -> ;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that
corresponds to your MySQL server version for the right syntax to use near 'wicke
d! awesome!' at line 1
mysql> w00t! roxor
    -> ;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that
corresponds to your MySQL server version for the right syntax to use near 'w00t!
 roxor' at line 1
mysql>

In case you've missed the previous "I [heart] MySQL" posts:

 

MySQL events

Thursday, December 14th, 2006

The journey "Fall in love with MySQL (again)" continues from here (triggers) and here (views). Next stop - MySQL events.

A cold shower first - events are not available in versions < 5.1.6. so chances of using them any time soon are pretty slim. You need to upgrade to 5.1. (beta) if you want to play around with them.

OK, so what are the events. Just like cronjobs. Like triggers but instead of firing when something happens with one of the tables, they fire on timely basis. They can be executed only once, on a specific date or they can occur in some defined interval. You have flexibility of setting start date and end date too. A quick example.

To test the events I'll create a table called `log` that has only one field - a timestamp. Then I'll setup an event to log into this table every time it fires.

mysql> CREATE TABLE log (
    -> eventtime DATETIME
    -> );
Query OK, 0 rows affected (0.06 sec)
mysql> select NOW();
+---------------------+
| NOW()               |
+---------------------+
| 2006-12-13 22:47:12 |
+---------------------+
1 row in set (0.00 sec)

Let's create the event.

mysql> CREATE EVENT testevent
    -> ON SCHEDULE EVERY 1 MINUTE
    -> STARTS '2006-12-13 22:48:00'
    -> DO
    -> INSERT INTO `log` VALUES (NOW());
Query OK, 0 rows affected (0.00 sec)

To list events, I need SHOW EVENTS statement.

mysql> SHOW EVENTSG;
*************************** 1. row ***************************
            Db: blog
          Name: testevent
       Definer: root@localhost
          Type: RECURRING
    Execute at: NULL
Interval value: 1
Interval field: MINUTE
        Starts: 2006-12-13 22:48:00
          Ends: NULL
        Status: ENABLED
1 row in set (0.00 sec)

You can see what happened - I set a start date/time for the event, then set an interval 1 minute and finally gave it something to do when the event happens - an INSERT statement.

Let's see if it works.

mysql> SELECT * FROM log;
Empty set (0.00 sec)

Nothing? Hmm, let's try again.

mysql> SELECT * FROM log;
Empty set (0.00 sec)

WTF!? What time is it?

mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2006-12-13 22:49:20 |
+---------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM log;
Empty set (0.00 sec)

Still nothing, although a minute has passed. Ahaaa! I need to enable these events in MySQL! That's as easy as setting a variable.

mysql> SET GLOBAL event_scheduler = 'ON';
Query OK, 0 rows affected (0.01 sec)

Good, now let's see.

mysql> SELECT * FROM log;
+---------------------+
| eventtime           |
+---------------------+
| 2006-12-13 22:50:00 |
+---------------------+
1 row in set (0.00 sec)

Beauty! The event was fired!

Trying again in a few minutes:

mysql> SELECT * FROM log;
+---------------------+
| eventtime           |
+---------------------+
| 2006-12-13 22:50:00 |
| 2006-12-13 22:51:00 |
| 2006-12-13 22:52:00 |
+---------------------+
3 rows in set (0.00 sec)

Great. Let's drop it for now and figure out something more interesting to do with the events (in a follow-up post :D ).

mysql> DROP EVENT testevent;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW EVENTS;
Empty set (0.01 sec)
 

MySQL Views

Monday, December 11th, 2006

Continuing (from here) the "Discover MySQL's enterprise features" trip, let's check out the Views.

As the name suggests, a view is like a specific predefined way to look at a table or tables. You can create a View out of one table or you can JOIN a few tables. Back to the `blog` database example.

Creating a `posts` table and inserting some data in it.

mysql> CREATE TABLE posts (
    -> id INT(11) not null auto_increment primary key,
    -> title char(255),
    -> body text
    -> );
Query OK, 0 rows affected (0.09 sec)

mysql> INSERT INTO posts VALUES
    -> ('','title 1', 'body 1'),
    -> ('','title 2', 'body 2'),
    -> ('','title 3', 'body 3'),
    -> ('','title 4', 'body 4'),
    -> ('','title 5', 'body 5'),
    -> ('','title 6', 'body 6')
    -> ;
Query OK, 6 rows affected, 6 warnings (0.03 sec)
Records: 6  Duplicates: 0  Warnings: 6

Is the data there?

mysql> SELECT * FROM posts;
+----+---------+--------+
| id | title   | body   |
+----+---------+--------+
|  1 | title 1 | body 1 |
|  2 | title 2 | body 2 |
|  3 | title 3 | body 3 |
|  4 | title 4 | body 4 |
|  5 | title 5 | body 5 |
|  6 | title 6 | body 6 |
+----+---------+--------+
6 rows in set (0.00 sec)

Now we're interested in the last 5 posts (we want to create an RSS feed or display in the "see also" nav or something similar)

mysql> SELECT * FROM posts ORDER BY id DESC LIMIT 0,5;
+----+---------+--------+
| id | title   | body   |
+----+---------+--------+
|  6 | title 6 | body 6 |
|  5 | title 5 | body 5 |
|  4 | title 4 | body 4 |
|  3 | title 3 | body 3 |
|  2 | title 2 | body 2 |
+----+---------+--------+
5 rows in set (0.00 sec)

Good, it works. Now let's create a view using only ID and title.

mysql> CREATE VIEW last_posts AS
    -> SELECT id, title FROM posts ORDER BY id DESC LIMIT 0,5;
Query OK, 0 rows affected (0.01 sec)

Yep, that easy! Now we can SELECT from the view as if it was a normal table.

mysql> SELECT * FROM last_posts;
+----+---------+
| id | title   |
+----+---------+
|  6 | title 6 |
|  5 | title 5 |
|  4 | title 4 |
|  3 | title 3 |
|  2 | title 2 |
+----+---------+
5 rows in set (0.00 sec)

The similarity with a table goes even further, the view will SHOW up as a table…

mysql> SHOW TABLES;
+----------------+
| Tables_in_blog |
+----------------+
| last_posts     |
| posts          |
+----------------+
2 rows in set (0.00 sec)

(BTW, SHOW FIELDS FROM last_posts will also work)

… but you can always tell if a SHOWn table is a table or a view …

mysql> SHOW FULL TABLES;
+----------------+------------+
| Tables_in_blog | Table_type |
+----------------+------------+
| last_posts     | VIEW       |
| posts          | BASE TABLE |
+----------------+------------+
2 rows in set (0.00 sec)

… and you can get only views if you want to.

mysql> SHOW FULL TABLES WHERE Table_type = 'VIEW';
+----------------+------------+
| Tables_in_blog | Table_type |
+----------------+------------+
| last_posts     | VIEW       |
+----------------+------------+
1 row in set (0.00 sec)

The views you create will be displayed in phpMyAdmin as well.

A view can be used in JOINs as if it was a normal table.

mysql> SELECT last_posts.*, posts.body FROM last_posts
    -> LEFT JOIN posts ON
    -> last_posts.id = posts.id;
+----+---------+--------+
| id | title   | body   |
+----+---------+--------+
|  6 | title 6 | body 6 |
|  5 | title 5 | body 5 |
|  4 | title 4 | body 4 |
|  3 | title 3 | body 3 |
|  2 | title 2 | body 2 |
+----+---------+--------+
5 rows in set (0.00 sec)

mysql> SELECT last_posts.*, CONCAT(SUBSTRING(posts.body,1,3), '...')
    -> AS excerpt
    -> FROM last_posts
    -> LEFT JOIN posts ON
    -> last_posts.id = posts.id;
+----+---------+---------+
| id | title   | excerpt |
+----+---------+---------+
|  6 | title 6 | bod...  |
|  5 | title 5 | bod...  |
|  4 | title 4 | bod...  |
|  3 | title 3 | bod...  |
|  2 | title 2 | bod...  |
+----+---------+---------+
5 rows in set (0.01 sec)

And, as said in the beginnig, you can create a view from a table JOIN. Or even from joining a table and another view.

mysql> CREATE VIEW last_posts_details AS
    -> SELECT last_posts.*, CONCAT(SUBSTRING(posts.body,1,3), '...')
    -> AS excerpt
    -> FROM last_posts
    -> LEFT JOIN posts ON
    -> last_posts.id = posts.id;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM last_posts_details;
+----+---------+---------+
| id | title   | excerpt |
+----+---------+---------+
|  6 | title 6 | bod...  |
|  5 | title 5 | bod...  |
|  4 | title 4 | bod...  |
|  3 | title 3 | bod...  |
|  2 | title 2 | bod...  |
+----+---------+---------+
5 rows in set (0.00 sec)

mysql> muahaah-haha!
    -> ;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that
corresponds to your MySQL server version for the right syntax to use near 'muaha
ah-haha!' at line 1
mysql>

Although a view shows up in SHOW TABLES, you cannot drop it with DROP TABLE, you need DROP VIEW.

More in the MySQL manual's Views entry.

 

Quick MySQL 5 penetration survey

Sunday, December 10th, 2006

In a previous post about the MySQL triggers, I mentioned that inexpensive web hosts are now offering MySQL 5. This was based on ICDsoft's hosting plan, this is a (highly recommended, awesome support!) company I've been using for years and I was thinking that it represents the avegarge $50-60/year web hosting solutions offered worldwide, which is what most people use when they start a web site.

I decided to do a quick check what is the status of the MySQL 5 penetration today. Turns out, not so bright. Here are the results:

  • bluehost - 4
  • hostgator - 4
  • lunarpages - 5
  • ipowerweb - 4, on demand 5
  • anhosting - 4
  • hostmonster - 4
  • ixwebhosting - 4
  • startlogic - 4
  • powweb - 4
  • dreamhost - "slowly upgrading" to 5
  • site5 - 4, no specific plans for a 5 date
  • yahoo - 4
  • laughingsquid - 4

How was the survey conducted? Googling I found webhostingjury.com, than this webhostingjungle.com review site, and I checked the top ten hosts, all of them happened to be less than $10 a month. Then I added those that I know - site5 and dreamhost and finally those that are listed on wordpress.org/hosting. I can only hope that most hosts don't update their FAQ pages, but they do update their MySQL.

Interesting reads while doing the survey:

  • "We will upgrade … when cPanel upgrades" (source). I sent an email to cPanel® support to ask for their plans, waiting.
  • "We strongly recommend that you use the MySQL 5 database engine for your applications… better supported … more features" (ICDSoft's support)
  • "upgrade could cause some breakage of your application" (Dreamhost's wiki)

I guess that last one explains it best. Basically the same as what happened with hosts unwilling to upgrade to PHP5, because some older and popular free apps would be broken. I hope more hosts start offering both 4 and 5, like ICDSoft do, as a transition. And maybe the break-through will be when cPanel® upgrades, since this is probably what most hosts are using.

Anyway, the good news is that if you want good and cheap web host with MySQL 5 support, you have options, however limited.

Update:
I got a promising response from cPanel® support:

cPanel and WHM already supports MySQL 5 and in fact we recommend it for all new installations of our product.

However, the problem with MySQL 5 and why most web hosting companies hesitate to implement it has to do with PHP Scripts. Many PHP scripts have a habit of breaking under MySQL 5 and rather than having many angry customers, they'd rather stick with MySQL 4.1.

 

Performance tuning with MDB2

Saturday, December 9th, 2006

This is a follow-up to Lars' comment about the PEAR book. In the MDB2 chapter I showed an example how you can create custom debug handlers in MDB2 and then gave a suggestion about a useful application of this functionality for performance tuning. Basically the idea is that your custom debug handler collects all queries that are executed during the life of a given script. Then, once the script finishes execution, the debug handler reports the stats that it has collected. In the book, the example is how you count the number of times each distinct query is executed, this way you can spot problems caused by the OO abstraction. For example, say you have a come class Users that has a method loadUser(), which abstracts the database work. While debugging with the custom error handler, you might figure out that without noticing, you're calling this method in a few places and it makes the same repeating query(queries) over and over again. So you can now optimize/cache results and so on.

The suggestion I made in the book is that in addition to counting, you might want to try executing all SELECTs again, just to see how much time they take and you can execute them once again, prepending them with EXPLAIN to get some details on possible room for improvement.

Now here's one solution to this suggestion. What you can see in this script is:

  • Setting up MDB2
  • Declaring a custom debug handler class
  • "Attaching" it to the MDB2 instance
  • Registering it for execution at the end of each script
  • Testing it (creating a DB, table, some queries)

I hope you like it and try it out.

Here's the result of executing this script, you can see what you get back.

Room for improvement

Obviously, the method dumpInfo() can be improved. First, it can print out a nice table, instead of lazy print_r(). Then, it can include some logic, my idea is for it to "understand" the EXPLAIN results and to give you a hint by using colors, for exampe green background for queries that are OK, yellow for warnings and red for queries that definitelly need some work. Could be nice, no?

Test script

Kinda longish, but I hope I added enough comments. I also hope I didn't introduce any syntax errors while formatting it for posting here, chopping long lines, etc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
<?php

// PEAR error handling setup
require_once 'PEAR.php';
function pearError ($e)
{
  echo '<pre>';
  echo $e->getMessage().': '.$e->getUserinfo();
  echo '</pre>';
}
PEAR::setErrorHandling(
  PEAR_ERROR_CALLBACK,
  'pearError'
);

// creating MDB2 instance
require_once 'MDB2.php';
$dsn = 'mysql://root:test@localhost';
$mdb2 =& MDB2::factory($dsn);
$mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);

// The custom error handler
//
// It will collect all the queries being executed
// in the script, the collection is done by the
// collectInfo() method.
// Once the script finishes executing, we'll call
// the method executeAndExplain() which will
// execute all unique SELECTs once again
// in order to give us an info of how much time
// each query takes.
// Then executeAndExplain() will execute again
// all SELECTs, this time prepending an EXPLAIN
// so that we can get valuable
// optimization-related information
// Not only that but instead of simple EXPLAIN,
// we can use EXPLAIN EXTENDED and after that
// we can call SHOW WARNINGS -
// this will give us even more optimization hints
//
// http://dev.mysql.com/doc/refman/5.1/en/explain.html
// http://dev.mysql.com/doc/refman/5.1/en/show-warnings.html
//
class Explain_Queries
{
  // how many queries were executed
  var $query_count = 0;
  // which queries and their count
  var $queries = array();
  // results of EXPLAIN-ed SELECTs
  var $explains = array();
  // the MDB2 instance
  var $db = false;

  // constructor that accepts MDB2 reference
  function Explain_Queries(&$db) {
    $this->db = $db;
  }

  // this method is called on every query
  function collectInfo(
    &$db,
    $scope,
    $message,
    $is_manip = null)
  {
    // increment the total number of queries
    $this->query_count++;
    // the SQL is a key in the queries array
    // the value will be the count of how
    // many times each query was executed
    @$this->queries[$message]++;
  }

  // print the debug information
  function dumpInfo()
  {
    echo '<h3>Queries on this page</h3>';
    echo '<pre>';
    print_r($this->queries);
    echo '</pre>';
    echo '<h3>EXPLAIN-ed SELECTs</h3>';
    echo '<pre>';
    print_r($this->explains);
    echo '</pre>';
  }

  // the method that will execute all SELECTs
  // with and without an EXPLAIN and will
  // create $this->explains array of debug
  // information
  // SHOW WARNINGS will be called after each
  // EXPLAIN for more information
  function executeAndExplain() {

    // at this point, stop debugging
    $this->db->setOption('debug', 0);
    $this->db->loadModule('Extended');

    // take the SQL for all the unique queries
    $queries = array_keys($this->queries);
    foreach ($queries AS $sql) {

      // for all SELECTs…
      $sql = trim($sql);
      if (stristr($sql,"SELECT") !== false){
        // note the start time
        $start_time = array_sum(
            explode(" ", microtime())
        );
        // execute query
        $this->db->query($sql);
        // note the end time
        $end_time = array_sum(
            explode(" ", microtime())
        );
        // the time the query took
        $total_time = $end_time - $start_time;

        // now execute the same query with
        // EXPLAIN EXTENDED prepended
        $explain = $this->db->getAll(
          'EXPLAIN EXTENDED ' . $sql
        );

        $this->explains[$sql] = array();
        // update the debug array with the
        // new data from
        // EXPLAIN and SHOW WARNINGS
        if (!PEAR::isError($explain)) {
          $this->explains[$sql]['explain'] = $explain;
          $this->explains[$sql]['warnings'] =
               $this->db->getAll('SHOW WARNINGS');
        }

        // update the debug array with the
        // count and time
        $this->explains[$sql]['time'] = $total_time;
      }
    }
  }
}

// instance of the custom debug handler
$my_debug_handler = new Explain_Queries($mdb2);
// set debug option
$mdb2->setOption('debug', 1);
// set debug handler to the method that
// collects all queries
$mdb2->setOption(
  'debug_handler',
  array($my_debug_handler, 'collectInfo')
);
// register functions to be executed on shut down
// after the script has finished execution.
// Now that the show's over, it's the time to
// report what happened in this script db-access-wise
// First shutdown function executes the
// SELECTs again, the other one prints the results
register_shutdown_function(
  array($my_debug_handler, 'executeAndExplain')
);
register_shutdown_function(
  array($my_debug_handler, 'dumpInfo')
);

//
//
// At this point all MDB2 setup is done,
// time for the actual script to do something
//
//

// load the DB manager module
$mdb2->loadModule('Manager');

// drop database if it exists
// temporarily change the PEAR error handling
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
$mdb2->dropDatabase('test_db_explain');
PEAR::popErrorHandling();

// create and set a new database
$mdb2->createDatabase('test_db_explain');
$mdb2->setDatabase('test_db_explain');

// create table "events" from a definition array
// the table has event ID, name and date/time
$definition = array (
  'id' => array (
    'type' => 'integer',
    'unsigned' => 1,
    'notnull' => 1,
    'default' => 0,
  ),
  'name' => array (
    'type' => 'text',
    'length' => 255
  ),
  'datetime' => array (
    'type' => 'timestamp'
  )
);

$mdb2->createTable('events', $definition);

// create a primary key - the ID field
$definition = array (
  'primary' => true,
  'fields' => array (
    'id' => array()
  )
);
$mdb2->createConstraint(
  'events',
  'myprimekey',
  $definition
);

// load the class that has some static helper
// functions to work with MDB2's cross-RDBMS
// date format
MDB2::loadFile('Date');

// INSERT
// some data to insert into the events table
$data = array(
  // using MDB2-managed sequences
  'id'     => $mdb2->nextId('events'),
  'name'     => "Breakfast a Tiffany's",
  'datetime'   => MDB2_Date::unix2Mdbstamp(
    strtotime('Jan 15, 2007')
  )
);
// The "datetime" value shows how you can use
// any date format you wish as long as you're
// able to get a unix timestamp out of it
// In this case I'm using strtotime()
// Then there is a call to MDB2's date helper
// to get the MDB2 timestamp

// auto insert
// for the autoExecute() method we need to
// load the Extended module
$mdb2->loadModule('Extended');
$result = $mdb2->autoExecute(
  'events',
  $data,
  MDB2_AUTOQUERY_INSERT
);

//
// Time to SELECT something
//
// Using date helpers again
$start_date = MDB2_Date::date2Mdbstamp(0,0,0,12,31,1980);
$end_date   = MDB2_Date::date2Mdbstamp(0,0,0,12,31,2020);
$sql = 'SELECT * FROM %s WHERE %s > %s AND %s < %s';
$sql = sprintf(
  $sql,
  $mdb2->quoteIdentifier('events'),   // quote table name
  $mdb2->quoteIdentifier('datetime'), // quote field name
  $mdb2->quote($start_date, 'date'),  // quote data as date
  $mdb2->quoteIdentifier('datetime'), // quote field name
  $mdb2->quote($end_date,   'date')   // quote data as date
);
$res = $mdb2->getAll($sql); // execute

//
// * Bad practice code follows *
// Just some more inserts and selects, bad
// practice because these queries are not
// necessarily portable accross various RDBMS,
// lacking proper quoting and preparation
for ($i = 2; $i < 31; $i++) {
  $mdb2->query(
    'INSERT INTO events VALUES('
    . $i
    . ', "test event", "2005-05-05 00:00:00")'
  );
}
$res = $mdb2->getCol(
  'SELECT DISTINCT datetime FROM events'
);
$res = $mdb2->getRow(
  'SELECT * FROM events WHERE id
  IN (SELECT id FROM events WHERE id > 1)'
);
?>
 

MySQL triggers

Friday, December 8th, 2006

Now that even my favourite $50/year web hosts (example) are offering MySQL 5 and even recommending it over 4, why not take advantage of MySQL's "enterprise" features. Like triggers. OK, what's a trigger? It's something that happens as a result of something else. Clear? Yeah, maybe an example.

Say, as it often happens in life, you insert a row into a table. If there is a trigger that "listens" to inserts on this table, it will be, well… triggered.

Quick example. You have a blog app and a `blog` database that has a table called `posts`. You want to show some stats, like the total number of posts. Instead of using some (potentially heavy) queries that include some COUNT()-ing and SUM()-ing, you can "cache" the stats for faster display. So you create a table called `stats`. Let's see.

C:\Development\Apache\MySQL\bin>mysql -u root -p
Enter password: ****
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 240 to server version: 5.0.27-community-nt

Type 'help;' or 'h' for help. Type 'c' to clear the buffer.

mysql> u blog
Database changed
mysql> SHOW TABLES;
+----------------+
| Tables_in_blog |
+----------------+
| posts          |
| stats          |
+----------------+
2 rows in set (0.00 sec)

mysql> SHOW COLUMNS FROM posts;
+-------+------------------+------+-----+---------+----------------+
| Field | Type             | Null | Key | Default | Extra          |
+-------+------------------+------+-----+---------+----------------+
| id    | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| title | varchar(255)     | NO   |     |         |                |
| body  | text             | NO   |     |         |                |
+-------+------------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

mysql> SHOW COLUMNS FROM stats;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| key   | varchar(50) | NO   | PRI |         |       |
| value | int(11)     | NO   |     |         |       |
+-------+-------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql>

So far, so good. Initializing the stats…

mysql> INSERT INTO stats VALUES ('posts', 0);
Query OK, 1 row affected (0.02 sec)

Let's create the trigger!

mysql> CREATE TRIGGER mytrigger
    -> AFTER INSERT ON posts
    -> FOR EACH ROW
    -> UPDATE stats SET `value` = `value` + 1 WHERE `key` = 'posts';
Query OK, 0 rows affected (0.01 sec)

What does it mean? Well, after each row INSERT-ed in `posts`, it will update the `stats` table, incrementing the total number of posts. Does it work? But of course!

mysql> INSERT INTO posts VALUES ('', 'title1', 'body1');
Query OK, 1 row affected, 1 warning (0.04 sec)

mysql> INSERT INTO posts VALUES ('', 'title2', 'body2');
Query OK, 1 row affected, 1 warning (0.02 sec)

mysql> SELECT * FROM stats;
+-------+-------+
| key   | value |
+-------+-------+
| posts |     2 |
+-------+-------+
1 row in set (0.00 sec)

mysql> ha-haaaa!

To display the triggers you've created, use the SHOW TRIGGERS statement, as described in the MySQL manual. More on the triggers - using triggers, CREATE TRIGGER syntax

 

Enumerating with MySQL

Monday, May 15th, 2006

Here's how I got MySQL to do some enumerating for me, meaning increment a counter and add it to a select statement as in a numbered list. It is questionable if this type of functionality should be part of the "business" logic, as opposed to the display logic, but still, you never know.

What you need to do in this case is first to define the variable @inc using SET and you assign the default value of 0.
Then you include @inc as part of your SELECT statement. You can even use AS to nickname the variable expression.
Also as part of the SELECT you take care of incrementing the value in @inc.

Here's the thing:

SET @inc :=0;
SELECT
     @inc := @inc + 1 AS a, 
    `some_field` 
FROM 
    `some_table`;

Tested in MySQL versions (oldest to latest) 4.0.26, 4.1.10, 4.1.15 and 5.0.20

If anyone has an idea how to this in one shot, without executing the SET first, I'll be very interested. I played with IFNULL and IF in order to check if @inc was defined, and if not to define it, but hit a brick wall.

 

DB-2-MDB2

Saturday, February 4th, 2006

Intro

Recently I had to move an existing project from using PEAR::DB to PEAR::MDB2 - the new database abstraction layer. I took notes on the parts of the code I needed to change, I hope they can benefit someone who's doing the same. Many thanks go to Lukas Smith, the lead developer, he was always responding very fast to my reports and questions in the PEAR mailing list.

One thing to notice in MDB2 is that it tries not to do any unnecessary work and does many things only on demand. For example when you create an object, that doesn't mean that a connection is established. It is established only when you make the first real database access, a SELECT for example.

I assume you have an idea of PEAR::DB, since this posting illustrates a DB-to-MDB2 endeavour, but even if you don't, I hope the posting will still be useful as an intro to DB and MDB2.

Including the libs

So first off, including the libs (I assume you have PEAR on your machine).

require_once 'DB.php';
require_once 'MDB2.php';

One thing to note here is that installing MDB2 doesn't install any of the database wrappers. So if you use MySQL for example, you'd need to install it separately:
pear install MDB2_Driver_mysql-beta
in addition to
pear install MDB2-beta

MDB2 is now a stable release! So you can now remove the "-beta" monkier when installing the packages.

DSN

Next - the DSN string. It's the same for MDB2 as for DB.

$dsn = 'mysql://root@localhost/db2mdb2';

BTW, MDB2 can also accept an array of all the connection details, as opposed to a DSN string. And so does DB (Thanks for the clarification, Justin!)

Creating instances

$db =& DB::connect($dsn);
$mdb2 =& MDB2::factory($dsn);

MDB2 provides a factory method to create an instance. At this time no database connection is yet established. MDB2 also provides a singleton() method to create an instance.

Fetchmode

It is the same in both DB and MDB2, just note the prefix of the constant.

// set fetchmode
$db->setFetchMode(DB_FETCHMODE_ASSOC);
$mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC);

Simple SELECTs

There are the methods to select one row, one column, one cell and a bunch of records. DB prefixes them with get, while MDB2 uses query.

// select several records and shove them into an array
$all = $db->getAll('SELECT * FROM people');
$all = $mdb2->queryAll('SELECT * FROM people');

// select one cell
$one = $db->getOne('SELECT name FROM people WHERE id = 1');
$one = $mdb2->queryOne('SELECT name FROM people WHERE id = 1');

// one row
$row = $db->getRow('SELECT * FROM people WHERE id = 1');
$row = $mdb2->queryRow('SELECT * FROM people WHERE id = 1');

// a column
$col = $db->getCol('SELECT name FROM people');
$col = $mdb2->queryCol('SELECT name FROM people');

Quoting values

In DB, the suggested method to quote is quoteSmart(). In MDB2 it's quote() and it accepts a second parameter, which tells the type of the value to be quoted. If the second parameter is omitted, MDB2 will try to guess the type.

$one = $db->getOne(
         'SELECT name FROM people WHERE id = '
         . $db->quoteSmart(1)
       );

$one = $mdb2->queryOne(
         'SELECT name FROM people WHERE id = '
         . $db->quote(1, 'integer')
       );

Sequence tables

If you use sequence tables, both libs will provide you with a nextId() method:

echo $db->nextId('people_db');
echo $mdb2->nextId('people_mdb2');

The only difference is that when DB creates a sequence table (with one field and one value), the name of the field is id, where MDB2 will use sequence. If you're translating an existing project to MDB2 like me, and the sequence tables are already created by DB, you have the option of renaming this field in the database for all sequence tables, or you can set an MDB2 option and you're good to go.

$mdb2->setOption('seqcol_name','id');

Auto execute

Say you have the data:

$data = array('id' => 5, 'name' => 'Cameron');

To auto-insert it using DB, you'd do:

$db->autoExecute('people', $data, DB_AUTOQUERY_INSERT);

For MDB2, the auto execution is probably not considered an often-used feature, so it's not in the base instance. You need to load an additional module to have access to it:

$mdb2->loadModule('Extended');

Now you can

$mdb2->autoExecute('people', $data, MDB2_AUTOQUERY_INSERT);

The above will work in PHP5 only. In PHP4, due to the limited support of object overloading (Thanks again to Lukas for clarifying this!), you'd need to do:

$mdb2->extended->autoExecute('people', $data, MDB2_AUTOQUERY_INSERT);

Note that the second way will also work in PHP5.

Prepared statements

In DB:

$statement = $db->prepare('INSERT INTO people VALUES (?, ?)');
$data = array(6, 'Chris');
$db->execute($statement, $data);
$db->freePrepared($statement);

In MDB it's almost the same, only that the statement becomes an object and you call its (as opposed to MDB2 main object's) methods to execute and to release memory:

$statement = $mdb2->prepare('INSERT INTO people VALUES (?, ?)');
$data = array(7, 'Dave');
$statement->execute($data);
$statement->free();

Execute multiple

The same applies to executing a statement with with multiple "rows" of data from an array. executeMultiple() is in the Extended MDB2 module, so you need to load it:

DB:

$statement = $db->prepare('INSERT INTO people VALUES (?, ?)');
$data = array(
    array(8, 'James'),
    array(9, 'Cliff')
);

$db->executeMultiple($statement, $data);
$db->freePrepared($statement);

MDB2:

$statement = $mdb2->prepare('INSERT INTO people VALUES (?, ?)');
$data = array(
    array(10, 'Kirk'),
    array(11, 'Lars')
);

$mdb2->loadModule('Extended');
$mdb2->extended->executeMultiple($statement, $data);

$statement->free();

Transactions

In DB:

$db->autoCommit();
$result = $db->query('DELETE people'); // will cause an error

if (PEAR::isError($result)) {
    $db->rollback();     //echo 'rollback';
} else {
    $db->commit();     //echo 'commit';

}

In MDB2 you have to check if transactions are supported in your RDBMS. Then during the transaction, you can always check "Am I in transaction?"

if ($mdb2->supports('transactions')) {
    $mdb2->beginTransaction();

}
$result = $mdb2->query('DELETE people');
if (PEAR::isError($result)) {
    if ($mdb2->in_transaction) {
        $mdb2->rollback();         // echo 'rollback';
    }
} else {
    if ($mdb2->in_transaction) {
        $mdb2->commit();         // echo 'commit';
    }
}

Example script

You can download a script that has the examples above and play with it. Here's also the sql file to recreate the database:

Any questions or comments are welcome ;) Thanks for reading!