【问题标题】:Left join with nearest value without duplicates左连接与最近的值没有重复
【发布时间】:2015-11-29 08:24:46
【问题描述】:

我想在 MS SQL 中实现如下所示,使用 2 个表并通过连接而不是迭代。

From table A, I want each row to identify from table B which in the list is their nearest value, and when value has been selected, that value cannot re-used.如果您以前做过类似的事情,请提供帮助。先感谢您! #SOreadyToAsk

【问题讨论】:

  • 所以这是一种需要逐条评估记录的迭代?
  • 是但不是?说真的,在这种情况下,我想避免“光标”或“同时”。如果有任何可能的解决方法,比如标志或任何东西,那应该没问题。
  • 并选择了值时,该值不能重新使用 i> - 这是使用复合选择的交易断路器。我会在存储过程中执行此操作,可能使用临时表
  • 是的,临时表实际上还可以,但这里的挑战又是,该函数必须能够识别其与其对应的最接近值的关系,并且我猜之后可能会删除重复的关系..
  • “我猜之后可能会删除重复的关系”——这是什么意思?

标签: sql sql-server sql-server-2008


【解决方案1】:

我非常相信 这不是一个好的做法,因为我绕过了为自己制定的策略 SQL,它具有副作用(INSERT、UPDATE、DELETE)的功能是 NO,但由于我想在不产生迭代选项的情况下解决这个问题,所以我想出了这个,现在让我对事情有了更好的看法。

create table tablea
(
    num     INT,
    val     MONEY
)

create table tableb
(
    num     INT,
    val     MONEY
)

我创建了一个硬表临时表,我会不时删除它。

if((select 1 from sys.tables where name = 'temp_tableb') is not null) begin drop table temp_tableb end
select * into temp_tableb from tableb

我创建了一个执行 xp_cmdshell 的函数(这是副作用绕过发生的地方)

CREATE FUNCTION [dbo].[GetNearestMatch] 
(
    @ParamValue MONEY
)
RETURNS MONEY
AS
BEGIN

    DECLARE @ReturnNum MONEY
    , @ID INT

    SELECT TOP 1 
        @ID = num
        , @ReturnNum = val
    FROM temp_tableb ORDER BY ABS(val - @ParamValue)

    DECLARE @SQL varchar(500)
    SELECT @SQL = 'osql -S' + @@servername + ' -E -q "delete from test..temp_tableb where num = ' + CONVERT(NVARCHAR(150),@ID) + ' "'

    EXEC master..xp_cmdshell @SQL

    RETURN @ReturnNum

END

我在查询中的用法看起来像这样。

-- initialize temp
if((select 1 from sys.tables where name = 'temp_tableb') is not null) begin drop table temp_tableb end
select * into temp_tableb from tableb
-- query nearest match
select
    *
    , dbo.GetNearestMatch(a.val) AS [NearestValue]
from tablea a

给了我这个..

【讨论】:

  • 我知道你在那里做了什么 ;-)
【解决方案2】:

以下是使用 CTE 和窗口函数的基于集合的解决方案。

ranked_matches CTE 为TableA 中的每一行分配一个最接近的匹配排名,并为TableB 中的每一行分配一个最接近的匹配排名,使用index 值作为决胜局。

best_matches CTE 从ranked_matches 返回两个排名中排名最高(排名值为 1)的行。

最后,外部查询使用从TableAbest_matches CTE 的LEFT JOIN 来包含由于已分配关闭匹配而未分配最佳匹配的TableA 行。

请注意,这不会返回与示例结果中指示的索引 3 TableA 行的匹配项。此行的关闭匹配是 TableB 索引 3,相差 83。但是,该 TableB 行与 TableA 索引 2 行更接近匹配,相差 14,因此它已经被分配。如果这不是您想要的,请澄清您的问题。我认为可以对这种技术进行相应的调整。

CREATE TABLE dbo.TableA(
      [index] int NOT NULL
        CONSTRAINT PK_TableA PRIMARY KEY
    , value int
    );
CREATE TABLE dbo.TableB(
      [index] int NOT NULL
        CONSTRAINT PK_TableB PRIMARY KEY
    , value int
    );
INSERT  INTO dbo.TableA
        ( [index], value )
VALUES  ( 1, 123 ),
        ( 2, 245 ),
        ( 3, 342 ),
        ( 4, 456 ),
        ( 5, 608 );

INSERT  INTO dbo.TableB
        ( [index], value )
VALUES  ( 1, 152 ),
        ( 2, 159 ),
        ( 3, 259 );

WITH 
      ranked_matches AS (
        SELECT 
              a.[index] AS a_index
            , a.value AS a_value
            , b.[index] b_index
            , b.value AS b_value
            , RANK() OVER(PARTITION BY a.[index] ORDER BY ABS(a.Value - b.value), b.[index]) AS a_match_rank
            , RANK() OVER(PARTITION BY b.[index] ORDER BY ABS(a.Value - b.value), a.[index]) AS b_match_rank
        FROM dbo.TableA AS a
        CROSS JOIN dbo.TableB AS b
    )
    , best_matches AS (
        SELECT
              a_index
            , a_value
            , b_index
            , b_value
        FROM ranked_matches
        WHERE
                a_match_rank = 1
            AND b_match_rank= 1
    )
SELECT
      TableA.[index] AS a_index
    , TableA.value AS a_value
    , best_matches.b_index
    , best_matches.b_value
FROM dbo.TableA
LEFT JOIN best_matches ON
    best_matches.a_index = TableA.[index]
ORDER BY
    TableA.[index];

编辑:

虽然此方法使用 CTE,但不使用递归,因此不限于 32K 递归。不过,从性能的角度来看,这里可能还有改进的空间。

【讨论】:

  • CTE 肯定是一个选项,但是我无法想象形成大于最大可能递归的结果,尽管我也感谢时间,但我也没有放弃这种情况。
  • @kenlacoste,这是一个真正的基于集合的解决方案,没有递归和相关限制。我不想暗示这将在大型表中表现良好,因为在排名之前需要计算 ABS;需要进行全面扫描。可能还有其他解决方案可以表现更好。
  • @DanGuzman,我没有详细查看您的查询,但不幸的是它没有返回正确的结果。它返回第 3 行:a_index: 3, a_value: 342, b_index: NULL, b_value: NULL 而不是 b_index: 2, b_value: 159。抱歉,我没有注意到您在答案中写了它。是的,这就是挑战。
  • @VladimirBaranov,问题中的预期结果与问题不同,这就是我要求澄清的原因。问题从表 A 中说,从表 B 中确定哪个是最接近的值而不重用值。样本结果似乎来自表 B,确定了最近的表 A 值,或者当表 A 已被使用时,下一个最近的表 A 值。在后一种情况下,表 A 的值不是最接近表 B 的值。
  • @kenlacoste,请在您的示例数据中添加几行并获得预期结果,以更好地说明您真正需要的内容。也许,也可以一步一步地逐行解释结果如何以及为什么会这样。
【解决方案3】:

我认为没有光标是不可能的。

即使可以在没有游标的情况下执行此操作,也肯定需要自连接,可能不止一次。因此,性能可能很差,可能比直接游标更差。而且很可能很难理解逻辑并在以后维护此代码。有时光标很有用。

主要难点是这部分问题:

选择值后,该值不能重复使用。

几天前有一个similar question

逻辑很简单。游标遍历表A 的所有行,并且每次迭代都会向临时目标表添加一行。要确定要添加的value,我使用EXCEPT 运算符,该运算符从表B 中获取所有values,并从它们中删除之前使用过的所有values。我的解决方案假定表B 中的value 中没有重复项。 EXCEPT 运算符删除重复项。如果表B中的values不是唯一的,那么临时表将保持唯一的indexB而不是valueB,但主要逻辑保持不变。

这里是SQL Fiddle

样本数据

DECLARE @TA TABLE (idx int, value int);
INSERT INTO @TA (idx, value) VALUES
(1, 123),
(2, 245),
(3, 342),
(4, 456),
(5, 608);

DECLARE @TB TABLE (idx int, value int);
INSERT INTO @TB (idx, value) VALUES
(1, 152),
(2, 159),
(3, 259);

主查询将结果插入临时表@TDst。可以不使用显式变量@CurrValueB 来编写INSERT,但使用变量看起来更简洁。

DECLARE @TDst TABLE (idx int, valueA int, valueB int);

DECLARE @CurrIdx int;
DECLARE @CurrValueA int;
DECLARE @CurrValueB int;

DECLARE @iFS int;
DECLARE @VarCursor CURSOR;
SET @VarCursor = CURSOR FAST_FORWARD
FOR
    SELECT idx, value
    FROM @TA
    ORDER BY idx;

OPEN @VarCursor;

FETCH NEXT FROM @VarCursor INTO @CurrIdx, @CurrValueA;
SET @iFS = @@FETCH_STATUS;
WHILE @iFS = 0
BEGIN
    SET @CurrValueB = 
    (
        SELECT TOP(1) Diff.valueB
        FROM
            (
                SELECT B.value AS valueB
                FROM @TB AS B

                EXCEPT -- remove values that have been selected before

                SELECT Dst.valueB
                FROM @TDst AS Dst
            ) AS Diff
        ORDER BY ABS(Diff.valueB - @CurrValueA)
    );

    INSERT INTO @TDst (idx, valueA, valueB)
    VALUES (@CurrIdx, @CurrValueA, @CurrValueB);

    FETCH NEXT FROM @VarCursor INTO @CurrIdx, @CurrValueA;
    SET @iFS = @@FETCH_STATUS;
END;

CLOSE @VarCursor;
DEALLOCATE @VarCursor;

SELECT * FROM @TDst ORDER BY idx;

结果

idx    valueA    valueB
1      123       152
2      245       259
3      342       159
4      456       NULL
5      608       NULL

拥有以下索引会有所帮助:

TableA - (idx) include (value),因为我们SELECT idx, value ORDER BY idx;

TableB - (value) unique,临时目标表 - (valueB) unique filtered NOT NULL,帮助EXCEPT。因此,最好为结果(或永久表)使用临时#table 而不是表变量,因为表变量不能有索引。


另一种可能的方法是从表B(从原始或副本)中删除一行,因为它的值被插入到结果中。在这种方法中,我们可以避免一次又一次地执行EXCEPT,并且总体上可以更快,特别是如果最后将表B留空是可以的。不过,我不知道如何避免游标和按顺序处理单个行。

SQL Fiddle

DECLARE @TDst TABLE (idx int, valueA int, valueB int);

DECLARE @CurrIdx int;
DECLARE @CurrValueA int;

DECLARE @iFS int;
DECLARE @VarCursor CURSOR;
SET @VarCursor = CURSOR FAST_FORWARD
FOR
    SELECT idx, value
    FROM @TA
    ORDER BY idx;

OPEN @VarCursor;

FETCH NEXT FROM @VarCursor INTO @CurrIdx, @CurrValueA;
SET @iFS = @@FETCH_STATUS;
WHILE @iFS = 0
BEGIN
    WITH
    CTE
    AS
    (
        SELECT TOP(1) B.idx, B.value
        FROM @TB AS B
        ORDER BY ABS(B.value - @CurrValueA)
    )
    DELETE FROM CTE
    OUTPUT @CurrIdx, @CurrValueA, deleted.value INTO @TDst;

    FETCH NEXT FROM @VarCursor INTO @CurrIdx, @CurrValueA;
    SET @iFS = @@FETCH_STATUS;
END;

CLOSE @VarCursor;
DEALLOCATE @VarCursor;

SELECT
    A.idx
    ,A.value AS valueA
    ,Dst.valueB
FROM
    @TA AS A
    LEFT JOIN @TDst AS Dst ON Dst.idx = A.idx
ORDER BY idx;

【讨论】:

  • 也可以使用WHILE @@ROWCOUNT <> 0 UPDATE... 来实现,它与游标并没有太大区别,但至少需要更少的变量
  • 是的,对光标和while都做了同样的事情。尽管如此,我仍然希望有办法,我给这个 UP 和我一样的页面。谢谢
  • @kenlacoste,嗯,您选择的每个后续值(间接)取决于所有先前的选择。与运行总计类似,但您不仅需要检查一个先前的值,还需要检查所有先前的值。即使您使用的是 SQL Server 2012+,它显着扩展了您可以使用窗口函数执行的操作,我也看不到一个有效的基于集合的解决方案。让我们看看是否有人提出了一个好主意。也许递归......
  • 如果我理解正确,您可以在WHILE 解决方案中通过标记(更新)您已使用的行来节省一些精力,然后在任何后续选择中,使用 WHERE 子句忽略它们。同样,它是一个基于WHILE @@ROWCOUNT <> 0 的循环。即虽然没有更多的工作要做。你不需要记住你在循环中的位置或类似的东西。
  • 另一种可能的方法是从表 B 中删除一行(从原始或副本)
猜你喜欢
  • 2013-11-23
  • 2014-05-11
  • 2013-11-23
  • 2017-08-22
  • 1970-01-01
  • 2021-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多