I've got the following three tables defined:
table 'A':
-------------------
majorID | bigint (primary key)
-------------------
table 'B':
-------------------
majorID | bigint (foreign key to table 'A's majorID)
minorID | bigint (primary key)
totalSize | bigint
-------------------
table 'C':
-------------------
objectID | bigint (primary key)
majorID | bigint (foreign key to table 'A's majorID)
minorID | bigint (foreign key to table 'B's minorID)
startPoint | bigint
length | bigint
-------------------
What I'm looking to do is get a list of all rows in table 'B', but show how much space is left for each row.
The remaining space can be found by finding the highest "startPoint", adding the value o the "length" column for the row containing the highest "startPoint", then subtracting that combined value from the "totalSize" column in table 'B'
I am currently able to achieve this using the following code:
create table #results (MinorID bigint, MajorID bigint, RemainingSpace bigint)
DECLARE #MinorID bigint
DECLARE #TotalSpace bigint
DECLARE #MajorID bigint
DECLARE cur CURSOR FOR
SELECT MinorID, MajorID, TotalSize FROM B
OPEN cur
FETCH NEXT FROM cur INTO #MinorID,#MajorID, #TotalSpace
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #UsedSize bigint
SELECT TOP 1 #UsedSize = StartPoint + [length] FROM C
WHERE MinorID = #MinorID AND MajorID = #MajorID
ORDER BY StartPoint DESC
INSERT INTO #results VALUES (#MinorID,#MajorID,#TotalSpace - #UsedSize)
FETCH NEXT FROM cur INTO #MinorID,#MajorID, #TotalSpace
END
CLOSE cur
DEALLOCATE cur
SELECT * FROM #results
drop table #results
The problem is that I expect these tables are going to get VERY large, and I realise that running a cursor over the tables probably isn't the fastest way to achieve what I want.
However, I'm struggling to find a better solution (Monday morning blues), and was hoping someone more awake / better at SQL than me can suggest a solution!
note: The table designs are not "set in stone", so if the only solution is to denormalize the data so that table 'B' keeps a record of it's "taken space", then I'm open to that...
EDIT:
I went with a modified version of the accepted answer, as follows:
SELECT B.*, coalesce(C.StartPoint + C.Length,0) AS UsedSize
FROM TableB B
LEFT JOIN
(
SELECT *, DENSE_RANK() OVER(PARTITION BY C.MajorID, C.MinorID ORDER BY C.StartPoint DESC) AS Rank
FROM TableC C
) C
ON C.MajorID = B.MajorID
AND C.MinorID = B.MinorID
AND C.Rank = 1
Maybe you can work with the DENSE_RANK.
In this query i am joining the table C with the extra column Rank. This column is given the value 1 if its the highest startpoint. In the (AND C.Rank = 1) we only extract that row.
SELECT B.*, (C.StartPoint + C.Length) AS UsedSize
FROM TableB B
INNER JOIN
(
SELECT *, DENSE_RANK() OVER(PARTITION BY C.MajorID, C.MinorID ORDER BY C.StartPoint DESC) AS Rank
FROM TableC C
) C
ON C.MajorID = B.MajorID
AND C.MinorID = B.MinorID
AND C.Rank = 1
Maybe you are making things more complicated than they are. You are looking for the maximum startPoint for each minorID in order to add length and get thus the used size. But is it possible at all to have a lesser startPoint where its length is so big that adding both would exceed the maximum startPoint plus its length?
Is this possible ( max (startPoint) being lower than some other startPoint+length ):
minorID startPoint length
1 1 10
1 9 3
If not, what I assume, you can simply subtract max(startPoint + length):
select
minorID,
totalSize,
totalSize - (select max(startPoint + length) from C where C.minorID = B.minorID) as space_left
from B;
EDIT: I just read your comment that sometimes no C for a B exists. To account for this you will have to use ISNULL or COALESCE:
select
minorID,
totalSize,
totalSize - coalesce((select max(startPoint + length) from C where C.minorID = B.minorID), 0) as space_left
from B;
WITH UsedSpace AS
(
SELECT minorID, MAX(startPoint + length) AS used
FROM C
GROUP BY minorID
)
SELECT B.minorID, totalSize - COALESCE(UsedSpace.used, 0)
FROM B LEFT JOIN UsedSpace ON B.minorID = UsedSpace.minorID
You can use OUTER APPLY to get the top record from C ordered by StartPoint
SELECT B.MajorID,
B.MinorID,
B.TotalSize,
C.StartPoint,
C.Length,
SpaceRemaining = B.TotalSize - ISNULL(C.StartPoint + C.Length, 0)
FROM B
OUTER APPLY
( SELECT TOP 1 C.StartPoint, C.Length
FROM C
WHERE B.MinorID = c.MinorID
ORDER BY C.StartPoint DESC
) C;
Or you can use ROW_NUMBER to achieve the same result, depending on indexes etc, one may perform better than the other:
SELECT B.MajorID,
B.MinorID,
B.TotalSize,
C.StartPoint,
C.Length,
SpaceRemaining = B.TotalSize - ISNULL(C.StartPoint + C.Length, 0)
FROM B
LEFT JOIN
( SELECT C.MinorID,
C.StartPoint,
C.Length,
RowNumber = ROW_NUMBER() OVER(PARTITION BY C.MinorID ORDER BY C.StartPoint DESC, Length DESC)
FROM C
) C
ON B.MinorID = c.MinorID
AND C.Rownumber = 1;
Examples on SQL Fiddle
SELECT B.MajorId, B.MinorId, B.totalSize-(C.length+C.startPoint) as Space
from TABLEB B
LEFT JOIN (SELECT MAX(startPoint) maxSP,majorid, minorid FROM TABLEC GROUP BY MajorId, MinorId)
mxT ON B.majorID = mxT.majorID AND B.minorId=mxt.minorId
LEFT JOIN TABLEC C on C.majorid=mxt.MajorId AND C.minorId=mxt>MinorId AND C.startPoint=mxT.maxSP
Related
I need to be able to apply unique 8 character strings per row on a table that has almost 2.5 million records.
I have tried this:
UPDATE MyTable
SET [UniqueID]=SUBSTRING(CONVERT(varchar(255), NEWID()), 1, 8)
Which works, but when I check the uniqueness of the ID's, I receive duplicates
SELECT [UniqueID], COUNT([UniqueID])
FROM NicoleW_CQ_2019_Audi_CR_Always_On_2019_T1_EM
GROUP BY [UniqueID]
HAVING COUNT([UniqueID]) > 1
I really would just like to update the table, as above, with just a simple line of code, if possible.
Here's a way that uses a temporary table to assure the uniqueness
Create and fill a #temporary table with unique random 8 character codes.
The SQL below uses a FOR XML trick to generate the codes in BASE62 : [A-Za-z0-9]
Examples : 8Phs7ZYl, ugCKtPqT, U9soG39q
A GUID only uses the characters [0-9A-F].
For 8 characters that can generate 16^8 = 4294967296 combinations.
While with BASE62 there are 62^8 = 2.183401056e014 combinations.
So the odds that a duplicate is generated are significantly lower with BASE62.
The temp table should have an equal of larger amount of records than the destination table.
This example only generates 100000 codes. But you get the idea.
IF OBJECT_ID('tempdb..#tmpRandoms') IS NOT NULL DROP TABLE #tmpRandoms;
CREATE TABLE #tmpRandoms (
ID INT PRIMARY KEY IDENTITY(1,1),
[UniqueID] varchar(8),
CONSTRAINT UC_tmpRandoms_UniqueID UNIQUE ([UniqueID])
);
WITH DIGITS AS
(
select n
from (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) v(n)
),
NUMS AS
(
select (d5.n*10000 + d4.n*1000 + d3.n*100 + d2.n * 10 + d1.n) as n
from DIGITS d1
cross join DIGITS d2
cross join DIGITS d3
cross join DIGITS d4
cross join DIGITS d5
)
INSERT INTO #tmpRandoms ([UniqueID])
SELECT DISTINCT LEFT(REPLACE(REPLACE((select CAST(NEWID() as varbinary(16)), n FOR XML PATH(''), BINARY BASE64),'+',''),'/',''), 8) AS [UniqueID]
FROM NUMS;
Then update your table with it
WITH CTE AS
(
SELECT ROW_NUMBER() OVER (ORDER BY ID) AS RN, [UniqueID]
FROM YourTable
)
UPDATE t
SET t.[UniqueID] = tmp.[UniqueID]
FROM CTE t
JOIN #tmpRandoms tmp ON tmp.ID = t.RN;
A test on rextester here
Can you just use numbers and assign a randomish value?
with toupdate as (
select t.*,
row_number() over (order by newid()) as random_enough
from mytable t
)
update toupdate
set UniqueID = right(concat('00000000', random_enough), 8);
See: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/a289ed64-2038-415e-9f5d-ae84e50fe702/generate-random-string-of-length-5-az09?forum=transactsql
Alter: DECLARE #s char(5) and SELECT TOP (5) c1 to fix length you want.
I have two tables: table 1 a package_id and a timestamp column for which I have no weight information available, table 2 a package_id, a timestamp and a weight column where I do have the information.
What I'm trying to do is fill in the table 1 weight information based on table 2 using the following restrictions:
use the closest package_id available ie. for package_id 1 use 2 if available, if not 3 etc
if there is only one weight available use it for all the missing package_id's
if two weights are available, use the higher one ie. for package_id 5, if 4 and 6 are available use 6
The code:
IF OBJECT_ID('tempdb..#TIMEGAPS') IS NOT NULL DROP TABLE #TIMEGAPS
CREATE TABLE #TIMEGAPS (PACK_ID INT, Local_Time DATETIME)
IF OBJECT_ID('tempdb..#REALVALUES') IS NOT NULL DROP TABLE #REALVALUES
CREATE TABLE #REALVALUES (PACK_ID INT, Local_Time DATETIME, WEIGHT INT)
INSERT INTO #TIMEGAPS VALUES
(1,'2018-01-20 18:40:00.000'),
(1,'2018-01-20 18:50:00.000'),
(1,'2018-01-20 19:00:00.000'),
-----------------------------
(7,'2018-01-20 18:40:00.000'),
(7,'2018-01-20 18:50:00.000'),
(7,'2018-01-20 19:00:00.000'),
------------------------------
(12,'2018-01-20 18:40:00.000'),
(12,'2018-01-20 18:50:00.000'),
(12,'2018-01-20 19:00:00.000'),
(12,'2018-01-20 20:00:00.000')
INSERT INTO #REALVALUES VALUES
(2,'2018-01-20 18:40:00.000',50),
(3,'2018-01-20 18:40:00.000',70),
(4,'2018-01-20 18:40:00.000',150),
(5,'2018-01-20 18:40:00.000',60),
(6,'2018-01-20 18:40:00.000',45),
(8,'2018-01-20 18:40:00.000',55),
(9,'2018-01-20 18:40:00.000',25),
---------------------------------
(2,'2018-01-20 18:50:00.000',75),
(3,'2018-01-20 18:50:00.000',80),
(4,'2018-01-20 18:50:00.000',120),
(5,'2018-01-20 18:50:00.000',110),
(11,'2018-01-20 18:50:00.000',30),
---------------------------------
(8,'2018-01-20 19:00:00.000',70)
EDIT:
I've adapted the solution from here which I believe is what I need.
SELECT tg.PACK_ID, tg.Local_Time, p.WEIGHT
FROM #TIMEGAPS tg
OUTER APPLY
(
SELECT TOP 1 *, ABS(tg.PACK_ID - rv.PACK_ID) AS diff
FROM #REALVALUES rv
WHERE (tg.Local_Time = rv.Local_time OR rv.Local_time is null)
ORDER BY CASE WHEN rv.Local_time IS NULL THEN 2 ELSE 1 END,
ABS(rv.PACK_ID- tg.PACK_ID) ASC
) p
EDIT 2:
3. if two weights are available, use the highest PACK_ID ie. for package_id 5, if PACK_ID 4 and PACK_ID 6 are available use 6
Something like this?
It uses a row_number by distance.
SELECT
PACK_ID, Local_Time, WEIGHT
FROM (
SELECT g.PACK_ID, g.Local_Time, v.WEIGHT,
ROW_NUMBER() OVER (PARTITION BY g.PACK_ID, g.Local_Time
ORDER BY ABS(v.PACK_ID - g.PACK_ID), v.PACK_ID DESC) AS RN
FROM #TIMEGAPS AS g
JOIN #REALVALUES AS v ON v.Local_Time = g.Local_Time
) AS q
WHERE RN = 1
ORDER BY PACK_ID, Local_Time
I suppose we can start here. Make sure to initialize your tables before you run this. I assumed that #TIMEGAPS has a weight column based on your output.
DECLARE
#Pack_id INT
, #Weight_id INT
, #mloop INT = 0
DECLARE #possible TABLE (
id INT IDENTITY (1,1)
, pack_id INT
, weight )
BEGIN_LABEL:
SELECT TOP 1 #pack_id = PACK_ID
FROM #TIMEGAPS AS g
WHERE g.WEIGHT IS NULL
ORDER BY PACK_ID ASC
IF #pack_id IS NULL
BEGIN
PRINT 'Done'
EXIT
END
INSERT INTO #possible (pack_id , weight )
SELECT PACK_ID , WEIGHT
FROM #REALVALUES as r
LEFT JOIN #TIMEGAPS as g
ON g.WEIGHT = r.PACK_ID
WHERE g.WEIGHT IS NULL
ORDER BY ABS(#pack_id - PACK_ID) ASC , WEIGHT DESC
SELECT TOP 1 #Weight_id = weight
FROM #possible
ORDER BY id ASC
IF (#Weight_id IS NULL)
BEGIN
RAISERROR('No Weights available' , 18 , 1)
EXIT
END
UPDATE #TIMEGAPS
SET WEIGHT = #Weight_id
WHERE PACK_ID = #Pack_id
SET #mloop = #mloop + 1
IF #mloop > 99
BEGIN
PRINT 'Hit Safety'
EXIT
END
SELECT #Pack_id = NULL , #Weight_id = NULL;
DELETE #possible;
GOTO BEGIN_LABEL
SELECT g.PACK_ID , g.Local_Time , r.WEIGHT
FROM #TIMEGAPS AS g
INNER JOIN #REALVALUES AS r
r.PACK_ID = g.WEIGHT;
Im pretty sure RAISERROR() works in SQL 2008, but you can just replace them with print statements if they don't
I have a table like below. I'm trying to do a count of IDs that are not duplicated. I don't mean a distinct count. A distinct count would return a result of 7 (a, b, c, d, e, f, g). I want it to return a count of 4 (a, c, d, f). These are the IDs that do not have multiple type codes. I've tried the following queries but got counts of 0 (the result should be a count in the millions).
select ID, count (ID) as number
from table
group by ID
having count (ID) = 1
Select count (distinct ID)
From table
Having count (ID) = 1
ID|type code
a|111
b|222
b|333
c|444
d|222
e|111
e|333
e|555
f|444
g|333
g|444
thanks to #scaisEdge! The first query you provided gave me exactly what I'm looking for in the above question. Now that that's figured out my leaders have asked for it to be taken a step further to show the count of how many times there is an ID within a single type code. For example, we want to see
type code|count
111|1
222|1
444|2
There are 2 instances of IDs that have a single type code of 444 (c, f), there is one instance of an ID that has a single type code of 111 (a), and 222 (d). I've tried modifying the query as such, but have been coming across errors when running the query
select count(admin_sys_tp_cd) as number
from (
select cont_id from
imdmadmp.contequiv
group by cont_id
having count(*) =1) t
group by admin_sys_tp_cd
If you want the count Could be
select count(*) from (
select id from
my_table
group by id
having count(*) =1
) t
if you want the id
select id from
my_table
group by id
having count(*) =1
Hou about this you do a loop in a temporary table?:
select
*
into #control
from tablename
declare #acum as int
declare #code as char(3)
declare #id as char(1)
declare #id2 as int
select #acum=0
while exists (select* from #control)
begin
select #code = (select top 1 code from #control order by id)
select #id = (select top 1 id from #control order by id)
select #id2 =count(id) from #control where id in (select id from tablename where id = #id and code <> #code)
if #id2=0
begin
select #acum = #acum+1
end
delete #control
where id = #id --and code = #code
end
drop table #control
print #acum
Here's an example of what I've attempted thus far:
A mockup of what the tables look like:
Inventory
ID | lowrange | highrange
-------------------------------
1 | 15 | 20
2 | 21 | 30
Audit (not used in this query asides from the join)
MissingOrVoid
ID | Item | Missing | Void
---------------------------------
1 | 17 | 1 | 0
1 | 19 | 1 | 0
The most recent query I've attempted to use:
SELECT I.*,
SUM(
(I.HIGHRANGE - I.LOWRANGE + 1)
- (Count(M.Missing) from M where M.ID = I.ID)
- (Count(M.Void) from M where M.ID = I.ID)) AS Item_Quantity
FROM Inventory I
JOIN Audit A
ON A.ID = I.ID
JOIN MissingOrVoid M
ON M.ID = I.ID
The result should be:
ID | lowrange | highrange | Item_Quantity
-----------------------------------------------
1 | 15 | 20 | 4
2 | 21 | 30 | 10
I can't remember exactly where I've made changes, but in a previous attempt the error message received prior was "Cannot perform an aggregate function on an expression containing an aggregate or a subquery." Currently the error is incorrect syntax near "from" (the one beside M.Missing but in my minimal knowledge of SQL, it appears that these syntax issues cause an outright failure and there may be underlying issues with the query that aren't visible until all of the syntax problems are fixed).
The part where I'm really bombing on is obviously the SUM() section. I am far from a database architect so could someone explain how to correctly perform this and possibly point me in the direction of a resource to learn about this type of function?
Thanks
You almost had it right. I am guessing missing/void are BIT types, which you cannot SUM directly.
SELECT I.*,
(I.HIGHRANGE - I.LOWRANGE + 1)
- (select Count(nullif(M.Missing,0)) from MissingOrVoid M where M.ID = I.ID)
- (select Count(nullif(M.Void,0)) from MissingOrVoid M where M.ID = I.ID)
AS Item_Quantity
FROM Inventory I
If an item cannot both be missing and void, then
SELECT I.*,
I.HIGHRANGE - I.LOWRANGE + 1
- (select Count(case when M.Missing=1 or M.Void=1 then 1 end)
from MissingOrVoid M where M.ID = I.ID)
AS Item_Quantity
FROM Inventory I
In fact, if it is only present in MissingOrVoid when it is missing or void, then the CASE in the above query will always be true, so this simplifies to
SELECT I.*,
I.HIGHRANGE - I.LOWRANGE + 1
- (select Count(*) from MissingOrVoid M where M.ID = I.ID)
AS Item_Quantity
FROM Inventory I
Initially I have a question as to whether you need to sum those values. If your inventory table has one row per item, that shouldn't be necessary. I'll assume that your table can have multiple rows for a given item, though, and proceed from there.
I think the issue is just a problem with the construction of the subquery. I haven't tested this, but I think it should look more like:
select I.ID,
I.Item,
SUM(I.HighRange - I.LowRange + 1)
- (
select SUM(M.Missing + M.Void)
from dbo.Audit A
where A.ID = I.ID
)
from Inventory I
group by I.ID, I.Item
Is this what you're trying to do? I'm not sure what the numbers in the missing and void columns are unless they're just flags...
SELECT I.*,
((I.highrange - I.lowrange + 1)
- SUM(M.Missing)
- SUM(M.Void)) AS Item_Quantity
FROM Inventory I
JOIN MissingOrVoid M
ON M.ID = I.ID
The following query works. This assumes there is only one highrange and lowrange for each id.
CREATE TABLE #Inventory (ID INT,Lowrange INT,highrange INT)
CREATE TABLE #MissingOrVoid (Id INT,item INT, missing INT, void INT)
INSERT #Inventory
( ID, Lowrange, highrange )
VALUES ( 1, -- ID - int
15, -- Lowrange - int
20 -- highrange - int
)
INSERT #Inventory
( ID, Lowrange, highrange )
VALUES ( 2, -- ID - int
21, -- Lowrange - int
30 -- highrange - int
)
INSERT #MissingOrVoid
( Id, item, missing, void )
VALUES ( 1, -- Id - int
17, -- item - int
1, -- missing - int
0 -- void - int
)
INSERT #MissingOrVoid
( Id, item, missing, void )
VALUES ( 1, -- Id - int
19, -- item - int
1, -- missing - int
0 -- void - int
)
SELECT #Inventory.ID,
#Inventory.highrange,
#Inventory.Lowrange,
highrange-Lowrange+1
-SUM(ISNULL(missing,0))
-SUM(ISNULL(void,0)) AS ITEM_QUANTITY
FROM #Inventory
left JOIN #MissingOrVoid ON #Inventory.ID = #MissingOrVoid.Id
GROUP BY #Inventory.ID,#Inventory.highrange,#Inventory.Lowrange
DROP TABLE #Inventory
DROP TABLE #MissingOrVoid
I'd say this would work :
SELECT I.ID,I.Lowrange as Lowrange,
I.highrange as Highrange,
Highrange-Lowrange+1-COUNT(J.missing)-COUNT(J.void) AS ITEM_QUANTITY
FROM Inventory I
left JOIN ( select missing as missing, void as void, id from MissingOrVoid
) J
ON I.ID = J.Id
JOIN Audit A
ON A.ID = I.ID
GROUP BY I.ID,Highrange,Lowrange
But it looks like what RemoteSojourner suggested a lot (and his one is also more esthetic).
I'm going to give the derived table approach as it could be faster than a correlated subquery (which run row by row)
SELECT I.*,
I.HIGHRANGE - I.LOWRANGE + 1 - MissingVoidCount AS Item_Quantity
FROM Inventory I
JOIN
(SELECT ID,Count(*) AS MissingVoidCount FROM MissingOrVoid GROUP BY ID) M
on M.ID = I.ID
Of course in real life, I would never use select *. You also could use a CTE approach.
;WITH MissingVoid(ID, MissingVoidCount) AS
(
SELECT ID, Count(*) FROM MissingOrVoid GROUP BY ID
)
SELECT
I.*,
I.HIGHRANGE - I.LOWRANGE + 1 - MissingVoidCount AS Item_Quantity
FROM Inventory I
JOIN MissingVoid M
on M.ID = I.ID
I have a dynamic set of data X of the form:
----------------------------------
x.id | x.allocated | x.unallocated
----------------------------------
foo | 2 | 0
bar | 1 | 2
----------------------------------
And I need to get to a result of Y (order is unimportant):
----------------------------------
y.id | y.state
----------------------------------
foo | allocated
foo | allocated
bar | allocated
bar | unallocated
bar | unallocated
----------------------------------
I have a UTF based solution, but I'm looking for hyper-efficiency so I'm idly wondering if there's a statement based, non-procedural way to get this kind of "ungroup by" effect?
It feels like an unpivot, but my brain can't get there right now.
If you have a numbers table in your database, you could use that to help get your results. In my database, I have a table named Numbers with a Num column.
Declare #Temp Table(id VarChar(10), Allocated Int, UnAllocated Int)
Insert Into #Temp Values('foo', 2, 0)
Insert Into #Temp Values('bar',1, 2)
Select T.id,'Allocated'
From #Temp T
Inner Join Numbers
On T.Allocated >= Numbers.Num
Union All
Select T.id,'Unallocated'
From #Temp T
Inner Join Numbers
On T.unAllocated >= Numbers.Num
Using Sql Server 2005, UNPIVOT, and CTE you can try something like
DECLARE #Table TABLE(
id VARCHAR(20),
allocated INT,
unallocated INT
)
INSERT INTO #Table SELECT 'foo', 2, 0
INSERT INTO #Table SELECT 'bar', 1, 2
;WITH vals AS (
SELECT *
FROM
(
SELECT id,
allocated,
unallocated
FROM #Table
) p
UNPIVOT (Cnt FOR Action IN (allocated, unallocated)) unpvt
WHERE Cnt > 0
)
, Recurs AS (
SELECT id,
Action,
Cnt - 1 Cnt
FROM vals
UNION ALL
SELECT id,
Action,
Cnt - 1 Cnt
FROM Recurs
WHERE Cnt > 0
)
SELECT id,
Action
FROM Recurs
ORDER BY id, action
This answer is just to ping back to G Mastros and doesn't need any upvotes. I thought he would appreciate a performance boost to his already superior query.
SELECT
T.id,
CASE X.Which WHEN 1 THEN 'Allocated' ELSE 'Unallocated' END
FROM
#Temp T
INNER JOIN Numbers N
On N.Num <= CASE X.Which WHEN 1 THEN T.Allocated ELSE T.Unallocated END
CROSS JOIN (SELECT 1 UNION ALL SELECT 2) X (Which)