我希望在MS SQL中实现如下所示,使用2个表并通过join而不是迭代。
从表A中,我希望每一行从表B中识别出列表中最接近的值,并且当选择了值时,该值不能重复使用。如果您以前做过类似的事情,请帮忙。先感谢您! #SOreadyToAsk
答案 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
留空。尽管如此,我还是没有看到如何避免光标并按顺序处理各个行。
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
中的TableA
到best_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
给了我这个..