27、数据库分区键选择与表操作详解

数据库分区键选择与表操作详解

选择合适的分区键

设计数据库模式通常遵循一定的模式。在 Windows Azure 环境中,设计时要特别重视应用程序将执行的查询。可以先列出需要高性能的查询列表,以此为起点构建表模式和分区方案。具体步骤如下:
1. 确定关键查询 :列出系统将执行的关键查询,并按重要性和性能要求进行优先级排序。例如,显示购物车内容的查询速度必须比显示很少生成的报告的查询速度快得多。
2. 创建表模式 :根据关键查询创建表模式,确保在对性能敏感的查询中可以指定分区键。估计每个表和每个分区中预期的数据量。如果某个分区的数据量过大(例如,比其他分区大一个数量级),可以通过将其他属性连接到分区键来使分区更细化。例如,在构建 Web 日志分析器时,如果将 URL 作为分区键,对于非常热门的 URL 可能会有问题,此时可以将日期范围添加到分区键中,这样每个分区仅包含特定一天内某个 URL 的数据。
3. 选择行键 :为行键选择唯一标识符,行键在分区内必须是唯一的。例如,在某个表中,可以使用超级英雄的名字作为行键,因为它在分区内是唯一的。

如果发现分区方案效果不佳,可能需要动态更改。例如,在 Web 日志分析器示例中,可以使日期范围的大小动态变化。如果某一天(如周末)的数据量很大,可以仅在周末使用小时范围进行分区。应用程序需要从一开始就支持这种动态分区。

分区键和行键的作用

一般来说,分区键是数据分布和可扩展性的单位,而行键用于确保唯一性。如果数据模型的键只有一个属性,可以将其用作分区键(行键为空即可),每个分区有一行数据。如果键有多个属性,可以将属性分配到分区键和行键中,使每个分区有多个行。

测试理论

为了验证指定或不指定分区键、在单个分区或多个分区上执行查询的影响,可以构建一些基准测试。以下是测试的具体步骤和代码示例:
1. 创建测试实体和上下文类

public class TestEntity : TableServiceEntity
{
    public TestEntity(string id, string data)
        : base(id, data)
    {
        ID = id;
        Data = data;
    }
    //Parameter-less constructor always needed for
    // ADO.NET Data Services
    public TestEntity() { }
    public string ID { get; set; }
    public string Data { get; set; }
}

public class TestDataServiceContext : TableServiceContext
{
    public TestDataServiceContext (string baseAddress,
    StorageCredentials credentials): base(baseAddress, credentials)
    {}
    internal const string TestTableName = "TestTable";
    public IQueryable<TestEntity> TestTable
    {
        get
        {
            return this.CreateQuery<TestEntity>(TestTableName);
        }
    }
}
  1. 插入数据
    • 插入到同一个分区
CloudStorageAccount.Parse(ConfigurationSettings.AppSettings
["DataConnectionString"]);
var svc = new TestDataServiceContext(account.TableEndpoint.ToString(),
                                      account.Credentials);
for (int i = 1; i < 100000; i++)
{
    svc.AddObject("TestTable",
     new TestEntity("1", "RowKey_" + i.ToString() );
}
- **插入到 1000 个不同分区**
CloudStorageAccount.Parse(ConfigurationSettings.AppSettings
["DataConnectionString"]);
var svc = new TestDataServiceContext(account.TableEndpoint.ToString(),
 account.Credentials);
for (int i = 1; i < 100000; i++)
{
    svc.AddObject("TestTable2",
    new TestEntity2((i % 1000).ToString(), "RowKey_" + i.ToString()));
}
  1. 执行查询
// Single partition query
var query = from entity in svc.CreateQuery<TestEntity>("TestTable")
where entity.PartitionKey == "1" && entity.RowKey == "RowKey_55000"
select entity;
//Multiple partition query - no partition key specified
var query2 = from entity in svc2.CreateQuery<TestEntity2>("TestTable2")
                         where entity.RowKey == "RowKey_55553"
                         select entity;
//Multiple partition query - partition key specified in query
var query3 = from entity in svc2.CreateQuery<TestEntity2>("TestTable2")
where entity.PartitionKey == "553" && entity.RowKey == "RowKey_55553"
select entity;

以下是 1000 次迭代的查询性能比较:
| 查询类型 | 1000 次迭代时间(秒) |
| — | — |
| 单分区查询 | 26 |
| 多分区查询 - 未指定分区键 | 453 |
| 多分区查询 - 指定分区键 | 25 |

结果表明,访问单个分区(无论是因为所有数据都存储在该分区,还是在查询中指定了该分区)总是比不指定分区键快得多。但使用单个分区也有一些缺点。一般来说,查询时间受是否在查询中指定分区键的影响比分区方式的影响更大。

如果要进行类似的测试,需要在配置文件(App.config 或 web.config)中插入以下配置设置:

<system.net>
    <settings>
        <servicePointManager expect100Continue="false"
useNagleAlgorithm="false" />
    </settings>
</system.net>
理解分页

在某些情况下,Azure 表可能只返回匹配查询的实体的子集。例如,匹配查询的实体数量超过 1000 个、超过默认的 60 秒查询超时时间、总数据量超过 4 MB 等。也可以通过每次只请求特定数量的实体来强制 Azure 表返回子集。

在这些情况下,Azure 表会返回两个延续标记:一个用于下一个分区,一个用于下一行。当需要获取下一组结果时,将这些延续标记发送回 Azure 表服务,这些标记会告诉表服务查询从哪里继续。

在编写针对表的 .NET 代码时,如果不想处理延续标记,可以不处理。如果枚举查询的所有结果,存储客户端库会在底层自动处理延续标记。可以使用 AsTableServiceQuery 扩展方法自动处理延续标记。

但有时需要手动处理,例如在需要分页输出(如博客的多页,需要“上一页”和“下一页”链接)的情况下。以下是检索和插入延续标记的代码示例:

// 检索延续标记
var dsQuery = (DataServiceQuery<Contact>)query;
var res = dsQuery.Execute();
var qor = (QueryOperationResponse)res;
string partitionToken = null;
string rowToken = null;
qor.Headers.TryGetValue("x-ms-continuation-NextPartitionKey",
                  out partitionToken);
qor.Headers.TryGetValue("x-ms-continuation-NextRowKey", out rowToken);

// 插入延续标记
var dsQuery = (DataServiceQuery<Contact>)query;
query = query
  .AddQueryOption("NextPartitionKey", partitionToken)
  .AddQueryOption("NextRowKey", rowToken);

在生产代码中,需要检查是否只获取到了一个延续标记(例如在最后一行或最后一个分区的情况下)。

graph TD;
    A[开始查询] --> B{结果是否完整};
    B -- 否 --> C[获取延续标记];
    C --> D[插入延续标记到下一次查询];
    D --> E[继续查询];
    E --> B;
    B -- 是 --> F[结束查询];
更新实体

在 Azure 表中更新实体相对简单,关键是要理解各种合并/冲突选项。每个查询结果都会返回一个 ETag,它对应于实体的特定版本。如果实体发生变化,服务会确保它获得一个新的 ETag。更新实体的步骤如下:
1. 创建 DataServiceContext 对象 :创建一个 DataServiceContext 对象,并将其合并选项设置为以下选项之一:
- AppendOnly :接受新实体的客户端更改,但不接受现有实体的更改。如果实体存在于客户端缓存中,则不会从服务器加载。这是默认值。
- OverwriteChanges :始终使用服务器的值进行更新,覆盖任何客户端更改。
- PreserveChanges :保留客户端已更改的值,但从服务器更新其他值。当实体实例存在于客户端时,不会从服务器加载。不会丢失客户端更改。更新对象时,ETag 也会更新,因此如果服务器上的实体在客户端不知情的情况下发生了更改,会检测到错误。
- NoTracking :没有客户端跟踪。值始终从存储源读取,任何本地更改都会被覆盖。
2. 查询要更新的实体 :查询需要更新的实体。
3. 更新实体对象 :在代码中更新实体的对象表示。
4. 调用更新方法 :调用 DataServiceContext.UpdateObject 方法更新对象。
5. 保存更改 :调用 DataServiceContext.SaveChanges 方法将更改推送到服务器。

以下是一个简单的更新示例:

CloudStorageAccount.Parse(ConfigurationSettings.AppSettings
["DataConnectionString"]);
var svc = new TestDataServiceContext
 (account.TableEndpoint.ToString(), account.Credentials);
svc.MergeOption = MergeOption.PreserveChanges;
var query = from contact in svc.CreateQuery<Contact>("ContactTable")
            where contact.Name == "Steve Jobs"
            select contact;
var foundContact = query.FirstOrDefault<Contact>();
foundContact.Address = "One Infinite Loop, Cupertino, CA 95014";
svc.UpdateObject(foundContact);
svc.SaveChanges();

当调用 SaveChanges 时,HTTP 流量可以清楚地显示 ETag 匹配机制。查询时会返回服务器端对象的 ETag,调用 SaveChanges 时,会发送一个更新请求,要求“如果匹配之前的 ETag 则更新”。如果成功,服务器会更新实体并返回一个新的 ETag。

删除表和实体
  • 删除表 :删除表非常简单,只需调用 CloudTableClient.DeleteTable 方法即可。需要注意的是,删除大表可能需要一些时间。如果在删除表后立即尝试重新创建表,可能会收到错误消息。示例代码如下:
account.CreateCloudTableClient().DeleteTable("ContactsTable");

对应的 REST 请求也很简单,只需向表的 URL 发送一个 HTTP DELETE 请求即可:

DELETE /Tables('ContactTable') HTTP/1.1
  • 删除实体 :删除实体与删除表类似。可以先通过查询检索要删除的实体,然后调用 DataServiceContext.DeleteObject 方法,并保存更改。示例代码如下:
CloudStorageAccount.Parse(ConfigurationSettings.AppSettings
                ["DataConnectionString"]);
var svc = new TestDataServiceContext(account.TableEndpoint.ToString(),
                                    account.Credentials);
var item = (from contact in svc.CreateQuery<Contact>("ContactTable")
            where contact.Name == "Steve Jobs"
            select contact).Single();
svc.DeleteObject(item);
svc.SaveChanges();

但这种方法在删除大量实体时效率较低,因为需要先查询再删除。可以通过创建一个具有正确分区键和行键的虚拟本地实体,将其附加到 DataServiceContext 中,然后删除它。示例代码如下:

var svc =
   new TestDataServiceContext(account.TableEndpoint.ToString(),
account.Credentials);
var item = new Contact("CorrectPartitionKey", "CorrectRowKey");
svc.AttachTo("ContactTable", item, "*");
svc.DeleteObject(item);
svc.SaveChanges();

这种方法与正常删除方法的关键区别在于,正常删除操作会指定 ETag,而这种方法使用 If-Match: * 头,表示对对象的任何版本执行操作。选择哪种方法需要根据具体需求进行权衡。如果要删除实体的特定版本且不关心中间更新,可以选择前者;如果要彻底删除实体,无论自上次检索后发生了什么变化,都可以选择后者。

数据库分区键选择与表操作详解

操作总结与对比

为了更清晰地对比不同操作的特点和步骤,我们将前面提到的各种操作进行总结,形成以下表格:
| 操作类型 | 操作步骤 | 注意事项 |
| — | — | — |
| 选择分区键 | 1. 确定关键查询并排序;2. 根据查询创建表模式,考虑数据量调整分区;3. 选择唯一行键 | 分区方案不佳时可动态调整 |
| 测试理论 | 1. 创建测试实体和上下文类;2. 插入数据到不同分区;3. 执行不同类型查询 | 配置文件需添加特定设置 |
| 理解分页 | 1. 遇到子集结果获取延续标记;2. 手动处理时检索和插入标记 | 生产代码需检查标记完整性 |
| 更新实体 | 1. 创建 DataServiceContext 对象并设置合并选项;2. 查询实体;3. 更新对象;4. 调用更新方法;5. 保存更改 | 注意 ETag 匹配机制 |
| 删除表 | 调用 CloudTableClient.DeleteTable 方法或发送 HTTP DELETE 请求 | 删除大表需时间,勿立即重建 |
| 删除实体 | 1. 查询并删除;2. 创建虚拟实体附加并删除 | 选择合适删除方式取决于需求 |

操作流程梳理

下面通过 mermaid 流程图来梳理整个数据库表操作的主要流程:

graph LR;
    A[选择分区键] --> B[测试理论];
    B --> C[理解分页];
    C --> D[更新实体];
    D --> E[删除表或实体];
代码示例总结

为了方便大家查看和使用,我们将前面提到的主要代码示例再次整理如下:

测试实体和上下文类

public class TestEntity : TableServiceEntity
{
    public TestEntity(string id, string data)
        : base(id, data)
    {
        ID = id;
        Data = data;
    }
    //Parameter-less constructor always needed for
    // ADO.NET Data Services
    public TestEntity() { }
    public string ID { get; set; }
    public string Data { get; set; }
}

public class TestDataServiceContext : TableServiceContext
{
    public TestDataServiceContext (string baseAddress,
    StorageCredentials credentials): base(baseAddress, credentials)
    {}
    internal const string TestTableName = "TestTable";
    public IQueryable<TestEntity> TestTable
    {
        get
        {
            return this.CreateQuery<TestEntity>(TestTableName);
        }
    }
}

插入数据

// 插入到同一个分区
CloudStorageAccount.Parse(ConfigurationSettings.AppSettings
["DataConnectionString"]);
var svc = new TestDataServiceContext(account.TableEndpoint.ToString(),
                                      account.Credentials);
for (int i = 1; i < 100000; i++)
{
    svc.AddObject("TestTable",
     new TestEntity("1", "RowKey_" + i.ToString() );
}

// 插入到 1000 个不同分区
CloudStorageAccount.Parse(ConfigurationSettings.AppSettings
["DataConnectionString"]);
var svc = new TestDataServiceContext(account.TableEndpoint.ToString(),
 account.Credentials);
for (int i = 1; i < 100000; i++)
{
    svc.AddObject("TestTable2",
    new TestEntity2((i % 1000).ToString(), "RowKey_" + i.ToString()));
}

执行查询

// Single partition query
var query = from entity in svc.CreateQuery<TestEntity>("TestTable")
where entity.PartitionKey == "1" && entity.RowKey == "RowKey_55000"
select entity;
//Multiple partition query - no partition key specified
var query2 = from entity in svc2.CreateQuery<TestEntity2>("TestTable2")
                         where entity.RowKey == "RowKey_55553"
                         select entity;
//Multiple partition query - partition key specified in query
var query3 = from entity in svc2.CreateQuery<TestEntity2>("TestTable2")
where entity.PartitionKey == "553" && entity.RowKey == "RowKey_55553"
select entity;

分页操作

// 检索延续标记
var dsQuery = (DataServiceQuery<Contact>)query;
var res = dsQuery.Execute();
var qor = (QueryOperationResponse)res;
string partitionToken = null;
string rowToken = null;
qor.Headers.TryGetValue("x-ms-continuation-NextPartitionKey",
                  out partitionToken);
qor.Headers.TryGetValue("x-ms-continuation-NextRowKey", out rowToken);

// 插入延续标记
var dsQuery = (DataServiceQuery<Contact>)query;
query = query
  .AddQueryOption("NextPartitionKey", partitionToken)
  .AddQueryOption("NextRowKey", rowToken);

更新实体

CloudStorageAccount.Parse(ConfigurationSettings.AppSettings
["DataConnectionString"]);
var svc = new TestDataServiceContext
 (account.TableEndpoint.ToString(), account.Credentials);
svc.MergeOption = MergeOption.PreserveChanges;
var query = from contact in svc.CreateQuery<Contact>("ContactTable")
            where contact.Name == "Steve Jobs"
            select contact;
var foundContact = query.FirstOrDefault<Contact>();
foundContact.Address = "One Infinite Loop, Cupertino, CA 95014";
svc.UpdateObject(foundContact);
svc.SaveChanges();

删除表

account.CreateCloudTableClient().DeleteTable("ContactsTable");

删除实体

// 查询并删除
CloudStorageAccount.Parse(ConfigurationSettings.AppSettings
                ["DataConnectionString"]);
var svc = new TestDataServiceContext(account.TableEndpoint.ToString(),
                                    account.Credentials);
var item = (from contact in svc.CreateQuery<Contact>("ContactTable")
            where contact.Name == "Steve Jobs"
            select contact).Single();
svc.DeleteObject(item);
svc.SaveChanges();

// 创建虚拟实体附加并删除
var svc =
   new TestDataServiceContext(account.TableEndpoint.ToString(),
account.Credentials);
var item = new Contact("CorrectPartitionKey", "CorrectRowKey");
svc.AttachTo("ContactTable", item, "*");
svc.DeleteObject(item);
svc.SaveChanges();
实际应用建议

在实际应用中,我们可以根据不同的业务场景选择合适的操作方法。例如:
- 数据查询频繁且数据量较大 :选择合适的分区键和行键,确保查询时能指定分区键,提高查询性能。
- 需要分页展示数据 :掌握分页操作的方法,根据实际情况选择自动或手动处理延续标记。
- 数据更新操作较多 :理解不同的合并选项,根据业务需求选择合适的更新方式,确保数据的一致性和准确性。
- 需要删除大量数据 :考虑使用创建虚拟实体附加并删除的方法,提高删除效率。

通过以上对数据库分区键选择、表操作的详细介绍,包括具体的操作步骤、代码示例、性能测试和对比等内容,希望能帮助大家更好地理解和应用相关技术,在实际开发中做出更合适的决策。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值