二级索引

在 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。有关过滤备份操作的信息,请参阅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:您要执行的操作的 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";

注意:

  • 使用索引指令访问 字段。
  • 您不能为重复的协议缓冲区字段创建索引。

对包含 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) 违反唯一性而拒绝它们。

如果您希望索引强制使元组 (Key1, Key2Col1),则必须在表中为 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 将使用它来返回 生成第二个查询的速度快得多。

后续步骤