二级索引

在 Spanner 数据库中,Spanner 会自动创建一个 每个表的主键的索引。例如,您无需执行任何操作 将 Singers 的主键编入索引,因为系统会自动为您编制索引。

除此之外,您可以针对非主键列创建“二级索引”。针对非主键列添加二级索引有助于更高效地查找此类列中的数据。对于 例如,如果您需要按标题快速查找专辑, 在 AlbumTitle 上创建二级索引; 这样 Spanner 就无需扫描整个表。

如果上例中的查找是在读写事务中完成, 那么更高效的查找还能避免对整个表持有锁 它允许对表外的行执行并发插入和更新 “AlbumTitle”查询范围。

除了为查询带来的优势之外,二级索引还可以帮助 Spanner 更高效地执行扫描,从而启用索引扫描而不是全表扫描。

Spanner 会将以下数据存储在各个二级索引中:

随着时间的推移,Spanner 会逐步分析您的表,以确保为适合的查询使用二级索引。

添加二级索引

在创建表时添加二级索引最为高效。要同时创建表及其索引,请针对 在向 Spanner 发出的单个请求中添加新表和新索引。

在 Spanner 中,您还可以向现有 同时数据库继续处理流量与任何其他架构一样 更改,向现有数据库添加索引不会 需要使数据库离线,并且不会锁定整个列或表。

每当向现有表添加新索引时,Spanner 都会 自动回填或填充索引以反映最新视图 要编入索引的数据的大小。Spanner 会为您管理此回填过程,该过程会在后台使用低优先级的节点资源运行。在大多数情况下,无法加快该过程(例如,通过添加更多节点),并且回填不会显著影响数据库的性能。

创建索引可能需要几分钟到数小时才能完成。由于创建索引属于架构更新,因此须遵循与其他架构更新一样的性能限制。创建二级索引所需的时间取决于以下几个因素:

  • 数据集的大小
  • 实例的计算容量
  • 实例上的负载

如需查看索引回填流程的进度,请参阅进度部分

请注意,将提交时间戳列用作二级索引的第一部分可能会引发热点,并降低写入性能。

使用 CREATE INDEX 语句定义二级索引 架构中。下面是一些示例:

要按名字和姓氏将数据库中的所有 Singers 编入索引,请使用以下语句:

GoogleSQL

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

PostgreSQL

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

要按照 SongName 的值为数据库中的所有 Songs 创建索引,请使用以下语句:

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 规范的表时创建的

有关选择非交错索引和交错索引的详细信息和最佳做法,请参阅索引选项在其值单调递增或递减的列上使用交错索引

检查索引回填进度

控制台

  1. 在 Spanner 导航菜单中,点击操作标签页。操作页面会显示当前正在运行的操作的列表。

  2. 在列表中找到回填操作。如果它仍在运行, 结束时间列中的进度指示器会显示 操作完成,如下图所示:

    进度指示器的屏幕截图,显示 98%

gcloud

使用gcloud spanner operations describe 来检查操作进度。

  1. 获取操作 ID:

    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。如需了解如何过滤备份操作,请参阅 ListBackupOperationsRequest 中的 filter 字段。

    • --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:您要执行的操作的 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

获取操作 ID:

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

替换以下内容:

  • INSTANCE-NAME 替换为 Spanner 实例 名称。
  • DATABASE-NAME 替换为数据库的名称。

在使用任何请求数据之前,请先进行以下替换:

  • PROJECT-ID:项目 ID。
  • INSTANCE-ID:实例 ID。
  • DATABASE-ID:数据库 ID。
  • OPERATION-ID:操作 ID。

HTTP 方法和网址:

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 部分找到每个索引回填语句的进度。对于语句数组中的每个语句, 进度数组中存在相应的字段此进度数组顺序 与语句数组的顺序相对应。该 系统会相应地填充 startTimeprogressPercentendTime 字段。 请注意,输出不会显示回填 进度完成。

如果操作耗时过长,您可以取消该操作。如需了解详情,请参阅 取消索引创建操作

查看索引回填进度时的场景

在尝试检查 索引回填的进度。需要索引回填的索引创建语句是架构更新操作的一部分,并且架构更新操作中可能有多个语句。

第一个场景最简单,即索引创建语句是架构更新操作中的第一个语句。自索引创建以来 语句是第一条语句,也是第一条处理和执行的 执行顺序。 索引创建语句的 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. 前往 Google Cloud 控制台中的 Spanner 实例页面。

    转到“实例”页面

  2. 点击要查看的实例的名称。

  3. 在左侧窗格中,点击要查看的数据库,然后点击要查看的表。

  4. 点击索引标签页。Google Cloud 控制台会显示 索引。

  5. 可选:要获取有关索引的详细信息(例如其中包含的列),请点击相应索引的名称。

gcloud

使用 gcloud spanner databases ddl describe 命令:

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

gcloud CLI 会显示数据定义语言 (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 的读取接口这些部分中的示例假定您向 Albums 表添加了 MarketingBudget 列并创建了名为 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 表时,Spanner 会自动使用任何有可能提高查询效率的索引。因此,您不需要为 SQL 查询指定索引。但是,对于工作负载至关重要的查询,Google 建议您在 SQL 语句中使用 FORCE_INDEX 指令以实现更一致的性能。

在少数情况下,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 */

以下示例演示了指定索引的 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 会引发错误 问题:

下面的示例演示了如何编写和执行查询,以使用索引 AlbumsByAlbumTitle 提取 AlbumIdAlbumTitleMarketingBudget 的值:

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 时 要使用索引,您必须指定索引。读取接口不会自动选择索引。

此外,您的索引必须包含查询结果中显示的所有数据,属于主键的列除外。存在此限制是因为读取接口不支持索引和基表之间的联接。如果您需要在查询结果中包含其他列,可以使用以下几种方法:

  • 使用 STORINGINCLUDE 子句将额外的列存储在 索引。
  • 执行不包含其他列的查询,然后再使用主键发送另一个读取其他列的查询。

Spanner 按索引的升序排序返回索引中的值 键。要按降序顺序检索值,请完成以下步骤:

  • 为索引键添加注释 DESC。例如:

    CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle DESC);
    

    DESC 注释适用于单个索引键。如果索引包含多个键,并且您希望按照所有键的降序顺序显示查询结果,请为每个键添加 DESC 注释。

  • 如果读取接口指定了键范围,请确保该键范围也按降序顺序排列。换句话说,也就是起始键的值必须大于结束键的值。

以下示例演示了如何使用索引 AlbumsByAlbumTitle 检索 AlbumIdAlbumTitle 的值:

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 方言数据库)将列的副本存储在 索引中。此类索引为使用索引的查询和读取调用提供了便利,但会占用额外存储空间:

  • 使用索引并选择存储在 STORING 中的列的 SQL 查询,或 INCLUDE 子句不需要与基表进行额外联接。
  • 使用索引的 read() 调用可以读取由 STORING/INCLUDE 子句。

例如,假设您创建了 AlbumsByAlbumTitle 的备用版本。 该函数在索引中存储 MarketingBudget 列的副本(请注意, STORINGINCLUDE 子句以粗体显示):

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 值编入索引。例如,回顾一下表 Singers 上的索引 SingersByFirstLastName 的定义:

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

即使 FirstName 和/或 LastNameNULL,系统仍会将 Singers 的所有行编入索引。

图中显示了从过滤掉 NULL 值的索引中省略的行。

NULL 值编入索引后,您便可以针对包含 NULL 值的数据执行有效的 SQL 查询和读取。例如,使用以下 SQL 查询语句可查找 FirstNameNULL 的所有 Singers

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 索引特别适用于将大部分行都包含 NULL 值的稀疏列编入索引。在这些情况下,与包含 NULL 的一般索引相比,NULL_FILTERED 索引非常小,维护起来也更为高效。

下面是不将 NULL 编入索引的 SingersByFirstLastName 的一个替代定义:

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

NULL_FILTERED 关键字适用于所有索引键列。您无法针对每个列指定 NULL 过滤。

PostgreSQL

要过滤掉一个或多个编入索引的列中具有 null 值的行,请使用 WHERE COLUMN IS NOT NULL 谓词。 Null 过滤的索引对于稀疏索引特别有用 列,其中大多数行都包含 NULL 值。在这些情况下,与包含 NULL 值的一般索引相比,经过 null 过滤的索引非常小,维护起来也更为高效。

下面是不将 NULL 编入索引的 SingersByFirstLastName 的一个替代定义:

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

过滤掉 NULL 值可防止 Spanner 将其用于某些场景 查询。例如,Spanner 不会为此查询使用索引, 因为索引省略了 LastNameNULL 的所有 Singers 行;以 结果,则使用索引会阻止查询返回正确的行:

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;

索引 proto 字段

使用生成的列编制索引 存储在 PROTO 列中的 Protocol Buffers 中的字段,前提是 使用原始数据类型或 ENUM 数据类型。

如果您在协议消息字段上定义了索引,则无法修改或从 proto 架构中移除该字段。如需了解详情,请参阅 对包含 proto 字段索引的架构进行更新

以下是包含 SingerInfo proto 的 Singers 表示例 message 列。如需在 PROTOnationality 字段中定义索引,请执行以下操作: 您需要创建一个存储的生成列:

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);

它对 googlesql.example.SingerInfo proto 类型有以下定义:

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;
  }
}

然后,在该 proto 的 nationality 字段中定义索引:

GoogleSQL

CREATE INDEX SingersByNationality ON Singers(SingerNationality);

以下 SQL 查询使用上一个索引读取数据:

GoogleSQL

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

注意:

  • 使用索引指令访问 字段。
  • 您无法对重复的 Protocol Buffer 字段创建索引。

对包含 proto 字段索引的架构的更新

如果您在协议消息字段上定义了索引,则无法修改或从 proto 架构中移除该字段。这是因为在定义索引后 系统在每次更新架构时都会执行类型检查。 Spanner 会捕获路径中所有字段的类型信息 索引定义中所使用的标识符

唯一索引

索引可声明为是 UNIQUE 的。UNIQUE 索引会对要编入索引的数据施加一项限制,禁止给定索引键有重复条目。在提交事务时,Spanner 会强制执行此限制条件。 具体而言,任何会导致同一个键出现多个索引条目的事务都将提交失败。

如果某个表的开头包含非 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 中的以下两行的二级索引键 Key1Key2Col1 具有相同的值:

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

由于 Key2NULL 并且索引对 null 进行了过滤,因此行不会 存在于索引 ExampleIndex 中。由于这些代码不会插入到 索引,索引不会因为 (Key1, Key2, Col1) 违反唯一性而拒绝它们。

如果希望索引强制对元组(Key1Key2Col1)的值施加唯一性限制,那么您必须在表定义中为 Key2 添加注释 NOT NULL,或者创建不滤除 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 将使用它来返回 生成第二个查询的速度快得多。

后续步骤