I have a table called cycle, that has the following structure:
id
starts_at
ends_at
the id is not unique, so its registered:
id starts_at ends_at cycle_year cycle_number
120 jan-01-2017 jan-05-2017 2017 1
120 jan-06-2017 jan-11-2017 2017 2
120 jan-12-2017 jan-18-2017 2017 3
What I need to do, its to create a view like this:
id starts_at ends_at how_far_it_is_from_newest
120 jan-01-2017 jan-05-2017 2
120 jan-06-2017 jan-11-2017 1
120 jan-12-2017 jan-18-2017 0
Any ideas?
I have created this one, but it has a problem:
SET #counter = -1;
SELECT
*,
concat(year, LPAD(number, 3, '0')) as 'cycle_ref', // eg. 2017001
(#counter:=#counter + 1) as counter
FROM
cycle
where
id = 120
ORDER by
cycle_ref DESC
;
it return correctly, but if I take off the id=120, it will count regardless the id.
id starts_at ends_at how_far_it_is_from_newest
120 jan-12-2017 jan-18-2017 0
77 jan-16-2017 jan-28-2017 1 <-- wrong:should be 0
120 jan-06-2017 jan-11-2017 2 <-- wrong:should be 1
120 jan-01-2017 jan-05-2017 3 <-- wrong:should be 2
This worked to me:
DROP FUNCTION IF EXISTS initCycleIndex;
DELIMITER $$
CREATE FUNCTION initCycleIndex() returns INT
BEGIN
SET #counter:=-1;
RETURN(#counter);
END $$
DELIMITER ;
DROP FUNCTION IF EXISTS getCycleIndex;
DELIMITER $$
CREATE FUNCTION getCycleIndex() RETURNS INT
DETERMINISTIC
BEGIN
SET #counter:= #counter + 1;
return (#counter);
END $$
DELIMITER ;
CREATE OR REPLACE VIEW vw_cycle_indexed as
SELECT
c.*,
concat(c.year, LPAD(c.number, 3, '0')) as 'cycle_ref',
getCycleIndex() as 'index'
FROM
cycle c
JOIN (select initCycleIndex()) r
ORDER by
cycle_ref DESC
;
#SELECT * FROM vw_cycle_indexed where id = 178;
Multiple problems to address here.
To initialize your variables you don't need functions. Just do it in a subquery (or in separate SET statements).
Biggest problem: You haven't defined an order to determine what's the newest entry. Your "how far it is from newest" might change every once in a while.
All you have implemented is a row counter. Of course it continues counting, regardless if the id is totally different. You have to have another variable that holds the value of the previous row. You can do so, by assigning the value of the current row to a variable after you've checked the value of the variable. The order in the select part is therefore very important.
The IF() function is quite simple, when the previous row id is different from the current row, return 0, otherwise the incremented row counter. And that's why it's also important to first order by id in the ORDER clause.
Have a try with this one:
SELECT
c.*,
concat(year, LPAD(number, 3, '0')) as 'cycle_ref',
#counter := IF(#prevID != id, 0, #counter + 1),
#prevID := id
FROM
cycle c
, (SELECT #counter := -1, #prevID := NULL) var_init_subquery
ORDER by
id, ends_at DESC, cycle_ref
;
Only downside is, you can't use this statement in a VIEW, because variables aren't allowed there. I will try your approach with the functions, though. It might be a neat workaround.
Related
This came from a question in Python, I thought, I could do this directly in MySQL. The guy said, the .csv file has millions of rows, so I think this came originally from a MySQL table.
I have never made a MySQL function before, so it's all a bit new.
A simple table, 3 columns: id, price and signal_
The task is, look at each price.
The first reference price is the price in row 1 and going down.
Going down, if the price has increased by 1 or more, write 1 in the column signal_
Now the price in that row is the reference price.
Or if the price has decreased by 1 or more, that price is the new reference price.
Always write 1 in column signal_ if the price has increased by 1 or more.
Always write -1 in column signal_ if the price has decreased by 1 or more.
The column signal contains 0 to begin with.
I made this function, but it doesn't get everything right. I think, maybe the scope of the variables is causing the problem. Maybe #start is a local variable in the function and another variable in MySQL
DELIMITER //
CREATE FUNCTION comparePrice(p DECIMAL)
RETURNS INT DETERMINISTIC
BEGIN
DECLARE signalnum INT;
# #start is the first value in row 1 when we start
# if p > #start
IF p - #start >= 1 THEN SET signalnum := 1, #start := p;
# if #start - p >= 1 p is smaller than #start by 1 or more
ELSEIF #start - p >= 1 THEN SET signalnum := -1, #start := p;
# if the change is less than 1, signalnum = 0
ELSE SET signalnum := 0, #start = p;
END IF;
RETURN signalnum;
END; //
DELIMITER ;
I looked at threads for getting the value of the next price but I couldn't make it work.
# initialize #start
SELECT #start := price FROM prices_up_down WHERE id = 1;
UPDATE prices_up_down SET signal_ = comparePrice(price);
Maybe it will never work like this!
I will be very grateful for any advice!
EDIT: the problem is my user variable #start gets truncated, resulting in wrong values in the column signal_
Anyone know why #start is truncated??
You can use a common table expression (cte) to get each row along with the previous value, then just update signal with an expression comparing the two;
WITH cte AS (
-- Get id and price along with the price from the previous row (by id)
SELECT id, price new_price, LAG(price) OVER (ORDER BY id) old_price
FROM prices_up_down
)
-- Join the cte with the original table since cte's aren't updatable in MySQL
UPDATE prices_up_down JOIN cte ON prices_up_down.id = cte.id
-- Set signal to 1 if the diff in price is > 1, or -1 if it's < -1
SET `signal` =
CASE WHEN new_price-old_price > 1 THEN 1
WHEN new_price-old_price < -1 THEN -1
ELSE 0
END;
A DBfiddle to test with
As always, if the data is important, always back up your table and test things before running updates from random people on the Internet.
In the code below, I'm trying go through the results of endDateTable row by row, comparing the current row's endDate to the previous row's endDate. If there has been any change since the previous, we increment #revisionNum. However, upon populating the new table, all of the #revisionNum entries are 0. What am I doing wrong?
NOTE: I'm using prepared statements in this manner since doing a straightforward SELECT into a variable gives a syntax error due to the LIMIT clause not allowing a variable in our version of MySQL.
BEGIN
DECLARE _currentEndDate DATETIME DEFAULT now();
DECLARE _priorEndDate DATETIME DEFAULT now();
SET #ResultsCount = (SELECT COUNT(*) FROM mainTable);
SET #j = 0;
WHILE #j < #ResultsCount DO
SET #revisionNum = 0;
/*CURRENT END DATE*/
SET #appResultQueryCurrent = CONCAT('
SELECT
end_date
INTO _currentEndDate
FROM endDateTable
LIMIT ', #j, ', 1'
);
PREPARE currentQueryStmt FROM #appResultQueryCurrent;
EXECUTE currentQueryStmt;
/*PREVIOUS END DATE*/
SET #appResultQueryPrior = CONCAT('
SELECT
end_date
INTO _priorAppEndDate
FROM endDateTable
LIMIT ', IF(#j = 0, 0, #j - 1), ', 1'
);
PREPARE priorQueryStmt FROM #appResultQueryPrior;
EXECUTE priorQueryStmt;
SET #revisionNum = IF(
#j = 0 OR (_currentEndDate = _priorEndDate),
#revisionNum,
IF(
_currentEndDate != _priorEndDate,
#revisionNum + 1,
#revisionNum
)
);
INSERT INTO finalTable (RevisionNum)
SELECT
#revisionNum AS RevisionNum
FROM endDateTable;
SET #j = #j +1;
END WHILE;
END $$
You don't need a loop, you can use INSERT INTO ... SELECT ..., incrementing the variable in the select query.
You also need an ORDER BY criteria to specify how to order the rows when comparing one row to the previous row.
INSERT INTO finalTable (RevisionNum, otherColumn)
SELECT revision, otherColumn
FROM (
SELECT IF(end_date = #prev_end_date, #revision, #revision := #revision + 1) AS revision,
#prev_end_date := end_date,
otherColumn
FROM endDateTable
CROSS JOIN (SELECT #prev_end_date := NULL, #revision := -1) AS vars
ORDER BY id) AS x
DEMO
The offset value in the LIMIT clause is tenuous without an ORDER BY.
Without an ORDER BY clause, MySQL is free to return results in any sequence.
There is no guarantee that LIMIT 41,1 will return the row before LIMIT 42,1, or that it won't return the exact same row as LIMIT 13,1 did.
(A table in a relational database represents an unordered set of tuples, there is no guaranteed "order" or rows in a table.)
But just adding ORDER BY to the queries isn't enough to fix the Rube-Goldberg-esque rigmarole.
In the code shown, it looks like each time through the loop, we're inserting a copy of endDateTable into finalTable. If that's 1,000 rows in endDateTable, we're going to get 1,000,000 rows (1,000 x 1,000) inserted into finalTable. Not at all clear why we need so many copies.
Given the code shown, it's not clear what the objective is. Looks like we are conditionally incrementing revisionNum, the end result of which is the highest revision num. Just guessing here.
If there is some kind of requirement to do this in a LOOP construct, within a procedure, I'd think we'd do a cursor loop. And we can use procedure variables vs user-defined variables.
Something along these lines:
BEGIN
DECLARE ld_current_end_date DATETIME;
DECLARE ld_prior_end_date DATETIME;
DECLARE li_done INT;
DECLARE li_revision_num INT;
DECLARE lcsr_end_date CURSOR FOR SELECT t.end_date FROM `endDateTable` t ORDER BY NULL;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET li_done = TRUE;
SET li_done = FALSE;
SET li_revision_num = 0;
OPEN lcsr_end_date;
FETCH lcsr_end_date INTO ld_current_end_date;
SET ld_prior_end_date = ld_current_end_date;
WHILE NOT li_done DO
SET li_revision_num = li_revision_num + IF( ld_current_end_date <=> ld_prior_end_date ,0,1);
SET ld_prior_end_date := ld_current_end_date;
FETCH lcsr_end_date INTO ld_current_end_date;
END WHILE;
CLOSE lcsr_end_date;
INSERT INTO `finalTable` (revisionnum) VALUES (li_revision_num);
END $$
Note the "order by" clause on the SELECT, its not clear what the rows should be ordered on, so we're using a literal as a placeholder.
As the end result, we insert a single row into finalTable.
Again, it's not clear what the code in the question is supposed to achieve, but doing a cursor loop across ordered rows would be much more efficient than a bazillion dynamic SQL executions fetching individual rows.
I am trying to write a Stored Procedure to retrieve the median salary from a table and am having trouble figuring out how to retrieve the data from the cursor.
Currently my code is:
DELIMITER //
CREATE PROCEDURE MedianSalary(OUT median INT)
BEGIN
DECLARE counter int(5) DEFAULT 0;
DECLARE set_size int(5) DEFAULT (SELECT count(*) from employee);
DECLARE median_index int(5) DEFAULT (SELECT floor(count/2));
DECLARE all_salaries CURSOR
FOR SELECT salary from employee,
OPEN all_salaries;
WHILE #counter != #median_index
BEGIN
SET #counter = #counter + 1,
FETCH NEXT from all_salaries,
END;
FETCH all_salaries INTO median;
CLOSE all_salaries;
END //
DELIMITER ;
I can't seem to find any documentation similar to what I am trying to achieve, any help would be greatly appreciated.
I don't have an answer to your stored procedure problem, but note that we can actually find the median from a table in MySQL fairly easily using session variables to simulate the row number:
SET #row_number = 0;
SET #row_count = (SELECT COUNT(*) FROM yourtable);
SELECT AVG(salary) AS median
FROM
(
SELECT (#row_number:=#row_number + 1) AS rn, salary
FROM yourTable
ORDER BY salary
) t
WHERE
(#row_count % 2 = 0 AND rn IN (#row_count / 2, (#row_count / 2) + 1) OR
#row_count % 2 <> 0 AND rn = #row_count / 2);
Demo
Note the ugliness in the WHERE clause has to do with the edge case of your table having an even number of records. In this case, there technically is not a single median record, so instead I report the mean of the two records which sit about the median on either side.
I have a MYSQL table and two of the fields are called Rate_per_unit and Cost. First I want the field Rate_per_unit to populate itself from another table called SHD_TEACHER then I want the field COST to populate itself also from RATE in SHD_TEACHER and multiplies by UNITS.
I have the following code which is giving me an error:
CREATE TRIGGER RATE_PER_UNIT_1
BEFORE INSERT ON SHD_SCHEDULE
FOR EACH ROW
SET NEW.RATE_PER_UNIT =
(
SELECT RATE
FROM SHD_TEACHER
WHERE TEACHERID = NEW.TEACHER_ID
LIMIT 1
)
SET NEW.COST = (
SELECT RATE
FROM SHD_TEACHER
WHERE TEACHERID = NEW.TEACHER_ID
) * UNITS
Any help please?
thanks
using your syntax, I would expect a delimiter statement and a begin/end block. So, try this:
DELIMITER $$
CREATE TRIGGER RATE_PER_UNIT_1
BEFORE INSERT ON SHD_SCHEDULE
FOR EACH ROW
BEGIN
SET NEW.RATE_PER_UNIT =
(
SELECT RATE
FROM SHD_TEACHER t
WHERE t.TEACHERID = NEW.TEACHER_ID
LIMIT 1
)
SET NEW.COST = (
SELECT t.RATE
FROM SHD_TEACHER t
WHERE t.TEACHERID = NEW.TEACHER_ID
) * NEW.UNITS
END $$
DELIMITER ;
You have a limit 1 in the first subquery, suggesting that there might be multiple matches. If so, you will get a run-time error in the second. Also, UNITS is just hanging out there, all alone. I assumed it is in the NEW record.
Here is another way to write this:
DELIMITER $$
CREATE TRIGGER RATE_PER_UNIT_1
BEFORE INSERT ON SHD_SCHEDULE
FOR EACH ROW
BEGIN
SELECT NEW.RATE_PER_UNIT := t.RATE, NEW.COST := t.RATE * NEW.UNITS
FROM (SELECT t.*
FROM SHD_TEACHER t
WHERE t.TEACHERID = NEW.TEACHER_ID
LIMIT 1
) t
END $$
DELIMITER ;
I have a table t1 which have a column Marks in this column values are 10, 20, 30, 40.
Now I want to use a procedure to get this result:
Marks Total_Marks
10 10
20 30
30 60
40 100
DELIMITER //
CREATE PROCEDURE Total_Marks ( In Num Int(4) )
Begin
Declare Mark Int(4);
Declare Add_M Int(4);
DECLARE NO_MORE_ROWS BOOLEAN;
DECLARE DataCursor CURSOR FOR SELECT Marks
FROM t1 where marks = Num;
DECLARE DataCursor1 CURSOR FOR SELECT Sum(Marks) FROM t1;
OPEN DataCursor;
FETCH DataCursor INTO Mark;
CLOSE DataCursor;
OPEN DataCursor1;
READ_LOOP1: LOOP
FETCH DataCursor1 INTO Add_M;
IF NO_MORE_ROWS THEN
LEAVE READ_LOOP1;
END IF;
BEGIN
SET Add_M = SUM(Mark);
END;
END LOOP READ_LOOP1;
CLOSE dataCursor1;
SET NO_MORE_ROWS = FALSE;
end //
DELIMITER ;
You don't need a procedure at all, let alone cursors. But what you need, is a column that defines the order of the rows as mentioned in the comments.
create table foo (id int auto_increment primary key, bar int);
insert into foo(bar) values (10), (20), (30), (40);
In this example I introduced the column id for that matter. Or you can of course just order by your marks or whatever suits your needs.
select
bar
, #total := #total + bar as my_total
from
foo
, (select #total := 0) var_init
order by id
see it working live in this sqlfiddle
As explanation, with this cross joined query
, (select #total := 0) var_init
we initialize our variable holding the running total #total. It's the same as writing
set #total = 0;
select
bar
, #total := #total + bar as my_total
from
foo
order by id;
The rest is self explaining I guess.
You can read more about these type of variables here.
UPDATE (for completeness):
Here are two other possibilities how to solve it without variables. Although I like variables usually better, cause in this one
select
t1.bar
, sum(t2.bar)
from
foo t1
inner join foo t2 on t1.id >= t2.id
group by t1.id;
you end up with a potentially huge temporary table, since you join every row to all previous rows and then calculate the sum.
And in this solution
select
bar
, (select sum(bar) from foo sf where sf.id <= foo.id) as my_total
from
foo;
you have a dependent subquery executed for each row. This is even worse than the previous solution.
I posted those just for completeness and if you really can't use variables (because of creating a view for example).