אינדקסים משניים

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

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

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

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

מערכת Spanner שומרת את הנתונים הבאים בכל אינדקס משני:

עם הזמן, Spanner מנתח את הטבלאות כדי לוודא שהאינדקסים המשניים משמשים לשאילתות המתאימות.

הוספת אינדקס משני

הזמן היעיל ביותר להוספת אינדקס משני הוא כשיוצרים את הטבלה. כדי ליצור טבלה ואינדקסים שלה בו-זמנית, שולחים את הצהרות ה-DDL של הטבלה החדשה והאינדקסים החדשים בבקשה אחת ל-Spanner.

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

בכל פעם שמוסיפים אינדקס חדש לטבלה קיימת, Spanner מבצע backfill באופן אוטומטי, כלומר מאכלס את האינדקס כדי לשקף תצוגה עדכנית של הנתונים שמתווספים לאינדקס. מערכת Spanner מנהלת את תהליך המילוי החוזר הזה בשבילכם, והתהליך פועל ברקע באמצעות משאבי צומת בעדיפות נמוכה. מהירות מילוי החסר של האינדקס מותאמת לשינויים במשאבי הצומת במהלך יצירת האינדקס, ומילוי החסר לא משפיע באופן משמעותי על הביצועים של מסד הנתונים.

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

  • גודל מערך הנתונים
  • קיבולת החישוב של המכונה
  • העומס על המופע

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

חשוב לדעת שהשימוש בעמודה commit timestamp כחלק הראשון של האינדקס המשני עלול ליצור נקודות חמות ולפגוע בביצועים של פעולות כתיבה.

משתמשים בהצהרת CREATE INDEX כדי להגדיר אינדקס משני בסכימה. הנה כמה דוגמאות:

כדי ליצור אינדקס לכל Singers במסד הנתונים לפי השם הפרטי ושם המשפחה:

GoogleSQL

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

PostgreSQL

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

כדי ליצור אינדקס של כל Songs במסד הנתונים לפי הערך של SongName:

GoogleSQL

CREATE INDEX SongsBySongName ON Songs(SongName);

PostgreSQL

CREATE INDEX SongsBySongName ON Songs(SongName);

כדי להוסיף לאינדקס רק את השירים של זמר מסוים, משתמשים בסעיף INTERLEAVE IN כדי לשלב את האינדקס בטבלה Singers:

GoogleSQL

CREATE INDEX SongsBySingerSongName ON Songs(SingerId, SongName),
    INTERLEAVE IN Singers;

PostgreSQL

CREATE INDEX SongsBySingerSongName ON Songs(SingerId, SongName)
    INTERLEAVE IN Singers;

כדי להוסיף לאינדקס רק את השירים באלבום מסוים:

GoogleSQL

CREATE INDEX SongsBySingerAlbumSongName ON Songs(SingerId, AlbumId, SongName),
    INTERLEAVE IN Albums;

PostgreSQL

CREATE INDEX SongsBySingerAlbumSongName ON Songs(SingerId, AlbumId, SongName)
    INTERLEAVE IN Albums;

כדי ליצור אינדקס בסדר יורד של SongName:

GoogleSQL

CREATE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, SongName DESC),
    INTERLEAVE IN Albums;

PostgreSQL

CREATE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, SongName DESC)
    INTERLEAVE IN Albums;

הערה: ההערה הקודמת DESC רלוונטית רק ל-SongName. כדי ליצור אינדקס לפי סדר יורד של מפתחות אינדקס אחרים, צריך להוסיף להם את ההערה DESC: SingerId DESC, AlbumId DESC.

חשוב גם לזכור ש-PRIMARY_KEY היא מילה שמורה ואי אפשר להשתמש בה כשם של אינדקס. זה השם שניתן לפסאודו-אינדקס שנוצר כשיוצרים טבלה עם הגדרת PRIMARY KEY

פרטים נוספים ושיטות מומלצות לבחירת אינדקסים לא משולבים ואינדקסים משולבים זמינים במאמרים אפשרויות לאינדקסים ושימוש באינדקס משולב בעמודה שהערך שלה עולה או יורד באופן מונוטוני.

אינדקסים ושיבוץ

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

לדוגמה, נניח את הסכימה הבאה:

CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo PROTO<Singer>(MAX)
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
  PublisherId  INT64 NOT NULL
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE TABLE Songs (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  TrackId      INT64 NOT NULL,
  PublisherId  INT64 NOT NULL,
  SongName     STRING(MAX)
) PRIMARY KEY (SingerId, AlbumId, TrackId),
  INTERLEAVE IN PARENT Albums ON DELETE CASCADE;

CREATE TABLE Publishers (
  Id            INT64 NOT NULL,
  PublisherName STRING(MAX)
) PRIMARY KEY (Id);

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

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

אם רוצים ליצור אינדקס של Songs ב-(SingerId, AlbumId, SongName), אפשר לעשות את הפעולות הבאות:

CREATE INDEX SongsBySingerAlbumSongName
    ON Songs(SingerId, AlbumId, SongName);

אפשר גם ליצור אינדקס שמשולב עם ישות אב של Songs, כמו בדוגמה הבאה:

CREATE INDEX SongsBySingerAlbumSongName
    ON Songs(SingerId, AlbumId, SongName),
    INTERLEAVE IN Albums;

בנוסף, אפשר ליצור אינדקס של Songs ב-(PublisherId, SingerId, AlbumId, SongName) שמשולב עם טבלה שלא נמצאת בהיררכיה מעל Songs, כמו Publishers. הערה: המפתח הראשי של טבלת Publishers (id) לא מופיע כקידומת של העמודות עם האינדקס בדוגמה הבאה. הפעולה הזו עדיין מותרת כי Publishers.Id ו-Songs.PublisherId חולקים את אותו סוג, סדר מיון ואפשרות להגדרה כ-null.

CREATE INDEX SongsByPublisherSingerAlbumSongName
    ON Songs(PublisherId, SingerId, AlbumId, SongName),
    INTERLEAVE IN Publishers;

בדיקת ההתקדמות של מילוי חוסרים באינדקס

המסוף

  1. בתפריט הניווט של Spanner, לוחצים על הכרטיסייה Operations (פעולות). בדף Operations מוצגת רשימה של פעולות שפועלות.

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

    צילום מסך של אינדיקטור התקדמות שמראה 98%

gcloud

כדי לבדוק את התקדמות הפעולה, משתמשים ב-gcloud spanner operations describe.

  1. קבלת מזהה הפעולה:

    gcloud spanner operations list --instance=INSTANCE-NAME \
    --database=DATABASE-NAME --type=DATABASE_UPDATE_DDL

    מחליפים את מה שכתוב בשדות הבאים:

    • INSTANCE-NAME בשם של מכונת Spanner.
    • DATABASE-NAME בשם של מסד הנתונים.

    הערות שימוש:

    • כדי להגביל את הרשימה, מציינים את הדגל --filter. לדוגמה:

      • --filter="metadata.name:example-db" מציג רק את הפעולות במסד נתונים ספציפי.
      • --filter="error:*" מציג רק את פעולות הגיבוי שנכשלו.

      מידע על תחביר המסננים זמין במאמר gcloud topic filters. מידע על סינון פעולות גיבוי מופיע בשדה filter במאמר ListBackupOperationsRequest.

    • הדגל --type לא תלוי באותיות רישיות.

    הפלט אמור להיראות כך:

    OPERATION_ID     STATEMENTS                                                                                          DONE   @TYPE
    _auto_op_123456  CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName)                                 False  UpdateDatabaseDdlMetadata
                    CREATE INDEX SongsBySingerAlbumSongName ON Songs(SingerId, AlbumId, SongName), INTERLEAVE IN Albums
    _auto_op_234567                                                                                                      True   CreateDatabaseMetadata
    
  2. מריצים את gcloud spanner operations describe:

    gcloud spanner operations describe \
    --instance=INSTANCE-NAME \
    --database=DATABASE-NAME \
    projects/PROJECT-NAME/instances/INSTANCE-NAME/databases/DATABASE-NAME/operations/OPERATION_ID

    מחליפים את מה שכתוב בשדות הבאים:

    • INSTANCE-NAME: שם מכונת Spanner.
    • DATABASE-NAME: שם מסד הנתונים של Spanner.
    • PROJECT-NAME: שם הפרויקט.
    • OPERATION-ID: מזהה הפעולה שרוצים לבדוק.

    בקטע progress בפלט מוצג האחוז של הפעולה שהושלמה. הפלט אמור להיראות כך:

    done: true
    ...
      progress:
      - endTime: '2021-01-22T21:58:42.912540Z'
        progressPercent: 100
        startTime: '2021-01-22T21:58:11.053996Z'
      - progressPercent: 67
        startTime: '2021-01-22T21:58:11.053996Z'
    ...
    

REST v1

קבלת מזהה הפעולה:

  gcloud spanner operations list --instance=INSTANCE-NAME 
--database=DATABASE-NAME --type=DATABASE_UPDATE_DDL

מחליפים את מה שכתוב בשדות הבאים:

  • INSTANCE-NAME בשם של מכונת Spanner.
  • DATABASE-NAME בשם של מסד הנתונים.

לפני שמשתמשים בנתוני הבקשה, צריך להחליף את הנתונים הבאים:

  • PROJECT-ID: מזהה הפרויקט.
  • INSTANCE-ID: מזהה המכונה.
  • DATABASE-ID: מזהה מסד הנתונים.
  • OPERATION-ID: מזהה הפעולה.

ה-method של ה-HTTP וכתובת ה-URL:

GET https://spanner.googleapis.com/v1/projects/PROJECT-ID/instances/INSTANCE-ID/databases/DATABASE-ID/operations/OPERATION-ID

כדי לשלוח את הבקשה צריך להרחיב אחת מהאפשרויות הבאות:

אתם אמורים לקבל תגובת JSON שדומה לזו:

{
...
    "progress": [
      {
        "progressPercent": 100,
        "startTime": "2023-05-27T00:52:27.366688Z",
        "endTime": "2023-05-27T00:52:30.184845Z"
      },
      {
        "progressPercent": 100,
        "startTime": "2023-05-27T00:52:30.184845Z",
        "endTime": "2023-05-27T00:52:40.750959Z"
      }
    ],
...
  "done": true,
  "response": {
    "@type": "type.googleapis.com/google.protobuf.Empty"
  }
}

ב-gcloud וב-REST, אפשר לראות את ההתקדמות של כל הצהרת מילוי חוזר של אינדקס בקטע progress. לכל הצהרה במערך ההצהרות, יש שדה תואם במערך ההתקדמות. סדר המערך הזה של התקדמות התהליך תואם לסדר של מערך ההצהרות. אחרי שהם יהיו זמינים, השדות startTime, progressPercent ו-endTime יאוכלסו בהתאם. שימו לב: בפלט לא מוצג זמן משוער לסיום ההתקדמות של מילוי החסר.

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

תרחישים שבהם אפשר לראות את ההתקדמות של מילוי החוסרים באינדקס

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

התרחיש הראשון הוא הפשוט ביותר, שבו הצהרת יצירת האינדקס היא ההצהרה הראשונה בפעולת עדכון הסכימה. מכיוון שהצהרת יצירת האינדקס היא ההצהרה הראשונה, היא הראשונה שעוברת עיבוד ומופעלת בגלל סדר ההפעלה. השדה startTime של הצהרת יצירת האינדקס יתמלא באופן מיידי בשעת ההתחלה של פעולת עדכון הסכימה. לאחר מכן, השדה progressPercent של הצהרת יצירת האינדקס מאוכלס כשההתקדמות של מילוי האינדקס היא מעל 0%. לבסוף, השדה endTime יאוכלס אחרי שהדוח יאושר.

התרחיש השני הוא כשמשפט יצירת האינדקס הוא לא המשפט הראשון בפעולת עדכון הסכימה. אף שדה שקשור להצהרה על יצירת האינדקס לא יאוכלס עד שההצהרות הקודמות יאושרו, בגלל סדר הביצוע. בדומה לתרחיש הקודם, אחרי שהדוחות הקודמים יאושרו, השדה startTime של הצהרת יצירת האינדקס יתמלא קודם, ואחריו השדה progressPercent. לבסוף, השדה endTime מתמלא אחרי שהדוח מסיים את השמירה.

ביטול יצירת האינדקס

אפשר להשתמש ב-Google Cloud CLI כדי לבטל את יצירת האינדקס. כדי לאחזר רשימה של פעולות עדכון סכימה במסד נתונים של Spanner, משתמשים בפקודה gcloud spanner operations list וכוללים את האפשרות --filter:

gcloud spanner operations list \
    --instance=INSTANCE \
    --database=DATABASE \
    --filter="@TYPE:UpdateDatabaseDdlMetadata"

מאתרים את OPERATION_ID של הפעולה שרוצים לבטל, ואז משתמשים בפקודה gcloud spanner operations cancel כדי לבטל אותה:

gcloud spanner operations cancel OPERATION_ID \
    --instance=INSTANCE \
    --database=DATABASE

צפייה באינדקסים קיימים

כדי לראות מידע על אינדקסים קיימים במסד נתונים, אפשר להשתמש במסוףGoogle Cloud או ב-Google Cloud CLI:

המסוף

  1. נכנסים לדף Instances של Spanner במסוף Google Cloud .

    כניסה לדף Instances

  2. לוחצים על השם של המכונה שרוצים להציג.

  3. בחלונית הימנית, לוחצים על מסד הנתונים שרוצים לראות ואז על הטבלה שרוצים לראות.

  4. לוחצים על הכרטיסייה Indexes (אינדקסים). Google Cloud במסוף מוצגת רשימה של אינדקסים.

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

gcloud

משתמשים בפקודה gcloud spanner databases ddl describe:

    gcloud spanner databases ddl describe DATABASE \
        --instance=INSTANCE

ה-CLI של gcloud מדפיס את ההצהרות של שפת הגדרת הנתונים (DDL) כדי ליצור את הטבלאות והאינדקסים של מסד הנתונים. הצהרות CREATE INDEX מתארות את האינדקסים הקיימים. לדוגמה:

    --- |-
  CREATE TABLE Singers (
    SingerId INT64 NOT NULL,
    FirstName STRING(1024),
    LastName STRING(1024),
    SingerInfo BYTES(MAX),
  ) PRIMARY KEY(SingerId)
---
  CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName)

שאילתה עם אינדקס ספציפי

בקטעים הבאים מוסבר איך לציין אינדקס בהצהרת SQL ובממשק הקריאה של Spanner. בדוגמאות שבקטעים האלה מניחים שהוספתם עמודה MarketingBudget לטבלה Albums ויצרתם אינדקס בשם AlbumsByAlbumTitle:

GoogleSQL

CREATE TABLE Albums (
  SingerId         INT64 NOT NULL,
  AlbumId          INT64 NOT NULL,
  AlbumTitle       STRING(MAX),
  MarketingBudget  INT64,
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle);

PostgreSQL

CREATE TABLE Albums (
  SingerId         BIGINT NOT NULL,
  AlbumId          BIGINT NOT NULL,
  AlbumTitle       VARCHAR,
  MarketingBudget  BIGINT,
  PRIMARY KEY (SingerId, AlbumId)
) INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle);

ציון אינדקס בהצהרת SQL

כשמשתמשים ב-SQL כדי לשלוח שאילתה לטבלת Spanner, המערכת משתמשת באופן אוטומטי בכל האינדקסים שיכולים לייעל את השאילתה. כתוצאה מכך, לא צריך לציין אינדקס לשאילתות SQL. עם זאת, לשאילתות שחשובות לעומס העבודה שלכם, Google ממליצה להשתמש בהנחיות FORCE_INDEX בהצהרות SQL כדי לקבל ביצועים עקביים יותר.

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

כדי לציין אינדקס בהצהרת SQL, משתמשים ברמז FORCE_INDEX כדי לספק הוראת אינדקס. התחביר של הוראות האינדוקס הוא:

GoogleSQL

FROM MyTable@{FORCE_INDEX=MyTableIndex}

PostgreSQL

FROM MyTable /*@ FORCE_INDEX = MyTableIndex */

אפשר גם להשתמש בהוראת אינדקס כדי להנחות את Spanner לסרוק את טבלת הבסיס במקום להשתמש באינדקס:

GoogleSQL

FROM MyTable@{FORCE_INDEX=_BASE_TABLE}

PostgreSQL

FROM MyTable /*@ FORCE_INDEX = _BASE_TABLE */

אפשר להשתמש בהוראת אינדקס כדי להנחות את Spanner לסרוק אינדקס בטבלה עם סכימות בעלות שם:

GoogleSQL

FROM NAMED_SCHEMA_NAME.TABLE_NAME@{FORCE_INDEX="NAMED_SCHEMA_NAME.TABLE_INDEX_NAME"}

PostgreSQL

FROM NAMED_SCHEMA_NAME.TABLE_NAME /*@ FORCE_INDEX = TABLE_INDEX_NAME */

בדוגמה הבאה מוצגת שאילתת SQL שמציינת אינדקס:

GoogleSQL

SELECT AlbumId, AlbumTitle, MarketingBudget
    FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}
    WHERE AlbumTitle >= "Aardvark" AND AlbumTitle < "Goo";

PostgreSQL

SELECT AlbumId, AlbumTitle, MarketingBudget
    FROM Albums /*@ FORCE_INDEX = AlbumsByAlbumTitle */
    WHERE AlbumTitle >= 'Aardvark' AND AlbumTitle < 'Goo';

הוראת אינדקס עשויה לחייב את מעבד השאילתות של Spanner לקרוא עמודות נוספות שנדרשות בשאילתה אבל לא מאוחסנות באינדקס. מעבד השאילתות מאחזר את העמודות האלה על ידי צירוף האינדקס לטבלת הבסיס. כדי להימנע מהצטרפות נוספת, משתמשים בסעיף STORING (מסדי נתונים של ניב GoogleSQL) או בסעיף INCLUDE (מסדי נתונים של ניב PostgreSQL) כדי לאחסן את העמודות הנוספות באינדקס.

בדוגמה הקודמת, העמודה MarketingBudget לא מאוחסנת באינדקס, אבל שאילתת ה-SQL בוחרת את העמודה הזו. כתוצאה מכך, מערכת Spanner צריכה לחפש את העמודה MarketingBudget בטבלת הבסיס, ואז לצרף אותה לנתונים מהאינדקס כדי להחזיר את תוצאות השאילתה.

מערכת Spanner מעלה שגיאה אם יש בהוראת האינדקס אחת מהבעיות הבאות:

בדוגמאות הבאות מוצגות שאילתות ששולפות את הערכים של AlbumId, AlbumTitle ו-MarketingBudget באמצעות האינדקס AlbumsByAlbumTitle:

C++‎

void QueryUsingIndex(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;

  spanner::SqlStatement select(
      "SELECT AlbumId, AlbumTitle, MarketingBudget"
      " FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}"
      " WHERE AlbumTitle >= @start_title AND AlbumTitle < @end_title",
      {{"start_title", spanner::Value("Aardvark")},
       {"end_title", spanner::Value("Goo")}});
  using RowType =
      std::tuple<std::int64_t, std::string, absl::optional<std::int64_t>>;
  auto rows = client.ExecuteQuery(std::move(select));
  for (auto& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::move(row).status();
    std::cout << "AlbumId: " << std::get<0>(*row) << "\t";
    std::cout << "AlbumTitle: " << std::get<1>(*row) << "\t";
    auto marketing_budget = std::get<2>(*row);
    if (marketing_budget) {
      std::cout << "MarketingBudget: " << *marketing_budget << "\n";
    } else {
      std::cout << "MarketingBudget: NULL\n";
    }
  }
  std::cout << "Read completed for [spanner_query_data_with_index]\n";
}

C#‎


using Google.Cloud.Spanner.Data;
using System.Collections.Generic;
using System.Threading.Tasks;

public class QueryDataWithIndexAsyncSample
{
    public class Album
    {
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
        public long MarketingBudget { get; set; }
    }

    public async Task<List<Album>> QueryDataWithIndexAsync(string projectId, string instanceId, string databaseId,
        string startTitle, string endTitle)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";
        using var connection = new SpannerConnection(connectionString);
        using var cmd = connection.CreateSelectCommand(
            "SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@ "
            + "{FORCE_INDEX=AlbumsByAlbumTitle} "
            + $"WHERE AlbumTitle >= @startTitle "
            + $"AND AlbumTitle < @endTitle",
            new SpannerParameterCollection
            {
                { "startTitle", SpannerDbType.String, startTitle },
                { "endTitle", SpannerDbType.String, endTitle }
            });

        var albums = new List<Album>();
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            albums.Add(new Album
            {
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                AlbumTitle = reader.GetFieldValue<string>("AlbumTitle"),
                MarketingBudget = reader.IsDBNull(reader.GetOrdinal("MarketingBudget")) ? 0 : reader.GetFieldValue<long>("MarketingBudget")
            });
        }
        return albums;
    }
}

Go


import (
	"context"
	"fmt"
	"io"
	"strconv"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func queryUsingIndex(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{
		SQL: `SELECT AlbumId, AlbumTitle, MarketingBudget
			FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}
			WHERE AlbumTitle >= @start_title AND AlbumTitle < @end_title`,
		Params: map[string]interface{}{
			"start_title": "Aardvark",
			"end_title":   "Goo",
		},
	}
	iter := client.Single().Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return err
		}
		var albumID int64
		var marketingBudget spanner.NullInt64
		var albumTitle string
		if err := row.ColumnByName("AlbumId", &albumID); err != nil {
			return err
		}
		if err := row.ColumnByName("AlbumTitle", &albumTitle); err != nil {
			return err
		}
		if err := row.ColumnByName("MarketingBudget", &marketingBudget); err != nil {
			return err
		}
		budget := "NULL"
		if marketingBudget.Valid {
			budget = strconv.FormatInt(marketingBudget.Int64, 10)
		}
		fmt.Fprintf(w, "%d %s %s\n", albumID, albumTitle, budget)
	}
	return nil
}

Java

static void queryUsingIndex(DatabaseClient dbClient) {
  Statement statement =
      Statement
          // We use FORCE_INDEX hint to specify which index to use. For more details see
          // https://cloud.google.com/spanner/docs/query-syntax#from-clause
          .newBuilder(
              "SELECT AlbumId, AlbumTitle, MarketingBudget "
                  + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle} "
                  + "WHERE AlbumTitle >= @StartTitle AND AlbumTitle < @EndTitle")
          // We use @BoundParameters to help speed up frequently executed queries.
          //  For more details see https://cloud.google.com/spanner/docs/sql-best-practices
          .bind("StartTitle")
          .to("Aardvark")
          .bind("EndTitle")
          .to("Goo")
          .build();
  try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %s %s\n",
          resultSet.getLong("AlbumId"),
          resultSet.getString("AlbumTitle"),
          resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"));
    }
  }
}

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const instanceId = 'my-instance';
// const databaseId = 'my-database';
// const projectId = 'my-project-id';
// const startTitle = 'Ardvark';
// const endTitle = 'Goo';

// Imports the Google Cloud Spanner client library
const {Spanner} = require('@google-cloud/spanner');

// Instantiates a client
const spanner = new Spanner({
  projectId: projectId,
});

async function queryDataWithIndex() {
  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  const query = {
    sql: `SELECT AlbumId, AlbumTitle, MarketingBudget
                FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}
                WHERE AlbumTitle >= @startTitle AND AlbumTitle <= @endTitle`,
    params: {
      startTitle: startTitle,
      endTitle: endTitle,
    },
  };

  // Queries rows from the Albums table
  try {
    const [rows] = await database.run(query);

    rows.forEach(row => {
      const json = row.toJSON();
      const marketingBudget = json.MarketingBudget
        ? json.MarketingBudget
        : null; // This value is nullable
      console.log(
        `AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}, MarketingBudget: ${marketingBudget}`,
      );
    });
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    // Close the database when finished.
    database.close();
  }
}
queryDataWithIndex();

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Queries sample data from the database using SQL and an index.
 *
 * The index must exist before running this sample. You can add the index
 * by running the `add_index` sample or by running this DDL statement against
 * your database:
 *
 *     CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)
 *
 * Example:
 * ```
 * query_data_with_index($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 * @param string $startTitle The start of the title index.
 * @param string $endTitle   The end of the title index.
 */
function query_data_with_index(
    string $instanceId,
    string $databaseId,
    string $startTitle = 'Aardvark',
    string $endTitle = 'Goo'
): void {
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $parameters = [
        'startTitle' => $startTitle,
        'endTitle' => $endTitle
    ];

    $results = $database->execute(
        'SELECT AlbumId, AlbumTitle, MarketingBudget ' .
        'FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle} ' .
        'WHERE AlbumTitle >= @startTitle AND AlbumTitle < @endTitle',
        ['parameters' => $parameters]
    );

    foreach ($results as $row) {
        printf('AlbumId: %s, AlbumTitle: %s, MarketingBudget: %d' . PHP_EOL,
            $row['AlbumId'], $row['AlbumTitle'], $row['MarketingBudget']);
    }
}

Python

def query_data_with_index(
    instance_id, database_id, start_title="Aardvark", end_title="Goo"
):
    """Queries sample data from the database using SQL and an index.

    The index must exist before running this sample. You can add the index
    by running the `add_index` sample or by running this DDL statement against
    your database:

        CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)

    This sample also uses the `MarketingBudget` column. You can add the column
    by running the `add_column` sample or by running this DDL statement against
    your database:

        ALTER TABLE Albums ADD COLUMN MarketingBudget INT64

    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    params = {"start_title": start_title, "end_title": end_title}
    param_types = {
        "start_title": spanner.param_types.STRING,
        "end_title": spanner.param_types.STRING,
    }

    with database.snapshot() as snapshot:
        results = snapshot.execute_sql(
            "SELECT AlbumId, AlbumTitle, MarketingBudget "
            "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle} "
            "WHERE AlbumTitle >= @start_title AND AlbumTitle < @end_title",
            params=params,
            param_types=param_types,
        )

        for row in results:
            print("AlbumId: {}, AlbumTitle: {}, " "MarketingBudget: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"
# start_title = "An album title to start with such as 'Ardvark'"
# end_title   = "An album title to end with such as 'Goo'"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

sql_query = "SELECT AlbumId, AlbumTitle, MarketingBudget
             FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}
             WHERE AlbumTitle >= @start_title AND AlbumTitle < @end_title"

params      = { start_title: start_title, end_title: end_title }
param_types = { start_title: :STRING,     end_title: :STRING }

client.execute(sql_query, params: params, types: param_types).rows.each do |row|
  puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:MarketingBudget]}"
end

ציון אינדקס בממשק הקריאה

כשמשתמשים בממשק הקריאה של Spanner ורוצים ש-Spanner ישתמש באינדקס, צריך לציין את האינדקס. בממשק הקריאה, האינדקס לא נבחר באופן אוטומטי.

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

  • משתמשים בסעיף STORING או INCLUDE כדי לאחסן את העמודות הנוספות באינדקס.
  • מריצים שאילתה בלי לכלול את העמודות הנוספות, ואז משתמשים במפתחות הראשיים כדי לשלוח שאילתה נוספת שקוראת את העמודות הנוספות.

‫Spanner מחזיר ערכים מהאינדקס בסדר מיון עולה לפי מפתח האינדקס. כדי לאחזר ערכים בסדר יורד, מבצעים את השלבים הבאים:

  • מוסיפים הערה למפתח האינדקס עם DESC. לדוגמה:

    CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle DESC);
    

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

  • אם הקריאה מציינת טווח מפתחות, צריך לוודא שטווח המפתחות גם הוא בסדר יורד. במילים אחרות, הערך של מפתח ההתחלה צריך להיות גדול מהערך של מפתח הסיום.

בדוגמה הבאה אפשר לראות איך מאחזרים את הערכים של AlbumId ושל AlbumTitle באמצעות האינדקס AlbumsByAlbumTitle:

C++‎

void ReadDataWithIndex(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;

  auto rows =
      client.Read("Albums", google::cloud::spanner::KeySet::All(),
                  {"AlbumId", "AlbumTitle"},
                  google::cloud::Options{}.set<spanner::ReadIndexNameOption>(
                      "AlbumsByAlbumTitle"));
  using RowType = std::tuple<std::int64_t, std::string>;
  for (auto& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::move(row).status();
    std::cout << "AlbumId: " << std::get<0>(*row) << "\t";
    std::cout << "AlbumTitle: " << std::get<1>(*row) << "\n";
  }
  std::cout << "Read completed for [spanner_read_data_with_index]\n";
}

C#‎


using Google.Cloud.Spanner.Data;
using System.Collections.Generic;
using System.Threading.Tasks;

public class QueryDataWithIndexAsyncSample
{
    public class Album
    {
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
        public long MarketingBudget { get; set; }
    }

    public async Task<List<Album>> QueryDataWithIndexAsync(string projectId, string instanceId, string databaseId,
        string startTitle, string endTitle)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";
        using var connection = new SpannerConnection(connectionString);
        using var cmd = connection.CreateSelectCommand(
            "SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@ "
            + "{FORCE_INDEX=AlbumsByAlbumTitle} "
            + $"WHERE AlbumTitle >= @startTitle "
            + $"AND AlbumTitle < @endTitle",
            new SpannerParameterCollection
            {
                { "startTitle", SpannerDbType.String, startTitle },
                { "endTitle", SpannerDbType.String, endTitle }
            });

        var albums = new List<Album>();
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            albums.Add(new Album
            {
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                AlbumTitle = reader.GetFieldValue<string>("AlbumTitle"),
                MarketingBudget = reader.IsDBNull(reader.GetOrdinal("MarketingBudget")) ? 0 : reader.GetFieldValue<long>("MarketingBudget")
            });
        }
        return albums;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func readUsingIndex(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	iter := client.Single().ReadUsingIndex(ctx, "Albums", "AlbumsByAlbumTitle", spanner.AllKeys(),
		[]string{"AlbumId", "AlbumTitle"})
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var albumID int64
		var albumTitle string
		if err := row.Columns(&albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %s\n", albumID, albumTitle)
	}
}

Java

static void readUsingIndex(DatabaseClient dbClient) {
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .readUsingIndex(
              "Albums",
              "AlbumsByAlbumTitle",
              KeySet.all(),
              Arrays.asList("AlbumId", "AlbumTitle"))) {
    while (resultSet.next()) {
      System.out.printf("%d %s\n", resultSet.getLong(0), resultSet.getString(1));
    }
  }
}

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const instanceId = 'my-instance';
// const databaseId = 'my-database';
// const projectId = 'my-project-id';

// Imports the Google Cloud Spanner client library
const {Spanner} = require('@google-cloud/spanner');

// Instantiates a client
const spanner = new Spanner({
  projectId: projectId,
});

async function readDataWithIndex() {
  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  const albumsTable = database.table('Albums');

  const query = {
    columns: ['AlbumId', 'AlbumTitle'],
    keySet: {
      all: true,
    },
    index: 'AlbumsByAlbumTitle',
  };

  // Reads the Albums table using an index
  try {
    const [rows] = await albumsTable.read(query);

    rows.forEach(row => {
      const json = row.toJSON();
      console.log(`AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`);
    });
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    // Close the database when finished.
    database.close();
  }
}
readDataWithIndex();

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Reads sample data from the database using an index.
 *
 * The index must exist before running this sample. You can add the index
 * by running the `add_index` sample or by running this DDL statement against
 * your database:
 *
 *     CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)
 *
 * Example:
 * ```
 * read_data_with_index($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_data_with_index(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $keySet = $spanner->keySet(['all' => true]);
    $results = $database->read(
        'Albums',
        $keySet,
        ['AlbumId', 'AlbumTitle'],
        ['index' => 'AlbumsByAlbumTitle']
    );

    foreach ($results->rows() as $row) {
        printf('AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['AlbumId'], $row['AlbumTitle']);
    }
}

Python

def read_data_with_index(instance_id, database_id):
    """Reads sample data from the database using an index.

    The index must exist before running this sample. You can add the index
    by running the `add_index` sample or by running this DDL statement against
    your database:

        CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)

    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    with database.snapshot() as snapshot:
        keyset = spanner.KeySet(all_=True)
        results = snapshot.read(
            table="Albums",
            columns=("AlbumId", "AlbumTitle"),
            keyset=keyset,
            index="AlbumsByAlbumTitle",
        )

        for row in results:
            print("AlbumId: {}, AlbumTitle: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

result = client.read "Albums", [:AlbumId, :AlbumTitle],
                     index: "AlbumsByAlbumTitle"

result.rows.each do |row|
  puts "#{row[:AlbumId]} #{row[:AlbumTitle]}"
end

יצירת אינדקס לסריקות של אינדקס בלבד

אופציונלי: אפשר להשתמש בסעיף STORING (למסדי נתונים של GoogleSQL) או בסעיף INCLUDE (למסדי נתונים של PostgreSQL) כדי לאחסן עותק של עמודה באינדקס. סוג האינדקס הזה מספק יתרונות לשאילתות ולקריאות קריאה שמשתמשות באינדקס, אבל הוא דורש שימוש באחסון נוסף:

  • שאילתות SQL שמשתמשות באינדקס ובעמודות שנבחרו ומאוחסנות בסעיף STORING או INCLUDE לא דורשות איחוד נוסף עם טבלת הבסיס.
  • קריאות read() שמשתמשות באינדקס יכולות לקרוא עמודות שמאוחסנות על ידי פסוקית STORING/INCLUDE.

לדוגמה, נניח שיצרתם גרסה חלופית של AlbumsByAlbumTitle שמאחסנת עותק של העמודה MarketingBudget באינדקס (שימו לב לסעיף STORING או INCLUDE שמודגש):

GoogleSQL

CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget);

PostgreSQL

CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) INCLUDE (MarketingBudget);

עם האינדקס הישן AlbumsByAlbumTitle, ‏ Spanner צריך לבצע הצטרפות לאינדקס עם טבלת הבסיס, ואז לאחזר את העמודה מטבלת הבסיס. עם האינדקס החדשAlbumsByAlbumTitle2, Spanner קורא את העמודה ישירות מהאינדקס, וזה יעיל יותר.

אם משתמשים בממשק הקריאה במקום ב-SQL, האינדקס החדש AlbumsByAlbumTitle2 מאפשר גם לקרוא את העמודה MarketingBudget ישירות:

C++‎

void ReadDataWithStoringIndex(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;

  auto rows =
      client.Read("Albums", google::cloud::spanner::KeySet::All(),
                  {"AlbumId", "AlbumTitle", "MarketingBudget"},
                  google::cloud::Options{}.set<spanner::ReadIndexNameOption>(
                      "AlbumsByAlbumTitle2"));
  using RowType =
      std::tuple<std::int64_t, std::string, absl::optional<std::int64_t>>;
  for (auto& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::move(row).status();
    std::cout << "AlbumId: " << std::get<0>(*row) << "\t";
    std::cout << "AlbumTitle: " << std::get<1>(*row) << "\t";
    auto marketing_budget = std::get<2>(*row);
    if (marketing_budget) {
      std::cout << "MarketingBudget: " << *marketing_budget << "\n";
    } else {
      std::cout << "MarketingBudget: NULL\n";
    }
  }
  std::cout << "Read completed for [spanner_read_data_with_storing_index]\n";
}

C#‎


using Google.Cloud.Spanner.Data;
using System.Collections.Generic;
using System.Threading.Tasks;

public class QueryDataWithStoringIndexAsyncSample
{
    public class Album
    {
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
        public long? MarketingBudget { get; set; }
    }

    public async Task<List<Album>> QueryDataWithStoringIndexAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        var cmd = connection.CreateSelectCommand(
            "SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@ "
            + "{FORCE_INDEX=AlbumsByAlbumTitle2}");

        var albums = new List<Album>();
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            albums.Add(new Album
            {
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                AlbumTitle = reader.GetFieldValue<string>("AlbumTitle"),
                MarketingBudget = reader.IsDBNull(reader.GetOrdinal("MarketingBudget")) ? 0 : reader.GetFieldValue<long>("MarketingBudget")
            });
        }
        return albums;
    }
}

Go


import (
	"context"
	"fmt"
	"io"
	"strconv"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func readStoringIndex(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	iter := client.Single().ReadUsingIndex(ctx, "Albums", "AlbumsByAlbumTitle2", spanner.AllKeys(),
		[]string{"AlbumId", "AlbumTitle", "MarketingBudget"})
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var albumID int64
		var marketingBudget spanner.NullInt64
		var albumTitle string
		if err := row.Columns(&albumID, &albumTitle, &marketingBudget); err != nil {
			return err
		}
		budget := "NULL"
		if marketingBudget.Valid {
			budget = strconv.FormatInt(marketingBudget.Int64, 10)
		}
		fmt.Fprintf(w, "%d %s %s\n", albumID, albumTitle, budget)
	}
}

Java

static void readStoringIndex(DatabaseClient dbClient) {
  // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget.
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .readUsingIndex(
              "Albums",
              "AlbumsByAlbumTitle2",
              KeySet.all(),
              Arrays.asList("AlbumId", "AlbumTitle", "MarketingBudget"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %s %s\n",
          resultSet.getLong(0),
          resultSet.getString(1),
          resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"));
    }
  }
}

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const instanceId = 'my-instance';
// const databaseId = 'my-database';
// const projectId = 'my-project-id';

// Imports the Google Cloud Spanner client library
const {Spanner} = require('@google-cloud/spanner');

// Instantiates a client
const spanner = new Spanner({
  projectId: projectId,
});

// "Storing" indexes store copies of the columns they index
// This speeds up queries, but takes more space compared to normal indexes
// See the link below for more information:
// https://cloud.google.com/spanner/docs/secondary-indexes#storing_clause
async function readDataWithStoringIndex() {
  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  const albumsTable = database.table('Albums');

  const query = {
    columns: ['AlbumId', 'AlbumTitle', 'MarketingBudget'],
    keySet: {
      all: true,
    },
    index: 'AlbumsByAlbumTitle2',
  };

  // Reads the Albums table using a storing index
  try {
    const [rows] = await albumsTable.read(query);

    rows.forEach(row => {
      const json = row.toJSON();
      let rowString = `AlbumId: ${json.AlbumId}`;
      rowString += `, AlbumTitle: ${json.AlbumTitle}`;
      if (json.MarketingBudget) {
        rowString += `, MarketingBudget: ${json.MarketingBudget}`;
      }
      console.log(rowString);
    });
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    // Close the database when finished.
    database.close();
  }
}
readDataWithStoringIndex();

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Reads sample data from the database using an index with a storing
 * clause.
 *
 * The index must exist before running this sample. You can add the index
 * by running the `add_storing_index` sample or by running this DDL statement
 * against your database:
 *
 *     CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)
 *     STORING (MarketingBudget)
 *
 * Example:
 * ```
 * read_data_with_storing_index($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_data_with_storing_index(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $keySet = $spanner->keySet(['all' => true]);
    $results = $database->read(
        'Albums',
        $keySet,
        ['AlbumId', 'AlbumTitle', 'MarketingBudget'],
        ['index' => 'AlbumsByAlbumTitle2']
    );

    foreach ($results->rows() as $row) {
        printf('AlbumId: %s, AlbumTitle: %s, MarketingBudget: %d' . PHP_EOL,
            $row['AlbumId'], $row['AlbumTitle'], $row['MarketingBudget']);
    }
}

Python

def read_data_with_storing_index(instance_id, database_id):
    """Reads sample data from the database using an index with a storing
    clause.

    The index must exist before running this sample. You can add the index
    by running the `add_scoring_index` sample or by running this DDL statement
    against your database:

        CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)
        STORING (MarketingBudget)

    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    with database.snapshot() as snapshot:
        keyset = spanner.KeySet(all_=True)
        results = snapshot.read(
            table="Albums",
            columns=("AlbumId", "AlbumTitle", "MarketingBudget"),
            keyset=keyset,
            index="AlbumsByAlbumTitle2",
        )

        for row in results:
            print("AlbumId: {}, AlbumTitle: {}, " "MarketingBudget: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

result = client.read "Albums", [:AlbumId, :AlbumTitle, :MarketingBudget],
                     index: "AlbumsByAlbumTitle2"

result.rows.each do |row|
  puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:MarketingBudget]}"
end

שינוי אינדקס

אפשר להשתמש בהצהרה ALTER INDEX כדי להוסיף עמודות נוספות לאינדקס קיים או להסיר עמודות. הפעולה הזו יכולה לעדכן את רשימת העמודות שמוגדרת על ידי פסקה STORING (מסדי נתונים של ניב GoogleSQL) או פסקה INCLUDE (מסדי נתונים של ניב PostgreSQL) כשיוצרים את האינדקס. אי אפשר להשתמש בהצהרה הזו כדי להוסיף עמודות למפתח האינדקס או להסיר ממנו עמודות. לדוגמה, במקום ליצור אינדקס חדש AlbumsByAlbumTitle2, אפשר להשתמש ב-ALTER INDEX כדי להוסיף עמודה ל-AlbumsByAlbumTitle, כמו בדוגמה הבאה:

GoogleSQL

ALTER INDEX AlbumsByAlbumTitle ADD STORED COLUMN MarketingBudget

PostgreSQL

ALTER INDEX AlbumsByAlbumTitle ADD INCLUDE COLUMN MarketingBudget

כשמוסיפים עמודה חדשה לאינדקס קיים, Spanner משתמש בתהליך מילוי חוזר ברקע. במהלך מילוי החוסרים, אי אפשר לקרוא את העמודה באינדקס, ולכן יכול להיות שלא תהיה עלייה בביצועים כמו שציפיתם. אפשר להשתמש בפקודה gcloud spanner operations כדי להציג את רשימת הפעולות הממושכות ולראות את הסטטוס שלהן. מידע נוסף זמין במאמר בנושא תיאור פעולה.

אפשר גם להשתמש באפשרות ביטול הפעולה כדי לבטל פעולה שפועלת.

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

בדוגמה הבאה אפשר לראות איך משמיטים עמודה מאינדקס:

GoogleSQL

ALTER INDEX AlbumsByAlbumTitle DROP STORED COLUMN MarketingBudget

PostgreSQL

ALTER INDEX AlbumsByAlbumTitle DROP INCLUDE COLUMN MarketingBudget

אינדקס של ערכי NULL

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

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

כל השורות של Singers נוספות לאינדקס גם אם FirstName או LastName, או שניהם, הם NULL.

דיאגרמה שמציגה שורות שמושמטות מאינדקס מסונן של ערכי NULL.

כשמבצעים אינדוקס של ערכי NULL, אפשר לבצע שאילתות SQL וקריאות יעילות של נתונים שכוללים ערכי NULL. לדוגמה, אפשר להשתמש בהצהרת שאילתת ה-SQL הזו כדי למצוא את כל Singers עם NULL FirstName:

GoogleSQL

SELECT s.SingerId, s.FirstName, s.LastName
    FROM Singers@{FORCE_INDEX=SingersByFirstLastName} AS s
    WHERE s.FirstName IS NULL;

PostgreSQL

SELECT s.SingerId, s.FirstName, s.LastName
    FROM Singers /* @ FORCE_INDEX = SingersByFirstLastName */ AS s
    WHERE s.FirstName IS NULL;

סדר המיון של ערכי NULL

‫Spanner ממיין את NULL כערך הקטן ביותר לכל סוג נתונים נתון. אם ממיינים עמודה בסדר עולה (ASC), הערכים NULL ממוינים ראשונים. אם בוחרים למיין עמודה בסדר יורד (DESC), הערכים NULL יופיעו בסוף.

השבתת יצירת אינדקס לערכי NULL

GoogleSQL

כדי להשבית את יצירת האינדקס של ערכי null, מוסיפים את מילת המפתח NULL_FILTERED להגדרת האינדקס. אינדקסים של NULL_FILTERED שימושיים במיוחד לאינדקס של עמודות חסכוניות (sparse), שבהן רוב השורות מכילות ערך NULL_FILTERED.NULL במקרים כאלה, האינדקס NULL_FILTERED יכול להיות קטן בהרבה ויעיל יותר לתחזוקה מאשר אינדקס רגיל שכולל ערכים של NULL.

הנה הגדרה חלופית של SingersByFirstLastName שלא כוללת אינדקס של ערכי NULL:

CREATE NULL_FILTERED INDEX SingersByFirstLastNameNoNulls
    ON Singers(FirstName, LastName);

מילת המפתח NULL_FILTERED חלה על כל העמודות של מפתח האינדקס. אי אפשר לציין סינון של NULL על בסיס כל עמודה.

PostgreSQL

כדי לסנן שורות עם ערכי null בעמודה אחת או יותר עם אינדקס, משתמשים בפרדיקט WHERE COLUMN IS NOT NULL. אינדקסים עם סינון של ערכי Null שימושיים במיוחד לאינדוקס של עמודות דלילות, שרוב השורות שלהן מכילות ערך NULL. במקרים כאלה, האינדקס עם הסינון של ערכי null יכול להיות קטן בהרבה ויעיל יותר לתחזוקה מאשר אינדקס רגיל שכולל ערכי NULL.

הנה הגדרה חלופית של SingersByFirstLastName שלא כוללת אינדקס של ערכי NULL:

CREATE INDEX SingersByFirstLastNameNoNulls
    ON Singers(FirstName, LastName)
    WHERE FirstName IS NOT NULL
    AND LastName IS NOT NULL;

סינון ערכי NULL מונע מ-Spanner להשתמש בהם בחלק מהשאילתות. לדוגמה, Spanner לא משתמש באינדקס בשאילתה הזו, כי האינדקס משמיט שורות Singers שבהן LastName הוא NULL. כתוצאה מכך, שימוש באינדקס ימנע מהשאילתה להחזיר את השורות הנכונות:

GoogleSQL

FROM Singers@{FORCE_INDEX=SingersByFirstLastNameNoNulls}
    WHERE FirstName = "John";

PostgreSQL

FROM Singers /*@ FORCE_INDEX = SingersByFirstLastNameNoNulls */
    WHERE FirstName = 'John';

כדי לאפשר ל-Spanner להשתמש באינדקס, צריך לשכתב את השאילתה כך שהיא לא תכלול את השורות שגם לא נכללות באינדקס:

GoogleSQL

SELECT FirstName, LastName
    FROM Singers@{FORCE_INDEX=SingersByFirstLastNameNoNulls}
    WHERE FirstName = 'John' AND LastName IS NOT NULL;

PostgreSQL

SELECT FirstName, LastName
    FROM Singers /*@ FORCE_INDEX = SingersByFirstLastNameNoNulls */
    WHERE FirstName = 'John' AND LastName IS NOT NULL;

שדות פרוטו של אינדקס

אפשר להשתמש בעמודות שנוצרו כדי ליצור אינדקס לשדות ב-protocol buffers שמאוחסנים בעמודות PROTO, כל עוד השדות שעבורם נוצר האינדקס משתמשים בסוגי הנתונים הפרימיטיביים או ENUM.

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

הדוגמה הבאה היא של טבלת Singers עם עמודה של הודעת proto SingerInfo. כדי להגדיר אינדקס בשדה nationality של PROTO, צריך ליצור עמודה וירטואלית מאוחסנת:

GoogleSQL

CREATE PROTO BUNDLE (googlesql.example.SingerInfo, googlesql.example.SingerInfo.Residence);

CREATE TABLE Singers (
  SingerId INT64 NOT NULL,
  ...
  SingerInfo googlesql.example.SingerInfo,
  SingerNationality STRING(MAX) AS (SingerInfo.nationality) STORED
) PRIMARY KEY (SingerId);

ההגדרה של סוג ה-proto‏ googlesql.example.SingerInfo היא:

GoogleSQL

package googlesql.example;

message SingerInfo {
optional string    nationality = 1;
repeated Residence residence   = 2;

  message Residence {
    required int64  start_year   = 1;
    optional int64  end_year     = 2;
    optional string city         = 3;
    optional string country      = 4;
  }
}

לאחר מכן מגדירים אינדקס בשדה nationality של ה-proto:

GoogleSQL

CREATE INDEX SingersByNationality ON Singers(SingerNationality);

שאילתת ה-SQL הבאה קוראת נתונים באמצעות האינדקס הקודם:

GoogleSQL

SELECT s.SingerId, s.FirstName
FROM Singers AS s
WHERE s.SingerNationality = "English";

הערות:

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

עדכונים בסכימות שמכילות אינדקס בשדות פרוטו

אם מגדירים אינדקס בשדה של הודעת פרוטוקול, אי אפשר לשנות או להסיר את השדה הזה מסכימת ה-proto. הסיבה לכך היא שאחרי שמגדירים את האינדקס, מתבצעת בדיקת סוג בכל פעם שהסכימה מתעדכנת. ‫Spanner מתעד את פרטי הסוג של כל השדות בנתיב שמשמשים בהגדרת האינדקס.

אינדקסים ייחודיים

אפשר להצהיר על אינדקסים UNIQUE. אינדקסים של UNIQUE מוסיפים אילוץ לנתונים שמתווספים לאינדקס, שאוסר על כניסות כפולות למפתח אינדקס נתון. האילוץ הזה נאכף על ידי Spanner בזמן אישור העסקה. באופן ספציפי, כל עסקה שתגרום לכך שיהיו כמה רשומות אינדקס לאותו מפתח תיכשל.

אם טבלה מכילה נתוני UNIQUE שאינם נתוני UNIQUE, ניסיון ליצור אינדקס UNIQUE בטבלה ייכשל.

הערה לגבי אינדקסים מסוג UNIQUE NULL_FILTERED

UNIQUE NULL_FILTERED אינדקס לא אוכף ייחודיות של מפתח האינדקס כשחלק אחד לפחות ממפתח האינדקס הוא NULL.

לדוגמה, נניח שיצרתם את הטבלה והאינדקס הבאים:

GoogleSQL

CREATE TABLE ExampleTable (
  Key1 INT64 NOT NULL,
  Key2 INT64,
  Key3 INT64,
  Col1 INT64,
) PRIMARY KEY (Key1, Key2, Key3);

CREATE UNIQUE NULL_FILTERED INDEX ExampleIndex ON ExampleTable (Key1, Key2, Col1);

PostgreSQL

CREATE TABLE ExampleTable (
  Key1 BIGINT NOT NULL,
  Key2 BIGINT,
  Key3 BIGINT,
  Col1 BIGINT,
  PRIMARY KEY (Key1, Key2, Key3)
);

CREATE UNIQUE INDEX ExampleIndex ON ExampleTable (Key1, Key2, Col1)
    WHERE Key1 IS NOT NULL
    AND Key2 IS NOT NULL
    AND Col1 IS NOT NULL;

בשתי השורות הבאות ב-ExampleTable יש את אותם ערכים עבור מפתחות האינדקס המשני Key1, Key2 ו-Col1:

1, NULL, 1, 1
1, NULL, 2, 1

מכיוון שהערך של Key2 הוא NULL והאינדקס מסונן לפי ערך null, השורות לא יופיעו באינדקס ExampleIndex. הן לא מוכנסות לאינדקס, ולכן האינדקס לא ידחה אותן בגלל הפרת הייחודיות ב-(Key1, Key2, Col1).

אם רוצים שהאינדקס יאכוף את הייחודיות של ערכי הטופל (Key1,‏ Key2, ‏ Col1), צריך להוסיף את ההערה NOT NULL ל-Key2 בהגדרת הטבלה או ליצור את האינדקס בלי לסנן ערכי null.

הסרת אינדקס

משתמשים בהצהרה DROP INDEX כדי להסיר אינדקס משני מהסכימה.

כדי להסיר את האינדקס בשם SingersByFirstLastName:

DROP INDEX SingersByFirstLastName;

יצירת אינדקס לסריקה מהירה יותר

כש-Spanner צריך לבצע סריקת טבלה (במקום חיפוש באינדקס) כדי לאחזר ערכים מעמודה אחת או יותר, אפשר לקבל תוצאות מהירות יותר אם קיים אינדקס לעמודות האלה, ובסדר שצוין בשאילתה. אם אתם מבצעים לעיתים קרובות שאילתות שדורשות סריקות, כדאי ליצור אינדקסים משניים כדי שהסריקות האלה יתבצעו בצורה יעילה יותר.

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

לדוגמה, השאילתה הבאה תמיד מחזירה תוצאה מהירה, למרות ש-Spanner צריך לסרוק את Songs כדי למצוא את הערך הנמוך ביותר של SongId:

SELECT SongId FROM Songs LIMIT 1;

SongId הוא המפתח הראשי של הטבלה, שמאוחסן (כמו כל המפתחות הראשיים) בסדר עולה. מערכת Spanner יכולה לסרוק את האינדקס של המפתח הזה ולמצוא את התוצאה הראשונה במהירות.

עם זאת, בלי עזרה של אינדקס משני, השאילתה הבאה לא תחזיר תוצאות כל כך מהר, במיוחד אם Songs מכיל הרבה נתונים:

SELECT SongId FROM Songs ORDER BY SongId DESC LIMIT 1;

למרות ש-SongId הוא המפתח הראשי של הטבלה, ל-Spanner אין דרך לאחזר את הערך הכי גבוה בעמודה בלי לבצע סריקה מלאה של הטבלה.

הוספת האינדקס הבא תאפשר לשאילתה הזו להחזיר תוצאות מהר יותר:

CREATE INDEX SongIdDesc On Songs(SongId DESC);

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

המאמרים הבאים