“תסביר לי” – גרסת ה SQL
לקחת את הביצועים ברצינות, בעזרת MySQL Performance Schema
נושאים מתקדמים ב MySQL: חלק א’ – עבודה עם JSON
נושאים מתקדמים ב MySQL: חלק ב’ – json_each ו Generated Columns
בפוסט הקודם דיברנו על שימוש ב json column ב MySQL.
אחת המגבלות הגדולות של שדות json הם מהירות החיפוש בהם:
- אם השדות שמורים כ json – יש לקרוא את ה json כולו מהדיסק לזיכרון – על מנת לשלוף ערך בודד.
- אם השדות שמורים כ text column – אזי יש עוד עבודת CPU של פענוח מבנה ה json – זמן לא זניח ב jsons גדולים. להזכיר: בעמודה מטיפוס json הם שמורים כ key-value עם גישה מהירה יותר לפי מפתח.
הנה סכמה כזו לדוגמה:
את השדות x ו y – נרצה לרוב להחזיק גם בתוך ה josn וגם בצד לצורך גישה מהירה / שימוש באינדקסים.
למה שכפול נתונים?
אם נאחסן אותם רק מחוץ ל json – העבודה עם אובייקט ה json תהיה מסורבלת יותר.
לפעמים השדה שאנו מוציאים הוא גם ערך וגם מיקום באובייקט – למשל: המחיר של ההזמנה המאוחרת ביותר (כאשר יש לנו מערך של גרסאות של הזמנות).
את התהליך הנ”ל – ניתן לבצע בצורה פשוטה יותר בעזרת יכולת של MySQL שנקראת generated column, או בעברית “עמודות מחוללות” (מלשון לחולל, ולא מלשון חילול).
נתחיל בדוגמה, איך מגדירים עמודה שכזו:
ADD COLUMN state VARCHAR(30) GENERATED ALWAYS
AS (json_unquote(json_extract(`json_data`,‘$.address.state’))) STORED
אני מוסיף שערכו יהיה הביטוי (expression) שבסוגריים לאחר המילה השמורה AS.
- חשוב לי להשתמש ב json_unquote על מנת שהעמודה לא תכיל מירכאות – וכך אוכל לבצע חיפוש יעיל מול האינדקס.
- הביטוי GENERATED ALWAYS הוא רשות – ועוזר להבליט את העובדה שמדובר ב generated column – עבור קוראים עתידיים.
תזכורת חשובה: אם השאילתה עושה פעולה כלשהי על שדה מאונדקס (נאמר: state) – לא יהיה שימוש באינדקס. למשל:
WHERE my_state = json_unquote(state)
לא טוב!
WHERE json_quote(my_state) = state
בגדול יש שני סוגים של generated columns:
- Virtual – המחושב בזמן הגישה לרשומה.
- בסיס הנתונים ישמור את הביטוי החישובי (expression) כ metadata על הסכמה, ובכל גישה לרשומה / הפעלת trigger – יתבצע החישוב מחדש. קונספט דומה מאוד ל “calculated field” במערכות BI.
- אנו חוסכים מקום בדיסק – אבל מכבידים על ה CPU בכל גישה.
- זהו ערך ברירת המחדל – אם לא ציינתם כלום.
- Stored – הערכת הביטוי תבוצע בזמן יצירה / עדכון של הרשומה, וישמר לדיסק כמו עמודות אחרות.
- יש מחיר בנפח אכסון, אך לרוב – הרבה פחות עבודת CPU.
- זו הגישה הטבעית בעבודה עם json.
לצורך העניין generated column יכול לשמש לצרכים נוספים מלבד json.
למשל:
id VARCHAR(32),
start_date_millis INT, # WARN: https://en.wikipedia.org/wiki/Year_2038_problem
end_date_millis INT, # WARN: https://en.wikipedia.org/wiki/Year_2038_problem
start_date_sec AS (state_date_millis * 1000) VIRTUAL
;
יש כמה מגבלות על שימוש ב generated columns שכדאי להכיר. בביטוי של עמודה מחוללת לא ניתן להשתמש ב:
- פונקציות לא דטרמיניסטיות – כאלו שיציגו ערכים שונים אם הופעלו פעם שניה. למשל: ()now או ()current_user.
- sub-queries
- Parameters / Variables או User Defined Functions.
- עמודת AUTO_GENERATED או תכונת AUTO_INCREMENT.
- שדה generated יכול להיות מבוסס רק על שדות generated המופיעים לפניו.
- לא ניתן להשתמש על ה stored generated column באילוצים של FK מסוגי ON_UPDATE / ON_DELETE
עוד פרט מעניין: ניתן להשתמש באינדקס (משני) גם על virtual generated column מעל innoDB. הוא יהיה מסוג BTree בלבד (כלומר: לא FULLTEXT או GIS).
בהוספת האינדקס ייתכן שתצטרכו להוסיף הוראת WITH VALIDATION – על מנת לא לקבל שגיאה שאורך השדה המחושב שהגדרתם הוא קטן מדי.
קצת יותר על השימוש בפונקציות ה JSON של MySQL
התבקשתי ע״י קורא לפרט קצת יותר איך עם פונקציות ה json של MySQLשהזכרתי בפוסט הקודם. אתן דוגמה מבוססת על מקרה שהשתמשתי בו לאחרונה.
רקע: אני רוצה לבצע מיגרציה לשם של אלמנט בתוך מבנה json שאנו עושים בו שימוש. שם האלמנט היום הוא breakdown אבל השם לא כ”כ נכון – ואני רוצה לשנות אותו ל amounts. הנה דוגמה פשוטה למבנה ה json הקיים לפני השינוי:
וכך אני רוצה שמבנה יראה לאחר השינוי:
אני מתחיל בשאילתה פשוטה של SELECT על מנת לראות אם קלעתי נכון ב query.
בחרתי לי רשומה שהמפתח שלה הוא ‘009277a371b8c3def40996a754085030’ על מנת לבצע את הניסויים ב scale קטן.
FROM `payments`
WHERE id = ‘009277a371b8c3def40996a754085030’;
FROM `payments`
WHERE id = ‘009277a371b8c3def40996a754085030’;
SET `financial_attribution` = Json_insert(`financial_attribution`, ‘$[0].amounts’,
financial_attribution -> ‘$[0].breakdown’)
;
אפשרות אחרת היא פשוט להשאיר את הנתונים העודפים – אם הם לא מפריעים לנו.
עוד דרך מהירה (ולעתים יותר יעילה) לבצע תיקונים ב jsons היא בעזרת ()REPLACE פשוט על טקסט – אבל חשוב מאוד להיזהר מהחלפות לא-צפויות.
השלמות ליכולות ה json של MySQL 5.7
אם אתם זוכרים את הפוסט הקודם – הבטחתי לכם workarounds למחסור של MySQL 5.7 ב-2 פונקציות שימושיות בעבודה עם json.
מהדרך בה אני משתמש ניתן לגזור, בקלות יחסית, וריאציות נוספות – לשימושכם.
קריאת הערך האחרון במערך שבתוך json
אני רוצה לקבל את השדה האחרון במערך שבתוך json:
זה קצת תרגיל: אני מוצא את אורך המערך (במקרה שלנו, תחת תכונה בשם from) ואז מרכיב שאילתה בעזרת ()CONCAT – ומריץ אתה. הנה קלט לדוגמה שעליו תעבוד השאילתה:
התוצאה תהיה ״c״.
הנה דוגמה ב DB Fiddle שאפשר קצת ״לשחק״ איתה:
שימו לב ל-2 מקרי-קצה בתחתית הדוגמה.
לי זה מעולם לא הפריע, כי עבדתי עם מבנים ידועים שהם מערך – אבל אפשר גם לבנות פונקציה מורכבת יותר, ולהתגונן בפני המקרים הללו (או פשוט לשדרג ל MySQL 8).
על בסיס הדרך בה קראנו את האיבר האחרון, אנחנו יכולים לטייל על כל המערך. עם מעט עזרה נוספת.
הנה דוגמה לשאילתה כזו:
JSON_EXTRACT(`data`, concat(‘$.items[‘, n – 1, ‘].price’)) as price
from `my_table`
join numbers on (n <= JSON_LENGTH(`data`->’$.items’) and n > 0)
;
כאשר התוצאה נראית כך:
n הוא מספר האיבר ברשימה (אינדקס על בסיס 1), ו price הוא… המחיר.
והנתונים נראים כך:
עשינו join עם טבלה של מספרים (0-255 לרוב מספיקה) ואז, עבור כל מספר באורך המערך של ה items – ביצענו פעולת שליפה מתוך ה json על המקום הזה, בהתבסס על ״תרגיל״ ה CONCAT שהשתמשנו בו קודם לכן.
הכי-אלגנטי-בעולם? – לא.
עובד ושימושי – כן!
הנה אתם יכולים לשחק ב fiddle שיצרתי לקוד הזה: https://www.db-fiddle.com/f/dmA8af4CHJ3xkx4fzV99Zw/0
בוודאי שמתם לב למבנה קצת לא-שגרתי, שנועד בכדי ליצור את ה View המספק את המספרים. גם כאן יש תרגיל ״מוכר״ בעולם של MySQL:
AS SELECT 0 n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL
SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL
SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL
SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL
SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL
SELECT 15
AS SELECT ( hi.n * 16 + lo.n ) AS n
FROM generator_16 lo, generator_16 hi
עזבו לרגע את המבנה העמוס. אם תקראו את הקוד – הוא נראה מאוד הגיוני.
מדוע הסרבול הזה? האם אין דרך יותר פשוטה? – הנה פוסט יפה עם כל הפרטים.
אפשר למצוא את Fiddle ממקוד של ה generator על מנת ״לשחק״ איתו: https://www.db-fiddle.com/f/jCRetSiTaKqz5SUiQQG8Py/0
סיכום
טוב, נראה לי שהכנסו דיי מידע לפוסט יחיד.
מבחינתי הנושאים הללו מכסים יפה את העבודה ב MySQL (״הוסמכת – סרג׳יו״), ומכאן ניתן להמשיך הלאה, לנושאים מתקדמים אחרים ב MySQL.
שיהיה בהצלחה!