G_H
asked on
MySQL PHP Speed Improvement (Bad DB design?)
I have been asked to look at the improving the "speed" performance of a PHP page. The page in question lists the 200 most recent orders. It was taking about 40 seconds to run the script, which is just plain too long.
What I have found so far:
1. The page defaults to 200 orders (I will change this to 50 or 100 when I get FTP access).
2. The structure has 2 important tables (orders and order_products). "orders" has a Primany Key. "order_products" has no keys at all.
3. The orders table had 23199 rows. I removed lots of old orders to reduce this to 9078. This improved load time to 15 to 20 seconds.
4. The table definitions are:
So, to the REAL question:
How can I improve speed, with minimal changes to the PHP code (there is tons of it). My best guess is that I need to get some sort of key on the order_products table.
GH
What I have found so far:
1. The page defaults to 200 orders (I will change this to 50 or 100 when I get FTP access).
2. The structure has 2 important tables (orders and order_products). "orders" has a Primany Key. "order_products" has no keys at all.
3. The orders table had 23199 rows. I removed lots of old orders to reduce this to 9078. This improved load time to 15 to 20 seconds.
4. The table definitions are:
# Table "orders_products" DDL
CREATE TABLE `orders_products` (
`orderId` int(11) NOT NULL default '0',
`productOptionId` int(11) NOT NULL default '0',
`colourId` int(11) NOT NULL default '0',
`cost` float NOT NULL default '0',
`quantity` int(11) NOT NULL default '0'
) ENGINE=MyISAM DEFAULT CHARSET=latin1
# Table "orders" DDL
CREATE TABLE `orders` (
`orderId` int(11) NOT NULL auto_increment,
`statusId` int(11) NOT NULL default '0',
`deliveryId` int(11) NOT NULL default '0',
`notes` text NOT NULL,
`specialInstructions` text NOT NULL,
`createdOn` datetime NOT NULL default '0000-00-00 00:00:00',
`createdBy` varchar(100) NOT NULL default '',
`modifiedOn` datetime NOT NULL default '0000-00-00 00:00:00',
`modifiedBy` varchar(100) NOT NULL default '',
`totalOrderAmount` float NOT NULL default '0',
`deliveryMethodId` int(11) NOT NULL,
PRIMARY KEY (`orderId`)
) ENGINE=MyISAM AUTO_INCREMENT=25879 DEFAULT CHARSET=latin1
So, to the REAL question:
How can I improve speed, with minimal changes to the PHP code (there is tons of it). My best guess is that I need to get some sort of key on the order_products table.
GH
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
ASKER
Sorry, re-posting, hoping to get everything in my mind into this box this time...
Because the fields orders_products.productOpt ionId and orders_products.orderId are not unique, should I be trying to index them individually, or as a collective (multi-column index)?
GH
Because the fields orders_products.productOpt
GH
Whenever there is a performance problem in a PHP / MySQL web site, the issues are always found in the I/O subsystem and that is almost always the data base. You can use EXPLAIN SELECT to get some vision into what your SELECT queries are doing. You can use ALTER TABLE to add indexes as Brian suggested (and it appears from the CREATE TABLE statements that the indexes are missing). Those would be my first changes. Next I would scan the code for the word, "SELECT" and make sure that you do not have a query doing something like SELECT * FROM... WHERE NOT IN (SELECT * FROM... WHERE NOT IN (... etc. These kinds of queries cause table scan - all of the data has to be inspected multiple times to complete the query. Table scans are a "bad dog" thing in data base access.
If you need to time the script or part of it, this little stopwatch class may be helpful.
If you need to time the script or part of it, this little stopwatch class may be helpful.
<?php // RAY_oop_stopwatch.php
error_reporting(E_ALL);
// DEMONSTRATE A SCRIPT TIMER FOR ALL OR PART OF A SCRIPT PHP 5+
// MAN PAGE http://us.php.net/manual/en/function.microtime.php
class StopWatch
{
protected $a, $z;
public function __construct()
{
$this->a = array();
$this->z = array();
}
// A METHOD TO CAPTURE A START TIME
public function start($name='TIMER')
{
$this->a[$name] = microtime(TRUE);
}
// A METHOD TO CAPTURE AN END TIME
public function stop($name='ALL')
{
if ($name == 'ALL')
{
foreach ($this->a as $name => $start_time)
{
if (!isset($this->z[$name])) $this->z[$name] = microtime(TRUE);
}
}
else
{
$this->z[$name] = microtime(TRUE);
}
}
// A METHOD TO READ OUT THE TIMER(S)
public function readout($m=1000, $eol=PHP_EOL)
{
$str = NULL;
foreach ($this->a as $name => $start_time)
{
$str .= $name;
if (!isset($this->z[$name]))
{
$str .= " IS STILL RUNNING";
}
else
{
$lapse_time = $this->z[$name] - $start_time;
$lapse_msec = $lapse_time * $m;
$lapse_echo = number_format($lapse_msec, 1);
$str .= " $lapse_echo";
}
$str .= $eol;
}
return $str;
}
}
// DEMONSTRATE THE USE -- INSTANTIATE THE STOPWATCH OBJECT
$sw = new Stopwatch;
// SET STOPWATCH NAMES
$go = 'GOOGLE ONLY';
$gy = 'GOOGLE AND YAHOO!';
$yo = 'YAHOO! ONLY';
// START SOME TIMERS
$sw->start($go);
$sw->start($gy);
// PERFORM SOME ACTIVITY THAT YOU WANT TO TIME
$page = 'http://google.com';
$html = file_get_contents($page);
// STOP ONE OF THE STOPWATCHES AND START THE OTHER
$sw->stop($go);
$sw->start($yo);
// PERFORM SOME OTHER ACTIVITY THAT YOU WANT TO TIME
$page = 'http://yahoo.com';
$html = file_get_contents($page);
// REPORT THE STOPWATCHES CONTENT (TWO WILL BE INCOMPLETE)
echo nl2br($sw->readout());
// STOP ALL OF THE REMAINING STOPWATCHES
$sw->stop();
// REPORT THE STOPWATCHES CONTENT AGAIN
echo nl2br($sw->readout());
"Because the fields orders_products.productOpt ionId and orders_products.orderId are not unique, should I be trying to index them individually, or as a collective (multi-column index)?"
Just index them as single indexes. Multi-column indexes have some restrictions that could cause you problems. See http://dev.mysql.com/doc/refman/5.0/en/multiple-column-indexes.html
Just index them as single indexes. Multi-column indexes have some restrictions that could cause you problems. See http://dev.mysql.com/doc/refman/5.0/en/multiple-column-indexes.html
ASKER
@bportlock
I was reading that as you posted!
Is the "KEY" in the structure above (Post #34925231) the same as "INDEX"?
Sorry, but I use Navicat to handle my MySQL databases, and that is what it has given me if I want to have an "Index" which is not a Primary Key...
GH
I was reading that as you posted!
Is the "KEY" in the structure above (Post #34925231) the same as "INDEX"?
Sorry, but I use Navicat to handle my MySQL databases, and that is what it has given me if I want to have an "Index" which is not a Primary Key...
GH
"Is the "KEY" in the structure above (Post #34925231) the same as "INDEX"?"
Yes.
Yes.
One last note, then I'll sign off on this Q. Have a look at this query:
SELECT * FROM orders
INNER JOIN order_products ON orders.id = order_products.id
WHERE statusId = 123
Is statusId a unique column? If so, you can add LIMIT 1 to the end of the query and MySQL will stop work when it finds the match. Often this little change alone, adding a LIMIT, will make great improvements in performance.
And if you're looking for the first 200 elements of the data base, LIMIT 200 would seem to make sense.
SELECT * FROM orders
INNER JOIN order_products ON orders.id = order_products.id
WHERE statusId = 123
Is statusId a unique column? If so, you can add LIMIT 1 to the end of the query and MySQL will stop work when it finds the match. Often this little change alone, adding a LIMIT, will make great improvements in performance.
And if you're looking for the first 200 elements of the data base, LIMIT 200 would seem to make sense.
ASKER
@Ray_Paseur
This is all built dynamically, and there are loads of bits like that, but very few are unique.
@bportlock
OK, My understanding has grown!
I have now finally got the FTP access, I can see the SELECT:
I guess every item in the INNER JOIN's should really be indexed. Currently only those which are PK's are...
This is all built dynamically, and there are loads of bits like that, but very few are unique.
@bportlock
OK, My understanding has grown!
I have now finally got the FTP access, I can see the SELECT:
SELECT
o.orderId,
s.description,
o.createdOn,
CONCAT(c.title,' ',c.firstName,' ',c.lastName) as 'customerName',
totalOrderAmount
FROM
orders o
INNER JOIN orders_status s ON s.statusId = o.statusId
INNER JOIN orders_customers oc ON oc.orderId = o.orderId
INNER JOIN customers c ON c.customerId = oc.customerId
GROUP BY
o.orderId
ORDER BY
o.createdOn DESC LIMIT 0, 50
I guess every item in the INNER JOIN's should really be indexed. Currently only those which are PK's are...
Ensure that all these columns have indexes (keys)
INNER JOIN orders_status s ON s.statusId = o.statusId
INNER JOIN orders_customers oc ON oc.orderId = o.orderId
INNER JOIN customers c ON c.customerId = oc.customerId
You should see a massive improvement in speed. If you don't get a big improvement then run the query with the word EXPLAIN in fron of it and see where the table scans are
http://dev.mysql.com/doc/refman/5.0/en/explain.html
INNER JOIN orders_status s ON s.statusId = o.statusId
INNER JOIN orders_customers oc ON oc.orderId = o.orderId
INNER JOIN customers c ON c.customerId = oc.customerId
You should see a massive improvement in speed. If you don't get a big improvement then run the query with the word EXPLAIN in fron of it and see where the table scans are
http://dev.mysql.com/doc/refman/5.0/en/explain.html
ASKER
@bportlock
Thank you for taking the time to teach me a bit more about MySQL!
@Ray_Paseur
Thanks for your hep too.
Thank you for taking the time to teach me a bit more about MySQL!
@Ray_Paseur
Thanks for your hep too.
So, things are running better?
ASKER
I have not made the changes yet, but I am sure this is the issue.
I'll post again when the changes have been made and evaluated. I have got to wait for a "quiet time" to play with the live DB...
GH
I'll post again when the changes have been made and evaluated. I have got to wait for a "quiet time" to play with the live DB...
GH
ASKER
I still have no FTP access, I will look at the SQL SELECTS when I get it.
Would this be better:
Open in new window
GH