The document discusses database queries and optimization. It begins with an example of a complex database query and explains how to detect problematic queries using tools like slow query log and pt-query-digest. It then discusses indexing strategies and when to use indexes. The document also describes a case study of a client's jobs search site that was experiencing high database load due to inefficient queries in a loop, and how batching the queries into a single query solved the problem.
Human Factors of XR: Using Human Factors to Design XR Systems
Beyond PHP: How Code Impacts the Database and Overall System Performance
1. Beyond PHP :
It's not (just) about the code
Wim Godden
Cu.be Solutions
2. Who am I ?
Wim Godden (@wimgtr)
Founder of Cu.be Solutions (http://cu.be)
Open Source developer since 1997
Developer of OpenX
Zend Certified Engineer
Zend Framework Certified Engineer
MySQL Certified Developer
Speaker at PHP and Open Source conferences
3. Cu.be Solutions ?
Open source consultancy
PHP-centered
High-speed redundant network (BGP, OSPF, VRRP)
High scalability development
Nginx + extensions
MySQL Cluster
Projects :
mostly IT & Telecom companies
lots of public-facing apps/sites
4. Who are you ?
Developers ?
Anyone setup a MySQL master-slave ?
Anyone setup a site/app on separate web and database server ?
→ How much traffic between them ?
5. The topic
Things we take for granted
Famous last words : "It should work just fine"
Works fine today
→ might fail tomorrow
Most common mistakes
PHP code ↔ PHP ecosystem
How-to & How-NOT-to
7. Database queries – complexity
SELECT DISTINCT n.nid, n.uid, n.title, n.type, e.event_start, e.event_start AS
event_start_orig, e.event_end, e.event_end AS event_end_orig, e.timezone,
e.has_time, e.has_end_date, tz.offset AS offset, tz.offset_dst AS offset_dst,
tz.dst_region, tz.is_dst, e.event_start - INTERVAL IF(tz.is_dst, tz.offset_dst,
tz.offset) HOUR_SECOND AS event_start_utc, e.event_end - INTERVAL
IF(tz.is_dst, tz.offset_dst, tz.offset) HOUR_SECOND AS event_end_utc,
e.event_start - INTERVAL IF(tz.is_dst, tz.offset_dst, tz.offset) HOUR_SECOND +
INTERVAL 0 SECOND AS event_start_user, e.event_end - INTERVAL IF(tz.is_dst,
tz.offset_dst, tz.offset) HOUR_SECOND + INTERVAL 0 SECOND AS
event_end_user, e.event_start - INTERVAL IF(tz.is_dst, tz.offset_dst, tz.offset)
HOUR_SECOND + INTERVAL 0 SECOND AS event_start_site, e.event_end INTERVAL IF(tz.is_dst, tz.offset_dst, tz.offset) HOUR_SECOND + INTERVAL 0
SECOND AS event_end_site, tz.name as timezone_name FROM node n INNER
JOIN event e ON n.nid = e.nid INNER JOIN event_timezones tz ON tz.timezone =
e.timezone INNER JOIN node_access na ON na.nid = n.nid LEFT JOIN
domain_access da ON n.nid = da.nid LEFT JOIN node i18n ON n.tnid > 0 AND
n.tnid = i18n.tnid AND i18n.language = 'en' WHERE (na.grant_view >= 1 AND
((na.gid = 0 AND na.realm = 'all'))) AND ((da.realm = "domain_id" AND da.gid = 4)
OR (da.realm = "domain_site" AND da.gid = 0)) AND (n.language ='en' OR
n.language ='' OR n.language IS NULL OR n.language = 'is' AND i18n.nid IS NULL)
AND ( n.status = 1 AND ((e.event_start >= '2010-01-31 00:00:00' AND
e.event_start <= '2010-03-01 23:59:59') OR (e.event_end >= '2010-01-31 00:00:00'
AND e.event_end <= '2010-03-01 23:59:59') OR (e.event_start <= '2010-01-31
00:00:00' AND e.event_end >= '2010-03-01 23:59:59')) ) GROUP BY n.nid HAVING
(event_start >= '2010-02-01 00:00:00' AND event_start <= '2010-02-28 23:59:59')
OR (event_end >= '2010-02-01 00:00:00' AND event_end <= '2010-02-28 23:59:59')
OR (event_start <= '2010-02-01 00:00:00' AND event_end >= '2010-02-28
23:59:59') ORDER BY event_start ASC;
8. Database - indexing
'select id from stock where status = 2 order by qty'
→ aggregate index on (status, qty)
'select id from stock where status > 2 order by qty'
→ aggregate index on (status, qty) ?
→ No : range selection stops use of aggregate index
→ separate index on status and qty
9. Database - indexing
Indexes make database faster
→ Let's index everything !
→ DON'T :
Insert/update/delete → Index modification
Each query → evaluation of all indexes
"Relational schema design is based on data
but index design is based on queries"
(Bill Karwin, Percona)
10. Databases – detecting problematic queries
Slow query log
→ SET GLOBAL slow_query_log = ON;
Queries not using indexes
→ In my.cnf/my.ini : 'log_queries_not_using_indexes'
General query log
→ SET GLOBAL general_log = ON;
→ Turn it off quickly !
Percona Toolkit (Maatkit)
pt-query-digest
16. Databases – when to use / not to use
Good at :
Fetching data
Storing data
Searching through data
Bad at :
select `someField` from `bigTable` where crc32(`field`) = "something"
→ full table scan
17. For / foreach
$customers = CustomerQuery::create()
->filterByState('SC')
->find();
foreach ($customers as $customer) {
$contacts = ContactsQuery::create()
->filterByCustomerid($customer->getId())
->find();
foreach ($contacts as $contact) {
doSomestuffWith($contact);
}
}
19. Better...
10001 → 1 query
Sadly : people still produce code with query loops
Usually :
Growth not anticipated
Internal app → Public app
20. The origins of this talk
Customers :
Projects we built
Projects we didn't build, but got pulled into
Fixes
Changes
Infrastructure migration
15 years of 'how to cause mayhem with a few lines of code'
21. Client X
Jobs search site
Monitor job views :
Daily hits
Weekly hits
Monthly hits
Which user saw which job
22. Client X
Originally : when user viewed job details
Now : when job is in search result
Search for 'php' → 50 jobs = 50 jobs to be updated
→ 50 updates for shown_today
→ 50 updates for shown_week
→ 50 updates for shown_month
→ 50 inserts for shown_user
23. Client X : the code
foreach ($jobs as $job) {
$db->query("
insert into shown_today(
jobId,
number
) values(
" . $job['id'] . ",
1
)
on duplicate key
update
number = number + 1
");
$db->query("
insert into shown_week(
jobId,
number
) values(
" . $job['id'] . ",
1
)
on duplicate key
update
number = number + 1
");
$db->query("
insert into shown_month(
jobId,
number
) values(
" . $job['id'] . ",
1
)
on duplicate key
update
number = number + 1
");
$db->query("
insert into shown_user(
jobId,
userId,
when
) values (
" . $job['id'] . ",
" . $user['id'] . ",
now()
)
");
}
28. Client X : possible cause ?
Code changes ?
→ According to developers : none
Action : turn on general log, analyze with pt-query-digest
→ 50+-fold increase in queries
→ Developers : 'Oops we did make a change'
After 3 days : 2,5 days behind
Every hour : 50 min extra lag
29. Client X : But why is the slave lagging ?
File :
master-bin-xxxx.log
um
g d ad
n lo e
Bi thr
Master
p
Slave I/O thread
File :
master-bin-xxxx.log
Sl
av
th e S
re Q
ad L
Slave
32. Client X : fix ?
foreach ($jobs as $job) {
$db->query("
insert into shown_today(
jobId,
number
) values(
" . $job['id'] . ",
1
)
on duplicate key
update
number = number + 1
");
$db->query("
insert into shown_week(
jobId,
number
) values(
" . $job['id'] . ",
1
)
on duplicate key
update
number = number + 1
");
$db->query("
insert into shown_month(
jobId,
number
) values(
" . $job['id'] . ",
1
)
on duplicate key
update
number = number + 1
");
$db->query("
insert into shown_user(
jobId,
userId,
when
) values (
" . $job['id'] . ",
" . $user['id'] . ",
now()
)
");
}
33. Client X : the code change
$todayQuery = "
insert into shown_today(
jobId,
number
) values ";
foreach ($jobs as $job) {
$todayQuery .= "(" . $job['id'] . ", 1),";
}
$todayQuery = substr($todayQuery, -1);
$todayQuery .= "
)
on duplicate key
update
number = number + 1
";
$db->query($todayQuery);
Result : insert into shown_today values (5, 1), (8, 1), (12, 1), (18, 1), ...
Careful : max_allowed_packet !
34. Client X : the chosen solution
$db->autocommit(false);
foreach ($jobs as $job) {
$db->query("
insert into shown_today(
jobId,
number
) values(
" . $job['id'] . ",
1
)
on duplicate key
update
number = number + 1
");
$db->query("
insert into shown_week(
jobId,
number
) values(
" . $job['id'] . ",
1
)
on duplicate key
update
number = number + 1
");
$db->query("
insert into shown_month(
jobId,
number
) values(
" . $job['id'] . ",
1
)
on duplicate key
update
number = number + 1
");
$db->query("
insert into shown_user(
jobId,
userId,
when
) values (
" . $job['id'] . ",
" . $user['id'] . ",
now()
)
");
}
$db->commit();
35. Client X : conclusion
For loops are bad (we already knew that)
Add master/slave and it gets much worse
Use transactions : it will provide huge performance increase
Result : slave caught up 5 days later
36. Database → Network
Customer Y
Top 10 site in Belgium
Growing rapidly
At peak traffic :
Unexplicable latency on database
Load on webservers : minimal
Load on database servers : acceptable
39. Client Y : network overload
Cause : Drupal hooks → retrieving data that was not needed
Only load data you actually need
Don't know at the start ? → Use lazy loading
Caching :
Same story
Memcached/Redis are fast
But : data still needs to cross the network
40. Network trouble : more than just traffic
Customer Z
150.000 visits/day
News ticker :
XML feed from other site (owned by same customer)
Cached for 15 min
41. Customer Z – fetching the feed
if (filectime(APP_DIR . '/tmp/ScrambledSiteName.xml') < time() - 900) {
unlink(APP_DIR . '/tmp/ScrambledSiteName.xml');
file_put_contents(
APP_DIR . '/tmp/ScrambledSiteName.xml',
file_get_contents('http://www.scrambledsitename.be/xml/feed.xml')
);
}
$xmlfeed = ParseXmlFeed(APP_DIR . '/tmp/ScrambledSiteName.xml');
What's wrong with this code ?
42. Customer Z – no feed without the source
Feed source
43. Customer Z – no feed without the source
Feed source
44. Customer Z : timeout
default_socket_timeout : 60 sec by default
Each visitor : 60 sec wait time
People keep hitting refresh → more load
More active connections → more load
Apache hits maximum connections → entire site down
47. Network resources
Use timeouts for all :
fopen
curl
SOAP
…
Data source trusted ?
→ setup a webservice
→ let them push updates when their feed changes
→ less load on data source
→ no timeout issues
Add logging → early detection
48. Logging
Logging = good
Logging in PHP using fopen
→ bad idea : locking issues
→ Use file_put_contents($filename, $data, FILE_APPEND)
For Firefox : FirePHP (add-on for Firebug)
Debug logging = bad on production
Watch your logs !
Don't log on slow disks → I/O bottlenecks
49. File system : I/O bottlenecks
Causes :
Excessive writes (database updates, logfiles, swapping, …)
Excessive reads (non-indexed database queries, swapping, small file
system cache, …)
How to detect ?
top
Cpu(s):
0.2%us,
iostat
avg-cpu:
%user
0.10
Device:
sda
sdb
dm-0
dm-1
3.0%sy,
0.0%ni, 61.4%id, 35.5%wa,
%nice %system %iowait
0.00
0.96
53.70
tps
120.40
2.10
4.20
0.00
Blk_read/s
0.00
0.00
0.00
0.00
%steal
0.00
Blk_wrtn/s
123289.60
4378.10
36.80
0.00
0.0%hi,
0.0%si,
0.0%st
%idle
45.24
Blk_read
0
0
0
0
Blk_wrtn
616448
18215
184
0
See iowait ? Stop worrying about php, fix the I/O problem !
50. File system
Worst of all : NFS
PHP files → lstat calls
Templates → same
Sessions
→ locking issues
→ corrupt data
→ store sessions in database, Memcached, Redis, ...
51. Much more than code
XML feed
User
Network
Webserver
DB
server
{"33":"Grouping\nWorks fine, but :\nmaximum size of string ?\nPHP = no limit\nMySQL = max_allowed_packet\n","11":"time spent per query pattern\nhow many queries of that query pattern\n","39":"Databases → network\nWhat other network related issues ?\n","17":"Get back to what I said\nLots of people use ORM\n- easier\n- don't need to write queries\n- object-oriented\nbut people start doing this\nImagine 10000 customers → 10001 queries\n","6":"Let's talk about code\nWithout : we don't exist\nWhat are most common mistakes in ecosystem\nLet's start with the database\n","45":"Better, not perfect.\nWhat else is wrong ?\nMultiple visitors hit expiring cache\n→ file delete\n→ xml feed hit a lot \n","34":"All in a single commit\nNote : transaction has max. size\nPossible : combination with previous solution\n","51":"How do you treat your data :\n- where do you get it\n- how long did you have to wait to get it\n- how is it transported\n- how is it processed\nminimize the amount of data :\nretrieved\ntransported\nprocessed,\nsent to db and users\n","29":"Master : 16 CPU cores\n12 cores for SQL\n1 core for binlog dump\nrest for system\nSlave : 16 CPU cores\n1 core for slave I/O\n1 core for slave SQL\n","18":"Not best code\nUses deprecated mysql extension\nno error handling\n","46":"Better, not perfect.\nWhat else is wrong ?\nMultiple visitors hit expiring cache\n→ file delete\n→ xml feed hit a lot \n","37":"took few moments to figure out\nNo network monitoring\n→ iptraf\n→ 100Mbit/sec limit\n→ packets dropped\n→ connections dropped\nCustomer : upgrade switch\nUs : why 100Mbit/sec ?\n","4":"5kbit/sec or 100Mbit/sec ?\n","43":"Server on which feed located : crashed\nFine for few minutes (cache)\n15 minutes : file_get_contents uses default_socket_timeout\n"}