SQL: insert to unique column with shift - sql

I need to store data with varchar name and Integer intValue. All integer values are unique and I need to keep up that contract
I need to write query to add the element using the following rule: if after insertion there is an intValue duplication - we need to increase intValue of existed element to resolve conflict. Repeat that operation until no conflict left.
Example:
B | 2 | | B | 2 |
C | 3 | | E | 3 |
D | 4 | => insert (E 3) => | C | 4 |
A | 1 | | D | 5 |
Z | 7 | | A | 1 |
| Z | 7 |
The only idea is to run update query in a loop but that looks too unefficient.
I need to write this query in Spring JPA, so the only requirement that the query should not be database specific
Business case:
Let's say there is a people in the queue. And intValue is position in the queue. So, "Add" means that some person come, pay money and say: I dont wanna be the last in the queue. I want to be, for example, the 3rd. So you take the money and put that person in a queue so other people after him - increments their position.
The only difference from the queue - that in my case there are gaps allowed

Aha, we might say that the gaps are occasioned by people leaving the queue.
Lets try this. Loops are inevitable--either server does them, or we can do as SQL.
-- prepare test data
declare #PeopleQueue table (pqname varchar(100), intValue int);
insert into #PeopleQueue
SELECT 'B' AS pqname, 2 as intValue UNION ALL
SELECT 'C' AS pqname, 3 as intValue UNION ALL
SELECT 'D' AS pqname, 4 as intValue UNION ALL
SELECT 'A' AS pqname, 1 as intValue UNION ALL
SELECT 'Z' AS pqname, 7 as intValue
;
--SELECT '' AS pqname, 0 as intValue UNION ALL
Select * from #PeopleQueue; - verify good test data
-- Solve the problem
Declare #pqnameNEW varchar(100) = 'E';
Declare #intNEW int = 3; -- 3 for conflict, or for no conflict, use 13
Declare #intHIGH int;
IF EXISTS ( SELECT 1 FROM #PeopleQueue WHERE intValue = #intNEW )
BEGIN
-- find the end of the sequence, before the gap
SET #intHIGH = (
SELECT TOP 1
intValue
FROM #PeopleQueue pq
WHERE NOT EXISTS
(
SELECT NULL
FROM #PeopleQueue pn
WHERE pn.intValue = pq.intValue + 1
)
AND pq.intValue >= #intNEW
)
;
-- now Update all from intNEW thru intHIGH
UPDATE #PeopleQueue
SET intValue = intValue + 1
WHERE intValue >= #intNEW
AND intValue <= #intHIGH
End;
-- finally insert the new item
INSERT into #PeopleQueue Values (#pqnameNEW, #intNEW);
Select * from #PeopleQueue; -- verify correct solution
Edited--11/28 17:00
Or, estimate the number of Bump-the-Line-Inserts (vs append to the end inserts), and design the intValues to be originally in multiples of ten (10) so that long sequences of updates are minimized.

update queue
SET intValue = intValue + 1
WHERE intValue >= 3
AND intValue <= (
SELECT q1.intValue
FROM queue as q1 LEFT JOIN queue AS q2 ON q1.intValue + 1 = q2.intValue
WHERE q2.name is NULL AND q1.intValue > 3
ORDER BY q1.intValue
LIMIT 1
)

Related

SQL - Set column value to the SUM of all references

I want to have the column "CurrentCapacity" to be the SUM of all references specific column.
Lets say there are three rows in SecTable which all have FirstTableID = 1. Size values are 1, 1 and 3.
The row in FirstTable which have ID = 1 should now have a value of 5 in the CurrentCapacity column.
How can I make this and how to do automatically on insert, update and delete?
Thanks!
FirstTable
+----+-------------+-------------------------+
| ID | MaxCapacity | CurrentCapacity |
+----+-------------+-------------------------+
| 1 | 5 | 0 (desired result = 5) |
+----+-------------+-------------------------+
| 2 | 5 | 0 |
+----+-------------+-------------------------+
| 3 | 5 | 0 |
+----+-------------+-------------------------+
SecTable
+----+-------------------+------+
| ID | FirstTableID (FK) | Size |
+----+-------------------+------+
| 1 | 1 | 2 |
+----+-------------------+------+
| 2 | 1 | 3 |
+----+-------------------+------+
In general, a view is a better solution than trying to keep a calculated column up-to-date. For your example, you could use this:
CREATE VIEW capacity AS
SELECT f.ID, f.MaxCapacity, COALESCE(SUM(s.Size), 0) AS CurrentCapacity
FROM FirstTable f
LEFT JOIN SecTable s ON s.FirstTableID = f.ID
GROUP BY f.ID, f.MaxCapacity
Then you can simply
SELECT *
FROM capacity
to get the results you desire. For your sample data:
ID MaxCapacity CurrentCapacity
1 5 5
2 5 0
3 5 0
Demo on SQLFiddle
Got this question to work with this trigger:
CREATE TRIGGER UpdateCurrentCapacity
ON SecTable
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON
DECLARE #Iteration INT
SET #Iteration = 1
WHILE #Iteration <= 100
BEGIN
UPDATE FirstTable SET FirstTable.CurrentCapacity = (SELECT COALESCE(SUM(SecTable.Size),0) FROM SecTable WHERE FirstTableID = #Iteration) WHERE ID = #Iteration;
SET #Iteration = #Iteration + 1
END
END
GO
Personally, I would not use a trigger either or store CurrentCapacity as a value since it breaks Normalization rules for database design. You have a relation and can already get the results by creating a view or setting CurrentCapacity to a calculated column.
Your view can look like this:
SELECT Id, MaxCapacity, ISNULL(O.SumSize,0) AS CurrentCapacity
FROM dbo.FirstTable FT
OUTER APPLY
(
SELECT ST.FirstTableId, SUM(ST.Size) as SumSize FROM SecTable ST
WHERE ST.FirstTableId = FT.Id
GROUP BY ST.FirstTableId
) O
Sure, you could fire a proc every time a row is updated/inserted or deleted in the second table and recalculate the column, but you might as well calculate it on the fly. If it's not required to have the column accurate, you can have a job update the values every X hours. You could combine this with your view to have both a "live" and "cached" version of the capacity data.

Generating a Sequential Client Number

I have a client based system that is needing a sequential client number in the form of the following.
First client would get A001, and then each new client through to A999. Once it hits A999, it would continue to B001-B999, and so on until Z001-Z999, when it would reset to AA001-AA999 and so on through the alphabet.
Does anyone see a way of how this could be achieved?
This will give you the exact numbers you asked for from A001 to ZZ999. If you want more numbers than that you will need to add logic for a third letter, etc. Note that you aren't getting 1000 numbers per letter, which makes things slightly more awkward.
WITH Numbers AS (
SELECT 1 AS number
UNION ALL
SELECT number + 1 AS number FROM Numbers WHERE number < 701298)
SELECT
number,
CASE WHEN number > 25974 THEN CHAR(64 + (number - 1) / 25974) ELSE '' END --This is the first letter (optional)
+ CHAR(65 + ((number - 1) / 999) % 26) --This is the second letter
+ FORMAT(CASE WHEN number < 1000 THEN number ELSE CASE WHEN number % 999 = 0 THEN 999 ELSE number % 999 END END, 'd3') --This is the three digit number
AS client_id
FROM
Numbers
OPTION (MAXRECURSION 0);
The Numbers CTE is just to get a suitable number of numbers (1 - 701,298). Once I have them I need to find the boundaries when the second letter changes (every 999 numbers) or the first letter changes (every 26 * 999 = 25974 numbers). Note that the first letter is suppressed until needed.
This gives you 27 * 26 * 999 client ids (the first letter can be blank or A-Z = so 27 options, the second letter can be A-Z = 26 options, the number can be 001-999 = 999 options). That's a grand total of 701,298 client ids.
I would suggest either using an IDENTITY column, or a SEQUENCE to get the "internal" id (which would be a primary key candidate), and then use a function to calculate the client id from this number. That's safer for multiple users, etc. You could use a calculated column, but that's a pretty big overhead?
I'd use simple integers as the key and a stored procedure (or calculated column) which translates to your desired format.
I't essentially a numeric operation, check this SQL which calculates the format.
It does assume that you have no more than 2 letters in the beginning, so number of clients is under 26 * 26 * 1000.
select tmp.num as client_num, CONCAT(
CASE WHEN tmp.num < 26000 THEN '' ELSE CHAR(ASCII('A') - 1 + (tmp.num / 26000)) END,
CHAR (ASCII('A') + (tmp.num / 1000) % 26),
RIGHT('000'+CAST(tmp.num % 1000 AS VARCHAR(3)),3)) as client_id
from
(select 1 as 'num'
union
select 10
union
select 150
union
select 1000
union
select 25999
union
select 26000
union
select 27000
union
select 100000) tmp
Returns table:
+------------+-----------+
| client_num | client_id |
+------------+-----------+
| 1 | A001 |
| 10 | A010 |
| 150 | A150 |
| 1000 | B000 |
| 25999 | Z999 |
| 26000 | AA000 |
| 27000 | AB000 |
| 100000 | CW000 |
+------------+-----------+
EXAMPLE from the comments: (may not be an answer, posted here just because it is long)
CREATE SEQUENCE Numbers
INCREMENT BY 1
MINVALUE 1
MAXVALUE 999
CYCLE
;
--DROP TABLE test_DL
Create table test_DL
(
VendorName varchar(50),
VendorId as LeadingCharacters + CAST(FORMAT(TailingNumbers,'000') as VARCHAR(10)),
LeadingCharacters VARCHAR(50),
TailingNumbers INT DEFAULT(NEXT VALUE FOR Numbers),
[Counter] INT IDENTITY(1,1)
)
--ALTER SEQUENCE Numbers RESTART WITH 1
DECLARE #CONTROL INT = 0
WHILE (#CONTROL < 250)
BEGIN
INSERT INTO test_DL (VendorName)
VALUES ('THIS'),('IS'),('AN'),('EXAMPLE')
SET #CONTROL = #CONTROL + 1
END
;
WITH CTE
AS
(
SELECT *, ROW_NUMBER()OVER(ORDER BY [Counter],TailingNumbers) as RowNumber
FROM test_DL
)
UPDATE CTE
SET LeadingCharacters = CASE WHEN RowNumber <= 999 THEN 'A' WHEN 999 < RowNumber AND RowNumber < 2* 999 THEN 'B' END --The MOST ANNOYING PART is here, you need to manually category all the possibles
SELECT * FROM test_DL --Run this to check the result
Above method will be very dumb for future updates. Just give you some ideas lol

Use WITH to loop over a set of data in SQL

Given the following fields below, I'm trying to loop to the first iteration of the total set of iterations.
+-------------------+----------------------+------------------------+
| id | nextiterationId | iterationCount |
+-------------------+----------------------+------------------------+
| 110001 | 110002 | 0 |
| 110002 | 110003 | 1 |
| 110003 | 110004 | 2 |
| 110004 | 1 | 3 |
So if I call a SP/function using one of the values of the id field, I need it return the prior iterations of the id given until iterationCount = 0.
So If I use id of 110003(send that as the parameter), the first thing it should query is an id field having a nextIterationID of 110003. That would be the first loop.
Since the iterationCount is not 0 yet, it would keep looping. Then it would look for an id where nextIterationID is 110002 based on first loop determination, so second loop will find "id" of 110001 and return it. And since that record iterationCount = 0, it would stop the loop.
It's okay if I call the SP/function using 110003, which is the 3rd iteration, and it would not return the 110004, 4th iteration. I only need it to go back given the id.
A while ago I did this using a WITH and maybe WHILE using both somehow, but I can't recall how to do this now. I need the format returned in a way so that I can use it in a larger SELECT statements.
Here is a recursive cte solution. Let me know if it needs any tweaks.
--Throwing your table into a temp table
CREATE TABLE #yourTable (ID INT,NextIterationID INT,IterationCount INT)
INSERT INTO #yourTable
VALUES
(110001,110002,0),
(110002,110003,1),
(110003,110004,2),
(110004,1,3)
--Now for the actual work
--Here is your parameter
DECLARE #param INT = 110003;
--Recursive CTE
WITH yourCTE
AS
(
--Initial Row
SELECT ID,
NextIterationID,
IterationCount
FROM #yourTable
WHERE NextIterationID = #param
UNION ALL
--Finding all previous iterations
SELECT #yourTable.*
FROM #yourTable
INNER JOIN yourCTE
ON yourcte.ID = #yourTable.NextIterationID
--Where clause is not really necessary because once there are no more previous iterations, it will automatically stop
--WHERE yourCTE.IterationCount >= 0
)
SELECT *
FROM yourCTE
--Cleanup
DROP TABLE #yourTable
Results:
ID NextIterationID IterationCount
----------- --------------- --------------
110002 110003 1
110001 110002 0

How to get first n numbers from float

I have table A with two columns id(int) and f_value(float). Now I'd like to select all rows where f_value starts from '123'. So for the following table:
id | f_value
------------
1 | 12
2 | 123
3 | 1234
I'd like to get the second and third row. I tried to use LEFT with cast but that was a disaster. For the following query:
select f_value, str(f_value) as_string, LEFT(str(f_value), 2) left_2,
LEFT(floor(f_value), 5) flor_5, LEFT('abcdef', 5) test
from A
I got:
f_value | as_string | left_2 | flor_5 | test
------------------------------------------------
40456510 | 40456510 | | 4.045 | abcde
40454010 | 40454010 | | 4.045 | abcde
404020 | 404020 | | 40402 | abcde
40452080 | 40452080 | | 4.045 | abcde
101020 | 101020 | | 10102 | abcde
404020 | 404020 | | 40402 | abcde
The question is why left works fine for 'test' but for other returns such weird results?
EDIT:
I made another test I now I'm even more confused. For query:
Declare #f as float
set #f = 40456510.
select LEFT(cast(#f as float), LEN(4045.)), LEFT(404565., LEN(4045.))
I got:
|
------------
4.04 | 4045
Is there a default cast which causes this?
Fiddle SQL
Seems like your query is a bit wrong. The LEFT part should go in the WHERE-Clause, not the SELECT-part.
Also, just use LIKE and you should be fine:
SELECT f_value, str(f_value) as_string, LEFT(str(f_value), 2) left_2,
LEFT(floor(f_value), 5) flor_5
WHERE f_value LIKE '123%'
CREATE TABLE #TestTable(ID INT, f_value FLOAT)
INSERT INTO #TestTable
VALUES (1,22),
(2,123),
(3,1234)
SELECT *
FROM #TestTable
WHERE LEFT(f_value,3)='123'
DROP TABLE #TestTable
I hope this will help.
The replace get rid of the period in the float, by multiplying by 1 any 0 in front will be removed.
SELECT f_value
FROM your_table
WHERE replace(f_value, '.', '') * 1 like '123%'
I found the solution. The problem was that SQL Server uses the exponential representation of floats. To resolve it you need to first convert float to BigInt and then use Left on it.
Example:
Select * from A where Left(Cast(float_value as BigInt), 4) = xxxx
/*
returns significant digits from #f (a float) as an integer
negative sign is stripped off
*/
declare #num_digits int = 3; /* needs to be positive; accuracy diminishes with larger values */
with samples(num, f) as (
select 1, cast(123.45 as float) union
select 2, 123456700 union
select 3, -1.234567 union
select 4, 0.0000001234
)
select num, f,
case when f = 0 or #num_digits < 1 then 0 else
floor(
case sign(log10(abs(f)))
when -1 then abs(f) * power(10e0, -floor(log10(abs(f))) + #num_digits - 1)
when 1 then abs(f) / power(10e0, ceiling(log10(abs(f))) - #num_digits)
end
)
end as significant_digits
from samples
order by num;
sqlfiddle
Convert the FLOAT value to DECIMAL then to VARCHAR using CAST AND use LIKE to select the value starting with 4045.
Query
SELECT * FROM tbl
WHERE CAST(CAST(f_value AS DECIMAL(20,12)) AS VARCHAR(MAX)) LIKE '4045%';
Fiddle demo for reference

MySQL: Get Root Node of Parent-Child Structure

I have a table similar to this:
=================
| Id | ParentId |
=================
| 1 | 0 |
-----+-----------
| 2 | 1 |
-----+-----------
| 3 | 0 |
-----+-----------
| 4 | 3 |
-----+-----------
| 5 | 3 |
-----+-----------
| 6 | 0 |
-----+-----------
| 7 | 6 |
-----+-----------
| 8 | 7 |
-----------------
Given an Id, I need to know its root "node" Id. So,
Given 1, return 1
Given 2, return 1
Given 3, return 3
Given 4, return 3
Given 5, return 3
Given 6, return 6
Given 7, return 6
Given 8, return 7
There is no limit to the levels of the hierarchy. Is there a SQL that can do what I need?
Actually, you can quite easily do this using a function.
Try running the following .sql script on your favorite empty test database.
--
-- Create the `Nodes` table
--
CREATE TABLE `Nodes` (
`Id` INT NOT NULL PRIMARY KEY
,`ParentId` INT NOT NULL
) ENGINE=InnoDB;
--
-- Put your test data into it.
--
INSERT INTO `Nodes` (`Id`, `ParentId`)
VALUES
(1, 0)
, (2, 1)
, (3, 0)
, (4, 3)
, (5, 3)
, (6, 0)
, (7, 6)
, (8, 7);
--
-- Enable use of ;
--
DELIMITER $$
--
-- Create the function
--
CREATE FUNCTION `fnRootNode`
(
pNodeId INT
)
RETURNS INT
BEGIN
DECLARE _Id, _ParentId INT;
SELECT pNodeId INTO _ParentId;
my_loop: LOOP
SELECT
`Id`
,`ParentId`
INTO
_Id
,_ParentId
FROM `Nodes`
WHERE `Id` = _ParentId;
IF _ParentId = 0 THEN
LEAVE my_loop;
END IF;
END LOOP my_loop;
RETURN _Id;
END;
$$
--
-- Re-enable direct querying
--
DELIMITER ;
--
-- Query the table using the function to see data.
--
SELECT
fnRootNode(`Nodes`.`Id`) `Root`
,`Nodes`.`Id`
,`Nodes`.`ParentId`
FROM `Nodes`
ORDER BY
fnRootNode(`Nodes`.`Id`) ASC
;
-- EOF
Output will be:
Root Id ParentId
==== ==== ========
1 1 0
1 2 1
3 3 0
3 4 3
3 5 3
6 6 0
6 7 6
6 8 7
Here is a short query doing what you're asking, assuming your table is called foo and that you want to know the root of <id>:
SELECT f.Id
FROM (
SELECT #id AS _id, (SELECT #id := ParentId FROM foo WHERE Id = _id)
FROM (SELECT #id := <id>) tmp1
JOIN foo ON #id <> 0
) tmp2
JOIN foo f ON tmp2._id = f.Id
WHERE f.ParentId = 0
This is quite difficult to do in MySQL because it doesn't yet support recursive common table expressions.
I'd suggest instead using a nested sets model, or else storing the root node in the row and updating it as the structure changes.
In short: no. Look at regular Bill Karwin's excellent presentation about hierarchical models and it's uses, shortcomings, and how to get around those: http://www.slideshare.net/billkarwin/models-for-hierarchical-data
I used #Kris answer for a while successfully, until I faced an issue where a child node might got deleted (accidentally), as a result the function gets into an infinite loop and hangs the mysql database at all, following is the modified version which works in my case:
DELIMITER $$
CREATE FUNCTION `FindRootNode`(InputValue INT(11)) RETURNS INT(11)
NO SQL
BEGIN
DECLARE ReturnValue, _ParentId INT;
SELECT InputValue INTO _ParentId;
REPEAT
SET ReturnValue = _ParentId;
SELECT IFNULL((SELECT parent_id FROM TableName WHERE id=ReturnValue), 0) INTO _ParentId;
UNTIL _ParentId = 0
END REPEAT;
RETURN ReturnValue;
END $$
DELIMITER ;
Usage1
SELECT CompanyCategoryTestRoot(HERE_COMES_CHILD_NODE_VALUE)