如何以最快的方式获取给定 id 的最后一条记录? (微软 SQL)

Posted

技术标签:

【中文标题】如何以最快的方式获取给定 id 的最后一条记录? (微软 SQL)【英文标题】:How can I get the last record for a given id in the fastest way? (MS SQL) 【发布时间】:2019-11-15 08:15:21 【问题描述】:

我想寻求您的帮助。

我有一个如下所示的表格:

id | sequenceId
---------------
1 | 1
1 | 2
1 | 3
2 | 1
2 | 2
2 | 3
2 | 4
...

还有其他列,但现在不重要了。另一件事,密钥是一个密钥对 (id,sequenceId),它们在表中被索引。我想要的是获取给定 ID 的最后一行。 例如,如果 myId = 1 -> 给我 (1,3),myId = 2 -> 给我 (2,4) 记录等等。 在我的表中,有 500 个 id,每个 id 有 50 000 个序列 id,所以记录的大小是 500 * 50 000

我的查询:

SELECT
     myId AS 'MyId',
     MAX(sequenceId) AS 'SequenceId'
FROM myTable
WHERE myId in (SELECT myId from @MyIds)
GROUP BY(myId)
OPTION (RECOMPILE);

不幸的是,这并没有我想要的那么快。在我的尝试中,@MyIds 包含所有 id,1 - 500,在这种情况下,执行时间约为 1 秒。但我想让它更快。

你知道我怎样才能让它更快吗?也许另一个查询比我使用的更好?

感谢您的回答。

兄弟。

【问题讨论】:

桌子上有索引吗? OP 的文本:还有其他列,但现在不重要了。另一件事,key 是一个 keypair (id,sequenceId),它们在表中被索引。 您可以尝试 EXISTS 或 JOIN 而不是 IN。然而,对于这样简单的查询,优化器可能会选择相同的执行路径。您也可以尝试使用临时表而不是表变量。以下是一些详细介绍这两个主题的文章:explainextended.com/2009/06/16/in-vs-join-vs-exists 和 sqlshack.com/when-to-use-temporary-tables-vs-table-variables 你是如何测量时间的? 请为您的表索引添加表类型的定义和create index 语句。在性能优化方面,这些细节至关重要。 【参考方案1】:

您的查询是正确且相对最优的;除了用索引临时表替换表变量之外,您可能不会通过以其他方式重写它来获得任何改进。

性能优化通常与索引有关。根据id 列是否被索引,以下选项之一应该会有所帮助:

create index [IX_mytable_myid_sequenceid] on dbo.mytable (myid, sequenceid desc);

如果表上的聚集索引是在myId列上创建的,那么可以节省一点空间:

create index [IX_mytable_sequenceid] on dbo.mytable (sequenceid desc);

排序顺序很重要,因为不幸的是,当 SQL Server 必须执行向后扫描/查找时,它不能使用并行计划。但是,您可以尝试ascdescsequenceId 列的排序;很有可能,这对您的特定情况没有影响。

无论索引如何,您都可能需要将表变量替换为临时表。根据您使用的 SQL Server 版本,基数估计器假定表变量有 1 行或 100 行。如果您的数据量未达到估计值,则执行计划将是次优的。所以代码应该是这样的:

create table #list (Id int primary key);

insert into #list (Id)
-- Assuming there are no duplicates, otherwise add DISTINCT
select MyId from @MyIds;

SELECT
     t.myId AS 'MyId',
     MAX(t.sequenceId) AS 'SequenceId'
FROM myTable t
  inner join #list l on l.Id = t.myId
GROUP BY t.myId
-- OPTION (RECOMPILE);

是否应该离开option 子句取决于性能。

【讨论】:

我看不出没有id 列的索引将如何使OP 的查询受益,因为他没有按sequenceid 过滤。 我用“INNER JOIN”试了一下,执行时间比较慢,所以我用“IN” @EzLo, "如果表上的聚集索引是在 id 列上创建的"。 @RogerWolf,是的,即使使用Id 聚集,它也会使用聚集索引而不是仅使用sequenceId 的非聚集索引,但也许我错过了一些东西。 说得好。我的Plus1!请注意,您可以按降序返回行而不会丢失并行性;请参阅下面的帖子,了解如何避免对此类情况进行向后扫描。【参考方案2】:

首先,@MyIds 是一个表变量,不是吗?你如何声明这个?它被索引了吗?在上面添加主键:

DECLARE @MyIds TABLE (ID INT PRIMARY KEY)

其次,确保您的密钥在myId+sequenceId 而不是sequenceId+myId

第三,避免IN子句有很多项目,这是一个瓶颈

这应该是你最好的选择:

SELECT myId MyId, MAX(sequenceId) SequenceId
FROM myTable t
WHERE EXISTS (SELECT TOP 1 'X' X from @MyIds m WHERE m.myId = t.myId)
GROUP BY myId

你也可以尝试分组后强制过滤,试试看:

SELECT * 
FROM (
    SELECT TOP (9223372036854775807) myId MyId, MAX(sequenceId) SequenceId
    FROM myTable t
    GROUP BY myId
    ORDER BY myId
) T
WHERE EXISTS (SELECT TOP 1 'X' X from @MyIds m WHERE m.myId = t.myId)

【讨论】:

@MyIds 是在我正在使用的存储过程中创建的 TYPE。 我已经尝试过这种方式,但不幸的是,性能并没有那么好。【参考方案3】:

您可以尝试如下 INNER JOIN-

SELECT
A.myId AS 'MyId',
MAX(A.sequenceId) AS 'SequenceId'
FROM myTable A
INNER JOIN @MyIds B
ON A.myId = B.myId
GROUP BY(A.myId)

以下脚本将为您返回每个 myID 的最大序列值-

SELECT * FROM 
(
    SELECT myId,sequenceId, 
    ROW_NUMBER() OVER(PARTITION BY myId ORDER BY sequenceId DESC) RN
    FROM  myTable
)A
WHERE RN = 1

【讨论】:

我用“INNER JOIN”试了一下,执行时间比较慢,所以我用“IN”【参考方案4】:
select
    id.id as id,
    seq as maxSequence,
    data.someData as someData
from
(select id, max(sequenceId) as seq from #tab group by id) id
left join #tab data on id.id = data.id and id.seq = data.sequenceId

【讨论】:

【参考方案5】:

我会推荐以下内容:

select i.myId,
       (select max(t.sequenceId)
        from myTable t
        where t.myId = i.myId
       )
from @MyIds i;

然后,为了提高性能,您需要在myTable(myId, sequenceId desc) 上建立索引。

【讨论】:

索引不应该是myTable(myId, sequenceId desc) 以在myId 的第一场比赛中获得最大的sequenceId @HABO 。 . .我认为这是一个更好的选择。即使使用升序索引,SQL Server 也可能很智能。【参考方案6】:

如前所述 - 如果您在 myId 上有索引,则您的查询 应该 运行 sequenceId。列存储索引和/或batch mode processing 可以显着加快速度。如果您可以在索引中添加过滤器,那就更好了。内存优化表和/或其他对象也可以加快速度。说了这么多,让我介绍一种新的索引——Virtual Index。您可以利用 RangeAB 或 Jeff Moden 的 FnTally。

使用 dbo.rangeAB 进行虚拟索引

首先进行快速热身。让我们创建一个查询,以升序和降序返回数字 1 到 10。让我们在没有索引的情况下使用并行执行计划。

SELECT   r.RN, r.Op
FROM     dbo.rangeAB(1,10,1,1) AS r
ORDER BY r.RN
OPTION (QUERYTRACEON 8649)

返回:

RN                   Op
-------------------- --------------------
1                    10
2                    9
3                    8
4                    7
5                    6
6                    5
7                    4
8                    3
9                    2
10                   1

执行计划:

看 ^^^ 没有排序!!!因此,对于降序 ORDER BY,您的查询如下所示:

-- Last 3 Numbers - no index, no sort + Descending Order + Parallelism (if you want it)
SELECT TOP (3) r.Op
FROM     dbo.rangeAB(1,10,1,1) AS r
ORDER BY r.RN ASC
--OPTION (QUERYTRACEON 8649);

这里我们有一个虚拟的 FORWARD-ORDER 扫描,它以降序返回行。 不需要索引,不需要排序运算符! 这不是技巧,让我们调用两次函数并做一些需要排序的事情(从单独的函数调用中按两列分组、连接、传统聚合和我们'将以按窗口排名函数排序的 ORDER BY(不)排序来结束它...

DECLARE @rows INT = 10;

SELECT
  RN1      = r.RN,
  RN1_DESC = @rows+1-r.RN,
  RN2      = r2.RN,
  RN1_Low  = MIN(r.RN),
  RN1_High = MAX(r.RN),
  RN1_Avg  = AVG(r.RN)
FROM      dbo.rangeAB(1,@rows,1,1) AS r
LEFT JOIN dbo.rangeAB(1,3,1,1)  AS r2 ON r.RN = r2.RN
GROUP BY  r.RN, r2.RN 
ORDER BY  DENSE_RANK() OVER (ORDER BY r.RN);

返回:

RN1   RN1_DESC    RN2      RN1_Low    RN1_High   RN1_Avg
----- ----------- -------- ---------- ---------- --------------------
1     10          1        1          1          1
2     9           2        2          2          2
3     8           3        3          3          3
4     7           NULL     4          4          4
5     6           NULL     5          5          5
6     5           NULL     6          6          6
7     4           NULL     7          7          7
8     3           NULL     8          8          8
9     2           NULL     9          9          9
10    1           NULL     10         10         10

结果集并不是为了有意义,而是我感兴趣的执行计划;我们来看一下。

返回您的查询

-- Sample data
DECLARE @table TABLE (id INT NOT NULL, sequenceId INT NOT NULL)--, INDEX xxx(id,sequenceId))
INSERT @table VALUES(1,1),(1,2),(1,3),(2,1),(2,2),(2,3),(2,4)

SELECT r.RN, sequenceId = MAX(t.sequenceId)
FROM        
(
  SELECT MIN(t.id), MAX(t.id), MIN(t.sequenceId), MAX(t.sequenceId) 
  FROM   @table AS t
) AS mm(Mn,Mx,Mns,Mxs)
CROSS APPLY dbo.rangeAB(mm.Mn,mm.Mx,1,1)   AS r
CROSS APPLY dbo.rangeAB(mm.Mns,mm.Mxs,1,1) AS r2
JOIN        @table                         AS t 
  ON        r.RN = t.id AND r2.RN = Mxs
GROUP BY    r.RN
OPTION (QUERYTRACEON 8649);

无索引、无排序、无 I/O、无并行度损失(无论方向如何)且无 RBAR!

【讨论】:

以上是关于如何以最快的方式获取给定 id 的最后一条记录? (微软 SQL)的主要内容,如果未能解决你的问题,请参考以下文章

Laravel - 以一对多关系获取最后一条记录

如何获取mysql重复项中的最后一条数据

如何计算给定间隔内的记录?

GMail API:获取收件箱中最早的电子邮件的最快方式

获取每个 ID 的最后一条记录

如何从 Sqlite 获取最后一条记录?