שפת טיפול בנתונים (DML) שחולקו למחיצות

שפת טיפול בנתונים עם חלוקה למחיצות (partitioned DML) מיועדת לסוגים הבאים של עדכונים ומחיקות בכמות גדולה:

  • ניקוי תקופתי ו-garbage collection. לדוגמה, מחיקה של שורות ישנות או הגדרת עמודות ל-NULL.
  • מילוי חוזר של עמודות חדשות בערכי ברירת מחדל. לדוגמה, אפשר להשתמש בהצהרת UPDATE כדי להגדיר את הערך של עמודה חדשה ל-False, אם הערך הנוכחי הוא NULL.

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

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

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

DML ו-DML עם חלוקה למחיצות

‫Spanner תומך בשני מצבי ביצוע של משפטי DML:

  • DML, שמתאים לעיבוד עסקאות. מידע נוסף מופיע במאמר בנושא שימוש ב-DML.

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

בטבלה הבאה מפורטים כמה מההבדלים בין שני מצבי ההפעלה.

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

ניתן לחלוקה למחיצות ואידמפוטנטי

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

אפשר לחלק את פקודות ה-DML האלה באופן מלא, כי כל פקודה יכולה לחול על שורה אחת בטבלה:

UPDATE Singers SET LastName = NULL WHERE LastName = '';

DELETE FROM Albums WHERE MarketingBudget > 10000;

אי אפשר לחלק את פקודת ה-DML הזו באופן מלא, כי היא ניגשת לכמה טבלאות:

# Not fully partitionable
DELETE FROM Singers WHERE
SingerId NOT IN (SELECT SingerId FROM Concerts);

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

פקודת ה-DML הזו היא אידמפוטנטית:

UPDATE Singers SET MarketingBudget = 1000 WHERE true;

פקודת ה-DML הזו היא לא אידמפוטנטית:

UPDATE Singers SET MarketingBudget = 1.5 * MarketingBudget WHERE true;

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

כשמשתמשים בפקודת DML עם חלוקה למחיצות כדי למחוק שורות בטבלת אב, יכול להיות שהפעולה תיכשל עם השגיאה: The transaction contains too many mutations. המצב הזה קורה אם בטבלת ההורה יש טבלאות צאצא משולבות שמכילות אינדקס גלובלי. מוטציות בשורות של טבלת הצאצא עצמה לא נספרות במגבלת המוטציות של העסקה. עם זאת, מוטציות מקבילות לרשומות האינדקס נספרות. אם מושפעים מספר גדול של רשומות באינדקס של טבלת צאצא, יכול להיות שהעסקה תחרוג ממגבלת השינויים.

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

  1. מריצים מחיקה מחולקת במחיצות בטבלאות הצאצא.
  2. מריצים מחיקה עם חלוקה למחיצות בטבלת האב.

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

נעילת שורות

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

הרצה ועסקאות

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

אפשר להריץ רק פקודת DML אחת עם חלוקה למחיצות בקריאה לשיטה של ספריית הלקוח.

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

חלוקה למחיצות של DML לא תומכת בביצוע (commit) או בהחזרה למצב קודם (rollback). ‫Spanner מבצע ומחיל את פקודת ה-DML באופן מיידי.

  • אם מבטלים את הפעולה, Spanner מבטל את המחיצות הפועלות ולא מתחיל את שאר המחיצות. ‫Spanner לא מבטל מחיצות שכבר בוצעו.
  • אם ביצוע ההצהרה גורם לשגיאה, הביצוע נפסק בכל המחיצות ו-Spanner מחזיר את השגיאה הזו עבור כל הפעולה. דוגמאות לשגיאות: הפרות של אילוצים של סוגי נתונים, הפרות של UNIQUE INDEX והפרות של ON DELETE NO ACTION. בהתאם לנקודת הזמן שבה הביצוע נכשל, יכול להיות שההצהרה הופעלה בהצלחה בחלק מהמחיצות, ויכול להיות שהיא לא הופעלה אף פעם במחיצות אחרות.

אם פקודת ה-DML עם החלוקה למחיצות מצליחה, Spanner מריץ את הפקודה לפחות פעם אחת מול כל מחיצה של טווח מפתחות.

מספר השורות ששונו

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

הגבלות על עסקאות

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

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

תכונות שלא נתמכות

חלק מהתכונות של DML עם חלוקה למחיצות לא נתמכות ב-Spanner:

  • אין תמיכה ב-INSERT.
  • מסוףGoogle Cloud : אי אפשר להפעיל הצהרות DML עם חלוקה למחיצות במסוףGoogle Cloud .Google Cloud
  • תוכניות שאילתה ופרופילים: Google Cloud CLI וספריות הלקוח לא תומכים בתוכניות שאילתה ובפרופילים.
  • שאילתות משנה שקוראות מטבלה אחרת, או משורה אחרת באותה טבלה.

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

שיטות מומלצות

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

  • הימנעות מריבוי פעולות בו-זמניות: הפעלה בו-זמנית של מספר גדול של הצהרות DML עם חלוקה למחיצות (לדוגמה, יותר מ-100) עלולה להוביל למאבק על נעילה בטבלאות מערכת פנימיות, ולפגוע בביצועים. במקום להריץ מספר גדול של הצהרות בו-זמניות, כדאי להשתמש בהצהרת DML אחת עם חלוקה למחיצות.
  • שימוש ב-PDML_MAX_PARALLELISM: כדי להגדיל את התפוקה של פקודת DML מחולקת אחת, במיוחד בטבלאות עם הרבה פיצולים, מגדירים ערך גבוה יותר לרמז להצהרה PDML_MAX_PARALLELISM. כך אפשר להשתמש ביותר מקביליות באופן פנימי בהצהרה אחת. הגדרה של ערך גבוה יותר ל-PDML_MAX_PARALLELISM מובילה לשימוש רב יותר במחשוב, ולכן כדאי לנסות לאזן בין השימוש במחשוב לבין מהירות העיבוד המוגברת.
  • מאפשרים ל-Spanner לטפל בחלוקה למחיצות: לא מומלץ לבצע שרדינג של הנתונים באופן ידני (לדוגמה, באמצעות טווחי מפתחות ראשיים) ולהפעיל הצהרות DML מחולקות נפרדות בכל שארד. ה-DML המחולק למחיצות נועד לחלק את העבודה ביעילות בין כל המחיצות בטבלה. פיצול מותאם אישית של נתונים (sharding) לרוב מגדיל את התקורה ויכול להחמיר את התחרות על משאבים.
  • הסבר על היקף החלוקה למחיצות: פעולות DML מחולקות למחיצות ומבוצעות במקביל בכל הפיצולים במסד הנתונים כולו, ולא רק בפיצולים שמכילים נתונים של הטבלה שמשנים. כלומר, במסדי נתונים עם מספר גדול של פיצולים, יכול להיות שיהיו תקורה גם אם טבלת היעד קטנה או שהנתונים ששונו הם מקומיים. יכול להיות ש-DML עם חלוקה למחיצות לא יהיה הבחירה היעילה ביותר לשינוי חלק קטן מאוד במסד נתונים גדול.
  • כדאי לשקול חלופות למחיקות קטנות ותכופות: בתרחישי שימוש שכוללים מחיקות תכופות של מספר קטן של שורות ידועות, שימוש בהצהרות DML בתוך טרנזקציות או ב-BatchWrite API עשוי להניב ביצועים טובים יותר ותקורה נמוכה יותר מאשר שימוש ב-DML עם חלוקה למחיצות.

דוגמאות

בדוגמה הבאה של קוד מתבצע עדכון של העמודה MarketingBudget בטבלה Albums.

C++‎

משתמשים בפונקציה ExecutePartitionedDml() כדי להפעיל פקודת DML עם חלוקה למחיצות.

void DmlPartitionedUpdate(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("UPDATE Albums SET MarketingBudget = 100000"
                            "  WHERE SingerId > 1"));
  if (!result) throw std::move(result).status();
  std::cout << "Updated at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_update]\n";
}

C#‎

משתמשים ב-ExecutePartitionedUpdateAsync() method כדי להריץ פקודת DML עם חלוקה למחיצות.


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

public class UpdateUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> UpdateUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        using var cmd = connection.CreateDmlCommand("UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) updated...");
        return rowCount;
    }
}

Go

משתמשים ב-PartitionedUpdate() method כדי להריץ פקודת DML עם חלוקה למחיצות.


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func updateUsingPartitionedDML(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: "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err
	}
	fmt.Fprintf(w, "%d record(s) updated.\n", rowCount)
	return nil
}

Java

משתמשים ב-executePartitionedUpdate() method כדי להריץ פקודת DML עם חלוקה למחיצות.

static void updateUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records updated.\n", rowCount);
}

Node.js

משתמשים ב-runPartitionedUpdate() method כדי להריץ פקודת DML עם חלוקה למחיצות.

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

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

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

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

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1',
  });
  console.log(`Successfully updated ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

משתמשים ב-executePartitionedUpdate() method כדי להריץ פקודת DML עם חלוקה למחיצות.

use Google\Cloud\Spanner\SpannerClient;

/**
 * Updates sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. 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
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function update_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1'
    );

    printf('Updated %d row(s).' . PHP_EOL, $rowCount);
}

Python

משתמשים ב-execute_partitioned_dml() method כדי להריץ פקודת DML עם חלוקה למחיצות.

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"

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

row_ct = database.execute_partitioned_dml(
    "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

print("{} records updated.".format(row_ct))

Ruby

משתמשים ב-execute_partitioned_update() method כדי להריץ פקודת DML עם חלוקה למחיצות.

# 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

row_count = client.execute_partition_update(
  "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

puts "#{row_count} records updated."

בדוגמה הבאה של הקוד מוצגת מחיקה של שורות מהטבלה Singers, על סמך העמודה SingerId.

C++‎

void DmlPartitionedDelete(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("DELETE FROM Singers WHERE SingerId > 10"));
  if (!result) throw std::move(result).status();
  std::cout << "Deleted at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_delete]\n";
}

C#‎


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

public class DeleteUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> DeleteUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        using var cmd = connection.CreateDmlCommand("DELETE FROM Singers WHERE SingerId > 10");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) deleted...");
        return rowCount;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func deleteUsingPartitionedDML(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: "DELETE FROM Singers WHERE SingerId > 10"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err

	}
	fmt.Fprintf(w, "%d record(s) deleted.", rowCount)
	return nil
}

Java

static void deleteUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "DELETE FROM Singers WHERE SingerId > 10";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records deleted.\n", rowCount);
}

Node.js

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

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

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

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

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'DELETE FROM Singers WHERE SingerId > 10',
  });
  console.log(`Successfully deleted ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Delete sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. 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
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function delete_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'DELETE FROM Singers WHERE SingerId > 10'
    );

    printf('Deleted %d row(s).' . PHP_EOL, $rowCount);
}

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml("DELETE FROM Singers WHERE SingerId > 10")

print("{} record(s) deleted.".format(row_ct))

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

row_count = client.execute_partition_update(
  "DELETE FROM Singers WHERE SingerId > 10"
)

puts "#{row_count} records deleted."

מה השלב הבא?