1

I have an sql query that collects the year to date totals for all products in the inventory. This query runs quickly, returning around 5000 results in a little over a second.

The query used is

SELECT `ITINCODE` as Code, `IT_product_id` as ID,
SUM(IF(YEAR(`ITDATE`) = YEAR(CURDATE() ), IF(ITTYPE = "O",`ITVAL`, -`ITVAL` ), 0) ) as 'YTD_VAL'
FROM `FITEMS`
WHERE (ITTYPE = 'O' OR `ITTYPE` = 'R') AND ITBACKORDER = 'N' AND ITAMNT > 0 AND YEAR(`ITDATE`) >= YEAR(CURDATE() ) -1
GROUP BY `ITINCODE`
ORDER BY YTD_VAL DESC

I want to take the values in YTD_VAL and store them in the actual products table so that they can be used as an ORDER BY on other queries. I added a new field called ytd_val to the products table and then ran

UPDATE products p SET p.ytd_val = 
(SELECT SUM(IF(YEAR(`ITDATE`) = YEAR(CURDATE() ), IF(ITTYPE = 'O',`ITVAL`, -`ITVAL` ), 0) ) as 'YTD_VAL'
FROM `FITEMS`
WHERE ITINCODE  = p.products_model AND (ITTYPE = 'O' OR `ITTYPE` = 'R') AND ITBACKORDER = 'N' AND ITAMNT > 0 AND YEAR(`ITDATE`) >= YEAR(CURDATE() ) -1
GROUP BY `ITINCODE`
ORDER BY YTD_VAL DESC)

The idea was to run this by cron job each night so that the values were updated to reflect the previous days sales.

However, running this query has taken 10 plus minutes and it still hasn't completed.

ITINCODE in FITEMS table is the same as products_model in the products table. IT_product_id in the FITEMS table is the same as products_id in the products table.

What can I do to speed up the query? I thought that as the original results query returns quickly enough that simply updating the values on another table would take seconds longer!

Table structure is as follows:

show create table fitems\G;

Create Table: CREATE TABLE `fitems` (
  `ITUNIQUEREF` int(11) unsigned NOT NULL,
  `ITAMNT` int(11) NOT NULL,
  `ITREF` int(11) unsigned NOT NULL,
  `ITTYPE` char(1) NOT NULL,
  `ITVAL` decimal(10,4) NOT NULL,
  `ITVAT` decimal(10,4) NOT NULL,
  `ITPRICE` decimal(10,4) NOT NULL,
  `ITDATE` date NOT NULL,
  `ITBACKORDER` char(1) NOT NULL,
  `ITDISC` decimal(10,2) NOT NULL,
  `ITORDERREF` int(11) NOT NULL,
  `ITTREF` int(11) unsigned NOT NULL,
  `ITDATEDLY` date NOT NULL,
  `ITINCODE` char(20) NOT NULL,
  `IT_product_id` int(11) unsigned NOT NULL,
  `ITBUILT` int(11) NOT NULL,
  PRIMARY KEY (`ITUNIQUEREF`),
  KEY `ITREF` (`ITREF`,`ITTYPE`,`ITDATE`,`ITBACKORDER`,`ITORDERREF`,`ITDATEDLY`,`ITINCODE`,`IT_product_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

.

show create table products\G;

CREATE TABLE `products` (
  `products_id` int(11) NOT NULL,
  `products_type` int(11) NOT NULL DEFAULT '1',
  `products_quantity` float NOT NULL DEFAULT '0',
  `products_model` varchar(32) CHARACTER SET utf8 DEFAULT NULL,
  `products_image` varchar(64) CHARACTER SET utf8 DEFAULT NULL,
  `products_price` decimal(15,4) NOT NULL DEFAULT '0.0000',
  `products_group_a_price` decimal(15,4) NOT NULL,
  `products_group_b_price` decimal(15,4) NOT NULL,
  `products_group_c_price` decimal(15,4) NOT NULL,
  `products_group_d_price` decimal(15,4) NOT NULL,
  `products_group_e_price` decimal(15,4) NOT NULL,
  `products_group_f_price` decimal(15,4) NOT NULL,
  `products_group_g_price` decimal(15,4) NOT NULL,
  `products_virtual` tinyint(1) NOT NULL DEFAULT '0',
  `products_date_added` datetime NOT NULL DEFAULT '0001-01-01 00:00:00',
  `products_last_modified` datetime DEFAULT NULL,
  `products_date_available` datetime DEFAULT NULL,
  `products_weight` float NOT NULL DEFAULT '0',
  `products_status` tinyint(1) NOT NULL DEFAULT '0',
  `products_tax_class_id` int(11) NOT NULL DEFAULT '0',
  `manufacturers_id` int(11) DEFAULT NULL,
  `products_ordered` float NOT NULL DEFAULT '0',
  `products_quantity_order_min` float NOT NULL DEFAULT '1',
  `products_quantity_order_units` float NOT NULL DEFAULT '1',
  `products_priced_by_attribute` tinyint(1) NOT NULL DEFAULT '0',
  `product_is_free` tinyint(1) NOT NULL DEFAULT '0',
  `product_is_call` tinyint(1) NOT NULL DEFAULT '0',
  `products_quantity_mixed` tinyint(1) NOT NULL DEFAULT '0',
  `product_is_always_free_shipping` tinyint(1) NOT NULL DEFAULT '0',
  `products_qty_box_status` tinyint(1) NOT NULL DEFAULT '1',
  `products_quantity_order_max` float NOT NULL DEFAULT '0',
  `products_sort_order` int(11) NOT NULL DEFAULT '0',
  `products_canonical` text COLLATE utf8_unicode_ci NOT NULL,
  `products_discount_type` tinyint(1) NOT NULL DEFAULT '0',
  `products_discount_type_from` tinyint(1) NOT NULL DEFAULT '0',
  `products_price_sorter` decimal(15,4) NOT NULL DEFAULT '0.0000',
  `master_categories_id` int(11) NOT NULL DEFAULT '0',
  `products_mixed_discount_quantity` tinyint(1) NOT NULL DEFAULT '1',
  `metatags_title_status` tinyint(1) NOT NULL DEFAULT '0',
  `metatags_products_name_status` tinyint(1) NOT NULL DEFAULT '0',
  `metatags_model_status` tinyint(1) NOT NULL DEFAULT '0',
  `metatags_price_status` tinyint(1) NOT NULL DEFAULT '0',
  `metatags_title_tagline_status` tinyint(1) NOT NULL DEFAULT '0',
  `pricing_group` varchar(16) COLLATE utf8_unicode_ci DEFAULT NULL,
  `ytd_val` int(20) NOT NULL,
  PRIMARY KEY (`products_id`),
  KEY `idx_products_date_added_zen` (`products_date_added`),
  KEY `idx_products_status_zen` (`products_status`),
  KEY `idx_products_date_available_zen` (`products_date_available`),
  KEY `idx_products_ordered_zen` (`products_ordered`),
  KEY `idx_products_model_zen` (`products_model`),
  KEY `idx_products_price_sorter_zen` (`products_price_sorter`),
  KEY `idx_master_categories_id_zen` (`master_categories_id`),
  KEY `idx_products_sort_order_zen` (`products_sort_order`),
  KEY `idx_manufacturers_id_zen` (`manufacturers_id`),
  KEY `products_price` (`products_price`),
  KEY `products_status_products_price` (`products_status`,`products_price`),
  FULLTEXT KEY `idx_enhanced_products_model` (`products_model`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
Drew
  • 24,851
  • 10
  • 43
  • 78
Steve Price
  • 600
  • 10
  • 28
  • show the create table statements for each – Drew Nov 26 '15 at 03:17
  • @Drew I've added the create table statements, and the Primary key info for the two tables in question. – Steve Price Nov 26 '15 at 03:25
  • That table is overly complicated and trying to do the job of several tables. For example, the columns `products_group_a_price` through `products_group_g_price` scream, "what if you need an H?" You're also using way too many indexes which will slow down operations which change the table. See http://stackoverflow.com/questions/4120160/mysql-too-many-indexes – Ultimater Nov 26 '15 at 04:00
  • The indexes are those set by the authors of the zen cart, with possibly some others added by the previous developer. If i showed you the amount of indexes on a vanilla install you would be shocked. All bar 9 of the fields in that products table are the original fields from zen cart. To start splitting the table would be a huge amount of work across potentially hundreds of files. I agree, the structure leaves a lot to be desired, but making changes at that level would lead to huge complications every time the core is updated. – Steve Price Nov 26 '15 at 04:08

4 Answers4

2

SELECT is always way faster then UPDATE.

To speed up an update:

  • Look at the indexes on the table you are updating: Are they all needed? If not, remove unneeded ones (I would at least remove idx_products_status_zen since that is also covered by products_status_products_price)

  • Look at the data model: Can you partition the table you are updating? If so, that will speed up the update since the indexes you are updating will be smaller and thus quicker to update;

  • Use InnoDB. It is faster;

  • Do you need to be ACID compliant? If not, then change the settings of InnoDB to speed up the system.

  • If you are really using MySQL: Switch to MariaDB: It is about 8% faster;

  • Install monitoring to see where your bottleneck is: IO or CPU: If it is IO on read, then try compression.

Norbert
  • 6,026
  • 3
  • 17
  • 40
  • he is on MyISAM. His fulltext search indexes may mandate MyISAM depending on version. You are certainly right about that one index – Drew Nov 26 '15 at 03:37
  • Ok, thanks to @Drew: Do split your table so that the full text index can be managed separately. – Norbert Nov 26 '15 at 03:43
  • Nice job Norbert. Haven't seen ya in a while :) – Drew Nov 26 '15 at 17:10
  • 1
    @Drew : Yes, the repeated comments with: Please read what I wrote, the answer is there – Norbert Nov 26 '15 at 20:03
1

If you are satisfied with first query performance.

You can just transform your second query to use INNER JOIN like:

http://sqlfiddle.com/#!9/1028a/1

UPDATE products p 
INNER JOIN (
  SELECT SUM(IF(YEAR(`ITDATE`) = YEAR(CURDATE() ), IF(ITTYPE = 'O',`ITVAL`, -`ITVAL` ), 0) ) as 'YTD_VAL',
  ITINCODE
FROM `FITEMS`
WHERE (ITTYPE = 'O' OR `ITTYPE` = 'R') 
  AND ITBACKORDER = 'N' AND ITAMNT > 0 AND YEAR(`ITDATE`) >= YEAR(CURDATE() ) -1
GROUP BY `ITINCODE`
  ) t
ON t.ITINCODE  = p.products_model
SET p.ytd_val = t.YTD_VAL

UPDATE And by the way you don't need that ORDER BY YTD_VAL DESC it has no sense in this particular case I guess.

UPDATE 2

UPDATE products p 
    INNER JOIN (
      SELECT SUM(IF(YEAR(`ITDATE`) = YEAR(CURDATE() ), IF(ITTYPE = 'O',`ITVAL`, -`ITVAL` ), 0) ) as 'YTD_VAL',
      IT_product_id 
    FROM `FITEMS`
    WHERE (ITTYPE = 'O' OR `ITTYPE` = 'R') 
      AND ITBACKORDER = 'N' AND ITAMNT > 0 AND YEAR(`ITDATE`) >= YEAR(CURDATE() ) -1
    GROUP BY `IT_product_id`
      ) t
    ON t.IT_product_id = p.products_id 
    SET p.ytd_val = t.YTD_VAL
Alex
  • 16,739
  • 1
  • 28
  • 51
  • Stangely, the original query returns 4150 rows, but the UPDATE query only report 2803 affected rows. I'm taking a stab in the dark here, but possibly not all the original ITINCODE rows have a matching products_model. I will try using IT_products_id and products_id instead – Steve Price Nov 26 '15 at 04:02
  • @StevePrice . . . Your original code sets unmatched `products_model` values to `NULL`. This version does not change them. – Gordon Linoff Nov 26 '15 at 04:07
  • What I meant was that the original query that collects the data only, not updating the other table, returns 4150 results. One would assume that if 4150 results are available, then that number of results should be updated. – Steve Price Nov 26 '15 at 04:11
  • Show me result of `SELECT COUNT(DISTINCT products_model) FROM products` please? – Alex Nov 26 '15 at 04:19
  • And by the way did you get your query speed up? – Alex Nov 26 '15 at 04:20
  • @Alex The result of SELECT COUNT(DISTINCT products_model) FROM products is 19861. The original query is now much faster, returning (4150 total, Query took 0.1002 seconds.) The query you gave above returns 2918 rows affected. (Query took 0.2006 seconds.) – Steve Price Nov 26 '15 at 04:52
  • I tried changing ON t.ITINCODE = p.products_model to ON t.IT_product_id = p.products_id, but it gave an error saying that IT_product_id was an unknown column – Steve Price Nov 26 '15 at 04:59
  • exactly. if you want to join `ON t.IT_product_id` you should `GROUP BY IT_product_id` instead of `ITINCODE` and `SELECT ... IT_product_id ...` – Alex Nov 26 '15 at 05:02
  • UPDATE products p INNER JOIN ( SELECT SUM(IF(YEAR(`ITDATE`) = YEAR(CURDATE() ), IF(ITTYPE = 'O',`ITVAL`, -`ITVAL` ), 0) ) as 'YTD_VAL', ITINCODE FROM `FITEMS` WHERE (ITTYPE = 'O' OR `ITTYPE` = 'R') AND ITBACKORDER = 'N' AND ITAMNT > 0 AND YEAR(`ITDATE`) >= YEAR(CURDATE() ) -1 GROUP BY `IT_product_id` ) t ON t.IT_product_id = p.products_id SET p.ytd_val = t.YTD_VAL #1054 - Unknown column 't.IT_product_id' in 'on clause' – Steve Price Nov 26 '15 at 05:05
1

I am not one to say do it, but consider the case for Covering Indexes. One in particular on table fitems. Those columns are relatively slim for that query. I will spook up one to try on particular columns. We have seen cases where terribly long queries can be completed in snappy time. But no promises. Ah, I see you have some. Was looking at the first edit with the alter tables below it. I will keep looking.

Covering Index

A covering index is one in which the query can be resolved via a quick stroll through the index b-tree, without requiring a lookup into the actual table data page. These are the ultimate nirvana for speed. Percona quick article on it.

Explain

And run the query (the update one) thru Explain and examine the output. See also the article Using Explain to Write Better Mysql Queries.

Note, some of these comments are for those that follows, not necessarily this op. He seems to have his house in order pretty well.

Drew
  • 24,851
  • 10
  • 43
  • 78
0

In your subquery, neither the order by nor the group by are necessary, so the update can be written as:

UPDATE products p
    SET p.ytd_val = (SELECT SUM(IF(YEAR(`ITDATE`) = YEAR(CURDATE() ), IF(ITTYPE = 'O',`ITVAL`, -`ITVAL` ), 0) )
                     FROM `FITEMS`
                     WHERE FITEMS.ITINCODE = p.products_model AND
                           ITTYPE IN ('O', 'R') AND
                           ITBACKORDER = 'N' AND
                           ITAMNT > 0 AND
                           YEAR(`ITDATE`) >= YEAR(CURDATE() ) - 1
                    );

For this, you want an index on FITEMS(ITINCODE, ITBACKORDER, ITTYPE, ITAMNT, ITVAL). This might significantly speed your query.

Gordon Linoff
  • 1,242,037
  • 58
  • 646
  • 786