{"id":4294,"date":"2011-11-01T09:55:47","date_gmt":"2011-11-01T07:55:47","guid":{"rendered":"http:\/\/code.openark.org\/blog\/?p=4294"},"modified":"2011-11-01T09:57:56","modified_gmt":"2011-11-01T07:57:56","slug":"self-throttling-mysql-queries","status":"publish","type":"post","link":"https:\/\/code.openark.org\/blog\/mysql\/self-throttling-mysql-queries","title":{"rendered":"Self throttling MySQL queries"},"content":{"rendered":"<p>Recap on the problem:<\/p>\n<ul>\n<li>A query takes a long time to complete.<\/li>\n<li>During this time it makes for a lot of I\/O.<\/li>\n<li>Query&#8217;s I\/O overloads the db, making for other queries run slow.<\/li>\n<\/ul>\n<p>I introduce the notion of self-throttling queries: queries that go to sleep, by themselves, throughout the runtime. The sleep period means the query does not perform I\/O at that time, which then means other queries can have their chance to execute.<\/p>\n<p>I present two approaches:<\/p>\n<ul>\n<li>The naive approach: for every <strong>1,000<\/strong> rows, the query sleep for <strong>1<\/strong> second<\/li>\n<li>The factor approach: for every <strong>1,000<\/strong> rows, the query sleeps for the amount of time it took to iterate those <strong>1,000<\/strong> rows (effectively doubling the total runtime of the query).<!--more--><\/li>\n<\/ul>\n<h4>Sample query<\/h4>\n<p>We use a simple, single-table scan. No aggregates (which complicate the solution considerably).<\/p>\n<blockquote>\n<pre>SELECT\r\n  rental_id,\r\n  TIMESTAMPDIFF(DAY, rental_date, return_date) AS rental_days\r\nFROM\r\n  sakila.rental\r\n;<\/pre>\n<\/blockquote>\n<h4>The naive solution<\/h4>\n<p>We need to know every <strong>1,000<\/strong> rows. So we need to count the rows. We do that by using a counter, as follows:<\/p>\n<blockquote>\n<pre>SELECT\r\n  rental_id,\r\n  TIMESTAMPDIFF(DAY, rental_date, return_date) AS rental_days,\r\n  @row_counter := @row_counter + 1\r\nFROM\r\n  sakila.rental,\r\n  (SELECT @row_counter := 0) sel_row_counter\r\n;<\/pre>\n<\/blockquote>\n<p>A thing that bothers me, is that I wasn&#8217;t asking for an additional column. I would like the result set to remain as it were; same result structure. We also want to sleep for <strong>1<\/strong> second for each <strong>1,000<\/strong> rows. So we merge the two together along with one of the existing columns, like this:<\/p>\n<blockquote>\n<pre>SELECT\r\n  rental_id +\r\n    IF(\r\n      (@row_counter := @row_counter + 1) % 1000 = 0,\r\n      SLEEP(1), 0\r\n    ) AS rental_id,\r\n  TIMESTAMPDIFF(DAY, rental_date, return_date) AS rental_days\r\nFROM\r\n  sakila.rental,\r\n  (SELECT @row_counter := 0) sel_row_counter\r\n;<\/pre>\n<\/blockquote>\n<p>To remain faithful to <a href=\"http:\/\/code.openark.org\/blog\/mysql\/slides-from-my-talk-programmatic-queries-things-you-can-code-with-sql\">my slides<\/a>, I rewrite as follows, and this is <em>the naive solution<\/em>:<\/p>\n<blockquote>\n<pre>SELECT\r\n  rental_id +\r\n    CASE\r\n      WHEN <strong>(@row_counter := @row_counter + 1) % 1000 = 0<\/strong> THEN <strong>SLEEP(1)<\/strong>\r\n      ELSE <strong>0<\/strong>\r\n    END AS rental_id,\r\n  TIMESTAMPDIFF(DAY, rental_date, return_date) AS rental_days\r\nFROM\r\n  sakila.rental,\r\n  (SELECT @row_counter := 0) sel_row_counter\r\n;<\/pre>\n<\/blockquote>\n<p>The <strong>WHEN<\/strong> clause always returns <strong>0<\/strong>, so it does not affect the value of <strong>rental_id<\/strong>.<\/p>\n<h4>The factor approach<\/h4>\n<p>In the factor approach we wish to keep record of query execution, every <strong>1,000<\/strong> rows. I introduce a nested <strong>WHEN<\/strong> statement which updates time records. I rely on <strong>SYSDATE()<\/strong> to return the true time, and on <strong>NOW()<\/strong> to return query execution start time.<\/p>\n<blockquote>\n<pre>SELECT\r\n  rental_id +\r\n    CASE\r\n      WHEN (@row_counter := @row_counter + 1) IS NULL THEN NULL\r\n      WHEN <strong>@row_counter % 1000 = 0<\/strong> THEN\r\n        CASE\r\n          WHEN (@time_now := <strong>SYSDATE()<\/strong>) IS NULL THEN NULL\r\n          WHEN (@time_diff := (<strong>TIMESTAMPDIFF(SECOND, @chunk_start_time, @time_now)<\/strong>)) IS NULL THEN NULL\r\n          WHEN <strong>SLEEP(@time_diff)<\/strong> IS NULL THEN NULL\r\n          WHEN (@chunk_start_time := <strong>SYSDATE()<\/strong>) IS NULL THEN NULL\r\n          ELSE 0\r\n        END\r\n      ELSE 0\r\n    END AS rental_id,\r\n  TIMESTAMPDIFF(DAY, rental_date, return_date) AS rental_days\r\nFROM\r\n  sakila.rental,\r\n  (SELECT @row_counter := 0) sel_row_counter,\r\n  (SELECT @chunk_start_time := NOW()) sel_chunk_start_time\r\n;<\/pre>\n<\/blockquote>\n<h4>Proof<\/h4>\n<p>How can we prove that the queries do indeed work?<\/p>\n<p>We can see if the total runtime sums up to the number of sleep calls, in seconds; but how do we know that sleeps do occur at the correct times?<\/p>\n<p>A solution I offer is to use a stored routines which logs to a MyISAM table (a non transactional table) the exact time (using <strong>SYSDATE()<\/strong>) and value per row. The following constructs are introduced:<\/p>\n<blockquote>\n<pre><strong>CREATE TABLE<\/strong> test.proof(\r\n  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\r\n  dt DATETIME NOT NULL,\r\n  msg VARCHAR(255)\r\n) <strong>ENGINE=MyISAM<\/strong>;\r\n\r\nDELIMITER $$\r\n<strong>CREATE FUNCTION<\/strong> test.prove_it(message VARCHAR(255)) RETURNS TINYINT\r\nDETERMINISTIC\r\nMODIFIES SQL DATA\r\nBEGIN\r\n  <strong>INSERT INTO test.proof (dt, msg) VALUES (SYSDATE(), message); RETURN 0;<\/strong>\r\nEND $$\r\nDELIMITER ;<\/pre>\n<\/blockquote>\n<p>The <strong>prove_it()<\/strong> function records the immediate time and a message into the MyISAM table, which immediately accepts the write, being non-transactional. It returns with <strong>0<\/strong>, so we will now embed it within the query. Of course, the function itself incurs some overhead, but it will nevertheless convince you that <strong>SLEEP()<\/strong>s do occur at the right time!<\/p>\n<blockquote>\n<pre>SELECT\r\n  rental_id +\r\n    CASE\r\n      WHEN (@row_counter := @row_counter + 1) IS NULL THEN NULL\r\n      WHEN @row_counter % 1000 = 0 THEN\r\n        CASE\r\n          WHEN (@time_now := SYSDATE()) IS NULL THEN NULL\r\n          WHEN (@time_diff := (TIMESTAMPDIFF(SECOND, @chunk_start_time, @time_now))) IS NULL THEN NULL\r\n          WHEN SLEEP(@time_diff)<strong> + test.prove_it(CONCAT('will sleep for ', @time_diff, ' seconds'))<\/strong> IS NULL THEN NULL\r\n          WHEN (@chunk_start_time := SYSDATE()) IS NULL THEN NULL\r\n          ELSE 0\r\n        END\r\n      ELSE 0\r\n    END AS rental_id,\r\n  TIMESTAMPDIFF(DAY, rental_date, return_date) AS rental_days\r\nFROM\r\n  sakila.rental,\r\n  (SELECT @row_counter := 0) sel_row_counter,\r\n  (SELECT @chunk_start_time := NOW()) sel_chunk_start_time\r\n;\r\n\r\nmysql&gt; SELECT * FROM test.proof;\r\n+----+---------------------+--------------------------+\r\n| id | dt                  | msg                      |\r\n+----+---------------------+--------------------------+\r\n|  1 | 2011-11-01 09:22:36 | will sleep for 1 seconds |\r\n|  2 | 2011-11-01 09:22:36 | will sleep for 0 seconds |\r\n|  3 | 2011-11-01 09:22:36 | will sleep for 0 seconds |\r\n|  4 | 2011-11-01 09:22:36 | will sleep for 0 seconds |\r\n|  5 | 2011-11-01 09:22:36 | will sleep for 0 seconds |\r\n|  6 | 2011-11-01 09:22:36 | will sleep for 0 seconds |\r\n|  7 | 2011-11-01 09:22:38 | will sleep for 1 seconds |\r\n|  8 | 2011-11-01 09:22:38 | will sleep for 0 seconds |\r\n|  9 | 2011-11-01 09:22:38 | will sleep for 0 seconds |\r\n| 10 | 2011-11-01 09:22:38 | will sleep for 0 seconds |\r\n| 11 | 2011-11-01 09:22:38 | will sleep for 0 seconds |\r\n| 12 | 2011-11-01 09:22:40 | will sleep for 1 seconds |\r\n| 13 | 2011-11-01 09:22:40 | will sleep for 0 seconds |\r\n| 14 | 2011-11-01 09:22:40 | will sleep for 0 seconds |\r\n| 15 | 2011-11-01 09:22:40 | will sleep for 0 seconds |\r\n+----+---------------------+--------------------------+<\/pre>\n<\/blockquote>\n<p>The above query is actually very fast. Try adding <strong>BENCHMARK(1000,ENCODE(&#8216;hello&#8217;,&#8217;goodbye&#8217;))<\/strong> to rental_id so as to make it slower, or just use it on a really large table, see what happens (this is what I actually used to make the query run for several seconds in the example above).<\/p>\n<p>Observant reads will note that the <strong>&#8220;will sleep&#8230;&#8221;<\/strong> message actually gets written <em>after<\/em> the <strong>SLEEP()<\/strong> call. I leave this as it is.<\/p>\n<p>Another very nice treat of the code is that you don&#8217;t need sub-second resolution for it to work. If you look at the above, we don&#8217;t actually go to sleep every <strong>1,000<\/strong> rows (<strong>1,000<\/strong> is just too quick in the query &#8212; perhaps I should have used <strong>10,000<\/strong> seconds). But we <em>do<\/em> make it once a second has <em>elapsed<\/em>. Which means it works correctly <em>on average<\/em>. Of course, the entire discussion is only of interest when a query executes for a <em>substantial<\/em> number of seconds, so this is just an anecdote.<\/p>\n<h4>And the winner is&#8230;<\/h4>\n<p>Wow, this <a href=\"http:\/\/code.openark.org\/blog\/mysql\/contest-for-glory-write-a-self-throttling-mysql-query\">contest<\/a> was anything but popular. <strong><a href=\"http:\/\/marcalff.blogspot.com\/\">Marc Alff<\/a><\/strong> is the obvious winner: he is the <em>only<\/em> one to suggest a solution \ud83d\ude42<\/p>\n<p>But Marc uses a very nice trick: he reads the <strong>PERFORMANCE_SCHEMA<\/strong>. Now, I&#8217;m not sure how the\u00a0<strong>PERFORMANCE_SCHEMA<\/strong> gets updated. I know that the <strong>INFORMATION_SCHEMA.GLOBAL_STATUS<\/strong> table does not get updated by a query until the query completes (so you cannot expect a change in <strong>innodb_rows_read<\/strong> throughout the execution of the query). I just didn&#8217;t test it (homework, anyone?). If it does get updated, then we can throttle the query based on InnoDB page reads using a simple query. Otherwise, an access to <strong>\/proc\/diskstats<\/strong> is possible, assuming no <em>apparmor<\/em> or <em>SELinux<\/em> are blocking us.<\/p>\n<p>Marc also uses a stored function, which is the <em>clean<\/em> way of doing it; however I distrust the overhead incurred by s stored routine and prefer my solution (which is, admittedly, not a pretty SQL sight!).<\/p>\n<p>Happy throttling!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Recap on the problem: A query takes a long time to complete. During this time it makes for a lot of I\/O. Query&#8217;s I\/O overloads the db, making for other queries run slow. I introduce the notion of self-throttling queries: queries that go to sleep, by themselves, throughout the runtime. The sleep period means the [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"enabled":false},"version":2}},"categories":[5],"tags":[74,13,52,21,59],"class_list":["post-4294","post","type-post","status-publish","format-standard","hentry","category-mysql","tag-hack","tag-myisam","tag-performance","tag-sql","tag-stored-routines"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p2bZZp-17g","_links":{"self":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts\/4294","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/comments?post=4294"}],"version-history":[{"count":32,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts\/4294\/revisions"}],"predecessor-version":[{"id":4325,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts\/4294\/revisions\/4325"}],"wp:attachment":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/media?parent=4294"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/categories?post=4294"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/tags?post=4294"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}