【问题标题】:Efficient way of getting group ID without sorting无需排序即可获取组 ID 的有效方法
【发布时间】:2014-08-10 13:02:28
【问题描述】:

想象一下我有一个像这样的非规范化表:

CREATE TABLE Persons
(
    Id           int identity primary key,
    FirstName    nvarchar(100),
    CountryName  nvarchar(100)
)

INSERT INTO Persons
VALUES ('Mark',    'Germany'),
       ('Chris',   'France'),
       ('Grace',   'Italy'),
       ('Antonio', 'Italy'),
       ('Francis', 'France'),
       ('Amanda',  'Italy');

我需要构建一个查询来返回每个人的姓名,以及他们所在国家/地区的唯一 ID。 ID 不一定必须是连续的;更重要的是,它们不必按任何顺序排列。实现这一目标的最有效方法是什么?

最简单的解决方案似乎是DENSE_RANK

SELECT FirstName, 
       CountryName, 
       DENSE_RANK() OVER (ORDER BY CountryName) AS CountryId
FROM Persons

-- FirstName  CountryName  CountryId
-- Chris      France       1
-- Francis    France       1
-- Mark       Germany      2
-- Amanda     Italy        3
-- Grace      Italy        3
-- Antonio    Italy        3

但是,这会在我的 CountryName 列上产生排序,这会浪费性能。我想出了这个替代方案,它使用ROW_NUMBER 和众所周知的技巧来抑制其排序:

SELECT P.FirstName, 
       P.CountryName,
       C.CountryId
FROM Persons P
    JOIN (
        SELECT CountryName, 
               ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS CountryId
        FROM Persons
        GROUP BY CountryName
    ) C
    ON C.CountryName = P.CountryName

-- FirstName  CountryName  CountryId
-- Mark       Germany      2
-- Chris      France       1
-- Grace      Italy        3
-- Antonio    Italy        3
-- Francis    France       1
-- Amanda     Italy        3

我是否正确假设第二个查询通常会表现更好(不仅仅是在我设计的数据集上)?是否存在任何可能产生影响的因素(例如CountryName 上的索引)?有没有更优雅的表达方式?

【问题讨论】:

  • 您的第一个查询似乎比您的第二个查询更有效率,对我来说:i.imgur.com/IBalLbl.png
  • @Blorgbeard:谢谢,好点子。

标签: sql sql-server tsql row-number dense-rank


【解决方案1】:

您为什么认为聚合比窗口函数便宜?我问,因为我对两者都有一些经验,并且对此事没有强烈的意见。如果按下,我猜窗口函数会更快,因为它不必聚合所有数据然后将结果重新加入。

这两个查询将有非常不同的执行路径。查看哪个性能更好的正确方法是尝试一下。对您环境中足够大的数据样本运行这两个查询。

顺便说一句,我认为没有正确答案,因为性能取决于几个因素:

  • 索引了哪些列?
  • 数据有多大?它适合内存吗?
  • 有多少个不同的国家?

如果您关心性能,并且只想要一个唯一的编号,您可以考虑改用checksum()。这确实存在碰撞的风险。对于 200 个左右的国家来说,这种风险非常非常小。另外,您可以对其进行测试并在它确实发生时对其进行处理。查询将是:

SELECT FirstName, CountryName, CheckSum(CountryName) AS CountryId
FROM Persons;

【讨论】:

  • 谢谢!我假设聚合会更快,因为它可以使用哈希表执行(花费 O(n) 时间),而窗口函数需要完整排序(花费 O(n log n) 时间)。但我同意这个问题取决于实际数据。
  • 根据国名大小,也可以试试convert(int,convert(varbinary,CountryName))
【解决方案2】:

您的第二个查询很可能会避免排序,因为它会使用哈希匹配聚合来构建内部查询,然后使用哈希匹配连接将 ID 映射到实际记录。

这确实不排序,但必须扫描原始表两次。

我是否正确假设第二个查询总体上会表现更好(不仅仅是在我设计的数据集上)?

不一定。如果您在 CountryName 上创建了一个聚集索引,那么排序将不是问题,所有事情都将一次性完成。

有没有更优雅的表达方式?

“正确”的计划是一次性进行散列和散列查找。

读取的每条记录都必须与哈希表匹配。在匹配时,将返回存储的 ID;如果未命中,则将新国家/地区添加到哈希表中,分配新 ID,并返回新分配的 ID。

但我想不出办法让 SQL Server 在单个查询中使用这样的计划。

更新:

如果您有很多记录、几个国家/地区,最重要的是,CountryName 上有一个非聚集索引,您可以模拟松散扫描来构建国家/地区列表:

DECLARE  @country TABLE
         (
         id INT NOT NULL IDENTITY PRIMARY KEY,
         countryName VARCHAR(MAX)
         )
;

WITH    country AS
        (
        SELECT  TOP 1
                countryName
        FROM    persons
        ORDER BY
                countryName
        UNION ALL
        SELECT  (
                SELECT  countryName
                FROM    (
                        SELECT  countryName,
                                ROW_NUMBER() OVER (ORDER BY countryName) rn
                        FROM    persons
                        WHERE   countryName > country.countryName
                        ) q
                WHERE   rn = 1
                )
        FROM    country
        WHERE   countryName IS NOT NULL
        )
INSERT
INTO    @country (countryName)
SELECT  countryName
FROM    country
WHERE   countryName IS NOT NULL
OPTION  (MAXRECURSION 0)

SELECT  p.firstName, c.id
FROM    persons p
JOIN    @country c
ON      c.countryName = p.countryName

【讨论】:

  • 是的,我认为您回答的最后一部分是我的目标(尽管我对 SQL 术语的了解不够,无法表达它)。我希望查询优化器足够聪明,可以为我的第二个查询提供它,但 Blorgbeard 发布的计划证实它没有。
  • 感谢您提出的解决方案!明天我去试一下。
  • 我想我明白了你的 CTE 的要点。但是,我不明白为什么它比使用distinct 填充@country 更有利:INSERT INTO @country SELECT DISTINCT CountryName FROM Persons。会吗?
  • @Douglas: SELECT countryname FROM persons 必须阅读整个表格。 CTE 必须进行与国家一样多的索引搜索(加上一次最终搜索)。如果您有一张包含几个国家/地区的巨大表格(您很可能拥有),那么 CTE 会更有效率(当然,前提是您在 country 上有一个索引)。
  • 如果表在CountryName 上有索引,那么SELECT DISTINCT CountryName FROM Persons 只会引发索引扫描。您是说 CTE 的索引查找序列会比单个索引扫描更有效吗?
【解决方案3】:

group by use 也在后台排序操作符(组基于'排序和比较',如 C# 中的 Icomparable)

【讨论】:

  • 不一定。 GROUP BY 可以通过散列而不是排序来执行。
猜你喜欢
  • 1970-01-01
  • 2014-05-18
  • 2020-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-17
  • 2013-06-14
  • 2023-03-04
相关资源
最近更新 更多