Recent Discovery
Among everything else I tried, I replaced my JMeter profile with a custom JavaScript that hit each of my API endpoints in turn in an infinite loop, and then ran this script in parallel in different browsers (one Firefox, one Chrome, one Safari) - to try and rule out issues related to all of my connections coming from the same source (same user agent, same cookies, same session ID, etc)
When I did this, I noticed that all of my issues went away. The queries ran in parallel and the app was a lot more responsive than JMeter would have you believe
It seems impossible to me that JMeter would be serializing the requests, since it's a de facto standard for load testing. So I started trying to reproduce the behavior
In an attempt to re-create the JMeter I created the following two PHP scripts which (hopefully) simulated my Yii application:
slow.php
<?php
session_start();
$_SESSION['some'] = 'value';
// Yii is calling session_write_close() almost immediately after
// the session is initialized, but to try and exacerbate issues,
// I've commented it out:
// session_write_close();
$dsn = "mysql:host=localhost;dbname=platypus;unix_socket=/tmp/mysql.sock";
$pdo = new PDO($dsn, "user", "password");
// Yii was using whatever the default persistence behavior was,
// but to try and exacerbate issues I set this flag:
$pdo->setAttribute(PDO::ATTR_PERSISTENT, true);
// Simulate a query running for 1 second by issuing a 1-second sleep
$pdo->query("DO SLEEP(1)");
echo "Done";
fast.php
<?php
session_start();
$_SESSION['some'] = 'value';
$dsn = "mysql:host=localhost;dbname=platypus;unix_socket=/tmp/mysql.sock";
$pdo = new PDO($dsn, "user", "password");
$pdo->setAttribute(PDO::ATTR_PERSISTENT, true);
// Simulate a query running for 0.1 seconds
$pdo->query("DO SLEEP(0.1)");
echo "Done";
Running JMeter against these two new endpoints there was no serialization of requests. Everything ran in parallel. fast.php always returned in 100-150ms and slow.php always returned in 1000-1050ms even as I scaled up to 3, 4, and 5 threads. I was able to watch things collapse at 11 threads, but that's because I exceeded the number of worker threads in PHP
So to summarize:
The issue only occurs when profiling my API with JMeter and is not inherent in the app itself
The issue isn't just a JMeter bug, but is somehow tied to my application or Yii 1.1
I tried but could not come up with a minimum repro case
Despite the issue being non-existent when profiling with others tools, lots of people responded and gave lots of helpful information:
Avoid persistent connections in PHP (could cause multiple requests to share a connection, probably not)
Avoid session locking by calling session_write_close() as early as possible
Ensure you have enough PHP worker threads to handle the number of simultaneous connections
MySQL fully supports parallel requests (if the hardware can handle it)
Be wary of table locking (any transaction with an UPDATE statement could potentially lock the tables)
MyISAM does table-level locking instead of row-level locking
Original Post
I inherited a web application and I'm trying to make sense of its performance profile so I can start optimizing it for speed.
One thing I noticed pretty early on is that requests to the server are being serialized. For instance, suppose I have three endpoints with response times like so:
/api/endpoint1 --> 50ms
/api/endpoint2 --> 1000ms
/api/endpoint3 --> 100ms
If I hit a single endpoint, I measure the appropriate response times. But when I set up a script to hit all 3 at once I will sometimes see something like the following:
endpoint1: 50ms
endpoint2: 1050ms
endpoint3: 1150ms
Clearly the call to endpoint3 was queued and waiting for the call to endpoint2 to finish before it got a response.
My first thought was that this should be trivially solved with multithreading, so I took a look at the server configuration. PHP-FPM's process manager was set to "dynamic" with "start_servers" of 1, "max_children" of 5, and "max_spare_servers" of 2. For testing purposes I swapped this to "static" so that 5 PHP processes would remain open for handling connections in parallel (more than the 3 for the number of endpoints I was hitting, so they should be able to process simultaneously)
This had no impact on performance, so I looked at my nginx config. "worker_processes" was set to 1 with "worker_connections" set to 1024. I know that nginx uses an event loop model, so it shouldn't be blocking while it waits for a response from PHP-FPM. But just in case, I bumped up "worker_processes" to 5
Still, no effect. So next I looked at the database. All 3 endpoints had to hit the database, and I know as a fact that the 1000ms response time is mostly spent waiting on a long-running database query. I tried setting "thread_pool-size" to 5 and also within the MySQL REPL I set "innodb_parallel_read_threads" and "mysqlx_min_worker_threads" to 5
Still, my requests were getting serialized. When I log into the MySQL REPL and type show processlist; while my script is running (using a while-true loop to repeatedly hit those 3 API endpoints) I noticed that there was only ever one connection to the web application's user
Unfortunately I'm not sure if my issue lies with the database (not allowing more than one connection), with PHP-FPM (not processing more than one request at a time), or with nginx (not forwarding more than one request at a time to PHP-FPM). I'm also not sure how to figure out which one is acting as the bottleneck
Update
Looking around some more I found this SO post which seems to suggest that MySQL doesn't support parallel queries from the same user (e.g. from the web application user)
Is this true? Surely such a ubiquitous database engine wouldn't have such a performance flaw, especially given how commonly it's used with AWS for massively scaled applications. I understand that for simple "read from disk" queries parallelizing them wouldn't improve performance since they'd just have to sit in a queue waiting on disk I/O, but modern databases have in-memory caches, and most of the really slow operations like filesort tend to happen in memory. There's no reason a disk-bound query couldn't run in parallel (make a request to disk and start waiting on I/O) while a cpu-bound query is busy sorting a table in RAM. The context switching may slightly slow down the cpu-bound queries, but if slowing those down from 1000ms to 1200ms means my 5ms query can run in 5 ms, I think that's worth it.
My queries
Here are the queries for my 3 endpoints. Note that the timings listed are the response time for the full HTTP pipeline (from browser request to response) so this includes overhead from nginx and PHP, plus any post-processing of the query done in PHP. That said, the query in endpoint 2 makes up 99% of the runtime, and locks the database so that endpoints 1 and 3 are queued up instead of returning quickly.
endpoint1 (50ms)
SELECT * FROM Widget WHERE id = 1 LIMIT 1
(Note that 50ms is the full response time for the endpoint, not how long the query takes. This query is clearly on the order of microseconds)
endpoint2 (1000ms)
USE platypus;
SELECT `t`.`(49 fields)` AS `t0_cX`,
`site`.`(29 fields)` AS `t2_cX`,
`customer`.`(26 fields)` AS `t4_cX`,
`domain`.`(20 fields)` AS `t6_c0`,
`domain-general_settings`.`(18 fields)` AS `t8_cX`,
`domain-access_settings`.`(17 fields)` AS `t9_cX`,
`customer-general_settings`.`(18 fields)` AS `t10_cX`,
`customer-access_settings`.`(17 fields)` AS `t11_cX`,
`site-general_settings`.`(18 fields)` AS `t12_cX`,
`site-access_settings`.`(17 fields)` AS `t13_cX`,
`backup_broadcast`.`(49 fields)` AS `t14_cX`,
`playlists`.`(11 fields)` AS `t16_cX`,
`section`.`(10 fields)` AS `t17_cX`,
`video`.`(16 fields)` AS `t18_cX`,
`general_settings`.`(18 fields)` AS `t19_cX`,
`access_settings`.`(17 fields)` AS `t20_cX`,
FROM `broadcast` `t`
LEFT OUTER JOIN `site` `site`
ON ( `t`.`site_id` = `site`.`id` )
LEFT OUTER JOIN `customer` `customer`
ON ( `site`.`customer_id` = `customer`.`id` )
LEFT OUTER JOIN `domain` `domain`
ON ( `customer`.`domain_id` = `domain`.`id` )
LEFT OUTER JOIN `generalsettings` `domain-general_settings`
ON ( `domain`.`general_settings_id` =
`domain-general_settings`.`id` )
LEFT OUTER JOIN `accesssettings` `domain-access_settings`
ON
( `domain`.`access_settings_id` = `domain-access_settings`.`id` )
LEFT OUTER JOIN `generalsettings` `customer-general_settings`
ON ( `customer`.`general_settings_id` =
`customer-general_settings`.`id` )
LEFT OUTER JOIN `accesssettings` `customer-access_settings`
ON ( `customer`.`access_settings_id` =
`customer-access_settings`.`id` )
LEFT OUTER JOIN `generalsettings` `site-general_settings`
ON ( `site`.`general_settings_id` =
`site-general_settings`.`id` )
LEFT OUTER JOIN `accesssettings` `site-access_settings`
ON ( `site`.`access_settings_id` =
`site-access_settings`.`id` )
LEFT OUTER JOIN `broadcast` `backup_broadcast`
ON ( `t`.`backup_broadcast_id` = `backup_broadcast`.`id` )
AND ( backup_broadcast.deletion IS NULL )
LEFT OUTER JOIN `playlist_broadcast` `playlists_playlists`
ON ( `t`.`id` = `playlists_playlists`.`broadcast_id` )
LEFT OUTER JOIN `playlist` `playlists`
ON
( `playlists`.`id` = `playlists_playlists`.`playlist_id` )
LEFT OUTER JOIN `section` `section`
ON ( `t`.`section_id` = `section`.`id` )
LEFT OUTER JOIN `video` `video`
ON ( `t`.`video_id` = `video`.`id` )
AND ( video.deletion IS NULL )
LEFT OUTER JOIN `generalsettings` `general_settings`
ON ( `t`.`general_settings_id` = `general_settings`.`id` )
LEFT OUTER JOIN `accesssettings` `access_settings`
ON ( `t`.`access_settings_id` = `access_settings`.`id` )
WHERE
(
(
t.id IN (
SELECT `broadcast`.id FROM broadcast
LEFT JOIN `mediashare` `shares`
ON ( `shares`.`media_id` = `broadcast`.`id` )
AND `shares`.media_type = 'Broadcast'
WHERE
(
(
broadcast.site_id IN(
'489', '488', '253', '1083', '407'
)
OR
shares.site_id IN(
'489', '488', '253', '1083', '407'
)
)
)
)
)
AND
(
(
(
(t.deletion IS NULL)
)
)
AND
(
IF(
t.backup_mode IS NULL,
t.status,
IF(
t.backup_mode = 'broadcast',
backup_broadcast.status,
IF(
t.backup_mode = 'embed',
IF(
t.backup_embed_status,
t.backup_embed_status,
IF(
'2020-01-08 16:34:52' < t.date,
1,
IF(
t.date > Date_sub(
'2020-01-08 16:34:52',
INTERVAL IF(t.expected_duration IS NULL, 10800, t.expected_duration) second
),
10,
12
)
)
),
t.status
)
)
) != 0
)
)
)
LIMIT 10;
This query takes roughly 1000ms to run, but the PHP for the endpoint is extremely simple (run the query, return the results as JSON) and only adds a couple milliseconds of overhead
endpoint 3 (100ms)
SELECT * FROM platypus.Broadcast
WHERE deletion IS NULL
AND site_id IN (SELECT id FROM platypus.Site
WHERE deletion IS NULL
AND customer_id = 7);
There's additional validation on the PHP side here which makes this endpoint take 100ms. The SQL, as you can see, is still fairly simple.
Create Table Statements
As there is a post length limit in StackOverflow, I cannot show the CREATE TABLE for every single table touched by endpoint 2, but I can show at least one table. Others use the same engine.
CREATE TABLE `Widget` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`widget_name` varchar(255) NOT NULL,
`widget_description` varchar(255) NOT NULL,
`status` varchar(255) NOT NULL,
`date_created` datetime NOT NULL,
`date_modified` datetime NOT NULL,
`auto_play` varchar(255) NOT NULL,
`on_load_show` varchar(255) NOT NULL,
`widget_content_source` varchar(255) NOT NULL,
`associated_sites` text NOT NULL,
`author_id` int NOT NULL,
`associated_sections` text,
`after_date` datetime DEFAULT NULL,
`before_date` datetime DEFAULT NULL,
`show_playlists` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`is_classic` tinyint(1) NOT NULL,
`default_site` int unsigned DEFAULT NULL,
`auth_code_url` varchar(255) DEFAULT NULL,
`widget_layout_id` int unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fk_Widget_widget_layout_id_WidgetLayout_id` (`widget_layout_id`),
CONSTRAINT `fk_Widget_widget_layout_id_WidgetLayout_id` FOREIGN KEY (`widget_layout_id`) REFERENCES `WidgetLayout` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1412 DEFAULT CHARSET=utf8
Note
Notice that endpoint 2 doesn't even touch the Widget table, but endpoint 1 (which ONLY touches the Widget table) is also queued up. This eliminates the possibility of table locking.
When observing the process list in MySQL, only one connection is ever being made to the database from the application user. The issue may therefore lie in my PHP configuration.
Explain for query 2
Attached is the EXPLAIN SELECT ... query for endpoint 2
Simpler experiments
To try and determine where the parallel pipeline was falling apart, I created two simple scripts:
sleep.php
<?php
sleep(5);
echo "Done sleeping";
return.php
<?php
echo "That's all";
Doing this (sleeping in PHP) and running my script to hit these two endpoints with 3 threads I saw no issues. return.php always came back in ~11 milliseconds, despite sleep.php taking 5066 on average. I then tried doing the sleeping in MySQL:
sleep.php
<?php
$pdo = new PDO("...", "user", "pass");
$pdo->query("DO SLEEP(5)");
echo "Done sleeping";
This, again, had no issues. The sleeping endpoint did not block the non-sleeping one.
This means that the issue does not exist at the nginx, PHP, or PDO level - but that there must be some kind of table or row locking going on. I'm going to re-enable the general query log and scan through every query being performed to see if I can figure out what's happening.
Final Update
If you scroll up to "Recent Discovery" at the top of this post, you'll notice that I've modified my understanding of the problem.
I was not having an issue with parallelization, but with JMeter. I have failed to create a simple repro case, but I know now that the issue does not lie with my application but rather with how I'm profiling it.
MySQL + PHP + Apache has 'always' been very good at running separate SQL statements in 'parallel'. If separate users issue HTTP requests, they will naturally go through Apache quickly (probably in sequence, but fast) and get to separate instances of PHP (assuming Apache has configured enough 'children'). Each PHP script will make its own connection MySQL. MySQL will accept multiple connections quite rapidly (assuming max_connections is high enough, which it is by default). Each MySQL connection will work independently (baring low-level database locks, mutexes, etc). Each will finish when it finishes, ditto for PHP, and Apache returning results to the user.
I assume (without knowing for sure) that nginx works similarly.
Note: I suggest that Apache (and nginx) does things serially. But I suspect it takes on the order of a millisecond to hand off an HTTP request to PHP, so this "serial" step won't explain the timings you found.
I conclude that one of these is not really happening:
The configuration at each step is not allowing 3 children/connections/etc, or
There are 3 separate HTTP requests.
There are 3 separate PHP scripts.
The 3 SQL statements are not blocking each other. (Please provide the SQL.) Note: ENGINE=MyISAM uses table locking; this, alone, might explain the problem. (Please provide SHOW CREATE TABLE.)
It may be possible (after seeing the SQL) to speed up the SQL, thereby diminishing the overall problem of sluggishness.
Queries
Assuming id is the PRIMARY KEY of each table, then these other indexes may be beneficial at speeding up Query 2:
backup_broadcast: (deletion, id)
shares: (media_type, media_id, site_id)
broadcast: (site_id, id)
video: (deletion, id)
playlists_playlists: (playlist_id, broadcast_id)
playlist_broadcast smells like a "many-to-many mapping" table. If so, I recommend following the tips in http://mysql.rjweb.org/doc.php/index_cookbook_mysql#many_to_many_mapping_table . (Ditto for any similar tables.)
OR and IN ( SELECT ... ) tend to be inefficient constructs. But it sounds like you don't have any control over the queries?
Is that a LIMIT without an ORDER BY?? Do you care which 10 rows you get?? It will not be predictable.
What happens with that huge number of columns? Seems like most of them will be the same every time you run the query, thereby making it mostly a waste of time??
For query 3, site needs INDEX(deletion, customer_id) (in either order). However reformulating it to use a JOIN or EXISTS would probably run faster.
I think you have an issue with php session locking : your second and third query are trying to access the same php session, and are waiting.
Try to call session_write_close as soon as you can, to free your php session. As soon as you can : when you are sure you will not write any more data in your php session.
Another thread where you can read about the dangers of not handling your session properly
A long explanation about what is a php session (I did not read it all, sorry)
An easy way to check this is to try with 2 browsers or in anonymous/incognito mode : your cookies will not be shared, and you should have 2 sessions, not blocking each other.
MySQL can handle a lot of parallel queries, but you can't do more than one query at the time for each connection. The way PHP is usually setup is that each request goes to a different thread/process, so each process will have its own connection to MySQL, thus the problem mentioned is avoided. Unless you use persistent connection inside PHP and then you might end up using the same connection for each request. If that's the case it should be easy to disable it and go back to the standard one database connection per request model.
My first guess is that endpoint 2 triggers some locking on the database and that's why endpoint3 query is queued until enpoint2's query finishes. This can be fixed by changing the logic in the code (avoid or minimize the locking of the database), or by changing database configuration or table engines used to better suit application needs. Example: InnoDB uses row level locking while MyISAM locks the whole table lock.
Profiling will be really helpful if you don't mind configuring it. I suggest to have a look at Blackfire.io, New Relic or xdebug profiling if you go this route. You will be able to find the bottlenecks faster this way.
HM... too long for a comment.
a little bit simplified every engine has one queue where it gathers querys to be computed, depending on hardware it uses 2 or 3 or even more threads to compute every query. More threads are running more time every query needs, because of locks, like it locks an entire Table, when it inserts a new row with autoincrement.(you will find with a search many examples for locks). Of course every query needs memory and other resources that they have to share with the rest of all computer software that is running on a server.
With clustes you pay the price with overhead to manage multiple sql servers.
So from sql server side, it is parallel, however you need the hardware to support many threads/many engines(which should only be uses very carefully)
Of course you can have many users in sql, but for convenience sake, you have usually one per APP or sometimes even one per server. But the same user can access the database simultaneously, but you can disable that of course.
Your php runs parallel, because webserver are build to run papallel requests and and there it doesn't matter if it runs php, Python(django) or javascript(nodejs) , apache, IIS, nginx and there are a lot more, every technology has there perks and of cause more module you add to en engine, so much slower it gets.
So everything is parallel to a certain degree and you can increase the power of such systems as you see in cloud providers or virtual servers and so on.
The limits you only notice when like the introduction of Pokemon go or new games where even the huge cloud providers crash. Or the disaster with ObamaCare where nothing was tested on that scale, whichever idi... was responsible,
Parallelizing such tasks is difficult, because in case of a webserver and sqlserver it has to a degree caches where they park requests that are often made, but usually every request needs its own data.
In reality everything is much more complicated, starting with cpus with 3 Pipelines , Multiple cores and shared memory(which caused Meltdown and their brothers), goes over tables or databases that reside only in memory for high performance or web server that run only in cache of cpus, which is much faster than memory or harddrives.....
Related
I'm running Ubuntu 13.04 with an nginx webserver installed. I'm writing a mini-social network for the users on my website, but for some reason the scripts I use to load things like profiles and "walls" are sometimes slow. Not all of them are slow, but especially the newsfeed script where it shows recent posts by friends.
I've added a bunch of microtime() checks throughout the script and it seems the query to get the recent posts is taking the most time. I tried to optimize it as much as possible but it still seems to be slow. I'm using MySQLi. Here is my query:
SELECT `id`,`posterName`, `posterUUID`, `message`, `postDate`, `likes`, `whoLiked`
FROM `wallposts`
WHERE (
`wallUUID` IN (' . implode(',', $friendStr) . ')
AND posterUUID = wallUUID
)
OR wallUUID="GLOBAL"
AND isDeleted=0
ORDER BY `postDate` DESC
LIMIT 25
Would it be faster to just use SELECT * since I'm pretty much selecting most of the columns anyway? I'm not sure what else to try, so that's why I came here.
Any help please as to what I could do/not do to keep it from taking 5+ seconds just for this query?
Several things:
using * instead of a list of columns is usually a bad idea, the risk is to add a column that you do not need and this column could be containing large amounts of binary data, this would make your query slower. So it's certainly not something to care about when you have speed problems.
you may have some priority of logical operators AND/OR problems
Your query is:
WHERE (A)
OR B
AND C
And I'm pretty sure you mean:
WHERE (
(A)
OR B
)
AND C
But AND takes precedence, so what you have is:
WHERE (A)
OR (
B
AND C
)
When in doubt use parenthesis (I'm in doubt there, but I would use parenthesis).
Your first WHERE condition is quite strange:
WHERE (
wallUUID IN (42,43,44,45,46)
AND posterUUID = wallUUID
)
That mean a filter on the friends identifiers for the wall posts, I guess, and then a filter which says for each row we need to have the same id for the poster uid and for the wall id.
I'm pretty that's not what you wanted. Maybe you need a join query here. Or maybe not, without the structure of your tables it's hard to guess
You will need a pretty decent indexation to get an optimized result on friend's posts results, an dindex which starts by the current user id, contain sthe right sort by date, the deletion thing, and certainly the friends identifiers.
user-friends relationships are hard to manage, especially when volumes gets bigger, usually building a social website involves pub/sub systems (publication subscriptions channels systems). You should study some pubsub databases schemas.
SELECT t1.id,t1.tx_id,t1.tx_date,t1.bx_date,t1.method,t1.theater_id,t1.showtime_id,t1.category_id,t1.amount,t1.fname,t1.status,t1.mobile,(CASE WHEN (t4.type = '1') THEN ( (t1.full_tickets * 2 ) + (t1.half_tickets)) ELSE ( t1.full_tickets + t1.half_tickets ) END) as no_seats ,u.username FROM 'reservation` as t1 LEFT JOIN `theatercategories` as t4 ON t1.category_id=t4.id JOIN `users` AS u ON u.id = t1.user_id WHERE t1.bx_date >= '2012-08-01' AND t1.bx_date <= '2012-08-31' ORDER BY t1.id desc
Above query returns "The connection was reset" error.
It loads 75,000 records(75,195 total, Query took 15.2673 sec). I use MYSQL with Joomla. What seemes be the issue ?
Please guide me. Thanks
There are a number of possible solutions ... depends on the "why" ... so it ends up being a bit of trial and error. On a fresh install, that's tricky to determine. But, if you made a recent "major" change that's a place to start looking - like modifying virtual hosts or adding/enabling XDebug.
Here's a list of things I've used/done/tried in the past
check for infinite loops ... in particular looping through a SQL
fetch result which works 99% of the time except the 1% it doesn't. In
one case, I was using the results of two previous queries as the
upper and lower bounds of a for loop ... and occasionally got a upper
bound of a UINT max ... har har har (vomit)
copying the ./php/libmysql.dll to the windows/system32 directory (Particularly if you see Parent: child process exited with status
3221225477 -- Restarting in your log files ... check out:
http://www.java-samples.com/showtutorial.php?tutorialid=1050)
if you modify PHP's error_reporting at runtime ... in certain circumstances this can cause PHP to degenerate into an unstable state
if, say, in your PHP code you modify the superglobals or fiddle
around with other deep and personal background system variables (Nah,
who would ever do such evil hackery? ahem)
if you convert your MySQL to something other than MyISAM or mysqli
There is a known bug with MySQL related to MyISAM, the UTF8 character set and indexes (http://bugs.mysql.com/bug.php?id=4541)
Solution is to use InnoDB dialect (eg sql set GLOBAL
storage_engine='InnoDb';)
Doing that changes how new tables are created ... which might slightly alter the way results are returned to a fetch statement ...
leading to an infinite loop, a malformed dataset, etc. (although this
change should not hang the database itself)
Other helpful items are to ramp up the debug reporting for PHP and
apache in their config files and restart the servers. The log files
sometimes give a clue as to at least where the problem might reside.
If it happens after your page content was finished it's more likely
in the php settings. If it's during page construction, check your PHP
code. Etc. etc.
Hope the above laundry list helps ...
just refresh you DB link, it might disconnected because of some reason,
This method is to select number of actions between User and TargetUser, e.g. just between two users. The result values of this method are dependent of GetTotalOfPossibleActions() return value which is dynamic (every user has its own number).
Question: Is it better to move values computation to PHP layer out of SQL?
public function GetAction() {
// ...
$MaxActionCount = $this->GetTotalOfPossibleActions();
return registry::getInstance()->get('DB')->select(
'SELECT
`Action`
, `HA`.`Id` AS `ActionId`
, IF(`Count` IS NULL
, IF('.$MaxActionCount.' % 2
, IF(`HA`.`Id` = 1
, CEIL('.$MaxActionCount.' / 2)
, FLOOR('.$MaxActionCount.' / 2))
, '.$MaxActionCount.' / 2)
, GREATEST(IF('.$MaxActionCount.' % 2
, IF(`HA`.`Id` = 1
, CEIL('.$MaxActionCount.' / 2) - CONVERT(`H`.`Count`, SIGNED)
, FLOOR('.$MaxActionCount.' / 2) - CONVERT(`H`.`Count`, SIGNED))
, '.$MaxActionCount.' / 2 - `H`.`Count`), 0)
) AS `CountLeft`
FROM `Help` AS `H`
RIGHT JOIN `HelpAction` AS `HA`
ON `H`.`ActionId` = `HA`.`Id`
AND `UserId` = '.$this->UserId.'
AND `TargetUserId` = '.$this->TargetUserId.'
AND `CreatedDate` = CURDATE()'));
}
There is no hard and fast answer, but the following is a general guide I apply.
PHP servers can be load balanced and multiplied. So if you're running short on CPU cycles, you can add another PHP server relatively easily. Conversely, MySQL server can't be easily multiplied. You can add replication servers and run complex "selects" on the slave servers, but replication adds a little stress and there is always a delay between an update on the master, and it being available on the slave. (Never believe the DB admin that says it's milliseconds; and it gets slower when the server is under stress / with backups etc.)
So +1 to PHP.
However, SQL is built and designed to do computations with the data. That's it's job. It's much more efficient at it (assuming you design the tables, queries and indexes correctly).
So +1 to mySQL.
If you do the computation on MySQL, you also don't need to transfer the entire data table to PHP to process and handle. This saves netwerk traffic between the two.
So +1 to mySQL.
But if you're struggling to get the queries right, or MySQL is using up all your memory, non-stop creating temporary tables and swapping to hard disk, it's a lot easier to debug and find the problem by breaking down the computation in PHP. MySQL EXPLAIN and slow query logs are frustrating to decypher sometimes.
So +1 to PHP.
So... 2 all. If you have both on one server, and the queries are not giving you problems, let MySQL do it's job. If you are having problems with queries, pull back into PHP. If you're on multiple servers, do as much in MySQL as you can without reaching a bottleneck. If MySQL is a bottleneck and replication not possible, pull back into PHP. But then check network traffic.
Then remember to re-evaluate when your load increases...
Update 2011/12/12: Now isolated as FastCGI on my (IIS-based) hosting package. I had them turn it off because a scheduled task kept timing out. I know where I am now. Thanks again, all.
Mark Iliff
Update 2011/12/11: OK, I have to put my hands up to inadvertent misdirection.
Thanks to your many suggestions I've now identified this as a problem with PHP pages on my hosting package, not specifically MySQL. An empty PHP page still takes 5-6 seconds to load, whereas the same page with an ASP or HTML extension loads too fast to measure. I'm taking this up with my hosting provider.
Sorry for not thinking to check this first and thanks for pitching in: much appreciated.
Mark Iliff
I'm relatively new to MySQL/PHP and suspect I'm doing something stupid with the following:
<?
// Slave page: block/unblock merchant
$id = $_POST["id"] ;
$val = $_POST["val"] ;
if ( isset( $id ) && isset( $val ) ) {
$conx = mysqli_connect ( "sql05", $dbAc, $dbPwd, "finewine" )
or die ("Conx failed") ;
// update record
$sql = "UPDATE wsMerchants SET
blockem = ".$val.", updateDT = '".date( "Y-m-d H:i:s" )."'
WHERE id = ".$id.";" ;
$result = mysqli_query( $conx, $sql ) ;
//tidy
mysqli_close( $conx ) ;
};
?>
Result of SHOW CREATE:
CREATE TABLE `wsmerchants` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`merchant` text NOT NULL,
`country` tinytext NOT NULL,
`blockem` tinyint(1) DEFAULT '0',
`benchmark` tinyint(1) DEFAULT '0',
`createDT` datetime NOT NULL,
`updateDT` datetime NOT NULL,
UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=119 DEFAULT CHARSET=utf8
I'm calling this from JQuery (using $.post). The db table has 29 records and 7 fields.
It works, but incredibly slowly.
With sad old Access (+ Classic ASP) queries like this are, for all practical purposes, instantaneous. In MySQL, according to the timer in Firebug, the query takes 5-6 seconds.
I'm running MySQL 5.0.51a + PHP 5.2.13 in a hosted Windows environment.
I've rootled around other questions in here but they mainly seem to involve complex SQL queries.
Other issues aside, I doubt the bottleneck here is the query.
First, try the query from the MySQL console to ensure your database server is ok.
Then, make a simple test script that only connects to the database and does not perform any queries. Make sure you place something after the connect call so that you can see when it finishes connecting.
That's my guess anyway - is that the bottleneck lies in the database connection.
One more thing, is this an AMP stack (WAMP, XAMPP) in Windows, or are you running PHP through IIS?
I don't see anything within that query that would take >5 seconds. Firebug is probably giving you the time of the total request, not just the query. That said if the latency is particularly high between the client and application server or the application server and the database then 5 seconds is not unthinkable. I would start by benchmarking the script to figure out exactly what is taking so long. You can do this by taking timestamps using microtime() throughout the script and then subtracting them to figure out how long each each part of the script is taking.
Judging from the conversation in the comments, it seems like your issue is with the network rather than with your code. My gut tells me it's probably a DNS issue, in which case you may want to look at this section of the MySQL manual. Alternatively, try using an IP address rather than "sql05" in your connection string, see if that speeds things up.
I've done some searching for this but haven't come up with anything, maybe someone could point me in the right direction.
I have a website with lots of content in a MySQL database and a PHP script that loads the most popular content by hits. It does this by logging each content hit in a table along with the access time. Then a select query is run to find the most popular content in the past 24 hours, 7 day or maximum 30 days. A cronjob deletes anything older than 30 days in the log table.
The problem I'm facing now is as the website grows the log table has 1m+ hit records and it is really slowing down my select query (10-20s). At first I though the problem was a join I had in the query to get the content title, url, etc. But now I'm not sure as in test removing the join does not speed the query as much as I though it would.
So my question is what is best practise of doing this kind of popularity storing/selecting? Are they any good open source scripts for this? Or what would you suggest?
Table scheme
"popularity" hit log table
nid | insert_time | tid
nid: Node ID of the content
insert_time: timestamp (2011-06-02 04:08:45)
tid: Term/category ID
"node" content table
nid | title | status | (there are more but these are the important ones)
nid: Node ID
title: content title
status: is the content published (0=false, 1=true)
SQL
SELECT node.nid, node.title, COUNT(popularity.nid) AS count
FROM `node` INNER JOIN `popularity` USING (nid)
WHERE node.status = 1
AND popularity.insert_time >= DATE_SUB(CURDATE(),INTERVAL 7 DAY)
GROUP BY popularity.nid
ORDER BY count DESC
LIMIT 10;
We've just come across a similar situation and this is how we got around it. We decided we didn't really care about what exact 'time' something happened, only the day it happened on. We then did this:
Every record has a 'total hits' record which is incremented every time something happens
A logs table records these 'total hits' per record, per day (in a cron job)
By selecting the difference between two given dates in this log table, we can deduce the 'hits' between two dates, very quickly.
The advantage of this is the size of your log table is only as big as NumRecords * NumDays which in our case is very small. Also any queries on this logs table are very quick.
The disadvantage is you lose the ability to deduce hits by time of day but if you don't need this then it might be worth considering.
You actually have two problems to solve further down the road.
One, which you've yet to run into but which you might earlier than you want, is going to be insert throughput within your stats table.
The other, which you've outlined in your question, is actually using the stats.
Let's start with input throughput.
Firstly, in case you're doing so, don't track statistics on pages that could use caching. Use a php script that advertises itself as an empty javascript, or as a one-pixel image, and include the latter on pages you're tracking. Doing so allows to readily cache the remaining content of your site.
In a telco business, rather than doing an actual inserts related to billing on phone calls, things are placed in memory and periodically sync'ed with the disk. Doing so allows to manage gigantic throughputs while keeping the hard-drives happy.
To proceed similarly on your end, you'll need an atomic operation and some in-memory storage. Here's some memcache-based pseudo-code for doing the first part...
For each page, you need a Memcache variable. In Memcache, increment() is atomic, but add(), set(), and so forth aren't. So you need to be wary of not miss-counting hits when concurrent processes add the same page at the same time:
$ns = $memcache->get('stats-namespace');
while (!$memcache->increment("stats-$ns-$page_id")) {
$memcache->add("stats-$ns-$page_id", 0, 1800); // garbage collect in 30 minutes
$db->upsert('needs_stats_refresh', array($ns, $page_id)); // engine = memory
}
Periodically, say every 5 minutes (configure the timeout accordingly), you'll want to sync all of this to the database, without any possibility of concurrent processes affecting each other or existing hit counts. For this, you increment the namespace before doing anything (this gives you a lock on existing data for all intents and purposes), and sleep a bit so that existing processes that reference the prior namespace finish up if needed:
$ns = $memcache->get('stats-namespace');
$memcache->increment('stats-namespace');
sleep(60); // allow concurrent page loads to finish
Once that is done, you can safely loop through your page ids, update stats accordingly, and clean up the needs_stats_refresh table. The latter only needs two fields: page_id int pkey, ns_id int). There's a bit more to it than simple select, insert, update and delete statements run from your scripts, however, so continuing...
As another replier suggested, it's quite appropriate to maintain intermediate stats for your purpose: store batches of hits rather than individual hits. At the very most, I'm assuming you want hourly stats or quarter-hourly stats, so it's fine to deal with subtotals that are batch-loaded every 15 minute.
Even more importantly for your sake, since you're ordering posts using these totals, you want to store the aggregated totals and have an index on the latter. (We'll get to where further down.)
One way to maintain the totals is to add a trigger which, on insert or update to the stats table, will adjust the stats total as needed.
When doing so, be especially wary about dead-locks. While no two $ns runs will be mixing their respective stats, there is still a (however slim) possibility that two or more processes fire up the "increment $ns" step described above concurrently, and subsequently issue statements that seek to update the counts concurrently. Obtaining an advisory lock is the simplest, safest, and fastest way to avoid problems related to this.
Assuming you use an advisory lock, it's perfectly OK to use: total = total + subtotal in the update the statement.
While on the topic of locks, note that updating the totals will require an exclusive lock on each affected row. Since you're ordering by them, you don't want them processed all in one go because it might mean keeping an exclusive lock for an extended duration. The simplest here is to process the inserts into stats in smaller batches (say, 1000), each followed by a commit.
For intermediary stats (monthly, weekly), add a few boolean fields (bit or tinyint in MySQL) to your stats table. Have each of these store whether they're to be counted for with monthly, weekly, daily stats, etc. Place a trigger on them as well, in such a way that they increase or decrease the applicable totals in your stat_totals table.
As a closing note, give some thoughts on where you want the actual count to be stored. It needs to be an indexed field, and the latter is going to be heavily updated. Typically, you'll want it stored in its own table, rather than in the pages table, in order to avoid cluttering your pages table with (much larger) dead rows.
Assuming you did all the above your final query becomes:
select p.*
from pages p join stat_totals s using (page_id)
order by s.weekly_total desc limit 10
It should be plenty fast with the index on weekly_total.
Lastly, let's not forget the most obvious of all: if you're running these same total/monthly/weekly/etc queries over and over, their result should be placed in memcache too.
you can add indexes and try tweaking your SQL but the real solution here is to cache the results.
you should really only need to caclulate the last 7/30 days of traffic once daily
and you could do the past 24 hours hourly ?
even if you did it once every 5 minutes, that's still a huge savings over running the (expensive) query for every hit of every user.
RRDtool
Many tools/systems do not build their own logging and log aggregation but use RRDtool (round-robin database tool) to efficiently handle time-series data. RRDtools also comes with powerful graphing subsystem, and (according to Wikipedia) there are bindings for PHP and other languages.
From your questions I assume you don't need any special and fancy analysis and RRDtool would efficiently do what you need without you having to implement and tune your own system.
You can do some 'aggregation' in te background, for example by a con job. Some suggestions (in no particular order) that might help:
1. Create a table with hourly results. This means you can still create the statistics you want, but you reduce the amount of data to (24*7*4 = about 672 records per page per month).
your table can be somewhere along the lines of this:
hourly_results (
nid integer,
start_time datetime,
amount integer
)
after you parse them into your aggregate table you can more or less delete them.
2.Use result caching (memcache, apc)
You can easily store the results (which should not change every minute, but rather every hour?), either in a memcache database (which again you can update from a cronjob), use the apc user cache (which you can't update from a cronjob) or use file caching by serializing objects/results if you're short on memory.
3. Optimize your database
10 seconds is a long time. Try to find out what is happening with your database. Is it running out of memory? Do you need more indexes?