בדף הזה מוסבר איך לנהל קשרים של מפתחות זרים במסד הנתונים.
מפתח זר הוא עמודה שמשותפת בין טבלאות כדי ליצור קישור בין נתונים קשורים. כשמשתמשים במפתח זר, Spanner מוודא שהקשר הזה נשמר.
בתרשים הבא מוצגת סכימה בסיסית של מסד נתונים שבו יש קשר בין הנתונים בטבלה אחת לבין הנתונים בטבלה אחרת.
איור 1. תרשים של סכימת מסד נתונים לעיבוד הזמנות
בסכימה שמוצגת באיור 1 יש שלוש טבלאות:
- בטבלה
Customersמופיעים השמות של כל הלקוחות. - בטבלה
Ordersמתבצע מעקב אחרי כל ההזמנות. - בטבלה
Productsמאוחסנים פרטי המוצרים של כל מוצר.
יש שני קשרי מפתח זר בין הטבלאות האלה:
מוגדר קשר של מפתח זר בין הטבלה
Ordersלבין הטבלהCustomersכדי להבטיח שאי אפשר ליצור הזמנה אם אין לקוח תואם.קשר של מפתח זר בין הטבלה
Ordersלבין הטבלהProductsמבטיח שלא ניתן ליצור הזמנה למוצר שלא קיים.
בדוגמה של הסכימה הקודמת, במאמר הזה מוסבר על הצהרות של שפת הגדרת נתונים (DDL) CONSTRAINT שאפשר להשתמש בהן כדי לנהל קשרים בין טבלאות במסד נתונים.
כברירת מחדל, כל המפתחות הזרים ב-Spanner הם מפתחות זרים מחייבים, שמבטיחים את שלמות ההפניה. ב-Spanner אפשר גם להשתמש במפתחות זרים אינפורמטיביים, שלא מאמתים או אוכפים את השלמות ההפניה. מידע נוסף זמין במאמרים השוואה בין מפתחות זרים ובחירת סוג המפתח הזר לשימוש. אם לא מציינים, המפתחות הזרים בדוגמאות בדף הזה הם מפתחות זרים מחייבים.
הוספת מפתח זר לטבלה חדשה
נניח שיצרתם טבלת Customers במסד הנתונים הבסיסי של הזמנות מוצרים. עכשיו צריך ליצור טבלת Orders לאחסון מידע על ההזמנות שהלקוחות מבצעים. כדי לוודא שכל ההזמנות תקפות, לא כדאי לאפשר למערכת להוסיף שורות לטבלה Orders אלא אם יש גם רשומה תואמת בטבלה Customers. לכן, צריך להגדיר מפתח זר מאולץ כדי ליצור קשר בין שתי הטבלאות. אפשרות אחת היא להוסיף עמודה CustomerID לטבלה החדשה ולהשתמש בה כמפתח זר כדי ליצור קשר עם העמודה CustomerID בטבלה Customers.
כשיוצרים טבלה חדשה עם מפתח זר, משתמשים ב-REFERENCE כדי ליצור קשר עם טבלה אחרת. הטבלה שמכילה את הצהרת REFERENCE נקראת טבלת ההפניה. הטבלה שצוינה בהצהרה REFERENCE
היא הטבלה שההפניה אליה נעשית. העמודה שמופיעה בשם בהצהרה REFERENCE נקראת עמודת ההפניה.
בדוגמה הבאה אפשר לראות איך משתמשים בהצהרת CREATE TABLE DDL כדי ליצור את הטבלה Orders עם אילוץ של מפתח זר שמפנה לטבלה CustomerID בטבלה Customers.
GoogleSQL
CREATE TABLE Orders (
OrderID INT64 NOT NULL,
CustomerID INT64 NOT NULL,
Quantity INT64 NOT NULL,
ProductID INT64 NOT NULL,
CONSTRAINT FK_CustomerOrder FOREIGN KEY (CustomerID) REFERENCES Customers (CustomerID)
) PRIMARY KEY (OrderID);
PostgreSQL
CREATE TABLE Orders (
OrderID BIGINT NOT NULL,
CustomerID BIGINT NOT NULL,
Quantity BIGINT NOT NULL,
ProductID BIGINT NOT NULL,
CONSTRAINT FK_CustomerOrder FOREIGN KEY (CustomerID) REFERENCES Customers (CustomerID),
PRIMARY KEY (OrderID)
);
ההצהרה הקודמת מכילה סעיף CONSTRAINT עם המאפיינים הבאים:
שימוש בתחביר
CONSTRAINTכדי לתת שם לאילוץ, וכך קל יותר להסיר את הטבלה באמצעות השם שבחרתם.האילוץ נקרא
FK_CustomerOrder. השמות של האילוצים הם בתחום של הסכימה וחייבים להיות ייחודיים בתוך הסכימה.הטבלה
Ordersשבה מגדירים את האילוץ היא הטבלה המפנה. הטבלהCustomersהיא הטבלה שההפניה מתייחסת אליה.העמודה שמפנה בטבלת ההפניות היא
CustomerID. הוא מפנה לשדהCustomerIDבטבלהCustomers. אם מישהו מנסה להוסיף שורה ל-OrdersעםCustomerIDשלא קיים ב-Customers, ההוספה נכשלת.
בדוגמה הבאה מוצגת הצהרה חלופית ליצירת טבלה. כאן, אילוץ המפתח הזר מוגדר ללא שם. כשמשתמשים בתחביר הזה, מערכת Spanner יוצרת שם בשבילכם. כדי לגלות את השמות של כל המפתחות הזרים, אפשר לעיין במאמר הצגת מאפיינים של קשר מפתח זר.
GoogleSQL
CREATE TABLE Orders (
OrderID INT64 NOT NULL,
CustomerID INT64 NOT NULL,
ProductID INT64 NOT NULL,
FOREIGN KEY (CustomerID) REFERENCES Customers (CustomerID)
) PRIMARY KEY (OrderID);
PostgreSQL
CREATE TABLE Orders (
OrderID BIGINT NOT NULL,
CustomerID BIGINT NOT NULL,
Quantity BIGINT NOT NULL,
ProductID BIGINT NOT NULL,
FOREIGN KEY (CustomerID) REFERENCES Customers (CustomerID),
PRIMARY KEY (OrderID)
);
הוספת מפתח זר לטבלה קיימת
אתם גם רוצים לוודא שהלקוחות יכולים להזמין רק מוצרים שקיימים. אם יש אילוצים קיימים בטבלה, צריך להסיר את כל האילוצים. ב-Spanner, כל האילוצים שנאכפים בטבלה חייבים להיות מוטמעים בו-זמנית בהצהרת DDL אחת של אצווה.
אם בטבלה שלכם אין אילוצים קיימים, אתם יכולים להשתמש בהצהרת ALTER TABLE DDL
כדי להוסיף אילוץ של מפתח זר נאכףלטבלה הקיימת Orders, כמו בדוגמה הבאה:
ALTER TABLE Orders
ADD CONSTRAINT DB_ProductOrder FOREIGN KEY (ProductID) REFERENCES Products (ProductID);
העמודה המפנה ב-Orders היא ProductID, והיא מפנה לעמודה ProductID ב-Products. אם אתם מסכימים ש-Spanner ייתן שמות לאילוצים האלה, אתם יכולים להשתמש בתחביר הבא:
ALTER TABLE Orders
ADD FOREIGN KEY (ProductID) REFERENCES Products (ProductID);
הוספת מפתח זר עם פעולת מחיקה לטבלה חדשה
נזכרים בדוגמה הקודמת שבה יש טבלה Customers במסד נתונים של הזמנות מוצרים, שצריך להוסיף לה טבלה Orders. רוצים להוסיף אילוץ של מפתח זר שמפנה לטבלה Customers. עם זאת, אתם רוצים לוודא שכשתמחקו בעתיד רשומה של לקוח, Spanner ימחק גם את כל ההזמנות של הלקוח הזה. במקרה כזה, כדאי להשתמש בפעולה ON
DELETE CASCADE עם אילוץ של מפתח זר.
ההוראה הבאה של CREATE TABLE DDL לטבלה Orders כוללת את אילוץ המפתח הזר שמפנה לטבלה Customers עם פעולה ON DELETE
CASCADE.
GoogleSQL
CREATE TABLE Orders (
OrderID INT64 NOT NULL,
CustomerID INT64 NOT NULL,
Quantity INT64 NOT NULL,
ProductID INT64 NOT NULL,
CONSTRAINT FK_CustomerOrder FOREIGN KEY (CustomerID)
REFERENCES Customers (CustomerID) ON DELETE CASCADE
) PRIMARY KEY (OrderID);
PostgreSQL
CREATE TABLE Orders (
OrderID BIGINT NOT NULL,
CustomerID BIGINT NOT NULL,
Quantity BIGINT NOT NULL,
ProductID BIGINT NOT NULL,
FOREIGN KEY (CustomerID)
REFERENCES Customers (CustomerID) ON DELETE CASCADE,
PRIMARY KEY (OrderID)
);
ההצהרה הקודמת מכילה אילוץ של מפתח זר עם פסקה של ON DELETE
CASCADE. העמודה CustomerID היא מפתח זר שמפנה לשדה CustomerID בטבלה Customers. כלומר, כל ערך CustomerID בטבלה Orders חייב להופיע גם בטבלה Customers. אם מישהו ינסה למחוק שורה מהטבלה Customers, כל השורות בטבלה Orders שמפנות לערך CustomerID שנמחק יימחקו גם הן באותה טרנזקציה.
הוספת מפתח זר עם פעולת מחיקה לטבלה
חשוב גם לוודא שהזמנות נוצרות רק למוצרים שקיימים.
אפשר להשתמש ב-ALTER TABLE כדי להוסיף עוד אילוץ של מפתח זר עם פעולת ON DELETE
CASCADE לטבלת ההזמנות באופן הבא:
ALTER TABLE Orders
ADD CONSTRAINT DB_ProductOrder FOREIGN KEY (ProductID)
REFERENCES Products (ProductID) ON DELETE CASCADE;
מחיקת שורה מהטבלה Products מוחקת את כל השורות בטבלה Orders שמפנות לערך ProductID שנמחק.
שימוש במפתחות זרים לצורכי מידע בלבד (GoogleSQL בלבד)
מפתחות זרים אינפורמטיביים מאפשרים לאופטימיזציה של שאילתות להשתמש בקשר של המפתח הזר בלי התקורה שנוצרת מבדיקות של שלמות רפרנציאלית שמבוצעות על ידי מפתחות זרים שנאכפים. מפתחות זרים אינפורמטיביים שימושיים כשאין אפשרות לאכוף שלמות הפניה קפדנית, או כשזה גורר תקורה משמעותית בביצועים.
נמשיך עם הדוגמה הקודמת ונניח שאתם רוצים ליצור מודל של הקשרים בין הטבלאות Customers, Orders ו-Products. עם זאת, אכיפה של שלמות רפרנציאלית קפדנית בנתוני הטבלאות עלולה לגרום לצווארי בקבוק בביצועים, במיוחד בתקופות שיא של קניות עם נפחי הזמנות גבוהים. בנוסף, יכול להיות שהלקוחות יבצעו הזמנות של מוצרים שהוצאו משימוש והוסרו מטבלת Products.
אפשר ליצור את הטבלה Orders באמצעות מפתחות זרים אינפורמטיביים:
CREATE TABLE Orders (
OrderID INT64 NOT NULL,
CustomerID INT64 NOT NULL,
Quantity INT64 NOT NULL,
ProductID INT64 NOT NULL,
CONSTRAINT FK_CustomerOrder FOREIGN KEY (CustomerID) REFERENCES Customers (CustomerID) NOT ENFORCED,
CONSTRAINT FK_ProductOrder FOREIGN KEY (ProductID) REFERENCES Products (ProductID) NOT ENFORCED
) PRIMARY KEY (OrderID);
כשיוצרים מפתח זר אינפורמטיבי עם NOT ENFORCED, מאפשרים את האפשרות שהזמנה תפנה ללקוח או למוצר שלא קיימים.
שימוש במפתח זר אינפורמטיבי במקום במגבלת מפתח זר מחייבת הוא בחירה טובה אם יש סיכוי שחשבון לקוח יימחק או שמוצר יפסיק להימכר. במקרה של מפתח זר אינפורמטיבי, Spanner לא מבצע אימות של תקינות רפרנציאלית. כך מצמצמים את התקורה של פעולות הכתיבה, ויכול להיות שהביצועים ישתפרו בזמנים של עומס בעיבוד הזמנות.
אתם יכולים לאפשר לאופטימיזטור של השאילתות להשתמש בקשרים כדי ליצור תוכניות שאילתות יעילות. כך תוכלו לשפר את הביצועים של שאילתות שמבצעות איחוד של הטבלאות בעמודות של מפתחות זרים. מידע נוסף זמין במאמר בנושא מפתח זר אינפורמטיבי לאופטימיזציה של שאילתות.
שליחת שאילתות לנתונים באמצעות קשרים של מפתחות זרים
SELECT * FROM Orders
INNER JOIN Customers ON Orders.CustomerID = Customers.CustomerID
INNER JOIN Products ON Orders.ProductsID = Products.ProductID;
שלמות הפניה עם מפתחות זרים שנאכפים
הסיבה העיקרית להוספת קשרים של מפתחות זרים עם אכיפה היא כדי ש-Spanner יוכל לשמור על השלמות ההפניה של הנתונים. אם משנים נתונים באופן שמפר את אילוץ המפתח הזר, העדכון נכשל ומוצגת שגיאה.
נתבונן בנתונים שבאיור 2. חלק מהלקוחות הזמינו מוצרים, כפי שמוצג בטבלה Orders. בגלל אילוץ המפתח הזר שנאכף, הנתונים שהוכנסו לטבלה Orders הם בעלי שלמות רפרנציאלית.
איור 2. נתוני מדגם במסד הנתונים של ההזמנות שלנו.
בדוגמאות הבאות אפשר לראות מה קורה כשמנסים לשנות את הנתונים באופן שיפגע בשלמות ההפניה.
מוסיפים שורה לטבלה
Ordersעם ערךCustomerIDשלא קיים ב-Customersמה יקרה אם תנסו לבצע את השינוי הבא, בהינתן נתוני הדוגמה מהתרשים הקודם?
INSERT INTO Orders (OrderID, ProductID, Quantity, CustomerID) VALUES (19, 337876, 4, 447);במקרה כזה, המערכת תנסה להוסיף שורה ל-
OrdersעםCustomerID(447) שלא קיים בטבלהCustomers. אם המערכת עשתה זאת, יהיה לכם הזמנה לא תקינה במערכת. עם זאת, בגלל אילוץ המפתח הזר שהוספתם לטבלהOrders, הטבלה מוגנת. הפונקציהINSERTנכשלת עם ההודעה הבאה, בהנחה שהמסגרת נקראתFK_CustomerOrder.Foreign key constraint `FK_CustomerOrder` is violated on table `Orders`. Cannot find referenced values in Customers(CustomerID).בניגוד למפתחות זרים נאכפים, מפתחות זרים אינפורמטיביים לא אוכפים שלמות הפניה. אם
FK_CustomerOrderהוא מפתח זר אינפורמטיבי, הצהרת ה-insert תצליח כי Spanner לא מאמת ש-CustomerIDהתואם קיים בטבלהCustomers. לכן, יכול להיות שהנתונים לא יעמדו בדרישות של השלמות הרפרנציאלית שמוגדרות על ידיFK_CustomerOrder.ניסיון למחוק שורה מהטבלה
Customersכשהלקוח מפנה אל אילוץ של מפתח זר נאכף.נניח שלקוח מבטל את המינוי לחנות הווירטואלית שלכם. אתם רוצים להסיר את הלקוח מהקצה האחורי, ולכן אתם מנסים לבצע את הפעולה הבאה.
DELETE FROM Customers WHERE CustomerID = 721;בדוגמה הזו, Spanner מזהה באמצעות אילוץ המפתח הזר שיש עדיין רשומות בטבלה
Ordersשמפנות לשורת הלקוח שאתם מנסים למחוק. במקרה כזה מוצגת השגיאה הבאה.Foreign key constraint violation when deleting or updating referenced row(s): referencing row(s) found in table `Orders`.כדי לפתור את הבעיה, צריך קודם למחוק את כל הרשומות שמפנות אל
Orders. אפשר גם להגדיר את המפתח הזר באמצעות הפעולהON DELETE CASCADEכדי לאפשר ל-Spanner לטפל במחיקה של רשומות שמפנות למפתח הזר.באופן דומה, אם
FK_CustomerOrderהוא מפתח זר אינפורמטיבי, פעולת המחיקה תצליח כי Spanner לא מבטיח את השלמות הרפרנציאלית של מפתחות זרים אינפורמטיביים.
הצגת המאפיינים של קשר בין מפתח זר למפתח ראשי
INFORMATION_SCHEMA ב-Spanner מכיל מידע על מפתחות זרים ועל האינדקסים שתומכים בהם. ריכזנו כאן כמה דוגמאות לשאלות שאפשר לענות עליהן באמצעות שאילתות ב-INFORMATION SCHEMA.
מידע נוסף על גיבוי אינדקסים זמין במאמר מפתחות זרים לגיבוי אינדקסים.
אילו אילוצים מוגדרים במסד הנתונים שלי?
SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, tc.CONSTRAINT_TYPE
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS as tc
WHERE tc.CONSTRAINT_TYPE = 'FOREIGN KEY';
אילו מפתחות זרים מוגדרים במסד הנתונים שלי?
SELECT rc.CONSTRAINT_NAME, rc.UNIQUE_CONSTRAINT_NAME, rc.SPANNER_STATE
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS as rc;
אילו אינדקסים הם אינדקסים משניים למפתחות זרים, שנקראים גם אינדקסים לגיבוי?
Spanner מנהל את האינדקסים שתומכים במפתחות זרים , כך ששאילתה של SPANNER_IS_MANAGED בתצוגה INDEXES מחזירה את כל האינדקסים התומכים.
SELECT i.TABLE_NAME, i.INDEX_NAME, i.INDEX_TYPE, i.INDEX_STATE,
i.IS_UNIQUE, i.IS_NULL_FILTERED, i.SPANNER_IS_MANAGED
FROM INFORMATION_SCHEMA.INDEXES as i
WHERE SPANNER_IS_MANAGED = 'YES';
מהי הפעולה הרפרנציאלית שמוגדרת עם אילוץ המפתח הזר?
SELECT rc.CONSTRAINT_NAME, rc.UNIQUE_CONSTRAINT_NAME, rc.DELETE_RULE,
rc.UPDATE_RULE
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS as rc;
SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, tc.CONSTRAINT_TYPE, tc.ENFORCED
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS as tc
WHERE tc.CONSTRAINT_TYPE = 'FOREIGN KEY';
מידע נוסף זמין במאמר בנושא סכימת מידע.
הסרת קשר של מפתח זר
הוראת ה-DDL הבאה מבטלת אילוץ של מפתח זר בטבלה Orders.
ALTER TABLE Orders
DROP CONSTRAINT FK_CustomerOrder;
כשמבטלים את האילוץ, המפתחות הזרים שמגבים את האינדקסים מוסרים אוטומטית.
תמיכה ביחסי מפתח זר מורכבים יותר
בנושאים הבאים מוסבר איך להשתמש במפתחות זרים כדי לאכוף קשרים מורכבים יותר בין טבלאות.
עמודות מרובות
מפתחות זרים יכולים להפנות לכמה עמודות. רשימת העמודות יוצרת מפתח שתואם למפתח הראשי של הטבלה או לאינדקס הגיבוי. הטבלה המפנה מכילה מפתחות זרים של מפתח הטבלה שאליה מתבצעת ההפניה.
בדוגמה הבאה, הגדרות המפתח הזר שנאכפות מציינות:
לכל ערך
SongNameבטבלהTopHitsצריך להיות ערך תואם בטבלהSongs.לכל זוג ערכים
SingerFirstNameו-SingerLastNameצריך להיות זוג ערכים תואםFirstNameו-LastNameבטבלהSingers.
GoogleSQL
CREATE TABLE TopHits (
Rank INT64 NOT NULL,
SongName STRING(MAX),
SingerFirstName STRING(MAX),
SingerLastName STRING(MAX),
-- Song names must either be NULL or have matching values in Songs.
FOREIGN KEY (SongName) REFERENCES Songs (SongName),
-- Singer names must either be NULL or have matching values in Singers.
FOREIGN KEY (SingerFirstName, SingerLastName)
REFERENCES Singers (FirstName, LastName)
) PRIMARY KEY (Rank);
PostgreSQL
CREATE TABLE TopHits (
Rank BIGINT NOT NULL,
SongName VARCHAR,
SingerFirstName VARCHAR,
SingerLastName VARCHAR,
-- Song names must either be NULL or have matching values in Songs.
FOREIGN KEY (SongName) REFERENCES Songs (SongName),
-- Singer names must either be NULL or have matching values in Singers.
FOREIGN KEY (SingerFirstName, SingerLastName)
REFERENCES Singers (FirstName, LastName),
PRIMARY KEY (Rank)
);
הפניות מעגליות
לפעמים יש בטבלאות תלויות מעגליות, אולי מסיבות שקשורות למערכות קודמות או בגלל דה-נורמליזציה. ב-Spanner, מפתחות זרים מאפשרים הפניות מעגליות. מכיוון שטבלה שאליה מתבצעת הפניה צריכה להתקיים לפני שמפתח זר יכול להפנות אליה, צריך להוסיף אחד מהמפתחות הזרים באמצעות הצהרת ALTER TABLE. הנה דוגמה
- יוצרים
TableA, בלי מפתח זר. - יוצרים את
TableBעם אילוץ של מפתח זר ב-TableA. - משתמשים ב-
ALTER TABLEב-TableAכדי ליצור הפניה למפתח זר אלTableB.
טבלאות עם הפניה עצמית
סוג מיוחד של הפניה מעגלית הוא טבלה שמגדירה מפתח זר שמפנה לאותה טבלה. לדוגמה, בקטע הקוד הבא מוצג מפתח זר כדי לוודא ש-ManagerId של עובד הוא גם עובד.
GoogleSQL
CREATE TABLE Employees (
EmployeeId INT64 NOT NULL,
EmployeeName STRING(MAX) NOT NULL,
ManagerId INT64,
FOREIGN KEY (ManagerId) REFERENCES Employees (EmployeeId)
) PRIMARY KEY (EmployeeId);
PostgreSQL
CREATE TABLE Employees (
EmployeeId BIGINT NOT NULL,
EmployeeName VARCHAR NOT NULL,
ManagerId BIGINT,
FOREIGN KEY (ManagerId) REFERENCES Employees (EmployeeId),
PRIMARY KEY (EmployeeId)
);