Menu

UPSERT ב-SQLite: ON CONFLICT DO UPDATE ו-DO NOTHING

איך UPSERT עובד ב-SQLite: פסוקית ON CONFLICT, הטבלה excluded, DO NOTHING מול DO UPDATE, ובמה הוא שונה מ-INSERT OR REPLACE.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

הכנסה, או עדכון אם השורה כבר קיימת

צורך נפוץ: להכניס שורה, אבל אם כבר קיימת שורה עם אותו מפתח, לעדכן אותה במקום. בלי UPSERT, הייתם כותבים קודם SELECT, ואז מתפצלים ל-INSERT או ל-UPDATE: שתי גישות למסד הנתונים ותנאי מרוץ ביניהן.

ה-UPSERT של SQLite עושה את זה בפקודה אחת:

בפעם הראשונה שמריצים את זה, השורה מוכנסת. הריצו שוב עם מחיר אחר ואותו sku, והשורה הקיימת מתעדכנת במקום. בלי כפילות, בלי שגיאה.

האנטומיה של ON CONFLICT

הצורה המלאה:

INSERT INTO table (...) VALUES (...)
ON CONFLICT(conflict_target) DO UPDATE SET col = expr, ...
WHERE condition;

שלושה חלקים חשובים:

  • conflict_target: העמודה או העמודות עם אילוץ UNIQUE או PRIMARY KEY שאתם מצפים להתנגשות בהן. SQLite משתמש בזה כדי לבחור באיזה אינדקס לעקוב.
  • DO UPDATE SET ...: מה לשנות בשורה הקיימת כשקורית התנגשות. (או DO NOTHING כדי לדלג בשקט.)
  • WHERE אופציונלי: תנאי נוסף שחייב להתקיים כדי שהעדכון באמת ירוץ.

יעד ההתנגשות חייב להתאים לאילוץ ייחודי אמיתי. ON CONFLICT(price) לא יעבור הידור אם price לא ייחודית: ל-SQLite אין מול מה לזהות התנגשות.

DO NOTHING: להכניס אם חסר, אחרת לדלג

הגרסה הפשוטה יותר. שימושית כשזורעים נתונים או רושמים אירועים, וכפילויות צריכות פשוט להיות מוזנחות בשקט:

ההכנסה השנייה נתקלת באותו event_id ובדרך כלל הייתה מעלה UNIQUE constraint failed. עם DO NOTHING, SQLite פשוט מדלג עליה. בלי חריגה, בלי שורה מושפעת.

זו "ההכנסה האידמפוטנטית" שאנשים משתמשים בשבילה לעיתים קרובות ב-INSERT OR IGNORE. ה-DO NOTHING של UPSERT עושה את אותה עבודה ומשתלב טוב יותר עם פסוקיות WHERE ו-RETURNING.

פסאודו הטבלה excluded

כשהתנגשות מתרחשת, פתאום יש שתי שורות במשחק: הקיימת בטבלה, והחדשה שניסיתם להכניס. SQLite נותן דרך לדבר על שתיהן.

  • שמות עמודות רגילים (price, name) מתייחסים לשורה הקיימת.
  • excluded.column מתייחס לשורה הנכנסת שנדחתה.

quantity = quantity + excluded.quantity נקרא "הכמות הקיימת ועוד החדשה". אחרי שתי הכנסות, ל-A-100 יש כמות 8. התבנית הזו, צבירה לתוך שורה קיימת, היא אחד הטריקים השימושיים ביותר של UPSERT.

UPSERT מותנה עם WHERE

ה-WHERE שבסוף מאפשר לדלג על העדכון אלא אם תנאי כלשהו מתקיים. הוא רץ מול השורה הקיימת (ויכול להתייחס ל-excluded.* עבור הנכנסת):

השורה החדשה נושאת updated_at ישן יותר, ולכן ה-WHERE שקרי והעדכון מדולג. השורה הקיימת שומרת על המחיר החדש יותר שלה. החליפו את התאריכים והעדכון ירוץ. זו התבנית הסטנדרטית של "דרוס רק בנתונים עדכניים יותר".

Upsert של כמה שורות

VALUES יכול להכיל שורות רבות, ו-ON CONFLICT חל על כל אחת בנפרד:

A-100 מתנגש ומתעדכן. A-200 ו-A-300 חדשים ומוכנסים. פקודה אחת, תוצאה מעורבת של הכנסה ועדכון. זו דרך נקייה לסנכרן אצווה של רשומות ממקור חיצוני.

UPSERT מול INSERT OR REPLACE

INSERT OR REPLACE נראה כאילו הוא עושה את אותו הדבר. הוא לא.

notes נעלמה. INSERT OR REPLACE מחק את שורה 1 לגמרי והכניס שורה חדשה: כל עמודה שלא ציינתם אופסה ל-NULL או לברירת המחדל שלה. הוא גם מפעיל טריגרים של DELETE ומתגלגל דרך מפתחות זרים עם ON DELETE.

UPSERT שומר על השורה:

notes עדיין שם. רק העמודות שצוינו ב-SET השתנו. פנו ל-UPSERT כברירת מחדל, ול-INSERT OR REPLACE רק כשאתם באמת רוצים סמנטיקה של מחיקה והכנסה מחדש.

כמה יעדי התנגשות

אם שורה עלולה להתנגש ביותר מאילוץ אחד, אפשר לשרשר פסוקיות ON CONFLICT:

האילוץ שמופעל ראשון מנצח, וה-DO UPDATE של הענף הזה רץ. בפועל, לרוב הטבלאות יש יעד התנגשות ברור אחד, המפתח הראשי או עמודה ייחודית אחת, ולעיתים רחוקות תצטרכו יותר מפסוקית אחת.

מלכודות נפוצות

כמה דברים שנושכים אנשים:

  • אין אינדקס ייחודי תואם, אין UPSERT. ON CONFLICT(col) דורש ש-col תהיה PRIMARY KEY או שיהיה לה אילוץ UNIQUE. אחרת SQLite נכשל עם "no such constraint".
  • DO UPDATE לא מופעל אם אין התנגשות. הוא חלופה להכנסה, לא התנהגות נוספת. בפעם הראשונה שמפתח נראה, רק ההכנסה רצה.
  • excluded היא לקריאה בלבד. אפשר לקרוא ממנה אבל לא לכתוב אליה. היעד של SET הוא תמיד השורה הקיימת.
  • rowid שנוצרים אוטומטית ב-INTEGER PRIMARY KEY. אם לא מספקים את ה-id, כל הכנסה מקבלת id חדש, ואין עם מה להתנגש. UPSERT הגיוני רק כשהעמודה המתנגשת מקבלת ערך דטרמיניסטי מהקוד הקורא.

הבא בתור: RETURNING

UPSERT לא אומר לכם כלום על אילו שורות הוכנסו ואילו עודכנו, או איך נראים הערכים הסופיים שלהן. בשביל זה צריך את פסוקית RETURNING: היא מחזירה את השורות המושפעות באותה פקודה, בלי צורך ב-SELECT נוסף. זה הבא.

שאלות נפוצות

מה זה UPSERT ב-SQLite?

UPSERT הוא INSERT שהופך ל-UPDATE (או לפעולה ריקה) כשהוא היה מפר אחרת אילוץ UNIQUE או PRIMARY KEY. כותבים אותו כ-INSERT ... ON CONFLICT(column) DO UPDATE SET ... או DO NOTHING. SQLite תומך בזה מאז גרסה 3.24.0 (2018).

מהי הטבלה excluded ב-UPSERT של SQLite?

excluded היא פסאודו טבלה מיוחדת שמכילה את השורה שניסיתם להכניס. בתוך DO UPDATE SET ..., מתייחסים לשורה הקיימת לפי שם העמודה ולשורה שנדחתה כ-excluded.column. כך SET price = excluded.price פירושו 'דרוס את המחיר במה שה-INSERT החדש הביא'.

מה ההבדל בין INSERT OR REPLACE ל-UPSERT?

INSERT OR REPLACE מוחק את השורה המתנגשת ומכניס שורה חדשה: זה מפעיל טריגרים של DELETE, שובר מפתחות זרים עם ON DELETE CASCADE, ומאפס כל עמודה לברירת המחדל שלה. UPSERT מעדכן את השורה הקיימת במקום, כך שרק העמודות שציינתם ב-SET משתנות. העדיפו UPSERT, אלא אם אתם באמת רוצים מחיקה והכנסה מחדש.

האם אפשר לבצע upsert לכמה שורות בבת אחת ב-SQLite?

כן. INSERT INTO t(...) VALUES (...), (...), (...) ON CONFLICT(col) DO UPDATE SET ... עובד מצוין. כל שורה נבדקת מול יעד ההתנגשות בנפרד, ושורת ה-excluded בתוך DO UPDATE מתייחסת לשורה הנכנסת שגרמה להתנגשות.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל