0

I've got some problems with the following code in my PHP/MySQL application. It works well and takes about 3-4 seconds, but first execution (per session) takes about 2 minutes. I think because there's some automated-cache mechanisms. There's a method to speed up the first execution? I've got the root access on this MySQL server, but I can't modify the DB structure.

The application is visible here http://hotel.crosstourpoint.eu/, and the slow script is that http://hotel.crosstourpoint.eu/ajax/html_hotel_details.php. To check out it search something in the main box. Example: type "Milano" and click "Cerca", click on the option "Milano", select start date and end date ("Giorno di arrivo - Giorno di partenza") and click again "Cerca". The Info (I) icon opens the slow script with an ajax call.

Thanks.

Code

<?php

// open mysqli connection
$mysqli = new mysqli('localhost', 'hotelbeds', 'import', 'hotelbeds');
if (mysqli_connect_errno()) { printf("Connect failed: %s\n", mysqli_connect_error()); exit(); }

$code   = (int) $_REQUEST['code'];
$h      = array();

// hotel position
$request = '
    SELECT
        NAME, LATITUDE, LONGITUDE
    FROM
        HOTELS
    WHERE
        HOTELCODE = ' . $code . '   ';

$stmt = $mysqli->prepare($request);
$stmt->execute();
$stmt->bind_result( $h['name'], $h['latitude'], $h['longitude'] );
$stmt->fetch();
$stmt->close();
unset($stmt);
unset($request);

// loading descriptions
$request = '
    SELECT
        HotelFacilities, HotelHowToGetThere, HotelComments
    FROM
        HOTEL_DESCRIPTIONS
    WHERE
        HotelCode = ' . $code . '
        AND
        LanguageCode = "' . HB_LANGCODE . '"    '; 

$stmt = $mysqli->prepare($request);
$stmt->execute();
$stmt->bind_result( $h['facilities'], $h['hotelhowtogetthere'], $h['comments'] );
$stmt->fetch();
$stmt->close();
unset($stmt);
unset($request);

// hotel images
$request = '
    SELECT
        IMAGEPATH
    FROM
        HOTEL_IMAGES
    WHERE
        HOTELCODE = ' . $code . '   '; 
$stmt = $mysqli->prepare($request);
$stmt->execute();
$stmt->bind_result( $imagepath );
$images = array();
while( $stmt->fetch() ) array_push( $images, $imagepath );
$stmt->close();
unset($stmt);
unset($request);

Tables structure

HOTELS: about 50.000 rows

HOTELS_DESCRIPTIONS about 600.000 rows

HOTELS_IMAGES: about 180.000 rows

CREATE TABLE `HOTELS` (
  `HOTELCODE` varchar(8) collate utf8_spanish_ci NOT NULL,
  `NAME` varchar(50) collate utf8_spanish_ci NOT NULL,
  `CATEGORYCODE` varchar(5) collate utf8_spanish_ci NOT NULL,
  `DESTINATIONCODE` varchar(3) collate utf8_spanish_ci NOT NULL,
  `ZONECODE` varchar(8) collate utf8_spanish_ci default NULL,
  `CHAINCODE` varchar(5) collate utf8_spanish_ci default NULL,
  `LICENCE` varchar(15) collate utf8_spanish_ci default NULL,
  `LATITUDE` varchar(45) collate utf8_spanish_ci default NULL,
  `LONGITUDE` varchar(45) collate utf8_spanish_ci default NULL,
  PRIMARY KEY  (`HOTELCODE`),
  KEY `HOTELS_CATEGORIES_FK` (`CATEGORYCODE`),
  KEY `HOTELS_ZONES_FK` (`ZONECODE`),
  CONSTRAINT `HOTELS_ZONES_FK` FOREIGN KEY (`ZONECODE`) REFERENCES `ZONES` (`ZONECODE`) ON DELETE CASCADE,
  CONSTRAINT `HOTELS_CATEGORIES_FK` FOREIGN KEY (`CATEGORYCODE`) REFERENCES `CATEGORIES` (`CategoryCode`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci COMMENT='Hotels'

CREATE TABLE `HOTEL_DESCRIPTIONS` (
  `HotelCode` varchar(8) collate utf8_spanish_ci NOT NULL,
  `LanguageCode` varchar(3) collate utf8_spanish_ci NOT NULL,
  `HotelFacilities` varchar(2000) collate utf8_spanish_ci default NULL,
  `HotelLocationDescription` varchar(2000) collate utf8_spanish_ci default NULL,
  `HotelRoomDescription` varchar(2000) collate utf8_spanish_ci default NULL,
  `HolelSportDescription` varchar(2000) collate utf8_spanish_ci default NULL,
  `HotelMealsDescription` varchar(2000) collate utf8_spanish_ci default NULL,
  `HotelPaymentMethods` varchar(2000) collate utf8_spanish_ci default NULL,
  `HotelHowToGetThere` varchar(2000) collate utf8_spanish_ci default NULL,
  `HotelComments` varchar(2000) collate utf8_spanish_ci default NULL,
  PRIMARY KEY  (`HotelCode`,`LanguageCode`),
  KEY `HOTEL_DESCRIPTIOS_LANGUAGES_FK` (`LanguageCode`),
  CONSTRAINT `HOTEL_DESCRIPTIOS_LANGUAGES_FK` FOREIGN KEY (`LanguageCode`) REFERENCES `LANGUAGES` (`LANGUAGECODE`) ON DELETE CASCADE,
  CONSTRAINT `HOTEL_DESCRIPTIOS_HOTELS_FK` FOREIGN KEY (`HotelCode`) REFERENCES `HOTELS` (`HOTELCODE`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci COMMENT='Hotel_Descriptions'

CREATE TABLE `HOTEL_IMAGES` (
  `HOTELCODE` varchar(8) collate utf8_spanish_ci NOT NULL,
  `IMAGECODE` varchar(3) collate utf8_spanish_ci NOT NULL,
  `ORDER_` varchar(5) collate utf8_spanish_ci NOT NULL,
  `VISUALIZATIONORDER` varchar(5) collate utf8_spanish_ci default NULL,
  `IMAGEPATH` varchar(2000) collate utf8_spanish_ci NOT NULL,
  PRIMARY KEY  (`HOTELCODE`,`IMAGECODE`,`ORDER_`),
  KEY `HOTEL_IMAGES_IMAGE_TYPES_FK` (`IMAGECODE`),
  CONSTRAINT `HOTEL_IMAGES_IMAGE_TYPES_FK` FOREIGN KEY (`IMAGECODE`) REFERENCES `IMAGE_TYPES` (`IMAGECODE`) ON DELETE CASCADE,
  CONSTRAINT `HOTEL_IMAGES_HOTELS_FK` FOREIGN KEY (`HOTELCODE`) REFERENCES `HOTELS` (`HOTELCODE`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci COMMENT='Hotels_Images'

Additional informations (edit):

Ubuntu 64bit 8.04.2 Linux hostname

2.6.24-23-server #1 SMP Wed Apr 1 22:14:30 UTC 2009 x86_64 GNU/Linux

mysql Ver 14.12 Distrib 5.0.51a, for debian-linux-gnu (x86_64) using readline 5.2

innodb_buffer_pool_size 512

Explains:

>> EXPLAIN SELECT NAME, LATITUDE, LONGITUDE  FROM HOTELS WHERE HOTELCODE = 136224
id | select_type | table | type | possibile_keys | key key_len | ref | rows
1 SIMPLE HOTELS ALL PRIMARY 47373 Using where

>> EXPLAIN  SELECT HotelFacilities, HotelHowToGetThere, HotelComments FROM   HOTEL_DESCRIPTIONS WHERE HotelCode = 136224 AND LanguageCode = "ITA"
id | select_type | table | type | possibile_keys | key key_len | ref | rows
1 SIMPLE HOTEL_DESCRIPTIONS ref PRIMARY,HOTEL_DESCRIPTIOS_LANGUAGES_FK HOTEL_DESCRIPTIOS_LANGUAGES_FK 11 const 75378 Using where

>> EXPLAIN SELECT IMAGEPATH FROM HOTEL_IMAGES WHERE HOTELCODE = 136224
id | select_type | table | type | possibile_keys | key key_len | ref | rows
1 SIMPLE HOTEL_IMAGES ALL PRIMARY 158786    Using where
Fabio Mora
  • 5,339
  • 2
  • 20
  • 30
  • Run the EXPLAIN SELECT before your queries and post the output. Also, what are the settings of InnoDB in your MySQL instance? What's the OS and what's the value of innodb_buffer_pool_size variable? You could also use 1 query to obtain all that info instead of 3 queries. There aren't that many rows and you're doing PK lookups, however your primary key is big so that might impact performance a little bit. – Michael J.V. May 05 '11 at 12:31
  • Thanks. I added the requested infos. I'll try to use 1 only query. – Fabio Mora May 05 '11 at 12:57
  • have the table got an index on HOTELCODE ? – Saic Siquot May 05 '11 at 13:24
  • 1
    @Luis Siquot: there aren't indexes on HOTELCODE in tables. – Fabio Mora May 05 '11 at 14:04
  • 1
    im afraid that is the big problem. – Saic Siquot May 05 '11 at 14:38

3 Answers3

2

You say you cannot change the DB structure, this is most unfortunate because I have mostly DB-structure advice to give...

Join the queries
Your queries are about as tight as they're going to get.

You might want to put them all in one big query like:

$request = 'SELECT         
  h.NAME, h.LATITUDE, h.LONGITUDE
  ,hd.HotelFacilities, hd.HotelHowToGetThere, hd.HotelComments
  ,hi.ImagePath
FROM HOTELS 
INNER JOIN HOTEL_DESCRIPTIONS hd ON (h.Hotelcode = hd.Hotelcode)
INNER JOIN HOTEL_IMAGES hi ON (h.Hotelcode = hi.Hotelcode)
WHERE HD.Hotelcode = "' .$code. '" AND HD.LanguageCode = "' . HB_LANGCODE . '"  ';

Optimize the cache
This will make sure more of them fit into the query-cache.
The delay on the first query is caused by a cold query cache,
see: http://www.mysqlperformanceblog.com/2006/07/27/mysql-query-cache/
For more info on this. (Note that the article states that prepared statements are not cached, this is no longer true; as of 5.1.17 prepared statements are cached.)

A few suggestions on table structure

Primary key
Make field hotelcode integer. Make it an autoincrement for table hotel only. Hotelcode is an int (see: $code = (int) $_REQUEST['code'];)
So why make it a varchar?

Use char(x) for small values of x, not varchar
Don't use a varchar(3), use a char(3). The varchar(3) is variable length and takes extra processing time to figure out the length of the string, with only 3 chars there's no real space saving. I'd recommend using char(x) for x < 8.

Foreign keys
Try and use only integers for foreign keys, they work faster and foreign keys are usually linked to some other table's primary key (PK), which should be integer anyway (see point above).

InnoDB and primary keys
In InnoDB the primary key is attached to all indexes, so making the primary key short speeds up every insert, update and select.
From: http://dev.mysql.com/doc/refman/5.0/en/innodb-physical-record.html

Each secondary index record also contains all the primary key fields defined for the clustered index key that are not in the secondary index. If any of these primary key fields are variable length, the record header for each secondary index will have a variable-length part to record their lengths, even if the secondary index is defined on fixed-length columns.

PK should not be a composite key
For tables that have a composite primary key, kill that and replace it with a autoincrement integer primary key (named id or something like that). This is because innoDB stores a copy of the PK in every index B+tree (see point above).
Replace the current primary key with a unique key, to make sure that no hotel has 2 descriptions in the same language etc..

Johan
  • 74,508
  • 24
  • 191
  • 319
  • Sensible advice for tuning the queries, however these have nothing to do with the issue of why "first execution (per session) takes about 2 minutes". – symcbean May 05 '11 at 13:24
  • +1 I agree with all except the last point on not having a composite key. – ypercubeᵀᴹ May 05 '11 at 13:26
  • Your big your query (at the moment) works a bit faster. Unfortunately I can't modify the DB structure because this is maintained by an external service that provides small differential updates each day. Thank you for interesting post. – Fabio Mora May 05 '11 at 14:46
  • @ypercube, note I said "no composity **primary** key", because in InnoDB the PK gets stored with each and every secondary key. – Johan May 05 '11 at 14:50
1

Johan has provided some good advice on query tuning, however as per my comment, this has nothing to do with why "first execution (per session) takes about 2 minutes". Even without these measures the queries should be taking fractions of a second - and there's nothing in the code you've shown which would explain why the slow behaviour is specific to "the first execution (per session)".

What does your slow query log show?

While I'm usually grateful that people cut out the unnecessary stuff from code posted here, in this case I think you've cut out whatever is actually causing the slowness.

What makes you think that the slowness is specific to the first execution per session?

I think you should be profiling the rest of the code in the script to find the problem.

symcbean
  • 47,736
  • 6
  • 59
  • 94
  • Thank you first. I added the application URL in the question and the cutted-out code. I held the cleanup code to avoid external problems. When I write "first execution" I mean the first access after a long time (30 or more minutes) on the web-application by someone, I think because MySQL keep open the connection between PHP5 and the MySQLd. I could be wrong, I'm not a MySQL expert. – Fabio Mora May 05 '11 at 13:53
0

I suggest just a workaround:
as soon as the session is started, run an ajax request that fires the same query with any $code value, just to wake up the database, so when the user arives to the need of the query it will take 3-4 seconds to execute

Saic Siquot
  • 6,513
  • 5
  • 34
  • 56
  • I suggest ajax because it do not afect loading time of any "normal" page, and even if the user jumps to other page, the ajax request was started, the db was waked up, and it does't mater to recive it response. – Saic Siquot May 05 '11 at 13:22
  • I have already tried this solution but the time between the first access request and the result is too small. Thanks. – Fabio Mora May 05 '11 at 13:56
  • do it anyway fired with homepage's onLoad – Saic Siquot May 05 '11 at 14:39
  • this will tax the MySQL database to no end, making **all** queries slower, it think it's better to dig out the slow-query-log and see what's the problem.... – Johan May 05 '11 at 14:52
  • the slow-query log was turned off. I turned it on just now. – Fabio Mora May 05 '11 at 15:04