【问题标题】:How to speed up a SQL Server query involving count(distinct())如何加快涉及 count(distinct()) 的 SQL Server 查询
【发布时间】:2011-01-04 08:52:33
【问题描述】:

我有一个看似简单的 SQL Server 查询,它花费的时间比我预期的要长。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT COUNT(DISTINCT(guid)) FROM listens WHERE url='http://www.sample.com/'

'guid' 是 varchar(64) NULL

'url' 是 varchar(900) NULL

在 guid 和 url 上有一个索引。

'listens'表中有超过700万行,其中17000行匹配有问题的url,查询结果为5500。

在具有 1GB RAM 的相当空闲的双核 AMD Opteron 2GHz 上在 SQL Server 2008 上运行此查询需要 1 多分钟。

任何想法如何降低执行时间?理想情况下应该在 1 秒以内!

【问题讨论】:

  • '在 guid 和 url 上有一个索引。'那是两个单独的索引还是一个组合索引?

标签: sql sql-server tsql sql-server-2008 query-optimization


【解决方案1】:

我知道这篇文章有点晚了。我正在寻找另一个优化问题。

注意:

  1. guid 是 VARCHAR(64) **并不是真正的 16 字节唯一标识符
  2. url 是 varchar(900),你有 700 万行。

我的建议:

  1. 为表创建一个新字段。 Column = URLHash AS UNIQUEIDENTIFIER 关于创造新纪录。 URLHash = CONVERT( UNIQUEIDENTIFIER, HASHBYTES('MD5', url) )
  2. 在 URLHash 上建立索引

然后在您的查询中: SELECT COUNT(DISTINCT(guid)) FROM listens WHERE URLHash = CONVERT( UNIQUEIDENTIFIER, HASHBYTES( 'MD5', 'http://www.sample.com/' ) )

这将为您提供一种非常快速的方法来唯一地寻找特定的 url,同时保持非常小的索引大小。

如果您需要进一步优化,您可能希望在 guid 上执行相同的哈希。对 16 字节的 uniqueidentifier 执行 distinct 比 varchar(64) 更快。


上面的假设是你没有在listen表中添加很多新行;即,新的记录率并没有那么重。原因是MD5算法,虽然提供了完美的色散;速度慢是出了名的。如果您以每秒数千条的速度添加新记录;然后在创建记录时计算 MD5 哈希会降低您的服务器速度(除非您有一个非常快的服务器)。另一种方法是实现您自己的非内置 FNV1a 哈希算法版本。与 MD5 相比,FNV1a 速度要快得多,但提供了非常好的分散/低碰撞率。

希望以上内容对以后遇到此类问题的人有所帮助。

【讨论】:

    【解决方案2】:

    一些提示...

    1) 重构您的查询,例如使用with 子句...

    与 url_entries 为 ( 选择向导 从听 其中 url='http://www.sample.com/' ) 选择 count(distinct(enries.guid)) 作为 distinct_guid_count 来自 url_entries 条目

    2) 准确告诉 SQL Server 在执行查询时必须扫描哪个索引(当然,按url 字段索引)。另一种方法 - 通过guid 简单地删除索引并单独通过url 保留索引。有关提示的更多信息,请查看 here。尤其是像select ... from listens with (index(index_name_for_url_field) )这样的结构

    3) 验证listens 表上的索引状态并更新index statistics

    【讨论】:

      【解决方案3】:

      无论如何,扫描这么大的索引都需要很长时间。
      您需要做的是缩短索引。
      您可以做的是有一个整数列,其中计算和存储 url 的校验和。 这样你的索引会变窄,计数会很快。

      请注意,校验和不是唯一的,但它足够唯一。 这是有关如何执行此操作的完整代码示例。我已经包含了两列的校验和,但它可能只需要一个。您还可以自己计算插入或更新的校验和并删除触发器。

      CREATE TABLE MyTable
      (
          ID INT IDENTITY(1,1) PRIMARY KEY,
          [Guid] varchar(64),
          Url varchar(900),
          GuidChecksum int,
          UrlChecksum int
      )
      GO
      
      CREATE TRIGGER trgMyTableCheckSumCalculation ON MyTable
      FOR UPDATE, INSERT
      as
      UPDATE t1
      SET    GuidChecksum = checksum(I.[Guid]),
             UrlChecksum = checksum(I.Url)
      FROM   MyTable t1 
             join inserted I on t1.ID = I.ID
      
      GO
      CREATE NONCLUSTERED INDEX NCI_MyTable_GuidChecksum ON MyTable(GuidChecksum)
      CREATE NONCLUSTERED INDEX NCI_MyTable_UrlChecksum ON MyTable(UrlChecksum)
      
      INSERT INTO MyTable([Guid], Url)
      select NEWID(), 'my url 1' union all
      select NEWID(), 'my url 2' union all
      select null, 'my url 3' union all
      select null, 'my url 4'
      
      SELECT  *
      FROM    MyTable
      
      SELECT  COUNT(GuidChecksum)
      FROM    MyTable
      WHERE   Url = 'my url 3'
      GO
      
      DROP TABLE MyTable
      

      【讨论】:

      • +1 如果您要添加一个示例,说明在这种情况下选择的外观。 (其中 url_crc = crc('url') 和 url = 'url') 或类似的东西。
      • 散列(这里称为“校验和”)不是答案,因为它不是唯一的,url 字段的真实值必须针对给定值进行测试。因此 SQL Server 必须读取字段的真实值。
      • -1 至少select count() ... 查询是错误的:1) 必须计算真正不同的 guid,而不是非唯一校验和 2) 必须在 WHERE 子句中添加 UrlChecksum,服务器没有任何理由通过 UrlChecksum 使用索引
      • 您确实意识到这是他应该使用的概念示例而不是实际解决方案?
      • 是的,我意识到这一点。但是查询的形式对于提出的问题至关重要。
      【解决方案4】:

      您最好的计划是一个范围寻求获得 17k 候选 URL 和不同的计数,以依赖于有保证的输入顺序,因此它不必排序。可以同时满足这两个要求的正确数据结构是(url, guid) 上的索引:

      CREATE INDEX idxListensURLGuid on listens(url, guid);
      

      您已经收到了大量关于所用密钥范围的反馈,您可以明确地寻求改进它们,如果可以的话,还可以增加那微不足道的 1Gb RAM。

      如果可以在 SQL 2008 EE 上部署,请确保为这种高度重复和广泛的索引打开 page compression。由于减少了 IO,它将在性能上创造奇迹。

      【讨论】:

        【解决方案5】:

        我敢打赌,如果您的机器中有超过 1GB 的内存,它的性能会更好(我遇到的所有 DBA 都希望在生产 SQL 服务器中至少有 4GB。)

        我不知道这是否重要,但如果你这样做

          SELECT DISTINCT(guid) FROM listens WHERE url='http://www.sample.com/'
        

        @rowcount 不会包含你想要的结果吗?

        【讨论】:

          【解决方案6】:

          在 url 上创建一个索引,覆盖 GUID:

          CREATE INDEX ix_listens_url__guid ON listens (url) INCLUDE (guid)
          

          在处理 url 作为标识符时,最好存储和索引 URL 哈希而不是整个 URL

          【讨论】:

          • 请注意,创建如此广泛的索引并不是一个好主意。它们只是占用空间,仅在少数情况下有用。我同意 url 哈希,尽管我更喜欢校验和(它更窄,inex 更快),正如我在回答中所说的那样。
          • 按 URL 列索引就够了,还有一个需要:如何告诉超级智能的 MS SQL Server 如何构建正确的查询计划 :)
          • 查询计划是您最不必担心的。问题出在 IO 上。巨大的索引会导致巨大的 IO。
          • 不能同意。正确的查询计划是第一位的。如果查询计划错误,您总是会因为不使用索引的全表扫描、全索引扫描、通过非必需表连接等而获得“巨大的 IO”。
          • 索引必须提供良好的数据粒度。从 700 万条记录中快速定位 17000 条记录就足够了。散列、缓存和其他搜索算法优化是 SQL Server 工作的一部分。
          【解决方案7】:

          您的GUID 列本质上会比bigint 占用更多空间(16 bytes) 更耗费人力。您是否可以将 GUID 列替换为自动递增的数字列,或者如果失败,则引入类型为 bigint/int 的新列,该列针对 GUID 列的每个新值递增(然后您可以使用GUID 确保全局唯一性,使用bigint/int 进行索引)?

          从上面的链接:

          在 16 字节处,唯一标识符数据 类型比较大 其他数据类型,例如 4 字节 整数。这意味着建立了索引 使用唯一标识符键可能是 比实施相对慢 使用 int 键的索引。

          是否有任何特殊原因使您在 guid 列中使用 varchar 而不是 uniqueidentifier

          【讨论】:

          • guid 是从外部来源提供的值。目前它确实看起来像一个唯一标识符,但这不能保证,所以它需要是一个字符串。我可能会创建另一个将 int 映射到 guid 的表,但这会使插入到“侦听”中的成本更高,并且我需要保持快速插入。
          猜你喜欢
          • 2016-07-26
          • 1970-01-01
          • 1970-01-01
          • 2020-04-05
          • 1970-01-01
          • 2023-02-21
          • 1970-01-01
          • 2016-01-14
          • 1970-01-01
          相关资源
          最近更新 更多