לפני כמה שבועות פרסמתי פוסט על אופטימיזציות.
הנה ההמשך.
דוגמא מעשית, שיפור ביצועים של שאילתות בFireStats (לגרסא 1.6)
מנגנון מקובל לשיפור ביצועים הוא מטמון (cache). הרעיון פשוט : בפעם הראשונה ששואלים אותנו משהו אנחנו מחשבים אותו וזוכרים את התוצאה. בפעם השניה אנחנו משתמשים בתוצאה שחישבנו קודם.
כשבודקים ביצועים, חשוב לוודא שהבדיקה נותנת תוצאות זהות בשתי הרצות שונות. במקרים רבים יש מנגנוני מטמון יגרמו לבדיקה לרוץ הרבה יותר מהר בפעם השניה, ובדרך כלל הם רק מפריעים בסיטואציות של בדיקת ביצועים כי אנחנו רוצים למדוד את השיפור שנובע מהקוד שלנו, ולא מזה שהCache הכיל את התוצאה שכבר חישבנו בהרצה הראשונה.
דוגמא לכך היא הCache של MySQL: ההרצה השניה של שאילת תמיד תהיה הרבה יותר מהירה מההרצה הראשונה, כי MySQL זוכר את התוצאות מהפעם הראשונה ואם שום דבר בנתונים לא השתנה הוא פשוט מחזיר את אותה תוצאה.
כדי לבטל את אותו Cache, אפשר להשתמש בפקודת הMySQL:
[code lang="sql"]
SET GLOBAL query_cache_size = 0;
[/code]
FireStats תומך בגרסאות MySQL ממשפחת 4.0, 4.1 ו5.0, כאשר ההבדל בין 4.0 ל4.1 כל כך משמעותי שיש שאילתות בFireStats שכתובות אחרת לכל אחת מהגרסאות.
נסיון לשיפור ביצועים, במיוחד כזה שמבוסס על שינויים בסכמת הנתונים יצריך שינויים בקוד שמטפל ב4.0 ובקוד של 4.1, לכן כדאי לבדוק שהכל עובד על השרת הרלוונטי. בנוסף, מכיוון שרוב (60%) משתמשי FireStats עובדים עם MySQL 5.X, כדי גם לבדוק את השיפור על השרת הזה.
כדי לבצע את זה, הרמתי שלושה שרתי MySQL, נציג אחד מכל משפחת גרסאות, והכנסתי לכל שרת חבילת נתונים די כבדה (וזהה בין השרתים) שמבוססים על סטטיסטיקות מהשרת שלי.
ובכל אחד מהשרתים ביטלתי את מטמון השאילתות (query_cache_size).
יצרתי טבלא בגנומטיק (כמו אקסל), שנראית ככה (תת טבלא כזו לכל בדיקה שאני רוצה)
נניח שהפונקציה שאני רוצה לשפר היא:
[code lang="php"]
function foo()
{
$sql = "SELECT * from …";
return query($sql);
}
[/code]
קודם כל הכפלתי את הקוד והוספתי הדפסה של שאילת הSQL שנשלחת לשרת:
[code lang="php"]
function foo()
{
if (true) // old code
{
$sql = "SELECT * from …";
echo $sql; return;
return query($sql);
}else{
$sql = "SELECT * from …";
echo $sql; return;
return query($sql);
}
}
[/code]
מכאן זו היתה שיטת העבודה:
1. הרצה של הקוד במצב הישן.
2. העתקה של השאילתה שהודפסה, והרצה שלה ישירות על כל אחד משלושת בסיסי הנהנתונים. ושמירה של הזמן שנדרש בטבלא מסודרת.
3. מעבר למצב חדש (פשוט לשנות את הfalse לtrue בראש הפונקציה), אופטימיזציה של השאילתה וחזרה על הבדיקה מול כל אחד מהשרתים, ושוב שמירה של התוצאות בטבלא, הפעם בעמודה של תוצאות אחרי אופטימיזציה.
העבודה השיטתית עזרה לי לא לשכוח דברים תוך כדי, ולוודא שאכן שיפרתי את התוצאות לפני שאני רץ להכניס את הקוד למערכת.
בנוסף, מכיוון שגם הקוד החדש וגם הישן היו לי מול העיניים, יכלתי לוודא שהם עושים בדיוק את אותו דבר.
חשוב לשים לב שלא ערבבתי אופטימיזציה של קוד הPHP עם שאילות הMYSQL. זה מבלבל מספיק גם ככה.
אז מה בעצם עשיתי?
פיירסטטס תומך בכמה אתרים בו זמנית, ולכן יש בו מזהה אתר. בהתחלה שמרתי את מזהה האתר בטבלאת הכניסות, בשלב מסויים הבנתי שאני צריך מזהה אתר גם בטבלאת הכתובות (URLים), כי הרי כל URL שיך לכל היותר לאתר אחד.
אז הוספתי את העמודה, אבל שכחתי למחוק את העמודה מטבלאת הכניסות.
מצב כזה של כפילות נתונים הוא לא מומלץ לפי הגישה המקובלת בתכנון בסיסי נתונים, כי הוא מאפשר מצבים של חוסר עקביות במידע.
באחת הגרסאות האחרונות של פיירסטטס תיקנתי את המעוות: הסרתי את העמודה מטבלאת הכניסות ותיקנתי את כל הקוד שהושפע מזה לבצע join עם טבלאת הכתובות כשהוא צריך גישה לאתר.
על פניו הדבר הנכון לעשות: אבל זה יצר בעיה חדשה:
כמעט כל השאילתות דרשו עכשיו join נוסף. עבור משתמשים עם בסיסי נתונים קטנים (נניח עד חצי מליון כניסות בטבלאת הכניסות ועד 50 אלף כתובות בטבלאת העסק עבד סביר, אבל עבור משתמשים עם בסיסי נתונים גדולים יותר העומס על השרת התחיל לגדול בצורה חדה ככל שנפח הנתונים עלה.
הפתרון שלי הוא לחזור בי מהדבר "הנכון" שעשתי קודם, כלומר להחזיר את עמודת הsite_id לטבלאת הכניסות, שתשמש לצרכי פילטור בזמן השאילות כדי להמנע מהjoin עם טבלאת הכתובות.
שיפור נוסף נבע משינוי מבני של השאילות ככה שיעבדו בצורה יותר יעילה.
כדי לעשות את זה, חשוב להבין מה עושה בסיס הנתונים כדי לחשב את השאילתה.
לשם כך אפשר להשתמש בפקודה explain, שמחזירה תיאור של האסטרטגיה שבה בסיס הנתונים יחשב את התוצאות.
explain מחזיר תוצאות די קריפטיות, לא משהו שתבינו בלי לקרוא את התיעוד.
בכך מקרה, חשוב לשים לב לעובדות הבאות:
בכל שלב, MySQL משתמש באינדקס אחד בלבד לכל היותר (אם יש אינדקס מתאים). ככל שהאינדקס יהיה יותר ספציפי ככה הביצועים יכולים להיות טובים יותר.
(יתכן שגרסאות עתידיות של MySQL ידעו להשתמש בכמה אינדקסים בשלב, אבל לדעתי זה עדיין לא פה).
ככה נראית הטבלא שלי אחרי שמילאתי אותה:
Query name |
MySQL Version |
4.0.17 |
4.1 |
5.0.51 |
|
Num page views for all hits in site=1 |
Original query time sec |
1.20 |
0.93 |
0.84 |
Optimized query time sec |
0.10 |
0.06 |
0.06 |
|
Improvement % |
91.67% |
93.55% |
92.86% |
|
|
|
|
|
|
|
Num page views for all hits in all sites |
Original query time sec |
1.17 |
0.91 |
0.84 |
|
Optimized query time sec |
0.11 |
0.08 |
0.08 |
|
Improvement % |
90.60% |
91.21% |
90.48% |
|
|
|
|
|
|
|
Num page views for for last last 14 days for all sites |
Original query time sec |
0.19 |
0.16 |
0.16 |
|
Optimized query time sec |
0.12 |
0.09 |
0.11 |
|
Improvement % |
36.84% |
43.75% |
31.25% |
|
|
|
|
|
|
|
Num all unique visitors for site_id = 1 |
Original query time sec |
2.93 |
1.24 |
1.13 |
|
Optimized query time sec |
1.75 |
0.68 |
0.72 |
|
Improvement % |
40.27% |
45.16% |
36.28% |
|
|
|
|
|
|
|
Num all unique visitors for all sites |
Original query time sec |
2.09 |
1.24 |
1.15 |
|
Optimized query time sec |
1.26 |
0.47 |
0.47 |
|
Improvement % |
39.71% |
62.10% |
59.13% |
|
|
|
|
|
|
|
Num unique visitors for last 14 days for all sites |
Original query time sec |
0.32 |
0.18 |
0.18 |
|
Optimized query time sec |
0.28 |
0.16 |
0.15 |
|
Improvement % |
12.50% |
11.11% |
16.67% |
|
|
|
|
|
|
|
Recent referrers for all sites |
Original query time sec |
2.54 |
3.01 |
3.01 |
|
Optimized query time sec |
2.30 |
1.54 |
1.78 |
|
Improvement % |
9.45% |
48.84% |
40.86% |
|
|
|
|
|
|
|
Recent referrers for site_id = 1 |
Original query time sec |
2.48 |
2.71 |
2.76 |
|
Optimized query time sec |
2.65 |
1.65 |
1.90 |
|
Improvement % |
−6.85% |
39.11% |
31.16% |
|
|
|
|
|
|
|
Search terms for site_id = 1 |
Original query time sec |
1.67 |
2.14 |
2.75 |
|
Optimized query time sec |
1.67 |
0.84 |
0.91 |
|
Improvement % |
Not optimized |
60.75% |
66.91% |
|
|
|
|
|
|
|
Popular pages (all) |
Original query time sec |
1.88 |
4.67 |
4.80 |
|
Optimized query time sec |
– |
1.73 |
1.96 |
|
Improvement % |
Not optimized |
62.96% |
59.17% |
|
|
|
|
|
|
|
Popular pages (site_id = 1) |
Original query time sec |
1.93 |
4.07 |
4.09 |
|
Optimized query time sec |
– |
1.88 |
2.11 |
|
Improvement % |
Not optimized |
53.81% |
48.41% |
|
|
|
|
|
|
|
get questagents (180 days, site_id = 1) |
Original query time sec |
6.89 |
2.90 |
2.35 |
|
Optimized query time sec |
5.07 |
1.00 |
1.10 |
|
Improvement % |
26.42% |
65.52% |
53.19% |
|
|
|
|
|
|
|
|
|
|
|
|
|
Countries (20 countries, 180 days, site_id = 1) |
Original query time sec |
1.95 |
1.08 |
1.08 |
|
Optimized query time sec |
0.30 |
0.20 |
0.23 |
|
Improvement % |
84.62% |
81.48% |
78.70% |
|
|
|
|
|
|
|
Hits table (100) |
Original query time sec |
0.30 |
0.61 |
0.42 |
|
Optimized query time sec |
– |
– |
– |
|
Improvement % |
Not optimized |
Not optimized |
Not optimized |