SQL Server : unexpected behaviour in while, when using INSERT INTO - sql-server-2008

I guess this is going to be a very easy one for anyone who knows SQL programming...
SQLFiddle
Here is the code I am executing against the database:
DECLARE #maxCounter int
-- Used to get the maximum bound number for my loop, basically what is the highest number of records. Tested, seems to work as expected.
SET #maxCounter = (SELECT TOP 1 COUNT(SN)
FROM TestResults
WHERE Type = 'EX'
GROUP BY SN
ORDER BY COUNT(SN) DESC)
CREATE TABLE #Info
(
DLoc VARCHAR(500),
DCode VARCHAR(500),
Dobs VARCHAR(500)
)
DECLARE #counter INT
SET #counter = 1
WHILE #counter <= #maxCounter
BEGIN
INSERT INTO #Info (DLoc)
VALUES ('Location_' + CAST(#counter AS VARCHAR(16)))
INSERT INTO #Info (DCode)
VALUES ('Code_' + CAST(#counter AS VARCHAR(16)))
INSERT INTO #Info (Dobs)
VALUES ('Observation_' + CAST(#counter AS VARCHAR(16)))
SET #counter = #counter + 1
END
SELECT * FROM #Info;
DROP TABLE #Info;
If you see some weird things in the code, then that is because I am a total beginner and do not know any better.
Expected output of the while loop:
+------------+---------+---------------+
| defLoc | defCode | obs |
+------------+---------+---------------+
| Location_1 | Code_1 | Observation_1 |
| Location_2 | Code_2 | Observation_2 |
| Location_3 | Code_3 | Observation_3 |
+------------+---------+---------------+
Unexpected output result:
defLoc | defCode | obs |
-----------------+-------------+---------------|
Location_1 | | |
| Code_1 | |
| | Observation_1 |
Location_2 | | |
| Code_2 | |
| | Observation_2 |
Location_3 | | |
| Code_3 | |
| | Observation_3 |
I have no clue where the empty cells come from...

You need to use ONE INSERT for each iteration, and specify all three columns and their values in one go:
WHILE #counter <= #maxCounter
BEGIN
INSERT INTO #Info (DLoc, DCode, Dobs)
VALUES ('Location_' + CAST(#counter AS VARCHAR(16)),
'Code_' + CAST(#counter AS VARCHAR(16)),
'Observation_' + CAST(#counter AS VARCHAR(16))
)
SET #counter = #counter + 1
END
Each INSERT will insert a whole row - the value you provided is inserted into the column you specified, but the other columns will all remain NULL.

You can insert each row with 3 different column values with a single insert statement in a loop:
WHILE #counter <= #maxCounter
BEGIN
INSERT INTO #Info (DLoc,DCode,Dobs)
VALUES (
'Location_' + CAST(#counter AS VARCHAR(16))
, 'Code_' + CAST(#counter AS VARCHAR(16))
, 'Observation_' + CAST(#counter AS VARCHAR(16))
);
SET #counter = #counter + 1;
END;
Alternatively, you could insert all rows at once in a single INSERT...SELECT statement using a tally table as the source. If you have no tally table, you can use a CTE to generate the set of numbers. The example below uses a CTE with ROW_NUMBER() that can be extended as needed.
WITH
t10 AS (SELECT n FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) t(n))
,t1k AS (SELECT 0 AS n FROM t10 AS a CROSS JOIN t10 AS b CROSS JOIN t10 AS c)
,t1m AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS num FROM t1k AS a CROSS JOIN t1k AS b CROSS JOIN t1k AS c)
INSERT INTO #Info (DLoc,DCode,Dobs)
SELECT
'Location_' + CAST(num AS VARCHAR(16))
, 'Code_' + CAST(num AS VARCHAR(16))
, 'Observation_' + CAST(num AS VARCHAR(16))
FROM t1m
WHERE num <= #maxCounter;
In SQL Server, set-based operations generally outperform procedural looping constructs by orders of magnitude.

Related

How can I add a sequencial number without cursor in mysql?

I have a table that can have some valid duplicated values so I need an additional column with the sequence number of appearance of said duplicate for future use.
A sample might be
ROW | COLUMN_A | COLUMN_B | COLUMN_C | SEQ_NUM <= Want this column
1 A B 1 1
2 A B 1 2
3 A B 2 1
4 A B 2 2
5 A B 2 3
The values are supposed to be unique like (COLUMN_A, COLUMB_B, COLUMN_C), but I cannot use a unique index because I need those duplicated values as well, I just need to keep track of the order of apparition. So I added a column SEQ_NUM to keep track of those repetitions.
And i fill it like this:
begin
declare done boolean default false;
declare _A varchar(1);
declare _B varchar(1);
declare _C integer unsigned;
declare cur cursor for
select COLUMN_A , COLUMN_B , COLUMN_C
from tmp_horario
group by COLUMN_A , COLUMN_B , COLUMN_C
having count(*) > 1; -- Here I loop throught the repeated values
declare continue handler for not found set done := true;
open cur;
loop_dup: loop
fetch cur into _A, _B, _C;
if done then
leave loop_dup;
end if;
set #_seq = 0; -- I initialize my sequence in 0 to start
update tmp_table h
set h.SEQ_NUM = (#_seq := #_seq + 1) -- Set the next sequential to the repeated values
where h.COLUMN_A = _A
and h.COLUMN_B = _B
and h.COLUMN_C = _C;
end loop loop_dup;
close cur;
end;
Note: The table has way more columns making the cursor (fetch into) a bigger pain.
As you can see that works like charm except that it takes my store from 20 s to 80 s which I find a little disappointing (already checked indexes and they are being properly used), I believe the problem lies in the use of the cursor.
My question then is: Is there a way of setting that famous sequential number in a single query without the cursor?.
Assuming you want this to happen when you insert a value to the table you could do this as such:
INSERT INTO tmp_horario(COLUMN_A, COLUMN_B, COLUMN_C, SEQ_NUM)
VALUE(A_VAL, B_VAL, C_VAL, (IFNULL((
SELECT MAX(SEQ_NUM)
FROM tmp_horario AS a
WHERE a.COLUMN_A = A_VAL AND a.COLUMN_B = B_VAL AND a.COLUMN_C = C_VAL), 0)+1));
The basic premise is you look for rows with the same values, get the maximum sequential value if one exists, and then add one to that for the new value. If no match is found then set the insert value to one. The IFNULL statement is really all you need to get the SEQ_NUM, should you need to adapt this query.
Yes pretty much like your cursor
DROP TABLE IF EXISTS T;
CREATE TABLE T(ROW INT, COLUMN_A VARCHAR(1), COLUMN_B VARCHAR(1), COLUMN_C VARCHAR(1), SEQ_NUM INT);
INSERT INTO T VALUES
(1 , 'A' , 'B' , 1,NULL),
(2 , 'A' , 'B' , 1,NULL),
(3 , 'A' , 'B' , 2,NULL),
(4 , 'A' , 'B' , 2,NULL),
(5 , 'A' , 'B' , 2,NULL);
UPDATE T
JOIN (
SELECT T.ROW,
IF(CONCAT(T.COLUMN_A,T.COLUMN_B,T.COLUMN_C) <> #P , #RN:=1,#RN:=#RN+1) RN,
#P:=CONCAT(T.COLUMN_A,T.COLUMN_B,T.COLUMN_C) P
FROM T , (SELECT #RN:=0,#P:=0) R
ORDER BY ROW
) S ON S.ROW = T.ROW
SET SEQ_NUM = S.RN
WHERE 1 = 1
MariaDB [sandbox]> SELECT * FROM T;
+------+----------+----------+----------+---------+
| ROW | COLUMN_A | COLUMN_B | COLUMN_C | SEQ_NUM |
+------+----------+----------+----------+---------+
| 1 | A | B | 1 | 1 |
| 2 | A | B | 1 | 2 |
| 3 | A | B | 2 | 1 |
| 4 | A | B | 2 | 2 |
| 5 | A | B | 2 | 3 |
+------+----------+----------+----------+---------+
5 rows in set (0.00 sec)

INSERT another field using number from the same id when insert new value

I'am using sql server 2012
my table like
table : Order
|idOrder| idCustomer | dateTransaction| invoice |
| 1001 | 104 | 2014-06-09 | |
idOrder using autoincrement, I want to ask how get id order that just inserted and combine with date transaction,
my SP for insertOrder :
BEGIN
INSERT INTO Order
(
idCustomer,
transactionDate,
invoice
)
VALUES
(
#idCustomer,
GETDATE(),
CAST(datepart(year, getdate()) AS VARCHAR(10)) + CAST(datepart(month, getdate()) AS VARCHAR(10)) + CAST(#idCustomer AS VARCHAR(10))
)
END
so far I get
|idOrder| idCustomer | dateTransaction| invoice |
| 1001 | 104 | 2014-06-09 | 201406104 |
what I want is
|idOrder| idCustomer | dateTransaction| invoice |
| 1001 | 104 | 2014-06-09 | 2014061001 | --> 1001 is idOrder
how to get idOrder (auto increment) into invoice field at the same time when insert new data / or
how exec SP update after exec SP insert to get the idOrder, because IdOrder is autoincrement so it's no a parameter
thanks
the complete code that works !
BEGIN
INSERT INTO Order
(
idCustomer,
transactionDate,
invoice
)
VALUES
(
#idCustomer,
GETDATE(),
CAST(datepart(year, getdate()) AS VARCHAR(10)) + CAST(datepart(month, getdate()) AS VARCHAR(10)) + CAST(#idCustomer AS VARCHAR(10))
)
END
DECLARE #ID INT
SELECT #ID = SCOPE_IDENTITY()
UPDATE TR_Order
SET Invoice = CAST(Invoice AS VARCHAR) + CAST(#ID AS VARCHAR)
WHERE IdOrder = #ID
First Insert the record and get the inserted ID by using
SCOPE_IDENTITY()
OR
##IDENTITY
DECLARE #ID INT
SELECT #ID = SCOPE_IDENTITY()
UPDATE TABLE_Name
SET Invoice = CAST(Invoice AS VARCHAR) + CAST(#ID AS VARCHAR)
WHERE IdOrder = #ID
Then update the table again, because you cannot get the ID until you insert the record.
Insert your record
Get the id
Perform Update to that record
Let's say we have this table
CREATE TABLE my_table (id INT IDENTITY(1,1), col1 VARCHAR(32));
Now here's your code
DECLARE #id INT;
-- here is your insert statement (1)
INSERT INTO my_table (col1) VALUES('000');
-- get the id (2)
SET #id = scope_identity();
-- perform update to that record
UPDATE my_table
SET col1=col1 + CAST(#id AS VARCHAR)
WHERE id=#id;
Demo

What is the opposite of GROUP_CONCAT in MySQL?

I seem to come against this problem a lot, where I have data that's formatted like this:
+----+----------------------+
| id | colors |
+----+----------------------+
| 1 | Red,Green,Blue |
| 2 | Orangered,Periwinkle |
+----+----------------------+
but I want it formatted like this:
+----+------------+
| id | colors |
+----+------------+
| 1 | Red |
| 1 | Green |
| 1 | Blue |
| 2 | Orangered |
| 2 | Periwinkle |
+----+------------+
Is there a good way to do this? What is this kind of operation even called?
You could use a query like this:
SELECT
id,
SUBSTRING_INDEX(SUBSTRING_INDEX(colors, ',', n.digit+1), ',', -1) color
FROM
colors
INNER JOIN
(SELECT 0 digit UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) n
ON LENGTH(REPLACE(colors, ',' , '')) <= LENGTH(colors)-n.digit
ORDER BY
id,
n.digit
Please see fiddle here. Please notice that this query will support up to 4 colors for every row, you should update your subquery to return more than 4 numbers (or you should use a table that contains 10 or 100 numbers).
I think it is what you need (stored procedure) : Mysql split column string into rows
DELIMITER $$
DROP PROCEDURE IF EXISTS explode_table $$
CREATE PROCEDURE explode_table(bound VARCHAR(255))
BEGIN
DECLARE id INT DEFAULT 0;
DECLARE value TEXT;
DECLARE occurance INT DEFAULT 0;
DECLARE i INT DEFAULT 0;
DECLARE splitted_value INT;
DECLARE done INT DEFAULT 0;
DECLARE cur1 CURSOR FOR SELECT table1.id, table1.value
FROM table1
WHERE table1.value != '';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
DROP TEMPORARY TABLE IF EXISTS table2;
CREATE TEMPORARY TABLE table2(
`id` INT NOT NULL,
`value` VARCHAR(255) NOT NULL
) ENGINE=Memory;
OPEN cur1;
read_loop: LOOP
FETCH cur1 INTO id, value;
IF done THEN
LEAVE read_loop;
END IF;
SET occurance = (SELECT LENGTH(value)
- LENGTH(REPLACE(value, bound, ''))
+1);
SET i=1;
WHILE i <= occurance DO
SET splitted_value =
(SELECT REPLACE(SUBSTRING(SUBSTRING_INDEX(value, bound, i),
LENGTH(SUBSTRING_INDEX(value, bound, i - 1)) + 1), ',', ''));
INSERT INTO table2 VALUES (id, splitted_value);
SET i = i + 1;
END WHILE;
END LOOP;
SELECT * FROM table2;
CLOSE cur1;
END; $$
This saved me many hours! Taking it a step further: On a typical implementation there would in all likelyhood be a table that enumerates the colours against an identitying key, color_list. A new colour can be added to the implementation without having to modify the query and the potentially endless union -clause can be avoided altogether by changing the query to this:
SELECT id,
SUBSTRING_INDEX(SUBSTRING_INDEX(colors, ',', n.digit+1), ',', -1) color
FROM
colors
INNER JOIN
(select id as digit from color_list) n
ON LENGTH(REPLACE(colors, ',' , '')) <= LENGTH(colors)-n.digit
ORDER BY id, n.digit;
It is important that the Ids in table color_list remain sequential, however.
No need for a stored procedure. A CTE is enough:
CREATE TABLE colors(id INT,colors TEXT);
INSERT INTO colors VALUES (1, 'Red,Green,Blue'), (2, 'Orangered,Periwinkle');
WITH RECURSIVE
unwound AS (
SELECT *
FROM colors
UNION ALL
SELECT id, regexp_replace(colors, '^[^,]*,', '') colors
FROM unwound
WHERE colors LIKE '%,%'
)
SELECT id, regexp_replace(colors, ',.*', '') colors
FROM unwound
ORDER BY id
;
+------+------------+
| id | colors |
+------+------------+
| 1 | Red |
| 1 | Green |
| 1 | Blue |
| 2 | Orangered |
| 2 | Periwinkle |
+------+------------+
notice this can be done without creating a temporary table
select id, substring_index(substring_index(genre, ',', n), ',', -1) as genre
from my_table
join
(SELECT #row := #row + 1 as n FROM
(select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) t,
(SELECT #row:=0) r) as numbers
on char_length(genre)
- char_length(replace(genre, ',', '')) >= n - 1
if delimiter is part of data but embedded by double quotes then how can we split it.
Example
first,"second,s",third
it should come as
first
second,s
third

Pair two types of values in MySQL

I have a table:
Type | Value
1 | '1test1'
2 | '2test1'
2 | '2test2'
2 | '2test3'
I want to get a result containing a pair where each entry from each type is used at least once, but not more than required.
From the example table above, I want the following result:
1test1 - 2test1
1test1 - 2test2
1test1 - 2test3
If the table is:
Type | Value
1 | '1test1'
1 | '1test2'
1 | '1test3'
2 | '2test1'
2 | '2test2'
2 | '2test3'
I want the following result:
1test1 - 2test1
1test2 - 2test2
1test3 - 2test3
If the table is:
Type | Value
1 | '1test1'
1 | '1test2'
2 | '2test1'
2 | '2test2'
2 | '2test3'
I want the following result:
'1test1' - '2test1'
'1test2' - '2test2'
'1test1' - '2test3'
'1test1' - '2test1'
'1test2' - '2test2'
'1test1' - '2test3'
I want each type to be repeated equally as other values in the same type. There shouldn't be a value from type that is repeated more often than other values in the same type.
What is the most elegant way to do it with SQL or stored procedure, or with a series of SQL statements?
It is somewhat easy to do when you have you don't have the same amount of rows for each type, but once you do, it gets somewhat tricky.
So I came up with this:
CREATE PROCEDURE test()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE type1, type2 INT;
DECLARE value1, value2 VARCHAR(5);
DECLARE cur1 CURSOR FOR SELECT type,value FROM testtable WHERE Type = 1;
DECLARE cur1 CURSOR FOR SELECT type,value FROM testtable WHERE Type = 2;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
if (SELECT COUNT(Value) FROM testtable WHERE Type = 1)
= (SELECT COUNT(Value) FROM testtable WHERE Type = 2)
then
OPEN cur1;
OPEN cur2;
CREATE TEMPORARY TABLE test1 (
Value1 varchar(12),
Value2 varchar(12)
)
read_loop: LOOP
FETCH cur1 INTO type1, value1;
FETCH cur2 INTO type2, value2;
IF done THEN
LEAVE read_loop;
END IF;
INSERT INTO test1 VALUES(value1, value2);
END LOOP;
CLOSE cur1;
CLOSE cur2;
SELECT * FROM test1;
DROP TABLE test1;
ELSE
SELECT t1.Value, t2.Value
FROM testtable t1
LEFT JOIN testtable t2 ON t2.Type = 2
WHERE t1.Type = 1
UNION SELECT t1.Value, t2.Value
FROM testtable t1
RIGHT JOIN testtable t2 ON t2.Type = 2
WHERE t1.Type = 1;
END IF;
END;
It's hideous, but it works for your three examples. Somewhat.
This is a somewhat contrived answer but I guess it fits the question:
create table stuff( idx tinyint unsigned, val varchar(50));
insert into stuff( idx, val ) values ( 1, '1val1'), (1, '1val2'), (2,'2val1'),
(2,'2val2'), (2, '2val3');
SELECT s0.val v0, s1.val v1 FROM stuff s0
JOIN stuff s1 ON s0.idx != s1.idx
where s0.idx = 1;
Here's a fiddle.
Is this what you want
SELECT
s.val AS One,
r.val AS Second
FROM stuff AS s
LEFT OUTER JOIN (SELECT * FROM stuff WHERE idx = 2) AS r ON r.idx <> s.idx
WHERE s.idx = 1
SQL Fiddle Demo
OUTPUT :
One | Second
--------------------
1val1 | 2val1
1val1 | 2val2
1val1 | 2val3
1val2 | 2val1
1val2 | 2val2
1val2 | 2val3

Counting changes in timeline with MySQL

I am new to MySQL and I need your help. I have a table with similar data
---------------------------------------------------
|RobotPosX|RobotPosY|RobotPosDir|RobotShortestPath|
---------------------------------------------------
|0.1 | 0.2 | 15 | 1456 |
|0.2 | 0.3 | 30 | 1456 |
|0.54 | 0.67 | 15 | 1456 |
|0.68 | 0.98 | 22 | 1234 |
|0.36 | 0.65 | 45 | 1234 |
|0.65 | 0.57 | 68 | 1456 |
|0.65 | 0.57 | 68 | 2556 |
|0.79 | 0.86 | 90 | 1456 |
---------------------------------------------------
As you can see there are repeated values in the column RobotShortestPath, But they are important. Each number represent a specific task. If the number repeats consecutively(ex: 1456), it means that Robot is performing that task, and when the number changes(ex: 1234) it means that it has switched to another task. And if the previous number(ex:1456) appears again it also means that robot is performing a new task(1456) after done with earlier task(1234).
So where I am stuck is I am unable to get no of tasks performed. I have used several things from my minimum knowledge like COUNT, GROUP BY but nothing seem to work.
Here the no.of tasks performed are 5 actually, but whatever I do I get only 3 as result.
SET #last_task = 0;
SELECT SUM(new_task) AS tasks_performed
FROM (
SELECT
IF(#last_task = RobotShortestPath, 0, 1) AS new_task,
#last_task := RobotShortestPath
FROM table
ORDER BY ??
) AS tmp
Update for multiple tables
From a database strcture normailization view, your better of with one table, and have a filed identifing what column is what robot, if that not posible for some reason, you can get that by union the tables:
SET #last_task = 0;
SELECT robot_id, SUM(new_task) AS tasks_performed
FROM (
SELECT
IF(#last_task = RobotShortestPath, 0, 1) AS new_task,
#last_task := RobotShortestPath
FROM (
SELECT 1 AS robot_id, robot_log_1.* FROM robot_log_1
UNION SELECT 2, robot_log_2.* FROM robot_log_2
UNION SELECT 3, robot_log_3.* FROM robot_log_3
UNION SELECT 4, robot_log_4.* FROM robot_log_4
UNION SELECT 5, robot_log_5.* FROM robot_log_5
) as robot_log
ORDER BY robot_id, robot_log_id
) AS robot_log_history
GROUP BY robot_id
ORDER BY tasks_performed DESC
As I understood, you need to track when RobotShortestPath is changed to another value. To achieve this you can use trigger like this:
delimiter |
CREATE TRIGGER track AFTER UPDATE ON yourtable
FOR EACH ROW BEGIN
IF NEW.RobotShortestPath != OLD.RobotShortestPath THEN
UPDATE tracktable SET counter=counter+1 WHERE tracker=1;
END IF;
END;
|
delimeter ;
set #n:=0, #i:=0;
select max(sno) from
(
select #n:=case when #i=RobotShortestPath then #n else #n+1 end as sno,
#i:=RobotShortestPath as dno
from table
) as t;
Try following query:
SET #cnt = 0, #r = -1;
SELECT IF(armed <> #r, #cnt:= #cnt + 1, 0), #r:= RobotShortestPath, #cnt FROM table;
SELECT #cnt AS count;