{"id":35259,"date":"2019-10-31T22:03:17","date_gmt":"2019-10-31T19:03:17","guid":{"rendered":"https:\/\/prohoster.info\/blog\/istoriya-odnogo-sql-rassledovaniya\/"},"modified":"2019-10-31T22:03:17","modified_gmt":"2019-10-31T19:03:17","slug":"istoriya-odnogo-sql-rassledovaniya","status":"publish","type":"post","link":"https:\/\/prohoster.info\/en\/blog\/administrirovanie\/istoriya-odnogo-sql-rassledovaniya","title":{"rendered":"The story of an SQL investigation.","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p>Last December, I received an interesting bug report from the VWO support team. The loading time for one of the analytical reports for a major corporate client seemed excessively long. Since this is in my area of responsibility, I immediately focused on solving the problem.<\/p>\n<p><\/p>\n<h2>Background<\/h2>\n<p><\/p>\n<p>To provide some context, let me briefly explain what VWO is. It is a platform that allows you to run various targeted campaigns on your websites: conduct A\/B experiments, track visitors and conversions, analyze sales funnels, display heatmaps, and replay visit recordings.<\/p>\n<p><\/p>\n<p>But the most important feature of the platform is reporting. All the aforementioned functions are interconnected. For corporate clients, a vast array of information would be utterly useless without a powerful platform to represent it in an analytical format.<\/p>\n<p><\/p>\n<p>Using the platform, you can make arbitrary queries on a large dataset. Here\u2019s a simple example:<\/p>\n<p><\/p>\n<pre>Show all clicks on the page \"abc.com\"\nFROM  TO \nfor people who\nused Chrome OR\n(were in Europe AND used iPhone)<\/pre>\n<p><\/p>\n<p>Notice the boolean operators. They are available for clients in the query interface to create arbitrarily complex queries for obtaining datasets.<\/p>\n<p><\/p>\n<h2>Slow query<\/h2>\n<p><\/p>\n<p>The client in question was trying to do something that intuitively should work quickly:<\/p>\n<p><\/p>\n<pre>Show all session recordings\nfor users who visited any page\nwith a URL containing \"\/jobs\"<\/pre>\n<p><\/p>\n<p>This site had an enormous amount of traffic, and we stored over a million unique URLs just for it. They wanted to find a fairly simple URL pattern related to their business model.<\/p>\n<p>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<h2>Preliminary Inquiry<\/h2>\n<p><\/p>\n<p>Let's take a look at what is happening in the database. Below is the original slow SQL query:<\/p>\n<p><\/p>\n<pre><code class=\"plaintext\">SELECT \n    count(*) \nFROM \n    acc_{account_id}.urls as recordings_urls, \n    acc_{account_id}.recording_data as recording_data, \n    acc_{account_id}.sessions as sessions \nWHERE \n    recording_data.usp_id = sessions.usp_id \n    AND sessions.referrer_id = recordings_urls.id \n    AND  (  urls &amp;&amp;  array(select id from acc_{account_id}.urls where url  ILIKE  '%enterprise_customer.com\/jobs%')::text[]   ) \n    AND r_time &gt; to_timestamp(1542585600) \n    AND r_time = 5 \n    AND recording_data.num_of_pages &gt; 0 ;<\/code><\/pre>\n<p><\/p>\n<p>Here are the timings:<\/p>\n<p><\/p>\n<pre>Planned time: 1.480 ms\nExecution time: 1431924.650 ms<\/pre>\n<p><\/p>\n<p>The query scanned 150 thousand rows. The query planner revealed a few interesting details but no obvious bottlenecks.<\/p>\n<p><\/p>\n<p>Let's study the query further. As you can see, it makes <code>JOIN<\/code> three tables:<\/p>\n<p><\/p>\n<ol>\n<li><strong>sessions<\/strong>: to display session information: browser, user agent, country, and so on.<\/li>\n<li><strong>recording_data<\/strong>: recorded URLs, pages, duration of visits<\/li>\n<li><strong>urls<\/strong>: to avoid duplication of extremely large URLs, we store them in a separate table.<\/li>\n<\/ol>\n<p><\/p>\n<p>Also, note that all our tables are already partitioned by <code>account_id<\/code>. Thus, the situation where one particularly large account causes issues for others is excluded.<\/p>\n<p><\/p>\n<h2>In search of clues<\/h2>\n<p><\/p>\n<p>Upon closer inspection, we see that something is wrong with this specific query. We should pay attention to this line:<\/p>\n<p><\/p>\n<pre><code class=\"plaintext\">urls &amp;&amp; array(\n\tselect id from acc_{account_id}.urls \n\twhere url ILIKE '%enterprise_customer.com\/jobs%'\n)::text[]<\/code><\/pre>\n<p><\/p>\n<p>The first thought was that perhaps, due to <code>ILIKE<\/code> on all these long URLs (we have more than 1.4 million <strong>unique\u00a0<\/strong>URLs collected for this account) the performance might suffer.<\/p>\n<p><\/p>\n<p>But no \u2014 that's not the issue!<\/p>\n<p><\/p>\n<pre><code class=\"plaintext\">SELECT id FROM urls WHERE url ILIKE '%enterprise_customer.com\/jobs%';\n  id\n--------\n ...\n(198661 rows)\n\nTime: 5231.765 ms<\/code><\/pre>\n<p><\/p>\n<p>The search query by pattern only takes 5 seconds. Searching by pattern across a million unique URLs is clearly not the problem.<\/p>\n<p><\/p>\n<p>The next suspect on the list \u2014 several <code>JOIN<\/code>. Perhaps their excessive use led to the slowdown? Typically <code>JOIN<\/code>'s are the most obvious candidates for performance issues, but I didn't believe our case was typical.<\/p>\n<p><\/p>\n<pre><code class=\"plaintext\">analytics_db=# SELECT\n    count(*)\nFROM\n    acc_{account_id}.urls as recordings_urls,\n    acc_{account_id}.recording_data_0 as recording_data,\n    acc_{account_id}.sessions_0 as sessions\nWHERE\n    recording_data.usp_id = sessions.usp_id\n    AND sessions.referrer_id = recordings_urls.id\n    AND r_time &gt; to_timestamp(1542585600)\n    AND r_time = 5\n    AND recording_data.num_of_pages &gt; 0 ;\n count\n-------\n  8086\n(1 row)\n\nTime: 147.851 ms<\/code><\/pre>\n<p><\/p>\n<p>And this was not our case either. <code>JOIN<\/code>'s turned out to be quite fast.<\/p>\n<p><\/p>\n<h2>Narrowing down the suspects<\/h2>\n<p><\/p>\n<p>I was ready to start modifying the query in search of any possible performance improvements. My team and I developed two main ideas:<\/p>\n<p><\/p>\n<ul>\n<li><strong>Use EXISTS for the URL subquery<\/strong>: We wanted to double-check if there were issues with the URL subquery. One way to achieve this is simply to use <code>EXISTS<\/code>. <code>EXISTS<\/code> <noindex><a rel=\"nofollow\" href=\"https:\/\/www.postgresql.org\/docs\/current\/functions-subquery.html#FUNCTIONS-SUBQUERY-EXISTS\">take the parameter<\/a><\/noindex> significantly improve performance as it ends immediately upon finding a single row that meets the condition.<\/li>\n<\/ul>\n<p><\/p>\n<pre><code class=\"plaintext\">SELECT\n\tcount(*) \nFROM \n    acc_{account_id}.urls as recordings_urls,\n    acc_{account_id}.recording_data as recording_data,\n    acc_{account_id}.sessions as sessions\nWHERE\n    recording_data.usp_id = sessions.usp_id\n    AND  (  1 = 1  )\n    AND sessions.referrer_id = recordings_urls.id\n    AND  (exists(select id from acc_{account_id}.urls where url  ILIKE '%enterprise_customer.com\/jobs%'))\n    AND r_time &gt; to_timestamp(1547585600)\n    AND r_time =5\n    AND recording_data.num_of_pages &gt; 0 ;\n count\n 32519\n(1 row)\nTime: 1636.637 ms<\/code><\/pre>\n<p><\/p>\n<p>Well, yes. A subquery, when wrapped in\u00a0<code>EXISTS<\/code>, makes everything super fast. The next logical question is why the query with <code>JOIN<\/code>-es and the subquery are fast individually but slow together?<\/p>\n<p><\/p>\n<ul>\n<li><strong>We move the subquery into a CTE <\/strong>: if the query is fast on its own, we can simply first calculate the quick result and then provide it to the main query.<\/li>\n<\/ul>\n<p><\/p>\n<pre><code class=\"plaintext\">WITH matching_urls AS (\n    select id::text from acc_{account_id}.urls where url  ILIKE  '%enterprise_customer.com\/jobs%'\n)\n\nSELECT \n    count(*) FROM acc_{account_id}.urls as recordings_urls, \n    acc_{account_id}.recording_data as recording_data, \n    acc_{account_id}.sessions as sessions,\n    matching_urls\nWHERE \n    recording_data.usp_id = sessions.usp_id \n    AND  (  1 = 1  )  \n    AND sessions.referrer_id = recordings_urls.id\n    AND (urls &amp;&amp; array(SELECT id from matching_urls)::text[])\n    AND r_time &gt; to_timestamp(1542585600) \n    AND r_time =5 \n    AND recording_data.num_of_pages &gt; 0;<\/code><\/pre>\n<p><\/p>\n<p>But even this was still very slow.<\/p>\n<p><\/p>\n<h2>Identifying the culprit<\/h2>\n<p><\/p>\n<p>All this time, one small detail kept flashing before my eyes, which I constantly dismissed. But since nothing else was left, I decided to take a look at it as well. I'm talking about <code>&amp;&amp;<\/code> the operator. While <code>EXISTS<\/code> just improved performance, <code>&amp;&amp;<\/code> was the only remaining common factor in all versions of the slow query.<\/p>\n<p><\/p>\n<p>Looking at <noindex><a rel=\"nofollow\" href=\"https:\/\/www.postgresql.org\/docs\/9.1\/functions-array.html\">documentation<\/a><\/noindex>, we see that <code>&amp;&amp;<\/code> is used when it is necessary to find common elements between two arrays.<\/p>\n<p><\/p>\n<p>In the original query, this is:<\/p>\n<p><\/p>\n<pre><code class=\"plaintext\">AND  (  urls &amp;&amp;  array(select id from acc_{account_id}.urls where url  ILIKE  '%enterprise_customer.com\/jobs%')::text[]   )<\/code><\/pre>\n<p><\/p>\n<p>Which means we are performing a pattern search on our URLs, then finding the intersection with all URLs with common records. This is a bit confusing because 'urls' here does not refer to a table containing all the URLs, but to the 'urls' column in the table <code>recording_data<\/code>.<\/p>\n<p><\/p>\n<p>As my suspicions regarding <code>&amp;&amp;<\/code>grew, I tried to find confirmation for them in the generated query plan. <code>EXPLAIN ANALYZE<\/code> (I already had a saved plan, but I usually find it easier to experiment in SQL than to understand the opaqueness of query planners).<\/p>\n<p><\/p>\n<pre><code class=\"plaintext\">Filter: ((urls &amp;&amp;&amp; ($0)::text[]) AND (r_time &gt; '2018-12-17 12:17:23+00'::timestamp with time zone) AND (r_time = '5'::double precision) AND (num_of_pages &gt; 0))\n                           Rows Removed by Filter: 52710<\/code><\/pre>\n<p><\/p>\n<p>There were several filter rows just from <code>&amp;&amp;<\/code>. Which meant that this operation was not only costly but also executed multiple times.<\/p>\n<p><\/p>\n<p>I checked this by isolating the condition<\/p>\n<p><\/p>\n<pre><code class=\"plaintext\">SELECT 1\nFROM \n    acc_{account_id}.urls as recordings_urls, \n    acc_{account_id}.recording_data_30 as recording_data_30, \n    acc_{account_id}.sessions_30 as sessions_30 \nWHERE \n\turls &amp;&amp; array(select id from acc_{account_id}.urls where url ILIKE '%enterprise_customer.com\/jobs%')::text[]<\/code><\/pre>\n<p><\/p>\n<p>This query was slow. Since <code>JOIN<\/code>-s are fast and subqueries are fast, only the <code>&amp;&amp;<\/code> operator remained.<\/p>\n<p><\/p>\n<p>But this is the key operation. We always need to search across the main URL table to look up by pattern, and we always need to find intersections. We can't search the URL records directly because they are simply IDs referring to <code>urls<\/code>.<\/p>\n<p><\/p>\n<h2>On the way to the solution<\/h2>\n<p><\/p>\n<p><code>&amp;&amp;<\/code> is slow because both sets are huge. The operation will be relatively fast if I replace <code>urls<\/code> to <code>{ \"http:\/\/google.com\/\", \"http:\/\/wingify.com\/\" }<\/code>.<\/p>\n<p><\/p>\n<p>I started looking for a way to do set intersection in Postgres without using <code>&amp;&amp;<\/code>, but without much success.<\/p>\n<p><\/p>\n<p>Ultimately, we decided to just solve the problem in isolation: give me all <code>urls<\/code> strings for which the url matches the template. Without additional conditions this will be \u2014\u00a0<\/p>\n<p><\/p>\n<pre><code class=\"plaintext\">SELECT urls.url\nFROM \n\tacc_{account_id}.urls as urls,\n\t(SELECT unnest(recording_data.urls) AS id) AS unrolled_urls\nWHERE\n\turls.id = unrolled_urls.id AND\n\turls.url ILIKE '%jobs%'<\/code><\/pre>\n<p><\/p>\n<p>Instead of\u00a0<code>JOIN<\/code> syntax I simply used a subquery and expanded <code>recording_data.urls<\/code> the array to directly apply the condition in <code>WHERE<\/code>.<\/p>\n<p><\/p>\n<p>The important thing here is that <code>&amp;&amp;<\/code> is used to check if the given record contains the corresponding URL. Squinting a bit, you can see in this operation the traversal of array elements (or table rows) and stopping upon meeting the condition (match). Does that ring a bell? Aha, <code>EXISTS<\/code>.<\/p>\n<p><\/p>\n<p>Since at <code>recording_data.urls<\/code> can be referenced from outside the subquery context when that happens, we can go back to our old friend <code>EXISTS<\/code> and wrap it with a subquery.<\/p>\n<p><\/p>\n<p>Putting everything together, we get the final optimized query:<\/p>\n<p><\/p>\n<pre><code class=\"plaintext\">SELECT \n    count(*) \nFROM \n    acc_{account_id}.urls as recordings_urls, \n    acc_{account_id}.recording_data as recording_data, \n    acc_{account_id}.sessions as sessions \nWHERE \n    recording_data.usp_id = sessions.usp_id \n    AND  (  1 = 1  )  \n    AND sessions.referrer_id = recordings_urls.id \n    AND r_time &gt; to_timestamp(1542585600) \n    AND r_time = 5 \n    AND recording_data.num_of_pages &gt; 0\n    AND EXISTS(\n        SELECT urls.url\n        FROM \n            acc_{account_id}.urls as urls,\n            (SELECT unnest(urls) AS rec_url_id FROM acc_{account_id}.recording_data) \n            AS unrolled_urls\n        WHERE\n            urls.id = unrolled_urls.rec_url_id AND\n            urls.url  ILIKE  '%enterprise_customer.com\/jobs%'\n    );\n<\/code><\/pre>\n<p><\/p>\n<p>And the final execution time <code>Time: 1898.717 ms<\/code> Time to celebrate?!?<\/p>\n<p><\/p>\n<p>Not so fast! First, we need to check the correctness. I was quite suspicious about <code>EXISTS<\/code> the optimization, as it alters the logic for an earlier completion. We must ensure that we haven't introduced an obscure error into the query.<\/p>\n<p><\/p>\n<p>A simple check involved performing <code>count(*)<\/code> both on slow and fast queries for a large variety of datasets. Then, for a small subset of data, I manually checked the accuracy of all results.<\/p>\n<p><\/p>\n<p>All checks yielded consistently positive results. We fixed everything!<\/p>\n<p><\/p>\n<h2>Lessons Learned<\/h2>\n<p><\/p>\n<p>Several lessons can be drawn from this story:<\/p>\n<p><\/p>\n<ol>\n<li>Query plans do not tell the whole story, but they can offer hints<\/li>\n<li>Main suspects are not always the actual culprits<\/li>\n<li>Slow queries can be broken down to isolate bottlenecks<\/li>\n<li>Not all optimizations are inherently reductive<\/li>\n<li>Using <code>EXIST<\/code>, where applicable, can lead to a significant performance boost<\/li>\n<\/ol>\n<p><\/p>\n<h2>Output<\/h2>\n<p><\/p>\n<p>We went from a query time of ~24 minutes down to 2 seconds \u2014 quite a substantial performance increase! Although this article turned out lengthy, all the experiments we conducted took place in one day and approximately took 1.5 to 2 hours for optimization and testing.<\/p>\n<p><\/p>\n<p>SQL is a wonderful language if you don't fear it, but rather try to understand and use it. With a solid grasp of how SQL queries are executed, how the database generates query plans, how indexes work, and simply the size of the data you're dealing with, you can excel at query optimization. Equally important, however, is to continue testing various approaches and gradually break down the problem to find the bottlenecks.<\/p>\n<p><\/p>\n<p>The best part of achieving such results is the noticeable visible improvement in performance \u2014 when a report that previously wouldn\u2019t even load now loads almost instantly.<\/p>\n<p><\/p>\n<p><strong>Special thanks\u00a0<\/strong>to my colleagues\u00a0<em>in the team Aditya Mishra<\/em>,\u00a0<em>Aditya Gaur\u00a0<\/em>and\u00a0<em><noindex><a rel=\"nofollow\" href=\"https:\/\/twitter.com\/s0ftvar\">Varun Malhotra\u00a0<\/a><\/noindex><\/em>for the brainstorming and\u00a0<em>Dinkar Pandir\u00a0<\/em>for identifying a critical error in our final request before we finally parted ways with it!<\/p>\n<p>Source: <a content=\"nofollow\" rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/post\/455832\/\">habr.com<\/a><\/p>","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"excerpt":{"rendered":"<p>\u0412 \u0434\u0435\u043a\u0430\u0431\u0440\u0435 \u043f\u0440\u043e\u0448\u043b\u043e\u0433\u043e \u0433\u043e\u0434\u0430 \u044f \u043f\u043e\u043b\u0443\u0447\u0438\u043b \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0439 \u043e\u0442\u0447\u0435\u0442 \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435 \u043e\u0442 \u043a\u043e\u043c\u0430\u043d\u0434\u044b\u00a0\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 VWO. \u0412\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0437 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u0434\u043b\u044f \u043a\u0440\u0443\u043f\u043d\u043e\u0433\u043e \u043a\u043e\u0440\u043f\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u043d\u0435\u043f\u043e\u043c\u0435\u0440\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0438\u043c. \u0410 \u0442\u0430\u043a \u043a\u0430\u043a \u044d\u0442\u043e \u0441\u0444\u0435\u0440\u0430 \u043c\u043e\u0435\u0439 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438, \u044f \u0442\u0443\u0442 \u0436\u0435 \u0441\u043e\u0441\u0440\u0435\u0434\u043e\u0442\u043e\u0447\u0438\u043b\u0441\u044f \u043d\u0430 \u0440\u0435\u0448\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b. \u041f\u0440\u0435\u0434\u044b\u0441\u0442\u043e\u0440\u0438\u044f \u0427\u0442\u043e\u0431\u044b \u0431\u044b\u043b\u043e \u043f\u043e\u043d\u044f\u0442\u043d\u043e \u043e \u0447\u0451\u043c \u0440\u0435\u0447\u044c, \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u0441\u043e\u0432\u0441\u0435\u043c \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u043e VWO. \u042d\u0442\u043e \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430, [&hellip;]<\/p>\n","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[688],"tags":[],"class_list":["post-35259","post","type-post","status-publish","format-standard","hentry","category-administrirovanie"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.2 - aioseo.com -->\n\t<meta name=\"description\" content=\"\u0412 \u0434\u0435\u043a\u0430\u0431\u0440\u0435 \u043f\u0440\u043e\u0448\u043b\u043e\u0433\u043e \u0433\u043e\u0434\u0430 \u044f \u043f\u043e\u043b\u0443\u0447\u0438\u043b \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0439 \u043e\u0442\u0447\u0435\u0442 \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435 \u043e\u0442 \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 VWO. \u0412\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0437 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u0434\u043b\u044f \u043a\u0440\u0443\u043f\u043d\u043e\u0433\u043e \u043a\u043e\u0440\u043f\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u043d\u0435\u043f\u043e\u043c\u0435\u0440\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0438\u043c.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Yuri Gagarin\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/prohoster.info\/en\/blog\/administrirovanie\/istoriya-odnogo-sql-rassledovaniya\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.2\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"\ud83e\udd47\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043e\u0434\u043d\u043e\u0433\u043e SQL \u0440\u0430\u0441\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u044f | ProHoster\" \/>\n\t\t<meta property=\"og:description\" content=\"\u0412 \u0434\u0435\u043a\u0430\u0431\u0440\u0435 \u043f\u0440\u043e\u0448\u043b\u043e\u0433\u043e \u0433\u043e\u0434\u0430 \u044f \u043f\u043e\u043b\u0443\u0447\u0438\u043b \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0439 \u043e\u0442\u0447\u0435\u0442 \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435 \u043e\u0442 \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 VWO. \u0412\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0437 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u0434\u043b\u044f \u043a\u0440\u0443\u043f\u043d\u043e\u0433\u043e \u043a\u043e\u0440\u043f\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u043d\u0435\u043f\u043e\u043c\u0435\u0440\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0438\u043c.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/prohoster.info\/en\/blog\/administrirovanie\/istoriya-odnogo-sql-rassledovaniya\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:width\" content=\"350\" \/>\n\t\t<meta property=\"og:image:height\" content=\"350\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2019-10-31T19:03:17+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2019-10-31T19:03:17+00:00\" \/>\n\t\t<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"\ud83e\udd47The Story of an SQL Investigation | ProHoster","description":"Last December, I received an interesting bug report from the VWO support team. The load time of one of the analytical reports for a major corporate client seemed excessively long.","canonical_url":"https:\/\/prohoster.info\/en\/blog\/administrirovanie\/istoriya-odnogo-sql-rassledovaniya","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"en_US","og:site_name":"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b","og:type":"article","og:title":"\ud83e\udd47\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043e\u0434\u043d\u043e\u0433\u043e SQL \u0440\u0430\u0441\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u044f | ProHoster","og:description":"\u0412 \u0434\u0435\u043a\u0430\u0431\u0440\u0435 \u043f\u0440\u043e\u0448\u043b\u043e\u0433\u043e \u0433\u043e\u0434\u0430 \u044f \u043f\u043e\u043b\u0443\u0447\u0438\u043b \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0439 \u043e\u0442\u0447\u0435\u0442 \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435 \u043e\u0442 \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 VWO. \u0412\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0437 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u0434\u043b\u044f \u043a\u0440\u0443\u043f\u043d\u043e\u0433\u043e \u043a\u043e\u0440\u043f\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u043d\u0435\u043f\u043e\u043c\u0435\u0440\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0438\u043c.","og:url":"https:\/\/prohoster.info\/en\/blog\/administrirovanie\/istoriya-odnogo-sql-rassledovaniya","og:image":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:secure_url":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:width":350,"og:image:height":350,"article:published_time":"2019-10-31T19:03:17+00:00","article:modified_time":"2019-10-31T19:03:17+00:00","article:publisher":"https:\/\/www.facebook.com\/prohoster","article:author":"https:\/\/www.facebook.com\/prohoster"},"aioseo_meta_data":{"post_id":"35259","title":null,"description":null,"keywords":null,"keyphrases":null,"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":null,"og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"","isEnabled":true},"graphs":[]},"schema_type":null,"schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":null,"robots_max_videopreview":null,"robots_max_imagepreview":"large","priority":null,"frequency":null,"local_seo":null,"seo_analyzer_scan_date":"2026-01-21 22:33:19","breadcrumb_settings":null,"limit_modified_date":false,"reviewed_by":null,"ai":null,"created":"2021-03-01 02:07:28","updated":"2026-01-21 22:33:19","focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"gt_translate_keys":[{"key":"link","format":"url"}],"_links":{"self":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/posts\/35259","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/comments?post=35259"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/posts\/35259\/revisions"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/media?parent=35259"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/categories?post=35259"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/tags?post=35259"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}