【问题标题】:Millions of rows table optimization SQL Server百万行表优化 SQL Server
【发布时间】:2020-10-07 07:13:01
【问题描述】:

我有一张包含数百万行的表格:

CREATE TABLE [dbo].[RequestIdentities]
(
    [Id] [bigint] IDENTITY(1,1) NOT NULL,
    [UniqueKey] [nvarchar](256) NULL,
    [Timestamp] [datetime] NULL,

    CONSTRAINT [PK_RequestIdentities] 
        PRIMARY KEY CLUSTERED ([Id] ASC)
                    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
                          IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
                          ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[RequestIdentities] 
    ADD CONSTRAINT [DF_RequestIdentities_Timestamp]  
        DEFAULT (GETDATE()) FOR [Timestamp]
GO 

Web API 使用 ADO.NET 操作数据库并执行以下操作:

  1. 执行这个查询:

     SELECT 1 
     FROM RequestIdentities WITH (nolock) 
     WHERE UniqueKey = @key
    
  2. 如果存在:

     if(reader.HasRows)
    
  3. 返回一个 http 响应。

  4. 否则,它会将 id 插入到表中:

     INSERT INTO RequestIdentities(UniqueKey) 
     VALUES(@key)
    

每分钟有数百个插入/更新,我可以做些什么来优化表,例如自定义统计信息/索引?

【问题讨论】:

  • 那是小数据和低流量。 all 表需要索引。 TOP 1 作为唯一键保证单个结果是没有意义的。充其量是无操作,最坏的情况是它会强制执行额外操作。 WITH (NOLOCK) 实际上是一个可怕的想法,导致结果脏或重复以及 more 锁。您没有提供有关此表的任何信息,除了它有一个 ID,因此无法说出可能需要优化的内容(如果有的话)
  • 您遇到过实际问题吗?顺便说一句,如果您只想检索单个值,请使用 ExecuteScalar 而不是 ExecuteReader。如果您想避免问题,请避免使用长期连接和事务。连接和事务累积锁,导致阻塞和延迟。这就是为什么创建和使用连接的常用方法是在 using 块内。
  • 顺便说一句为什么SELECT 1?如果您稍后需要读取该行的任何数据,则必须执行另一个查询。这是延迟的两倍,如果您使用事务,则使用更长时间的锁。如果要插入不存在的内容,可以使用INSERT ... WHERE NOT EXISTS()MERGE 在单个查询中执行操作。
  • BTW UniqueKey 需要唯一索引或约束,否则不能唯一
  • 如果您的流量非常高且内存足够,您可以使用内存表来减少延迟和锁定。在这种情况下,将表视为持久的 Redis 缓存,只会更好。

标签: sql sql-server ado.net database-optimization


【解决方案1】:

对于使用 SSD 的计算机上的现代数据库而言,100 万行数据并不多。几百个插入也不是很多。虽然您可以使用内存表等进行优化,但您必须首先消除现有问题。

在某些情况下,内存表也可以简化维护。

问题

此代码包含几个损害性能的问题。

  • WITH (NOLOCK) 是一个非常糟糕的主意,根本不会提高性能。它实际上需要*更广泛的锁(模式级),读取脏的、未提交的数据,可以两次返回相同的数据,甚至可以抛出错误。
  • 代码执行两次远程调用,导致延迟两倍,以插入单行。除了延迟之外,这意味着在SELECT 操作期间获取的锁需要保持比需要更长的时间,这可能会阻塞其他尝试使用同一个表的连接。
  • TOP 1 充其量是空操作。如果UniqueKey 真的是唯一的,那么只会返回一个结果。

修复

您可以通过删除提示和存在检查来改善这一点。 INSERT 查询可以包含 FROMWHERE 子句,这意味着您可以编写单个查询来仅插入新行。您可以使用 OUTPUT 子句返回新行的 ID。

首先,您需要在UniqueKey 列上添加唯一索引或约束。没有其中之一,这根本不是唯一的。任何人都可以插入重复值。一个 UNIQUE 约束实际上创建了一个 UNIQUE 索引:

CREATE UNIQUE INDEX IX_ RequestIdentities_UniqueKey   
   ON RequestIdentities (UniqueKey);

之后,您可以有条件地插入和检索新 ID:

INSERT INTO RequestIdentities (UniqueKey)
    OUTPUT inserted.ID
SELECT @key
FROM RequestIdentities
WHERE NOT EXISTS ( select * 
                   from RequestIdentities
                   where UniqueKey = @key)

查询优化器知道它不需要为EXISTS ( SELECT * 生成任何结果,因此不会影响性能。

此查询将插入新行并返回新 ID。这个操作是原子的(它要么成功要么回滚),所以不需要显式事务。

您可以使用ExecuteScalar() 通过SqlCommand 执行此查询。这将返回新 ID,如果没有结果,则返回 null,因为没有插入行:

using(var connection=new SqlConnection(connString))
using(var cmd=new SqlCommand(query,connection))
{
    cmd.Parameters.Add("@key",SqlDbType.NVarChar,256).Value=key;
    connection.Open();
    var result = cmd.ExecuteScalar();
    if (result!=null)
    {
        var newID=(long)result;
        //Use the ID
        ...
    }
}

您可以使用 C# 8 的模式匹配语法:

if(result is long newId)
{
   //Use the ID
}

如果这段代码太多,你可以使用像Dapper这样的微ORM:

using(var connection=new SqlConnection(connString))
{
    var result=connection.ExecuteScalar(query,new {key=keyValue});
    if (result is long new ID)
    {
        ...
    }
}

Dapper 被 StackOverflow 使用,所以它的性能是有保证的。

其他优化

如果你发现这个表的锁太多,一个可能的优化是使用memory optimized tables。数据库服务器已经在积极缓冲数据。

内存优化表的真正好处是不同的日志记录、锁定和访问模型。使用轻量级内存中的闩锁对象代替锁。由于数据已经在内存中,服务器可以使用不同的运算符和不同类型的索引来检索和修改对象。

This documentation example 为两个高流量表使用内存表:

  • Cart 是一个持久的内存表,其数据被持久化到磁盘。如果服务器出现故障,则保留购物车。
  • 用户会话是一个非持久内存表。如果服务器出现故障,我们将不关心会话

在这种情况下,表格可能是:

CREATE TABLE [dbo].[RequestIdentities]
(
    [Id] [bigint] IDENTITY(1,1) NOT NULL 
        PRIMARY KEY NONCLUSTERED,
    [UniqueKey] [nvarchar](256) NULL,
    [Timestamp] [datetime] NULL
)  
WITH (  
    MEMORY_OPTIMIZED = ON,  
    //Assuming we want to retain the data
    DURABILITY = SCHEMA_AND_DATA);  
go  

ALTER TABLE RequestIdentities  
    ADD CONSTRAINT RequestIdentities_UniqueKey  
    UNIQUE NONCLUSTERED (UniqueKey);  
go  

【讨论】:

  • 让我们再添加一个优化。也许“纠正”是一个更好的术语。实际数据列不应为空。目前所有这些都是可以为空的,这是逻辑上的废话。
  • @SM或者问题中缺少很多信息。
  • @PanagiotisKanavos 。 . .老实说,20 年前,在 SSD 和速度较慢的机器上,每分钟几百万行和几百次插入并不是什么大不了的事。
  • @GordonLinoff 考虑到有多少关于大数据和 50K 行表的 SO 问题,这需要每次都重复。
【解决方案2】:

您可以添加一列,其中包含您正在搜索的字段的hash 值。

首先,向表中添加新列:

ALTER TABLE [...]
ADD [UniqueKeyHash] VARBINARY(64);

然后,在上面添加索引:

CREATE INDEX IX_..._UniqueKeyHash ON [...]
(
    [UniqueKeyHash] 
);

填充值:

UPDATE [...]
SET [UniqueKeyHash] =  HASHBYTES('SHA2_512', UniqueKey);

修改 CRUD 例程也计算 HASH

然后,在搜索中:

DECLARE @UniqueKeyHash VARBINARY(64);

SET @UniqueKeyHash = HASHBYTES('SHA2_512', 'some value');

SELECT *
FROM [...]
WHERE [UniqueKeyHas] = @UniqueKeyHash;

或者,您可以将列添加为computed and persisted,以跳过修改CRUD 例程。

我在各个地方都使用这种搜索 - 其中一个是在 IP 地址表中,从该表中搜索每个用户登录并包含数百万条记录。

如果这对您来说太难了,您可以先在 UniqueKey 上创建索引。

【讨论】:

  • 为什么要这样做?问题是两个远程查询,缺少索引和NOLOCK。如果没有 UNIQUE 索引,UniqueKey 就不能是唯一的,因此无论如何都需要索引
  • 您是否考虑过内存表?
  • @PanagiotisKanavos 不,正如我所说,即使找到一个具有唯一索引的字符串也可能很慢。在我的情况下,查询花费了 1-2 秒,这在用户登录系统时太长了。添加hash 列和索引可以解决问题。
  • 索引哈希列与索引字符串列没有什么不同,除了 IO 开销。通过压缩可以更有效地处理。这会将稀缺的 IO 资源换成更丰富的 CPU 资源。在登录场景中,计算密码哈希所需的时间将超过 1"。
  • 当然有区别 :-) 在我的例子中是 2 秒。另外,请注意我说的是短字符串的 IP 地址......成像更大的东西。我看不出在这种情况下压缩是如何工作的——它是一个较慢的变体,而且对于这样的任务来说是多余的。
【解决方案3】:
  1. FILLFACTOR = 80 在非常大的表上,尤其是在 CLUSTERED IDENTITY INDEX 上是无稽之谈。你失去了 20% 的空间!
  2. 如果 UniqueKey 列是 UNIQUE,则添加一个 UNIQUE 约束。
  3. [Timestamp] 为保留字,请勿用作列名。

完成这项工作后,您可以使用以下查询:

INSERT INTO dbo.RequestIdentities(UniqueKey)
OUTPUT inserted.* INTO ...
SELECT @key
EXCEPT
SELECT UniqueKey
FROM   dbo.RequestIdentities
WHERE  UniqueKey = @Key;

Aand 将 OUTPUT 结果子句返回到您的客户端应用程序的表中

【讨论】:

    猜你喜欢
    • 2017-06-30
    • 1970-01-01
    • 1970-01-01
    • 2012-12-14
    • 1970-01-01
    • 2013-01-12
    • 2023-01-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多