Im creating a stored procedure/function in MS SQL which should return the missing series.
Example:
the table "orders" contain field name "ordNo".
ordNo
000001
000003
000005
the functions should return these values:
000002
000004
any idea?
thank you very much.
What about something simple like:
SELECT ordNo - 1
FROM Orders o
WHERE NOT EXISTS (
SELECT *
FROM Orders n
WHERE n.ordNo = o.OrdNo - 1
)
AND ordNo > 1
Edit: Ah - this won't find missing "runs" in the series though. Only single missing numbers.
Here's a version which I think will at least find the "from" and "to" values for "missing" order numbers:
SELECT (SELECT MAX(ordNo) + 1 FROM Orders m WHERE m.ordNo < o.OrdNo) fromOrdNo,
(ordNo - 1) toOrdNo
FROM Orders o
WHERE NOT EXISTS (
SELECT *
FROM Orders n
WHERE n.ordNo = o.OrdNo - 1
)
AND ordNo > 1
the pseudo code for it goes like this:
1.start at 00001
2.Increment by 1 and check if this exists in the table (ordNo).
3.If not exists, return the number, else repeat the process.
Perhaps the following example should help.
-- the table that will have rows numbered from 1 to 1000
select top 1000 identity(int,1,1) as id into #X from syscolumns
-- the table with missing values
select top 200 identity(int,1,2) as id into #Y from syscolumns
-- select * from #x
-- select * from #y
select #x.id, #y.id from #x
left outer join #y on #x.id = #y.id
where #y.id is null
You should have a temp table like #x, which will have all the values (including max value of the row). In above example, I am assuming the range is from 1 to 1000.
Related
I am a newbie poster but have spent a lot of time researching answers here. I can't quite figure out how to create a SQL result set using SQL Server 2008 R2 that should probably be using lead/lag from more modern versions. I am trying to aggregate data based on sequencing of one column, but there can be varying numbers of instances in each sequence. The only way I know a sequence has ended is when the next row has a lower sequence number. So it may go 1-2, 1-2-3-4, 1-2-3, and I have to figure out how to make 3 aggregates out of that.
Source data is joined tables that look like this (please help me format):
recordID instanceDate moduleID iResult interactionNum
1356 10/6/15 16:14 1 68 1
1357 10/7/15 16:22 1 100 2
1434 10/9/15 16:58 1 52 1
1435 10/11/15 17:00 1 60 2
1436 10/15/15 16:57 1 100 3
1437 10/15/15 16:59 1 100 4
I need to find a way to separate the first 2 rows from the last 4 rows in this example, based on values in the last column.
What I would love to ultimately get is a result set that looks like this, which averages the iResult column based on the grouping and takes the first instanceDate from the grouping:
instanceDate moduleID iResult
10/6/15 1 84
10/9/15 1 78
I can aggregate to get this result using MIN and AVG if I can just find a way to separate the groups. The data is ordered by instanceDate (please ignore the date formatting here) then interactionNum and the group separation should happen when the query finds a row where the interactionNum is <= than the previous row (will usually start over with '1' but not always, so prefer just to separate on a lower or equal integer value).
Here is the query I have so far (includes the joins that give the above data set):
SELECT
X.*
FROM
(SELECT TOP 100 PERCENT
instanceDate, b.ModuleID, iResult, b.interactionNum
FROM
(firstTable a
INNER JOIN
secondTable b ON b.someID = a.someID)
WHERE
a.someID = 2
AND b.otherID LIKE 'xyz'
AND a.ModuleID = 1
ORDER BY
instanceDate) AS X
OUTER APPLY
(SELECT TOP 1
*
FROM
(SELECT
instanceDate, d.ModuleID, iResult, d.interactionNum
FROM
(firstTable c
INNER JOIN
secondTable d ON d.someID = c.someID)
WHERE
c.someID = 2
AND d.otherID LIKE 'xyz'
AND c.ModuleID = 1
AND d.interactionNum = X.interactionNum
AND c.instanceDate < X.instanceDate) X2
ORDER BY
instanceDate DESC) Y
WHERE
NOT EXISTS (SELECT Y.interactionNum INTERSECT SELECT X.interactionNum)
But this is returning an interim result set like this:
instanceDate ModuleID iResult interactionNum
10/6/15 16:10 1 68 1
10/6/15 16:14 1 100 2
10/15/15 16:57 1 100 3
10/15/15 16:59 1 100 4
and the problem is that interactionNum 3, 4 do not belong in this result set. They would go in the next result set when I loop over this query. How do I keep them out of the result set in this iteration? I need the result set from this query to just include the first two rows, 'seeing' that row 3 of the source data has a lower value for interactionNum than row 2 has.
Not sure what ModuleID was supposed to be used, but I guess you're looking for something like this:
select min (instanceDate), [moduleID], avg([iResult])
from (
select *,row_number() over (partition by [moduleID] order by instanceDate) as RN
from Table1
) X
group by [moduleID], RN - [interactionNum]
The idea here is to create a running number with row_number for each moduleid, and then use the difference between that and InteractionNum as grouping criteria.
Example in SQL Fiddle
Here is my solution, although it should be said, I think #JamesZ answer is cleaner.
I created a new field called newinstance which is 1 wherever your instanceNumber is 1. I then created a rolling sum(newinstance) called rollinginstance to group on.
Change the last select to SELECT * FROM cte2 to show all the fields I added.
IF OBJECT_ID('tempdb..#tmpData') IS NOT NULL
DROP TABLE #tmpData
CREATE TABLE #tmpData (recordID INT, instanceDate DATETIME, moduleID INT, iResult INT, interactionNum INT)
INSERT INTO #tmpData
SELECT 1356,'10/6/15 16:14',1,68,1 UNION
SELECT 1357,'10/7/15 16:22',1,100,2 UNION
SELECT 1434,'10/9/15 16:58',1,52,1 UNION
SELECT 1435,'10/11/15 17:00',1,60,2 UNION
SELECT 1436,'10/15/15 16:57',1,100,3 UNION
SELECT 1437,'10/15/15 16:59',1,100,4
;WITH cte1 AS
(
SELECT *,
CASE WHEN interactionNum=1 THEN 1 ELSE 0 END AS newinstance,
ROW_NUMBER() OVER(ORDER BY recordID) as rowid
FROM #tmpData
), cte2 AS
(
SELECT *,
(select SUM(newinstance) from cte1 b where b.rowid<=a.rowid) as rollinginstance
FROM cte1 a
)
SELECT MIN(instanceDate) AS instanceDate, moduleID, AVG(iResult) AS iResult
FROM cte2
GROUP BY moduleID, rollinginstance
I made the following query for the SQL Server backend
SELECT TOP(1) (v.rownum + 99)
FROM
(
SELECT incrementNo-99 as id, ROW_NUMBER() OVER (ORDER BY incrementNo) as rownum
FROM proposals
WHERE [year] = '12'
) as v
WHERE v.rownum <> v.id
ORDER BY v.rownum
to find the first unused proposal number.
(It's not about the lastrecord +1)
But I realized ROW_NUMBER is not supported in access.
I looked and I can't find something similar.
Does anyone know how to get the same result as a ROW_NUMBER in access?
Maybe there's a better way of doing this.
Actually people insert their proposal No (incrementID) with no constraint. This number looks like this 13-152. xx- is for the current year and the -xxx is the proposal number. The last 3 digits are supposed to be incremental but in some case maybe 10 times a year they have to skip some numbers. That's why I can't have the auto increment.
So I do this query so when they open the form, the default number is the first unused.
How it works:
Because the number starts at 100, I do -99 so it starts at 1.
Then I compare the row number with the id so it looks like this
ROW NUMBER | ID
1 1 (100)
2 2 (101)
3 3 (102)
4 5 (104)<--------- WRONG
5 6 (105)
So now I know that we skip 4. So I return (4 - 99) = 103
If there's a better way, I don't mind changing but I really like this query.
If there's really no other way and I can't simulate a row number in access, i will use the pass through query.
Thank you
From your question it appears that you are looking for a gap in a sequence of numbers, so:
SELECT b.akey, (
SELECT Top 1 akey
FROM table1 a
WHERE a.akey > b.akey) AS [next]
FROM table1 AS b
WHERE (
SELECT Top 1 akey
FROM table1 a
WHERE a.akey > b.akey) <> [b].[akey]+1
ORDER BY b.akey
Where table1 is the table and akey is the sequenced number.
SELECT T.Value, T.next -1 FROM (
SELECT b.Value , (
SELECT Top 1 Value
FROM tblSequence a
WHERE a.Value > b.Value) AS [next]
FROM tblSequence b
) T WHERE T.next <> T.Value +1
Technologies: SQL Server 2008
So I've tried a few options that I've found on SO, but nothing really provided me with a definitive answer.
I have a table with two columns, (Transaction ID, GroupID) where neither has unique values. For example:
TransID | GroupID
-----------------
23 | 4001
99 | 4001
63 | 4001
123 | 4001
77 | 2113
2645 | 2113
123 | 2113
99 | 2113
Originally, the groupID was just chosen at random by the user, but now we're automating it. Thing is, we're keeping the existing DB without any changes to the existing data(too much work, for too little gain)
Is there a way to query "GroupID" on table "GroupTransactions" for the next available value of GroupID > 2000?
I think from the question you're after the next available, although that may not be the same as max+1 right? - In that case:
Start with a list of integers, and look for those that aren't there in the groupid column, for example:
;WITH CTE_Numbers AS (
SELECT n = 2001
UNION ALL
SELECT n + 1 FROM CTE_Numbers WHERE n < 4000
)
SELECT top 1 n
FROM CTE_Numbers num
WHERE NOT EXISTS (SELECT 1 FROM MyTable tab WHERE num.n = tab.groupid)
ORDER BY n
Note: you need to tweak the 2001/4000 values int the CTE to allow for the range you want. I assumed the name of your table to by MyTable
select max(groupid) + 1 from GroupTransactions
The following will find the next gap above 2000:
SELECT MIN(t.GroupID)+1 AS NextID
FROM GroupTransactions t (updlock)
WHERE NOT EXISTS
(SELECT NULL FROM GroupTransactions n WHERE n.GroupID=t.GroupID+1 AND n.GroupID>2000)
AND t.GroupID>2000
There are always many ways to do everything. I resolved this problem by doing like this:
declare #i int = null
declare #t table (i int)
insert into #t values (1)
insert into #t values (2)
--insert into #t values (3)
--insert into #t values (4)
insert into #t values (5)
--insert into #t values (6)
--get the first missing number
select #i = min(RowNumber)
from (
select ROW_NUMBER() OVER(ORDER BY i) AS RowNumber, i
from (
--select distinct in case a number is in there multiple times
select distinct i
from #t
--start after 0 in case there are negative or 0 number
where i > 0
) as a
) as b
where RowNumber <> i
--if there are no missing numbers or no records, get the max record
if #i is null
begin
select #i = isnull(max(i),0) + 1 from #t
end
select #i
In my situation I have a system to generate message numbers or a file/case/reservation number sequentially from 1 every year. But in some situations a number does not get use (user was testing/practicing or whatever reason) and the number was deleted.
You can use a where clause to filter by year if all entries are in the same table, and make it dynamic (my example is hardcoded). if you archive your yearly data then not needed. The sub-query part for mID and mID2 must be identical.
The "union 0 as seq " for mID is there in case your table is empty; this is the base seed number. It can be anything ex: 3000000 or {prefix}0000. The field is an integer. If you omit " Union 0 as seq " it will not work on an empty table or when you have a table missing ID 1 it will given you the next ID ( if the first number is 4 the value returned will be 5).
This query is very quick - hint: the field must be indexed; it was tested on a table of 100,000+ rows. I found that using a domain aggregate get slower as the table increases in size.
If you remove the "top 1" you will get a list of 'next numbers' but not all the missing numbers in a sequence; ie if you have 1 2 4 7 the result will be 3 5 8.
set #newID = select top 1 mID.seq + 1 as seq from
(select a.[msg_number] as seq from [tblMSG] a --where a.[msg_date] between '2023-01-01' and '2023-12-31'
union select 0 as seq ) as mID
left outer join
(Select b.[msg_number] as seq from [tblMSG] b --where b.[msg_date] between '2023-01-01' and '2023-12-31'
) as mID2 on mID.seq + 1 = mID2.seq where mID2.seq is null order by mID.seq
-- Next: a statement to insert a row with #newID immediately in tblMSG (in a transaction block).
-- Then the row can be updated by your app.
I want to create a sql query to split a single column value into multiple rows like:
SELECT ID, PRODUCT_COUNT FROM MERCHANT WHERE ID = 3050
ID PRODUCT_COUNT
----------- -------------
3050 591
Based on this result, I want 6 rows as follows:
ID RANGE
3050 0-100
3050 101-200
3050 201-300
3050 301-400
3050 401-500
3050 501-591
How can I acheive this in a query ?
WITH cte AS (
SELECT
m.ID,
PRODUCT_COUNT,
LoBound = (v.number - 1) * 100 + 1,
HiBound = v.number * 100
FROM MERCHANT m
INNER JOIN master..spt_values v
ON v.type = 'P' AND v.number BETWEEN 1 AND (m.PRODUCT_COUNT - 1) / 100 + 1
WHERE m.ID = 3050
)
SELECT
ID,
RANGE = CAST(CASE LoBound
WHEN 1 THEN 0
ELSE LoBound
END AS varchar)
+ '-'
+ CAST(CASE
WHEN HiBound < PRODUCT_COUNT THEN HiBound
ELSE PRODUCT_COUNT
END AS varchar)
FROM cte
The first CASE makes sure the first range starts with 0, not with 1, same as in your sample output.
Sorry... code removed. I made a mistake where if the Product_Count was evenly divisible by 100, it gave an incorrect final row.
UPDATE:
Andriy's code is still correct. I was missing a "-1" in mine. I've repaired that and reposted both the test setup and my alternative solution.
Both Andriy's and my code produce the output in the correct order for this experiment, but I added an ORDER BY to guarantee it.
Here's the code for the test setup...
--===== Conditionally drop and create a test table for
-- everyone to work against.
IF OBJECT_ID('tempdb..#Merchant','U') IS NOT NULL
DROP TABLE #Merchant
;
SELECT TOP 10000
ID = IDENTITY(INT,1,1),
Product_Count = ABS(CHECKSUM(NEWID()))%100000
INTO #Merchant
FROM sys.all_columns ac1
CROSS JOIN sys.all_columns ac2
;
ALTER TABLE #Merchant
ADD PRIMARY KEY CLUSTERED (ID)
;
--===== Make several entries where there's a known test setup.
UPDATE #Merchant
SET Product_Count = CASE
WHEN ID = 1 THEN 0
WHEN ID = 2 THEN 1
WHEN ID = 3 THEN 99
WHEN ID = 4 THEN 100
WHEN ID = 5 THEN 101
WHEN ID = 6 THEN 99999
WHEN ID = 7 THEN 100000
WHEN ID = 8 THEN 100001
END
WHERE ID < = 8
;
Here's the alternative I posted before with the -1 correction.
WITH
cteCreateRanges AS
(--==== This determines what the ranges are
SELECT m.ID,
RangeStart = t.Number*100+SIGN(t.Number),
RangeEnd =(t.Number+1)*100,
Product_Count
FROM master.dbo.spt_Values t
CROSS JOIN #Merchant m
WHERE t.Number BETWEEN 0 AND (m.Product_Count-1)/100
AND t.Type = 'P'
AND m.ID BETWEEN 1 AND 8 -- = #FindID -<<<---<<< Or use a single variable to find.
)--==== This makes the output "pretty" and sorts in correct order
SELECT ID,
[Range] = CAST(RangeStart AS VARCHAR(10)) + '-'
+ CASE
WHEN RangeEnd <= Product_Count
THEN CAST(RangeEnd AS VARCHAR(10))
ELSE CAST(Product_Count AS VARCHAR(10))
END
FROM cteCreateRanges
ORDER BY ID, RangeStart
;
Sorry about the earlier mistake. Thanks, Andriy, for catching it.
You could create a table like this (I am changing the first range to include 100 elements like the others to make it easier, and basing it at one, so that the indexes will match the total count):
CountRangeBoundary
MinIndexInRange
---------------
1
101
201
301
401
501
601
...
Then do a θ-join like this:
SELECT m.ID,
crb.MinIndexInRange AS RANGE_MIN,
MIN( crb.MinIndexInRange + 100, m.PRODUCT_COUNT) AS RANGE_MAX
FROM MERCHANT m
JOIN CountRangeBoundry crb ON crb.MinIndexInRange <= m.PRODUCT_COUNT
WHERE m.ID = 3050
It looks like those ranges are a piece of data, so they should really be in a table (even if you don't expect them to change, because they will). That has the nice side benefit of making this task trivial:
CREATE TABLE My_Ranges ( -- Use a more descriptive name
range_start SMALLINT NOT NULL,
range_end SMALLINT NOT NULL,
CONSTRAINT PK_My_Ranges PRIMARY KEY CLUSTERED (range_start)
)
GO
SELECT
P.id,
R.range_start,
CASE
WHEN R.range_end < P.product_count THEN R.range_end
ELSE P.product_count
END AS range_end
FROM
Products P
INNER JOIN My_Ranges R ON
R.range_start <= P.product_count
If your ranges will always be contiguous then you can omit the range_end column. Your query will become a little more complex, but you won't have to worry about ranges overlapping or gaps in your ranges.
You can try a recursive CTE.
WITH CTE AS
(
SELECT Id, 0 MinB, 100 MaxB, [Range]
FROM YourTable
UNION ALL
SELECT Id, CASE WHEN MinB = 0 THEN MinB+101 ELSE MinB+100 END, MaxB + 100, [Range]
FROM CTE
WHERE MinB < [Range]
)
SELECT Id,
CAST(MinB AS VARCHAR) + ' - ' + CAST(CASE WHEN MaxB>[Range] THEN [Range] ELSE MaxB END AS VARCHAR) [Range]
FROM CTE
WHERE MinB < [Range]
ORDER BY Id, [Range]
OPTION(MAXRECURSION 5000)
I put a limit to the recursion level on 5000, but you can change it (or leave it at zero, that means basically to keep doing recursion until it can)
I have a situation where I need to add an arbitrary unique id to each of a group of records. It's easier to visualize this below.
Edited 11:26 est:
Currently the lineNum field has garbage. This is running on sql server 2000. The sample that follows is what the results should look like but the actual values aren't important, the numbers could anything as long as the two combined fields can be used for a unique key.
OrderID lineNum
AAA 1
AAA 2
AAA 3
BBB 1
CCC 1
CCC 2
The value of line num is not important, but the field is only 4 characters. This needs to by done in a sql server stored procedure. I have no problem doing it programatically.
Assuming your using SQL Server 2005 or better you can use Row_Number()
select orderId,
row_number() over(PARTITION BY orderId ORDER BY orderId) as lineNum
from Order
While adding a record to the table, you could create the "linenum" field dynamically:
In Transact-SQL, something like this:
Declare #lineNum AS INT
-- Get next linenum
SELECT #lineNum = MAX(COALESCE(linenum, 0)) FROM Orders WHERE OrderID = #OrderID
SET #lineNum = #lineNum + 1
INSERT INTO ORDERS (OrderID, linenum, .....)
VALUES (#OrderID, #lineNum, ....)
You could create a cursor that reads all values sorted, then at each change in value resets the 1 then steps through incrementing each time.
E.g.:
AAA reset 1
AAA set 1 + 1 = 2
AAA set 2 + 1 = 3
BBB reset 1
CCC reset 1
CCC set 1 + 1 = 1
Hmmmmm, could you create a view that returns the line number information in order and group it based on your order ID? Making sure the line number is always returned in the same order.
Either that or you could use a trigger and on the insert calculate the max id for the order?
Or perhaps you could use a select from max statement on the insert?
Perhaps none of these are satisfactory?
If you're not using SQL 2005 this is a slightly more set based way to do this (I don't like temp tables much but I like cursors less):
declare #out table (id tinyint identity(1,1), orderid char(4))
insert #out select orderid from THESOURCETABLE
select
o.orderid, o.id - omin.minid + 1 as linenum
from #out o
inner join
(select orderid, min(id) minid from #out group by orderid) as omin on
o.orderid = omin.orderid