views:

573

answers:

3

EDIT:

I've narrowed my mysql wait timeout down to this line:

    IF @resultsFound > 0 THEN
        INSERT INTO product_search_query (QueryText, CategoryId) VALUES (keywords, topLevelCategoryId);
    END IF;

Any idea why this would cause a problem? I can't work it out!

Hi guys, I've written a stored proc to search for products in certain categories, due to certain constraints i came across, i was unable to do what i wanted (limiting, but whilst still returning the total number of rows found, with sorting, etc..)

It's meant splits up a string of category Ids, from 1,2,3 in to a temporary table, then builds the full-text search query based on sorting options and limits, executes the query string and then selects out the total number of results.

Now, I know I'm no MySQL guru, very far from it, i've got it working, but i keep getting time outs with product searches etc.. so I'm thinking this may be causing some kind of problem?

Does anyone have any ideas how I can tidy this up, or even do it in a much better way that I probably don't know about?

Thanks..

DELIMITER $$

DROP PROCEDURE IF EXISTS `product_search` $$
CREATE DEFINER=`root`@`localhost` PROCEDURE `product_search`(keywords text, categories text, topLevelCategoryId int, sortOrder int, startOffset int, itemsToReturn int)
BEGIN

declare foundPos tinyint unsigned;
declare tmpTxt text;
declare delimLen tinyint unsigned;
declare element text;
declare resultingNum int unsigned;

drop temporary table if exists categoryIds;
create temporary table categoryIds
(
`CategoryId` int
) engine = memory;


set tmpTxt = categories;

set foundPos = instr(tmpTxt, ',');
while foundPos <> 0 do
set element = substring(tmpTxt, 1, foundPos-1);
set tmpTxt = substring(tmpTxt, foundPos+1);
set resultingNum = cast(trim(element) as unsigned);

insert into categoryIds (`CategoryId`) values (resultingNum);

set foundPos = instr(tmpTxt,',');
end while;

if tmpTxt <> '' then
insert into categoryIds (`CategoryId`) values (tmpTxt);
end if;

CASE
  WHEN sortOrder = 0 THEN
    SET @sortString = "ProductResult_Relevance DESC";
  WHEN sortOrder = 1 THEN
    SET @sortString = "ProductResult_Price ASC";
  WHEN sortOrder = 2 THEN
    SET @sortString = "ProductResult_Price DESC";
  WHEN sortOrder = 3 THEN
    SET @sortString = "ProductResult_StockStatus ASC";
END CASE;

SET @theSelect = CONCAT(CONCAT("
    SELECT SQL_CALC_FOUND_ROWS
      supplier.SupplierId as Supplier_SupplierId,
      supplier.Name as Supplier_Name,
      supplier.ImageName as Supplier_ImageName,

      product_result.ProductId as ProductResult_ProductId,
      product_result.SupplierId as ProductResult_SupplierId,
      product_result.Name as ProductResult_Name,
      product_result.Description as ProductResult_Description,
      product_result.ThumbnailUrl as ProductResult_ThumbnailUrl,
      product_result.Price as ProductResult_Price,
      product_result.DeliveryPrice as ProductResult_DeliveryPrice,
      product_result.StockStatus as ProductResult_StockStatus,
      product_result.TrackUrl as ProductResult_TrackUrl,
      product_result.LastUpdated as ProductResult_LastUpdated,

      MATCH(product_result.Name) AGAINST(?) AS ProductResult_Relevance
    FROM
      product_latest_state product_result
    JOIN
      supplier ON product_result.SupplierId = supplier.SupplierId
    JOIN
      category_product ON product_result.ProductId = category_product.ProductId
    WHERE
      MATCH(product_result.Name) AGAINST (?)
    AND
      category_product.CategoryId IN (select CategoryId from categoryIds)
    ORDER BY
      ", @sortString), "
    LIMIT ?, ?;
  ");

    set @keywords = keywords;
    set @startOffset = startOffset;
    set @itemsToReturn = itemsToReturn;

    PREPARE TheSelect FROM @theSelect;
    EXECUTE TheSelect USING @keywords, @keywords, @startOffset, @itemsToReturn;

    SET @resultsFound = FOUND_ROWS();

    SELECT @resultsFound as 'TotalResults';

    IF @resultsFound > 0 THEN
        INSERT INTO product_search_query (QueryText, CategoryId) VALUES (keywords, topLevelCategoryId);
    END IF;

END $$

DELIMITER ;

Any help is very very much appreciated!

+3  A: 

There is little you can do with this query.

Try this:

  1. Create a PRIMARY KEY on categoryIds (categoryId)

    • Make sure that supplier (supplied_id) is a PRIMARY KEY

    • Make sure that category_product (ProductID, CategoryID) (in this order) is a PRIMARY KEY, or you have an index with ProductID leading.

Update:

If it's INSERT that causes the problem and product_search_query in a MyISAM table the issue can be with MyISAM locking.

MyISAM locks the whole table if it decides to insert a row into a free block in the middle of the table which can cause the timeouts.

Try using INSERT DELAYED instead:

IF @resultsFound > 0 THEN
    INSERT DELAYED INTO product_search_query (QueryText, CategoryId) VALUES (keywords, topLevelCategoryId);
END IF;

This will put the records into the insertion queue and return immediately. The record will be added later asynchronously.

Note that you may lose information if the server dies after the command is issued but before the records are actually inserted.

Update:

Since your table is InnoDB, it may be an issue with table locking. INSERT DELAYED is not supported on InnoDB.

Depending on the nature of the query, DML queries on InnoDB table may place gap locks which will lock the inserts.

For instance:

CREATE TABLE t_lock (id INT NOT NULL PRIMARY KEY, val INT NOT NULL) ENGINE=InnoDB;
INSERT
INTO    t_lock
VALUES
        (1, 1),
        (2, 2);

This query performs ref scans and places the locks on individual records:

-- Session 1
START TRANSACTION;
UPDATE  t_lock
SET     val = 3
WHERE   id IN (1, 2)

-- Session 2
START TRANSACTION;
INSERT
INTO    t_lock 
VALUES  (3, 3)
-- Success

This query, while doing the same, performs a range scan and places a gap lock after key value 2, which will not let insert key value 3:

-- Session 1
START TRANSACTION;
UPDATE  t_lock
SET     val = 3
WHERE   id BETWEEN 1 AND 2

-- Session 2
START TRANSACTION;
INSERT
INTO    t_lock 
VALUES  (3, 3)
-- Locks
Quassnoi
Thanks for your answer, it's not actually running slowly though..It just seems to do it once in a while, i just ran it and it took 0.0014s so no problem with actual speed?
Michael Baldry
This will run slowly if you will search for the keywords that occur frequently. Say, if you search for the keyword `'professional'` and `1,000,000` products contain this keyword, the engine will need to retrieve all these records and sort them according to your sort order. This could be improved with a usual predicate (not fulltext) by creating a composite index, but, unfortunately, `MySQL` does not allow mixing fulltext and non-fulltext keys in one index.
Quassnoi
I see, well, for my purposes at the moment, I think general speed of this won't be an issue. Thanks
Michael Baldry
`@Michael Baldry`: if the speed will be an issue, you can consider using `Sphinx` for your `FULLTEXT` queries. It is much faster than `MySQL` 's native `FULLTEXT` indexing engine.
Quassnoi
Quassnoi: The product_search_query table is InnoDB. I've tried DELAYED on my hunt for an answer and it had no effect. Once again, no speed issues, other than a timeout, when it works it works at a very reasonable speed.
Michael Baldry
A: 

Turn on slow queries, that will give you an idea of what is taking so long to execute that there is a timeout.

http://dev.mysql.com/doc/refman/5.1/en/slow-query-log.html

Pick the slowest query and optimise that. then run for a while and repeat.

There is some excellent information and tools here http://hackmysql.com/nontech

DC

UPDATE:

Either you have a network problem causing the timeout, if you are using a local mysql instance then that is unlikely, OR something is locking a table for far too long causing a timeout. the process that is locking the table or tables for far too long will be listed in the slow log as a slow query. you can also get the slow log query to display any queries that fail to use an index resulting in an inefficient query.

If you can get the problem to occur while you are present then you can also use a tool like phpmyadmin or the commandline to run "SHOW PROCESSLIST\G" this will give you a list of what queries are running while the problem is occurring.

You think the problem is in your insert statement, therefore something is locking that table. therefore you need to find what is locking that table, therefore you need to find what is running so slow its locking the table for far too long. Slow queries is one way to do that.

Other things to look at

CPU - is it idle or running at full pelt

IO - is io causing holdups

RAM - are you swapping all the time (will cause excessive io)

Does the table product_search_query use an index?

What is the primary key?

If your index uses strings that are too long? you may build a huge index file that causes very slow inserts (slow query log will also show that)

And yes the problem may be elsewhere, but you must start somewhere mustn't you.

DC

DeveloperChris
Once again, no speed issues, other than a timeout, when it works it works at a very reasonable speed. The timeout is caused by locking, not an actual speed issue.
Michael Baldry
See update above
DeveloperChris
A: 

Try wrapping your EXECUTE with the following:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;

EXECUTE TheSelect USING @keywords, @keywords, @startOffset, @itemsToReturn;

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ ;

I do something similiar in TSQL for all report stored proc and searches where repeatable reads aren't important to reduce locking/blocking issues with other processes running on the database.

Stephen Study