【问题标题】:Efficient SQL 2000 Query for Selecting Preferred Candy用于选择首选糖果的高效 SQL 2000 查询
【发布时间】:2009-06-28 17:04:06
【问题描述】:

(我希望我能想出一个更具描述性的标题...建议一个或编辑这篇文章,如果你能说出我所询问的查询类型)

数据库:SQL Server 2000

样本数据(假设 500,000 行):

命名 Candy PreferenceFactor 吉姆巧克力 1.0 布拉德柠檬滴 .9 布拉德巧克力 .1 克里斯巧克力 .5 克里斯糖果手杖 .5 还有 499,995 行...

请注意,具有给定“名称”的行数是无限的。

所需的查询结果:

吉姆巧克力 1.0 布拉德柠檬滴 .9 克里斯巧克力 .5 ~250,000 多行...

(由于 Chris 对 Candy Cane 和 Chocolate 的偏好相同,因此一致的结果就足够了。

问题: 如何从数据中选择名称、糖果,其中每个结果行都包含一个唯一的名称,这样选择的糖果对每个名称都有最高的 PreferenceFactor。 (首选快速有效的答案)。

表上需要哪些索引?如果 Name 和 Candy 是另一个表的整数索引(除了需要一些连接),这有什么不同吗?

【问题讨论】:

    标签: sql database sql-server-2000


    【解决方案1】:

    您会发现以下查询优于所有其他给出的答案,因为它适用于单次扫描。这模拟了 MS Access 的 First 和 Last 聚合函数,这基本上就是你正在做的事情。

    当然,您的 CandyPreference 表中可能会有外键而不是名称。要回答您的问题,实际上最好是 Candy 和 Name 是另一个表的外键。

    如果 CandyPreferences 表中还有其他列,那么拥有包含相关列的覆盖索引将产生更好的性能。使列尽可能小将增加每页的行数并再次提高性能。如果您最常使用 WHERE 条件进行查询以限制行,那么覆盖 WHERE 条件的索引就变得很重要。

    Peter 在这方面走在正确的轨道上,但有一些不必要的复杂性。

    CREATE TABLE #CandyPreference (
       [Name] varchar(20),
       Candy varchar(30),
       PreferenceFactor decimal(11, 10)
    )
    INSERT #CandyPreference VALUES ('Jim', 'Chocolate', 1.0)
    INSERT #CandyPreference VALUES ('Brad', 'Lemon Drop', .9)
    INSERT #CandyPreference VALUES ('Brad', 'Chocolate', .1)
    INSERT #CandyPreference VALUES ('Chris', 'Chocolate', .5)
    INSERT #CandyPreference VALUES ('Chris', 'Candy Cane', .5)
    
    SELECT
       [Name],
       Candy = Substring(PackedData, 13, 30),
       PreferenceFactor = Convert(decimal(11,10), Left(PackedData, 12))
    FROM (
       SELECT
          [Name],
          PackedData = Max(Convert(char(12), PreferenceFactor) + Candy)
       FROM CandyPreference
       GROUP BY [Name]
    ) X
    
    DROP TABLE #CandyPreference
    

    我实际上不推荐这种方法,除非性能很关键。执行此操作的“规范”方法是 OrbMan 的标准 Max/GROUP BY 派生表,然后加入它以获取所选行。但是,当有多个列参与 Max 的选择时,该方法开始变得困难,并且选择器的最终组合可以重复,也就是说,当没有列提供任意唯一性时,如这里的情况如果 PreferenceFactor 相同,我们使用名称。

    编辑:最好提供更多使用说明,以帮助提高清晰度并帮助人们避免问题。

    • 作为一般经验法则,在尝试提高查询性能时,如果可以节省 I/O,则可以做很多额外的数学运算。保存整个表查找或扫描可显着加快查询速度,即使包含所有转换和子字符串等等。
    • 由于精度和排序问题,在这种方法中使用浮点数据类型可能不是一个好主意。尽管除非您处理的是极大或极小的数字,否则无论如何都不应该在数据库中使用浮点数。
    • 最好的数据类型是那些在转换为二进制或字符后没有打包和排序的数据类型。 Datetime、smalldatetime、bigint、int、smallint 和 tinyint 都直接转换为二进制并正确排序,因为它们没有打包。使用二进制,避免 left() 和 right(),使用 substring() 将值可靠地返回到其原始值。
    • 我利用了此查询中小数点前只有一位数字的 Preference,允许直接转换为 char,因为小数点前总是至少有一个 0。如果可能有更多数字,则必须对转换后的数字进行十进制对齐,以便正确排序。最简单的方法可能是将您的偏好等级相乘,这样就没有小数部分,转换为 bigint,然后转换为 binary(8)。一般来说,数字之间的转换比 char 和另一种数据类型之间的转换要快,尤其是日期数学。
    • 注意空值。如果有,您必须将它们转换成某种东西,然后再返回。

    【讨论】:

    • 不错。单次扫描给我留下了深刻的印象。您能否建议对我的方法进行改进,仍然支持任意 ORDER BY?
    • 谢谢,彼得!一个复杂的 CASE 表达式确定将哪些列放入压缩值,加上字符位置和长度以将列拉回,就可以解决问题(更多的 CPU 仍然比更多的 I/O 便宜)。如果您真的对此感兴趣,请再次发表评论,我会尝试为您解决问题。或者,您可以通过访问我的个人资料给我发送消息吗?无论哪种方式。
    • 现在我明白你之前的意思了,相关子查询是不必要的。起初我认为这是对我如何打包和拆包价值观的批评。是的,我很想看看你将如何打包更多的值。可以说是一般形式。不,我不知道如何在 SO 上发送私人消息。我在个人资料页面上没有看到该选项,也没有看到任何联系信息。
    • 我在 cmets 的个人资料中添加了一个加密的电子邮件地址。
    • 谢谢 - 在我的生产数据库中,Name 和 Candy 都是外键,它们包含在索引中。在实现了这个解决方案和 OrbMan 的解决方案之后,我得到了基本相同的性能。我希望我可以在你们两个之间拆分“已接受”的答案,但 OrbMan 似乎解决了这个问题,而无需额外的包装复杂性。
    【解决方案2】:
    select c.Name, max(c.Candy) as Candy, max(c.PreferenceFactor) as PreferenceFactor
    from Candy c
    inner join (
        select Name, max(PreferenceFactor) as MaxPreferenceFactor
        from Candy
        group by Name
    ) cm on c.Name = cm.Name and c.PreferenceFactor = cm.MaxPreferenceFactor
    group by c.Name
    order by PreferenceFactor desc, Name
    

    【讨论】:

    • 我尝试了这个解决方案和 Emtucifor 的解决方案(似乎在投票中获胜)。在我的表中,我有对 Candy 和 Name 的外键引用,并且有一个涵盖这些列的索引。在我的测试中,Emtucifor 和 OrbMan 的解决方案在查询时间上是相同的,并且给出了相同的结果。由于 OrbMan 的解决方案不涉及打包和解包数据的复杂性,因此我选择了这个答案作为我接受的答案。
    • 如果性能相似,您选择此答案作为正确答案是正确的。这一切都取决于数据。您能否发布您使用的查询或将其发送到 stackoverflow dit com atdomain esquared dit mooo dit com 给我?谢谢。嘀->。 atdomain -> @
    【解决方案3】:

    我试过了:

    SELECT X.PersonName,
        (
            SELECT TOP 1 Candy
            FROM CandyPreferences
            WHERE PersonName=X.PersonName AND PreferenceFactor=x.HighestPreference
        ) AS TopCandy
    FROM 
    (
        SELECT PersonName, MAX(PreferenceFactor) AS HighestPreference
        FROM CandyPreferences
        GROUP BY PersonName
    ) AS X
    

    这似乎可行,但如果没有真实数据和实际负载,我无法谈论效率。

    不过,我确实在 PersonName 和 Candy 上创建了一个主键。使用 SQL Server 2008 并且没有其他索引显示它使用两次聚集索引扫描,因此可能会更糟。


    我玩得更多,因为我需要一个借口来玩“datadude”的数据生成计划功能。首先,我重构了一张表,让糖果名称和人名有单独的表。我这样做主要是因为它允许我使用测试数据生成而无需阅读文档。架构变为:

    CREATE TABLE [Candies](
        [CandyID] [int] IDENTITY(1,1) NOT NULL,
        [Candy] [nvarchar](50) NOT NULL,
     CONSTRAINT [PK_Candies] PRIMARY KEY CLUSTERED 
    (
        [CandyID] ASC
    ),
     CONSTRAINT [UC_Candies] UNIQUE NONCLUSTERED 
    (
        [Candy] ASC
    )
    )
    GO
    
    CREATE TABLE [Persons](
        [PersonID] [int] IDENTITY(1,1) NOT NULL,
        [PersonName] [nvarchar](100) NOT NULL,
     CONSTRAINT [PK_Preferences.Persons] PRIMARY KEY CLUSTERED 
    (
        [PersonID] ASC
    )
    )
    GO
    
    CREATE TABLE [CandyPreferences](
        [PersonID] [int] NOT NULL,
        [CandyID] [int] NOT NULL,
        [PrefernceFactor] [real] NOT NULL,
     CONSTRAINT [PK_CandyPreferences] PRIMARY KEY CLUSTERED 
    (
        [PersonID] ASC,
        [CandyID] ASC
    )
    )
    GO
    
    ALTER TABLE [CandyPreferences]  
    WITH CHECK ADD  CONSTRAINT [FK_CandyPreferences_Candies] FOREIGN KEY([CandyID])
    REFERENCES [Candies] ([CandyID])
    GO
    
    ALTER TABLE [CandyPreferences] 
    CHECK CONSTRAINT [FK_CandyPreferences_Candies]
    GO
    
    ALTER TABLE [CandyPreferences]  
    WITH CHECK ADD  CONSTRAINT [FK_CandyPreferences_Persons] FOREIGN KEY([PersonID])
    REFERENCES [Persons] ([PersonID])
    GO
    
    ALTER TABLE [CandyPreferences] 
    CHECK CONSTRAINT [FK_CandyPreferences_Persons]
    GO
    

    查询变成:

    SELECT P.PersonName, C.Candy
    FROM (
        SELECT X.PersonID,
            (
                SELECT TOP 1 CandyID
                FROM CandyPreferences
                WHERE PersonID=X.PersonID AND PrefernceFactor=x.HighestPreference
            ) AS TopCandy
        FROM 
        (
            SELECT PersonID, MAX(PrefernceFactor) AS HighestPreference
            FROM CandyPreferences
            GROUP BY PersonID
        ) AS X
    ) AS Y
    INNER JOIN Persons P ON Y.PersonID = P.PersonID
    INNER JOIN Candies C ON Y.TopCandy = C.CandyID
    

    对于 150,000 个糖果、200,000 个人和 500,000 个 CandyPreferences,查询耗时约 12 秒并产生了 200,000 行。


    下面的结果让我吃惊。我更改了查询以删除最终的“漂亮”连接:

    SELECT X.PersonID,
        (
            SELECT TOP 1 CandyID
            FROM CandyPreferences
            WHERE PersonID=X.PersonID AND PrefernceFactor=x.HighestPreference
        ) AS TopCandy
    FROM 
    (
        SELECT PersonID, MAX(PrefernceFactor) AS HighestPreference
        FROM CandyPreferences
        GROUP BY PersonID
    ) AS X
    

    现在 200,000 行需要两到三秒。

    现在,要明确一点,我在这里所做的一切都不是为了提高此查询的性能:我认为 12 秒是成功的。它现在说它花费了 90% 的时间在聚集索引查找上。

    【讨论】:

    • 检查外部选择列的嵌套选择语句效率低下,并且会减慢包含大量项目的查询。由于每个名称只执行一次,因此与其他人提出的建议相比,它的影响较小。
    • 你能形容“低效”吗?聚集索引扫描看起来并不是一件坏事。在 PersonName 上添加索引,Preference 将其更改为对新索引使用非聚集索引查找和非聚集索引扫描,而不是使用主键。无法判断没有测试数据的性能差异。
    • @Lucero:你错了。 SQL Server 可以毫无问题地将其转化为连接等价物。检查执行计划,我向你保证,如果它认为值得,它会这样做。
    • 你们是在 SQL Server 2000 上做的吗?请不要比较苹果和橘子。
    • @Lucero:好点。我没有在任何地方运行 SQL Server 2000。我想我从 2003 年左右就没有使用过它了。
    【解决方案4】:

    评论 Emtucifor 解决方案(因为我无法制作常规 cmets)

    我喜欢这个解决方案,但有一些 cmets 如何改进它(在这种特定情况下)。

    如果你把所有东西都放在一张桌子上,那就做不了什么,但是像 John Saunders 的解决方案那样只有几张桌子会让事情变得有点不同。

    当我们处理 [CandyPreferences] 表中的数字时,我们可以使用数学运算而不是串联来获得最大值。

    我建议 PreferenceFactor 是小数而不是实数,因为我相信我们在这里不需要实际数据类型的大小,更进一步我会建议小数(n,n),其中 n

    PackedData = Max(PreferenceFactor + CandyID)

    此外,如果我们知道我们有少于 1,000,000 个 CandyID,我们可以将 cast 添加为:

    PackedData = Max(Cast(PreferenceFactor + CandyID as decimal(9,3)))

    允许 sql server 在临时表中使用 5 个字节

    使用落地功能,拆包方便快捷。

    尼古拉

    --稍后添加--

    我测试了 John 和 Emtucifor 的两种解决方案(修改为使用 John 的结构并使用我的建议)。我还测试了有无连接。

    Emtucifor 的解决方案显然胜出,但利润并不大。如果 SQL Server 必须执行一些物理读取,情况可能会有所不同,但在所有情况下它们都是 0。

    这里是查询:

        SELECT
       [PersonID],
       CandyID = Floor(PackedData),
       PreferenceFactor = Cast(PackedData-Floor(PackedData) as decimal(3,3))
    FROM (
       SELECT
          [PersonID],
          PackedData = Max(Cast([PrefernceFactor] + [CandyID] as decimal(9,3)))
       FROM [z5CandyPreferences] With (NoLock)
       GROUP BY [PersonID]
    ) X
    
    SELECT X.PersonID,
            (
                    SELECT TOP 1 CandyID
                    FROM z5CandyPreferences
                    WHERE PersonID=X.PersonID AND PrefernceFactor=x.HighestPreference
            ) AS TopCandy,
                        HighestPreference as PreferenceFactor
    FROM 
    (
            SELECT PersonID, MAX(PrefernceFactor) AS HighestPreference
            FROM z5CandyPreferences
            GROUP BY PersonID
    ) AS X
    
    
    Select p.PersonName,
           c.Candy,
           y.PreferenceFactor
      From z5Persons p
     Inner Join (SELECT [PersonID],
                        CandyID = Floor(PackedData),
                        PreferenceFactor = Cast(PackedData-Floor(PackedData) as decimal(3,3))
                        FROM ( SELECT [PersonID],
                                      PackedData = Max(Cast([PrefernceFactor] + [CandyID] as decimal(9,3)))
                                 FROM [z5CandyPreferences] With (NoLock)
                                GROUP BY [PersonID]
                             ) X
                ) Y on p.PersonId = Y.PersonId
     Inner Join z5Candies c on c.CandyId=Y.CandyId
    
    Select p.PersonName,
           c.Candy,
           y.PreferenceFactor
      From z5Persons p
     Inner Join (SELECT X.PersonID,
                        ( SELECT TOP 1 cp.CandyId
                            FROM z5CandyPreferences cp
                           WHERE PersonID=X.PersonID AND cp.[PrefernceFactor]=X.HighestPreference
                        ) CandyId,
                        HighestPreference as PreferenceFactor
                   FROM ( SELECT PersonID, 
                                 MAX(PrefernceFactor) AS HighestPreference
                            FROM z5CandyPreferences
                           GROUP BY PersonID
                        ) AS X
                ) AS Y on p.PersonId = Y.PersonId
     Inner Join z5Candies as c on c.CandyID=Y.CandyId
    

    结果:

     TableName          nRows
     ------------------ -------
     z5Persons          200,000
     z5Candies          150,000
     z5CandyPreferences 497,445
    
    
    Query                       Rows Affected CPU time Elapsed time
    --------------------------- ------------- -------- ------------
    Emtucifor     (no joins)          183,289   531 ms     3,122 ms
    John Saunders (no joins)          183,289 1,266 ms     2,918 ms
    Emtucifor     (with joins)        183,289 1,031 ms     3,990 ms
    John Saunders (with joins)        183,289 2,406 ms     4,343 ms
    
    
    Emtucifor (no joins)
    --------------------------------------------
    Table               Scan count logical reads
    ------------------- ---------- -------------
    z5CandyPreferences           1         2,022 
    
    
    John Saunders (no joins)
    --------------------------------------------
    Table               Scan count logical reads
    ------------------- ---------- -------------
    z5CandyPreferences     183,290       587,677
    
    Emtucifor (with joins)
    --------------------------------------------
    Table               Scan count logical reads
    ------------------- ---------- -------------
    Worktable                    0             0
    z5Candies                    1           526
    z5CandyPreferences           1         2,022
    z5Persons                    1           733
    
    John Saunders (with joins) 
    --------------------------------------------
    Table               Scan count logical reads
    ------------------- ---------- -------------
    z5CandyPreferences      183292       587,912
    z5Persons                    3           802
    Worktable                    0             0
    z5Candies                    3           559
    Worktable                    0             0
    

    【讨论】:

    • 哇,谢谢你这样做 niikola。我确实在我的帖子中说过数学转换是最好的,但是当它需要最重要时,您似乎已经将偏好设置为最不重要?另外,我可以建议 3,300 次读取与 589,000 次相比是一个巨大的差异!对于非常繁忙的 OLTP 服务器,经过的客户端时间变得不那么重要(尽管仍然有意义),而 CPU 时间和读取变得更加重要。
    • 我同意经过的时间并不真正相关,可以忽略,因为这取决于太多其他事情。 3,300 对 589,000 读取看起来差别很大,但在它们只是逻辑读取之前,真正的差异可能相当小(这也取决于其他一些因素)。尽管如此,我总是会选择逻辑读取较少的解决方案,因为在行数增加的情况下,逻辑读取将变得更好或稍后变为物理读取。你是对的,我应该对索引和查询中的首选项以及最小聚合函数使用降序
    • Niikola,我是说您实际上是根据 CandyID 而不是最高 Preference 选择了错误的行。
    • 哎呀,你是对的,因为 CandyID 越高,它总是会返回 ID 最高的 Candy。为了纠正这个问题,我应该使用像 Cast(PreferenceFactor*1000 + CandyId/10000000000. as decimal(15,12)) 这样的东西来打包,这会使查询速度变慢。
    【解决方案5】:

    您可以使用以下选择语句

    select Name,Candy,PreferenceFactor
    from candyTable ct 
    where PreferenceFactor = 
        (select max(PreferenceFactor) 
         from candyTable where ct.Name = Name)
    

    但通过此选择,您将在结果集中出现 2 次“Chris”。

    如果你想获得用户最喜欢的食物而不是使用

    select top 1 Name,Candy,PreferenceFactor
    from candyTable ct
    where name = @name
    and PreferenceFactor= 
        (select max([PreferenceFactor]) 
         from candyTable where name = @name )
    

    我认为将名称和糖果更改为整数类型可能会帮助您提高性能。您还应该在两列上插入索引。

    [编辑] 改变了!给@

    【讨论】:

    • 第二个样本没用(“name != name”有什么用?)。无论如何,检查外部列的嵌套选择语句效率非常低,并且会随着项目的数量或多或少地以指数方式减慢查询。
    • 这不是 != 我写了 = !name !name 作为参数...我不知道为什么,但上次我第一次写答案时无法输入“@”信号。 . 奇怪的
    • 我已经编辑了答案...我复制了@信号,无法使用键盘功能。
    【解决方案6】:
    SELECT Name, Candy, PreferenceFactor
      FROM table AS a
     WHERE NOT EXISTS(SELECT * FROM table AS b
                       WHERE b.Name = a.Name
                         AND (b.PreferenceFactor > a.PreferenceFactor OR (b.PreferenceFactor = a.PreferenceFactor AND b.Candy > a.Candy))
    

    【讨论】:

    • 检查外部选择列的嵌套选择语句效率非常低,并且会随着项目的数量或多或少地以指数方式减慢查询速度。
    • 不,SQL server 擅长这些,通常会将它们转换为连接。
    • 2005 和 2008 版本已经朝着这个方向进行了优化,但是我记得 SQL Server 2000 中的几个查询与此非常相似(这是手头的版本,我使用了公共表表达式否则对于这种任务)有严重的性能问题;查看执行计划也显示了这一点。
    • 我喜欢这个,但我希望你将其格式化为更具可读性。
    • @Lucero:您的数据库可能缺少一些统计数据或其他问题。即使在 2000 年,我也经常做这种事情,而且对我来说一直很有效。
    【解决方案7】:
    select name, candy, max(preference)
    from tablename
    where candy=@candy
    order by name, candy
    

    通常需要对经常包含在 where 子句中的列进行索引。在这种情况下,我会说对 name 和 candy 列的索引将是最高优先级的。

    拥有列的查找表通常取决于列中重复值的数量。在 250,000 行中,如果只有 50 个重复值,那么您确实需要在那里有整数引用(外键)。在这种情况下,应该进行糖果引用,并且名称引用实际上取决于数据库中不同人员的数量。

    【讨论】:

    • 聚合函数(例如 MAX())只有在您执行 GROUP BY 时才有意义。但是一个简单的 GROUP BY 是行不通的,因为糖果和偏好必须保持关联。
    【解决方案8】:

    我将您的列名称更改为 PersonName 以避免任何常见的保留字冲突。

    SELECT     PersonName, MAX(Candy) AS PreferredCandy, MAX(PreferenceFactor) AS Factor
    FROM         CandyPreference
    GROUP BY PersonName
    ORDER BY Factor DESC
    

    【讨论】:

    • 不,行不通。你会得到正确的最大偏好因子,但它会返回最大的糖果,而不是最大偏好的糖果。
    猜你喜欢
    • 2011-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-09
    • 2018-01-21
    • 1970-01-01
    • 2017-12-19
    • 1970-01-01
    相关资源
    最近更新 更多