Updating table using looping cursors - sql-server-2008

To assist in understanding I have a table like this:
itemcode itemname icode serialnum
1 A 10 0
2 B 10 0
3 C 10 0
4 D 11 0
5 E 13 0
6 F 20 0
7 G 20 0
I want the result to look like the table below using a single update query with the help of looping cursors:
itemcode itemname icode serialnum
1 A 10 1
2 B 10 2
3 C 10 3
4 D 11 1
5 E 13 1
6 F 20 1
7 G 20 2
Item code is the primary key in this table. The logic behind generating the serial number is whenever icode changes the serial number gets reset to 1. I need a single update query to update the table with the help of cursors if someone can assist with a solution?

So you want a sequential serial number for every icode-group. Then you can use ROW_NUMBER with PARTITION BY icode:
WITH CTE AS
(
SELECT itemcode, itemname, icode, serialnum,
, ROW_NUMBER() OVER (PARTITION BY icode ORDER BY itemcode) AS RN
FROM dbo.TableName
)
UPDATE CTE SET serialnum = RN;
If you want it to be reculculated on every update you could use a trigger:
CREATE TRIGGER dbo.TableName_Updated
ON dbo.TableName
FOR UPDATE /* Fire this trigger when one or multiple rows are UPDATEd */
AS BEGIN
WITH CTE AS
(
SELECT itemcode, itemname, icode, serialnum,
, ROW_NUMBER() OVER (PARTITION BY icode ORDER BY itemcode) AS RN
FROM dbo.TableName t INNER JOIN INSERTED i
ON t.itemcode = i.itemcode
)
UPDATE CTE SET serialnum = RN
END

SQL Fiddle
MS SQL Server 2008 Schema Setup:
create table Item
(
itemcode int,
itemname char(1),
icode int,
serialnum int
)
insert into Item values
(1, 'A', 10, 0),
(2, 'B', 20, 0),
(3, 'C', 11, 0),
(4, 'D', 10, 0),
(5, 'E', 20, 0),
(6, 'F', 10, 0),
(7, 'G', 13, 0)
Query 1:
update I
set serialnum = rn
from
(
select serialnum,
row_number() over(partition by icode order by itemcode) as rn
from Item
) I
select *
from Item
Results:
| ITEMCODE | ITEMNAME | ICODE | SERIALNUM |
-------------------------------------------
| 1 | A | 10 | 1 |
| 2 | B | 20 | 1 |
| 3 | C | 11 | 1 |
| 4 | D | 10 | 2 |
| 5 | E | 20 | 2 |
| 6 | F | 10 | 3 |
| 7 | G | 13 | 1 |
Update
A version that uses a cursor instead of not using a cursor.
SQL Fiddle
declare #itemcode int
declare #rn int
declare ItemCursor cursor local static forward_only read_only for
select itemcode,
row_number() over(partition by icode order by itemcode) as rn
from Item
open ItemCursor
fetch next from ItemCursor
into #itemcode, #rn
while ##fetch_status = 0
begin
update Item
set serialnum = #rn
where itemcode = #itemcode
fetch next from ItemCursor
into #itemcode, #rn
end
close ItemCursor
deallocate ItemCursor

Related

MySQL: Sequentially number a column based on change in a different column

If I have a table with the following columns and values, ordered by parent_id:
id parent_id line_no
-- --------- -------
1 2
2 2
3 2
4 3
5 4
6 4
And I want to populate line_no with a sequential number that starts over at 1 every time the value of parent_id changes:
id parent_id line_no
-- --------- -------
1 2 1
2 2 2
3 2 3
4 3 1
5 4 1
6 4 2
What would the query or sproc look like?
NOTE: I should point out that I only need to do this once. There's a new function in my PHP code that automatically creates the line_no every time a new record is added. I just need to update the records that already exist.
Most versions of MySQL do not support row_number(). So, you can do this using variables. But you have to be very careful. MySQL does not guarantee the order of evaluation of variables in the select, so a variable should not be assigned an referenced in different expressions.
So:
select t.*,
(#rn := if(#p = parent_id, #rn + 1,
if(#p := parent_id, 1, 1)
)
) as line_no
from (select t.* from t order by id) t cross join
(select #p := 0, #rn := 0) params;
The subquery to sort the table may not be necessary. Somewhere around version 5.7, this became necessary when using variables.
EDIT:
Updating with variables is fun. In this case, I would just use subqueries with the above:
update t join
(select t.*,
(#rn := if(#p = parent_id, #rn + 1,
if(#p := parent_id, 1, 1)
)
) as new_line_no
from (select t.* from t order by id) t cross join
(select #p := 0, #rn := 0) params
) tt
on t.id = tt.id
set t.line_no = tt.new_line_no;
Or, a little more old school...
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(id SERIAL PRIMARY KEY
,parent_id INT NOT NULL
);
INSERT INTO my_table VALUES
(1, 2),
(2 , 2),
(3 , 2),
(4 , 3),
(5 , 4),
(6 , 4);
SELECT x.*
, CASE WHEN #prev = parent_id THEN #i := #i+1 ELSE #i := 1 END i
, #prev := parent_id prev
FROM my_table x
, (SELECT #prev:=null,#i:=0) vars
ORDER
BY parent_id,id;
+----+-----------+------+------+
| id | parent_id | i | prev |
+----+-----------+------+------+
| 1 | 2 | 1 | 2 |
| 2 | 2 | 2 | 2 |
| 3 | 2 | 3 | 2 |
| 4 | 3 | 1 | 3 |
| 5 | 4 | 1 | 4 |
| 6 | 4 | 2 | 4 |
+----+-----------+------+------+
You can use subquery if the row_number() doesn't help :
select t.*,
(select count(*)
from table t1
where t1.parent_id = t.parent_id and t1.id <= t.id
) as line_no
from table t;

Calculate the Average for the last x records belonging to a distinct groupset

I am trying to calculate the average of time for each part_id for the last 2 batches where the part_id is found and the average of time for each part_id for all the batches.
I have managed to isolate the Average of a part_id for the last 2 batches it was found but I can't integrated into the code so that it does the calculation for each part ID. I get an error Unknown column 'tst.part_id' in 'where clause', I need to pass that tst.part_id value to my nested select query.
Below is a fiddle I have:
http://sqlfiddle.com/#!9/77bea5/85
Query I am using:
SELECT tst.part_id,
AVG(tst.est_time) AS 'Average Time Overall',
(SELECT AVG(ft.avgLastMax)
FROM
(SELECT t2.avglst as 'avgLastMax', t2.numval as 'numval'
FROM (SELECT
#num:=CASE WHEN #last != tst3.batch_id
THEN #num:=(#num + 1)
ELSE #num:=#num END 'numval',
#last:=tst3.batch_id,
#name:=CASE WHEN #num > 2
THEN #name:=#name
ELSE #name:=(tst3.est_time) END 'avglst'
FROM test AS tst3,
( select #last:=0, #avg := 0, #name :=0 , #num :=0) var
/* GET AVERAGE FOR A SINGLE PART ID
WHERE tst3.part_id = 1 */
WHERE tst3.part_id = tst.part_id
ORDER BY tst3.run_id DESC) as t2
)
as ft
WHERE ft.numval <3) as 'AVG on last 2 batches'
FROM test AS tst
GROUP BY tst.part_id;
Here is what I am trying to get:
part_id AVG on last 2 batches Average Time Overall
1 27.25 25.67
2 22.5 22.5
3 16.67 16.67
4 47.5 47.5
Table Schema:
CREATE TABLE test
(`part_id` int, `est_time` int, `batch_id` int, `run_id` int, `line` varchar(1))
;
INSERT INTO test
(`part_id`, `est_time`, `batch_id`, `run_id`, `line`)
VALUES
(1, 20, 1, 1, 'T'),
(1, 25, 1, 2, 'T'),
(2, 30, 1, 3, 'T'),
(3, 15, 1, 4, 'T'),
(1, 10, 2, 5, 'X'),
(4, 40, 2, 8, 'X'),
(2, 15, 3, 9, 'T'),
(3, 15, 3, 10, 'T'),
(3, 20, 3, 11, 'T'),
(1, 34, 4, 12, 'X'),
(1, 32, 4, 13, 'X'),
(1, 33, 4, 14, 'X'),
(4, 55, 5, 15, 'T')
;
EDITED: Corrected the table and order by tst3.run_id DESC to get the last batch_id.
Assuming your first result is wrong...
SELECT a.part_id
, AVG(a.est_time)
FROM test a
JOIN
( SELECT x.part_id
, x.batch_id
FROM
( SELECT DISTINCT part_id
, batch_id
FROM test
) x
JOIN
( SELECT DISTINCT part_id
, batch_id
FROM test
) y
ON y.part_id = x.part_id
AND y.batch_id >= x.batch_id
GROUP
BY x.part_id
, x.batch_id
HAVING COUNT(*) <= 2
) b
ON b.part_id = a.part_id
AND b.batch_id = a.batch_id
GROUP
BY a.part_id;
+---------+-----------------+
| part_id | AVG(a.est_time) |
+---------+-----------------+
| 1 | 27.2500 |
| 2 | 22.5000 |
| 3 | 16.6667 |
| 4 | 47.5000 |
+---------+-----------------+
Or, faster... with variables...
SELECT part_id
, AVG(est_time) last_2_avg
FROM
( SELECT x.*
, CASE WHEN #part_id = part_id
THEN CASE WHEN #batch_id = batch_id THEN #i:=#i ELSE #i:=#i+1 END
ELSE #i:=1
END i
, #part_id := part_id
, #batch_id:= batch_id
FROM test x
, (SELECT #part_id := null, #batch_id:=null, #i:=1) vars
ORDER
BY part_id
, batch_id DESC
) a
WHERE a.i <= 2
GROUP
BY part_id;
+---------+------------+
| part_id | last_2_avg |
+---------+------------+
| 1 | 27.2500 |
| 2 | 22.5000 |
| 3 | 16.6667 |
| 4 | 47.5000 |
+---------+------------+
SELECT W.*,T1.ALLAVG
FROM (
SELECT V.PART_ID, AVG(V.EST_TIME) LAST2 FROM
(
SELECT U.PART_ID,U.MAXRN,X.RUN_ID,X.EST_TIME
FROM
(
/*LAST 2*/
SELECT S.PART_ID,MAX(S.RN) MAXRN
FROM
(
/*DERIVE A ROW NUMBER*/
SELECT T.PART_ID , T.RUN_ID,
IF (CONCAT(T.PART_ID,T.RUN_ID) <> #R ,#RN:=#RN+1,#RN:=1) RN,
#R:=CONCAT(T.PART_ID,T.RUN_ID) R
FROM (SELECT #RN:=0,#P:=0,#R:=0) RN, TEST T
ORDER BY T.PART_ID,T.RUN_ID
) S
GROUP BY S.PART_ID
) U
/*USING THE MAX ABOVE GET THE LAST 2 */
JOIN
(SELECT T.PART_ID , T.RUN_ID,T.EST_TIME,
IF (CONCAT(T.PART_ID,T.RUN_ID) <> #R1 ,#RN1:=#RN1+1,#RN1:=1) RN,
#R1:=CONCAT(T.PART_ID,T.RUN_ID) R
FROM (SELECT #RN1:=0,#P1:=0,#R1:=0) RN, TEST T
ORDER BY T.PART_ID,T.RUN_ID
) X ON X.PART_ID = U.PART_ID AND (X.RN BETWEEN U.MAXRN -1 AND U.MAXRN) #AMEND THIS FOR THE LAST N REQUIRED
) V
GROUP BY V.PART_ID
) W
JOIN
/*ALL*/
(SELECT T.PART_ID,AVG(T.EST_TIME) 'ALLAVG' FROM TEST T GROUP BY T.PART_ID) T1 ON T1.PART_ID = W.PART_ID
RESULT
+---------+---------+---------+
| PART_ID | LAST2 | ALLAVG |
+---------+---------+---------+
| 1 | 32.5000 | 25.6667 |
| 2 | 22.5000 | 22.5000 |
| 3 | 17.5000 | 16.6667 |
| 4 | 47.5000 | 47.5000 |
+---------+---------+---------+

Select From a table limiting by column of another table

I have a table named question_dispositions composed like this
Type | chapter | number
-------------------------
A | 1 | 3
B | 1 | 2
B | 4 | 1
The second table named question has columns type, text and chapter.
Type | chapter | text
-----------------------
A | 1 | T1
A | 1 | T2
B | 1 | T3
B | 1 | T4
B | 1 | T5
B | 2 | T6
B | 2 | T7
B | 3 | T8
B | 4 | T9
What I need is to fetch X random questions from table Question where the X is the column number from question_dispositions
So if I need questions of type B the result that I expect is this:
chapter | text
--------------
1 | T3
1 | T4
4 | T9
How can I Select random question limiting by the column number in another table? I tried with this query but returns every row in the table Question as expected because I can not limit with qd.count
SELECT * FROM
question_dispositions AS qd, question AS q
WHERE qd.chapter = q.chapter AND qd.type = q.type AND qd.type = 'B'
ORDER BY RAND() is a typical approach to randomness in MySQL but it is usually criticized because it does not scale well. Here however you appear to be selecting a quite small number of rows as questions but to achieve some randomness of those. So, here is an approach using ORDER BY RAND() which should be acceptable in performance for small result sets.
There is an added requirement to LIMIT by a stored Number, however in the small example supplied that number isn't consistent for type B so I chose to limit by the maximum of that number for the wanted type B. MySQL doesn't allow use of variables or subqueries with the LIMIT clause so instead a generated row number is used in a where clause to achieve that outcome.
Refer to this SQL Fiddle
Data:
CREATE TABLE question_dispositions
(`Type` varchar(1), `chapter` int, `number` int)
;
INSERT INTO question_dispositions
(`Type`, `chapter`, `number`)
VALUES
('A', 1, 3),
('B', 1, 2),
('B', 4, 1)
;
CREATE TABLE question
(`Type` varchar(1), `chapter` int, `text` varchar(2))
;
INSERT INTO question
(`Type`, `chapter`, `text`)
VALUES
('A', 1, 'T1'),
('A', 1, 'T2'),
('B', 1, 'T3'),
('B', 1, 'T4'),
('B', 1, 'T5'),
('B', 2, 'T6'),
('B', 2, 'T7'),
('B', 3, 'T8'),
('B', 4, 'T9')
;
Query:
SELECT *
FROM (
SELECT #rownum := #rownum + 1 AS rn , q.*
FROM question_dispositions AS qd
INNER JOIN question AS q ON qd.chapter = q.chapter AND qd.type = q.type
CROSS JOIN (SELECT #rownum := 0) r
WHERE qd.type = 'B'
ORDER BY RAND()
) d
WHERE rn <= (SELECT MAX(NUMBER) FROM question_dispositions WHERE type = 'B')
Example Results:
| rn | Type | chapter | text |
|----|------|---------|------|
| 1 | B | 1 | T3 |
| 2 | B | 1 | T4 |

How to update in MySql columns

There are 5 columns X and A, B, C, D in table t. Columns A, B, C, D are varchar. and column X has to show us how many of the next row repeating characters. I need help to update column X.
Example :
|ID | X | A | B | C | D |
=========================
| 4 | 1 | 7 | J | 7 | Q |
| 3 | 2 | K | Q | 8 | 8 |
| 2 | 3 | 7 | 8 | 9 | J | next row X=3
| 1 | 0 | 7 | J | 8 | K | 0 default
ID-1 is the first and X is zero by default and from there begin calculations ID-2 and X=3 because we have ID-1 "7" "J" and "8" and the next row ID-2 have the combination "7 8 9 J" in ID-1 there "7 J 8 -" and X should be 3. Values ​​of X can be between 0 and 4. ID-3, X=2 "- - 8 8" because in ID-2 have the combination "7 8 9 J" and i have 8 - twice in "K Q 8 8".
Added : 08/06
http://sqlfiddle.com/#!2/4586e/1
CREATE TABLE tmp
(
id int,
alnum CHAR(1),
cnt int,
PRIMARY KEY (id, alnum)
);
INSERT INTO tmp (id, alnum, cnt) SELECT id, A, 1 FROM tab ON DUPLICATE KEY UPDATE cnt = cnt + 1;
INSERT INTO tmp (id, alnum, cnt) SELECT id, B, 1 FROM tab ON DUPLICATE KEY UPDATE cnt = cnt + 1;
INSERT INTO tmp (id, alnum, cnt) SELECT id, C, 1 FROM tab ON DUPLICATE KEY UPDATE cnt = cnt + 1;
INSERT INTO tmp (id, alnum, cnt) SELECT id, D, 1 FROM tab ON DUPLICATE KEY UPDATE cnt = cnt + 1;
UPDATE tab INNER JOIN (
SELECT t1.id AS id, SUM(t2.cnt) AS sum
FROM tmp t1 INNER JOIN tmp t2 ON t1.id + 1 = t2.id
AND t1.alnum = t2.alnum
GROUP BY t1.id
) tmp3 ON tab.id = tmp3.id + 1
SET tab.X = tmp3.sum;
SELECT * FROM tab WHERE X > 4;
Original answer : 08/05
How about using tmp table which stores for each A, B, C, D columns into single rows. This makes us easy to calculate X value.
Below code assumes id is sequencial value. If not, please let me know there is another query for it.
CREATE TABLE tab
(
id INT,
X INT,
A CHAR(1),
B CHAR(1),
C CHAR(1),
D CHAR(1)
);
INSERT INTO tab VALUES (1, 0, '7', 'J', '8', 'K');
INSERT INTO tab VALUES (2, 0, '7', '8', '9', 'J');
INSERT INTO tab VALUES (3, 0, 'K', 'Q', '8', '8');
INSERT INTO tab VALUES (4, 0, '7', 'J', '7', 'Q');
CREATE TABLE tmp
(
id INT,
alnum CHAR(1)
);
INSERT INTO tmp SELECT id, A FROM tab;
INSERT INTO tmp SELECT id, B FROM tab;
INSERT INTO tmp SELECT id, C FROM tab;
INSERT INTO tmp SELECT id, D FROM tab;
UPDATE tab INNER JOIN (
SELECT t1.id AS id, COUNT(*) AS cnt
FROM tmp t1 INNER JOIN tmp t2 ON t1.id + 1 = t2.id
AND t1.alnum = t2.alnum
GROUP BY t1.id
) tmp3 ON tab.id = tmp3.id + 1
SET tab.X = tmp3.cnt;
mysql> SELECT * FROM tab ORDER BY id DESC;
+------+------+------+------+------+------+
| id | X | A | B | C | D |
+------+------+------+------+------+------+
| 4 | 1 | 7 | J | 7 | Q |
| 3 | 2 | K | Q | 8 | 8 |
| 2 | 3 | 7 | 8 | 9 | J |
| 1 | 0 | 7 | J | 8 | K |
+------+------+------+------+------+------+
4 rows in set (0.00 sec)
The X values in your example are from the "next" row, not the previous. Assuming you have an auto-incrementing id, you can calculate this information by looking at the next row. You can generate a query to do the calculation:
select ((case when t1.A in (tnext.A, tnext.B, tnext.C, tnext.D) then 1 else 0 end) +
(case when t1.B in (tnext.A, tnext.B, tnext.C, tnext.D) then 1 else 0 end) +
(case when t1.C in (tnext.A, tnext.B, tnext.C, tnext.D) then 1 else 0 end) +
(case when t1.D in (tnext.A, tnext.B, tnext.C, tnext.D) then 1 else 0 end)
) as X, t1.*
from (select t.*, (select max(id) from table t2 where t2.id > t.id) as nextid
from table t
) t1 left outer join
t tnext
on tnext.id = t1.nextid;
Depending on the database you are using, this code can be simplified and expressed differently. Also, the specific update syntax might depend on the database.

Update of MySQL table column to sequential digit based on another column

The current table looks something like this:
[id | section | order | thing]
[1 | fruits | 0 | apple]
[2 | fruits | 0 | banana]
[3 | fruits | 0 | avocado]
[4 | veggies | 0 | tomato]
[5 | veggies | 0 | potato]
[6 | veggies | 0 | spinach]
I'm wondering how to make the table look more like this:
[id | section | order | thing]
[1 | fruits | 1 | apple]
[2 | fruits | 2 | banana]
[3 | fruits | 3 | avocado]
[4 | veggies | 1 | tomato]
[5 | veggies | 2 | potato]
[6 | veggies | 3 | spinach]
"order" column updated to a sequential number, starting at 1, based on "section" column and "id" column.
You can do this with an update by using a join. The second table to the join calculates the ordering, which is then used for the update:
update t join
(select t.*, #rn := if(#prev = t.section, #rn + 1, 1) as rn
from t cross join (select #rn := 0, #prev := '') const
) tsum
on t.id = tsum.id
set t.ordering = tsum.rn
You don't want to do this as an UPDATE, as that will be really slow.
Instead, do this on INSERT. Here's a simple one-line INSERT that will grab the next order number and inserts a record called 'kiwi' in the section 'fruits'.
INSERT INTO `table_name` (`section`, `order`, `thing`)
SELECT 'fruits', MAX(`order`) + 1, 'kiwi'
FROM `table_name`
WHERE `section` = `fruits`
EDIT: You could also do this using an insert trigger, e.g.:
DELIMITER $$
CREATE TRIGGER `trigger_name`
BEFORE INSERT ON `table_name`
FOR EACH ROW
BEGIN
SET NEW.`order` = (SELECT MAX(`order`) + 1 FROM `table_name` WHERE `section` = NEW.`section`);
END$$
DELIMITER ;
Then you could just insert your records as usual, and they will auto-update the order value.
INSERT INTO `table_name` (`section`, `thing`)
VALUES ('fruits', 'kiwi')
Rather than storing the ordering, you could derive it:
SELECT t.id
,t.section
,#row_num := IF (#prev_section = t.section, #row_num+1, 1) AS ordering
,t.thing
,#prev_section := t.section
FROM myTable t
,(SELECT #row_num := 1) x
,(SELECT #prev_value := '') y
ORDER BY t.section, t.id
Note that order is a keyword and is therefore not the greatest for a column name. You could quote the column name or give it a different name...