左连接的最近值没有重复

时间:2015-09-03 13:13:32

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

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

从表A中,我希望每一行从表B中识别出列表中最接近的值,并且当选择了值时,该值不能重复使用。如果您以前做过类似的事情,请帮忙。先感谢您! #SOreadyToAsk

enter image description here

3 个答案:

答案 0 :(得分:2)

我认为没有游标就可以。

即使没有光标也可以这样做,它肯定需要自连接,可能不止一次。因此,性能可能很差,可能比直接光标更差。很可能很难理解逻辑并在以后维护这段代码。有时游标很有用。

主要困难在于问题的这一部分:

  

选择值后,该值无法重复使用。

几天前有一个similar question

逻辑是直截了当的。 Cursor循环遍历表A的所有行,每次迭代都会向临时目标表添加一行。要确定要添加的value我使用EXCEPT运算符从表values中获取所有B,并从中删除之前使用过的所有values。我的解决方案假设表value中的B中没有重复项。 EXCEPT运算符会删除重复项。如果表values中的B不是唯一的,那么临时表将保留唯一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。可以在不使用显式变量INSERT的情况下编写@CurrValueB,但它对变量看起来更清晰。

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;

答案 1 :(得分:2)

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

ranked_matches CTE为TableA中的每一行指定最接近的匹配排名以及TableB中每行的最接近匹配排名,使用index值作为打破平局。

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

最后,外部查询使用LEFT JOIN中的TableAbest_matches CTE,以包含未分配最佳匹配的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递归。不过,从性能的角度来看,这里可能还有改进的余地。

答案 2 :(得分:2)

我非常相信这不是一个好的做法,因为我绕过了为自己制作的策略,带有副作用(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

给了我这个..

enter image description here