Join tables but allow use of records once only - sql-server

CREATE TABLE #A (UpperLimit NUMERIC(4))
CREATE TABLE #B (Id NUMERIC(4), Amount NUMERIC(4))
INSERT INTO #A VALUES
(1000), (2000), (3000)
INSERT INTO #B VALUES
(1, 3100),
(2, 1900),
(3, 1800),
(4, 1700),
(5, 900),
(6, 800)
Given these 2 tables, I want to join Table A to B ON B.Amount < A.UpperLimit but each record from Table B can only be used once, so the desired output would be:
I could easily do this by plopping Table B's records into a temp table, cursor over table A taking top record < UpperLimit and Deleting that record from the temp table or some other programmatic solution, but I'd like to avoid that and I'm pretty sure this could be done with a "normal" (recursive CTE? Partition?) query.

You could achieve your desired output using below recursive CTE
WITH
DATA AS
(
SELECT * FROM #A A1 INNER JOIN #B B1 ON A1.UpperLimit >= B1.Amount
),
MA AS
(
SELECT MIN(UpperLimit) AS MinLimit, MAX(UpperLimit) AS MaxLimit FROM #A
),
RESULT AS
(
-- Get the first record corresponding with maximum upper limit
SELECT *
FROM DATA D1
WHERE NOT EXISTS
(SELECT 1
FROM DATA D2
WHERE D2.UpperLimit = D1.UpperLimit AND D2.Amount > D1.Amount)
AND D1.UpperLimit = (SELECT MaxLimit FROM MA)
-- Recursive get remain record corresponding with other upper limit
UNION ALL
SELECT D1.*
FROM RESULT R1 INNER JOIN DATA D1
ON (R1.UpperLimit > D1.UpperLimit AND R1.Id != D1.Id)
WHERE D1.UpperLimit >= (SELECT MinLimit FROM MA)
AND NOT EXISTS
(SELECT 1
FROM DATA D2
WHERE D2.UpperLimit = D1.UpperLimit AND D2.Amount > D1.Amount AND D2.Id != R1.Id)
)
SELECT DISTINCT * FROM RESULT ORDER BY UpperLimit DESC;
Demo: https://dbfiddle.uk/Y-m0K6Mk

Might be a bit lengthy but hopefully clear enough.
with a as
(select -- order and number rows in table A in some way
row_number() over (order by UpperLimit) as RnA,
*
from #a),
b as
(select -- order and number rows in table B in the same way
row_number() over (order by Amount) as RnB,
*
from #b),
m as
(select -- get and number all possible pairs of values from both tables considering the restriction
row_number() over (order by a.UpperLimit desc, b.Amount desc) as RnM,
*
from a
join b on
b.Amount < a.UpperLimit),
r as
(select -- use recursion to get all possible combinations of the value pairs with metrics of interest for comparison
convert(varchar(max), RnA) as ListA,
convert(varchar(max), RnB) as ListB,
RnA,
RnB,
1 as CountB,
convert(int, Amount) as SumB
from m
where RnM = 1
union all
select
r.ListA + ' ' + convert(varchar(max), m.RnA),
r.ListB + ' ' + convert(varchar(max), m.RnB),
m.RnA,
m.RnB,
r.CountB + 1,
r.SumB + convert(int, m.Amount)
from m
join r on
m.RnA < r.RnA and
m.RnB < r.RnB),
e as
(select top(1) -- select combinations of interest using metrics
ListA,
ListB
from r
order by CountB desc, SumB desc),
ea as
(select -- turn id list into table for table A
ea.Rn,
ea.Value
from e
cross apply(select row_number() over (order by (select null)) as Rn, Value from string_split(e.ListA, ' ')) as ea),
eb as
(select -- turn id list into table for table B
eb.Rn,
eb.Value
from e
cross apply(select row_number() over (order by (select null)) as Rn, Value from string_split(e.ListB, ' ')) as eb)
select -- get output table with actual values from the original tables
a.UpperLimit,
b.Amount,
b.Id
from ea
join eb on
ea.Rn = eb.Rn
join a on
ea.Value = a.RnA
join b on
eb.Value = b.RnB;

You can use an APPLY with a TOP 1 for this. Each row in the outer table gets only one row from the APPLY.
SELECT
*
FROM #A a
OUTER APPLY (
SELECT TOP (1) *
FROM #B b
WHERE b.Amount < a.UpperLimit
) b;
To simulate an inner-join (rather than a left-join) use CROSS APPLY.

This query returns very close to desired outcome.
WITH CTE AS (SELECT B.*,
ROW_NUMBER() OVER (PARTITION BY B.Value ORDER BY B.Value DESC) AS RowNum
FROM #B B),
cc as (SELECT A.Limit, CTE.*
FROM #A A
LEFT JOIN CTE ON CTE.Value < A.Limit AND CTE.RowNum = 1),
cc2 as (select *, MAX(Value) OVER ( PARTITION BY cc.Limit) as l1 from cc)
select Limit, ID, Value
from cc2
where Value = l1
This query use 3 Common Table Expressions. First sort Table B with ROW_NUMBER() function and PARTITION BY clause, second one JOIN Table A with Table B with the condition given and the third one filters the record that is in Limit on Table A and use the Limit only once.

Related

How to update a table rows with the pattern from first 10 records (similar to what fill handle in excel)

I want to update the below table where name is null to repeat the pattern from first 10 rows.
enter image description here
sql:
declare #name varchar(255)
set #name = select distinct name from #temp
update #temp
set name = #name
where name is not null
Obviously the above the query will not work due to multiple values. I want to update the table where it's null to fill with the pattern from above.
While I agree with Damien's suggestion that you not conflate databases and spreadsheets, you can do this quite easily with window functions and the modulo operator:
WITH x AS
(
SELECT dt,
name,
seq = ROW_NUMBER() OVER (ORDER BY dt),
c = COUNT(*) OVER()
FROM #temp
WHERE name IS NOT NULL
),
y AS
(
SELECT dt,
name,
seq = ROW_NUMBER() OVER (ORDER BY dt)
FROM #temp WHERE dt > (SELECT MAX(dt) FROM x)
)
UPDATE y SET y.name = x.name
FROM x INNER JOIN y
ON x.seq % x.c = y.seq % x.c;
Working example in this fiddle.
This doesn't validate, though, that the non-null values are all contiguous at the beginning - if you need the query to validate that, it's considerably more complicated.
I am just posting an Answer according to your picture.
I think if you want to use it some where else , then you should change it. (It is not General)
update
t1
set
t1.name = t2.name
from
(
select *,row_number() over (order by dt) - 1 as rn
from
(
select
*
from
tbl1
where
name is not null
)
as a
)
as t1
inner join
(
select
name,
day,
mod(rn, 10) as rn
from
(
select *,row_number() over (order by dt) - 1 as rn
from
(
select
*
from
tbl1
where
name is null
)
as b
)
as tmp
)
as t2
on t1.day = t2.day
and t1.rn = t2.rn

sql server - "Only one expression can be specified in the select list when the subquery is not introduced with EXISTS."

I have a query which inserts / copies data from table2 to table1, does anybody have any idea on how i can work around the following errors.
initially this was the code (omitted other columns), which results to "cannot insert duplicate key in object [TABLE1]" which i found out later that ROW_NUMBER gets only one row
modified it to get a sequence of rows:
(SELECT RIGHT (REPLICATE('0', 8) + LEFT((SELECT CAST (MAX(Code) AS int), RN = (SELECT ROW_NUMBER() OVER (ORDER BY [TABLE2].[U_PosNo])) FROM [TABLE1]), 8),8)),
now I'm getting "Only one expression can be specified in the select list when the sub query is not introduced with EXISTS."
INSERT INTO [TABLE1]
(
Code,
Name,
U_ProdNo,
....
'''
)
(SELECT
(SELECT RIGHT (REPLICATE('0', 8) + LEFT((SELECT CAST (MAX(Code) AS int) + ROW_NUMBER() OVER (ORDER BY [TABLE2].[U_PosNo]) FROM [TABLE1]), 8),8)),
(SELECT RIGHT (REPLICATE('0', 30) + LEFT((SELECT CAST (MAX(Name) AS int) + ROW_NUMBER() OVER (ORDER BY [TABLE2].[U_PosNo]) FROM [TABLE1]), 30),30)),
...
''')
You want the subquery only to get the max value for table1 and start the row number with that value, but, you are including your other values inside... Correlated subqueries must return a single value.
INSERT Table1 (...)
SELECT
RN = (SELECT MAX(CAST(Code AS INT)) FROM Table1) /* Subquery that returns single value */
+ ROW_NUMBER() OVER (ORDER BY [U_PosNo]) /* your row number */
, /*Other columns*/ ....
FROM Table2
To get the formatted code for your approach
SELECT
RIGHT(
REPLICATE('0', 8)
+ CAST(
(SELECT MAX(CAST(Code AS INT)) FROM Table1)
+ ROW_NUMBER() OVER (ORDER BY U_PosNo)
AS VARCHAR)
, 8)
FROM Table2
For the other subqueries I recommend you to join or apply table 3 since you are using the same condition for all of them....
Use join if you need to insert one record per row in Table3, Left if you want to insert even if there is no records there.
SELECT
RIGHT(
REPLICATE('0', 8)
+ CAST(
(SELECT MAX(CAST(Code AS INT)) FROM Table1)
+ ROW_NUMBER() OVER (ORDER BY U_PosNo)
AS VARCHAR)
, 8),
Table3.LPrice,
CASE
WHEN [Table2].[MaInC] = 1 THEN [Table2].[MatIdColumn]
ELSE [TABLE3].[Column2] END
FROM Table2
/*LEFT*/ JOIN Table3 ON [TABLE3].[ItemCode] = [Table2].[U_ItmNo]
If you need to use only one matching record from Table3, let's say the last one, use APPLY, CROSS if you want only records with Table3, OUTER if you want them even without it.
SELECT
RIGHT(
REPLICATE('0', 8)
+ CAST(
(SELECT MAX(CAST(Code AS INT)) FROM Table1)
+ ROW_NUMBER() OVER (ORDER BY U_PosNo)
AS VARCHAR)
, 8),
T3.LPrice,
CASE
WHEN [Table2].[MaInC] = 1 THEN [Table2].[MatIdColumn]
ELSE [T3].[Column2] END
FROM Table2
CROSS /*OUTER*/ APPLY(
SELECT TOP 1 * FROM Table3
WHERE [TABLE3].[ItemCode] = [Table2].[U_ItmNo]
ORDER BY DateField DESC
) T3
Assuming that the inner SELECT queries are fine, it might just work if you remove the parentheses around the outer SELECT statement:
INSERT INTO [TABLE1]
(
Code,
Name,
U_ProdNo,
....
)
SELECT
(SELECT RIGHT (REPLICATE('0', 8) + LEFT((SELECT CAST (MAX(Code) AS int) + ROW_NUMBER() OVER (ORDER BY [TABLE2].[U_PosNo]) FROM [TABLE1]), 8),8)),
(SELECT RIGHT (REPLICATE('0', 30) + LEFT((SELECT CAST (MAX(Name) AS int) + ROW_NUMBER() OVER (ORDER BY [TABLE2].[U_PosNo]) FROM [TABLE1]), 30),30)),
...

SQL Server: How do I get the highest value not set of an int column?

Let's take an example. These are the rows of the table I want get the data:
The column I'm talking about is the reference one. The user can set this value on the web form, but the system I'm developing must suggest the lowest reference value still not used.
As you can see, the smallest value of this column is 35. I could just take the smaller reference and sum 1, but, in that case, the value 36 is already used. So, the value I want is 37.
Is there a way to do this without a loop verification? This table will grow so much.
This is for 2012+
DECLARE #Tbl TABLE (id int, reference int)
INSERT INTO #Tbl
( id, reference )
VALUES
(1, 49),
(2, 125),
(3, 35),
(4, 1345),
(5, 36),
(6, 37)
SELECT
MIN(A.reference) + 1 Result
FROM
(
SELECT
*,
LEAD(reference) OVER (ORDER BY reference) Tmp
FROM
#Tbl
) A
WHERE
A.reference - A.Tmp != -1
Result: 37
Here is yet another place where the tally table is going to prove invaluable. In fact it is so useful I keep a view on my system that looks like this.
create View [dbo].[cteTally] as
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a cross join E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a cross join E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select N from cteTally
Next of course we need some sample data and table to hold it.
create table #Something
(
id int identity
, reference int
, description varchar(10)
)
insert #Something (reference, description)
values (49, 'data1')
, (125, 'data2')
, (35, 'data3')
, (1345, 'data4')
, (36, 'data5')
, (7784, 'data6')
Now comes the magic of the tally table.
select top 1 t.N
from cteTally t
left join #Something s on t.N = s.reference
where t.N >= (select MIN(reference) from #Something)
and s.id is null
order by t.N
This is ugly, but should get the job done:
select
top 1 reference+1
from
[table]
where
reference+1 not in (select reference from [table])
order by reference
I used a table valued express to get the next value. I first left outer joined the table to itself (shifting the key in the join by +1). I then looked only at rows that had no corresponding match (b.ID is null). The minimum a.ReferenceID + 1 gives us the answer we are looking for.
create table MyTable
(
ID int identity,
Reference int,
Description varchar(20)
)
insert into MyTable values (10,'Data')
insert into MyTable values (11,'Data')
insert into MyTable values (12,'Data')
insert into MyTable values (15,'Data')
-- Find gap
;with Gaps as
(
select a.Reference+1 as 'GapID'
from MyTable a
left join MyTable b on a.Reference = b.Reference-1
where b.ID is null
)
select min(GapID) as 'NewReference'
from Gaps
NewReference
------------
13
I hope the code was clearer than my description.
CREATE TABLE #T(ID INT , REFERENCE INT, [DESCRIPTION] VARCHAR(50))
INSERT INTO #T
SELECT 1,49 , 'data1' UNION ALL
SELECT 2,125 , 'data2' UNION ALL
SELECT 3,35 , 'data3' UNION ALL
SELECT 4,1345, 'data4' UNION ALL
SELECT 5,36 , 'data5' UNION ALL
SELECT 6,7784, 'data6'
SELECT TOP 1 REFERENCE + 1
FROM #T T1
WHERE
NOT EXISTS
(
SELECT 1 FROM #T T2 WHERE T2.REFERENCE = T1.REFERENCE + 1
)
ORDER BY T1.REFERENCE
--- OR
SELECT MIN(REFERENCE) + 1
FROM #T T1
WHERE
NOT EXISTS
(
SELECT 1 FROM #T T2 WHERE T2.REFERENCE = T1.REFERENCE + 1
)
How about using a Tally table. The following illustrates the concept. It would be better to use a persisted numbers table as opposed to the cte however the code below illustrates the concept.
For further reading as to why you should use a persisted table, check out the following link: sql-auxiliary-table-of-numbers
DECLARE #START int = 1, #END int = 1000
CREATE TABLE #TEST(UsedValues INT)
INSERT INTO #TEST(UsedValues) VALUES
(1),(3),(5),(7),(9),(11),(13),(15),(17)
;With NumberSequence( Number ) as
(
Select #start as Number
union all
Select Number + 1
from NumberSequence
where Number < #end
)
SELECT MIN(Number)
FROM NumberSequence n
LEFT JOIN #TEST t
ON n.Number = t.UsedValues
WHERE UsedValues IS NULL
OPTION ( MAXRECURSION 1000 )
You could try using a descending order:
SELECT DISTINCT reference
FROM `Resultsados`
ORDER BY `reference` ASC;
As far as I know, there is no way to do this without a loop. To prevent multiple values from returning be sure to use DISTINCT.

Insert row for each integer between 0 and <value> without cursor

I have a source table with id and count.
id count
a 5
b 2
c 31
I need to populate a destination table with each integer up to the count for each id.
id value
a 1
a 2
a 3
a 4
a 5
b 1
b 2
c 1
c 2
etc...
My current solution is like so:
INSERT INTO destination (id,value)
source.id
sequence.number
FROM
(VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)) AS sequence(number)
INNER JOIN
source ON sequence.number <= source.count
This solution has an upper limit and is plain lame. Is there anyway to replace the sequence with a set of all integers? Or another solution that does not use looping.
this should work:
WITH r AS (
SELECT id, count, 1 AS n FROM SourceTable
UNION ALL
SELECT id, count, n+1 FROM r WHERE n<count
)
SELECT id,n FROM r
order by id,n
OPTION (MAXRECURSION 0)
Unfortunately, there is not set of all integers in SQL Server. However, using a little trickery, you can easily generate such a set:
select N from (
select ROW_NUMBER() OVER (ORDER BY t1.object_id) AS N
from sys.all_objects t1, sys.all_objects t2
) AS numbers
where N between 1 and 1000000
will generate a set of all numbers from 1 through 1000000. If you need more than a few million numbers, add sys.all_objects to the cross join a third time.
You can find many examples in this page:
DECLARE #table TABLE (ID VARCHAR(1), counter INT)
INSERT INTO #table SELECT 'a', 5
INSERT INTO #table SELECT 'b', 3
INSERT INTO #table SELECT 'c', 31
;WITH cte (ID, counter) AS (
SELECT id, 1
FROM #table
UNION ALL
SELECT c.id, c.counter +1
FROM cte AS c
INNER JOIN #table AS t
ON t.id = c.id
WHERE c.counter + 1 <= t.counter
)
SELECT *
FROM cte
ORDER BY ID, Counter

Combine two tables in SQL Server

I have tow tables with the same number of rows
Example:
table a:
1,A
2,B
3,C
table b:
AA,BB
AAA,BBB,
AAAA,BBBB
I want a new table made like that in SQL SErver:
1,A,AA,BB
2,B,AAA,BBB
3,C,AAAA,BBBB
How do I do that?
In SQL Server 2005 (or newer), you can use something like this:
-- test data setup
DECLARE #tablea TABLE (ID INT, Val CHAR(1))
INSERT INTO #tablea VALUES(1, 'A'), (2, 'B'), (3, 'C')
DECLARE #tableb TABLE (Val1 VARCHAR(10), Val2 VARCHAR(10))
INSERT INTO #tableb VALUES('AA', 'BB'),('AAA', 'BBB'), ('AAAA', 'BBBB')
-- define CTE for table A - sort by "ID" (I just assumed this - adapt if needed)
;WITH DataFromTableA AS
(
SELECT ID, Val, ROW_NUMBER() OVER(ORDER BY ID) AS RN
FROM #tablea
),
-- define CTE for table B - sort by "Val1" (I just assumed this - adapt if needed)
DataFromTableB AS
(
SELECT Val1, Val2, ROW_NUMBER() OVER(ORDER BY Val1) AS RN
FROM #tableb
)
-- create an INNER JOIN between the two CTE which just basically selected the data
-- from both tables and added a new column "RN" which gets a consecutive number for each row
SELECT
a.ID, a.Val, b.Val1, b.Val2
FROM
DataFromTableA a
INNER JOIN
DataFromTableB b ON a.RN = b.RN
This gives you the requested output:
You could do a rank over the primary keys, then join on that rank:
SELECT RANK() OVER (table1.primaryKey),
T1.*,
T2.*
FROM
SELECT T1.*, T2.*
FROM
(
SELECT RANK() OVER (table1.primaryKey) [rank], table1.* FROM table1
) AS T1
JOIN
(
SELECT RANK() OVER (table2.primaryKey) [rank], table2.* FROM table2
) AS T2 ON T1.[rank] = T2.[rank]
Your query is strange, but in Oracle you can do this:
select a.*, tb.*
from a
, ( select rownum rn, b.* from b ) tb -- temporary b - added rn column
where a.c1 = tb.rn -- assuming first column in a is called c1
if there is not column with numbers in a you can do same trick twice
select ta.*, tb.*
from ( select rownum rn, a.* from a ) ta
, ( select rownum rn, b.* from b ) tb
where ta.rn = tb.rn
Note: be aware that this can generate random combination, for example
1 A AA BB
2 C A B
3 B AAA BBB
because there is no order by in ta and tb

Resources