Mysql, tree, Hierarchical query, performance - mysql

My question is based on the following article (the table and the function hierarchy_connect_by_parent_eq_prior_id) http://explainextended.com/2009/03/17/hierarchical-queries-in-mysql/
Lets assume that the table t_hierarchy has two additional fields (beside id and parent) typ1(char) and time(int). the field typ1 can have two values A and B.
My goal is to display the whole tree as described in the article but I need an extra field in the result that displays the time of the current node (if typ1 = B) and of all of its descendants (if typ1 = B). So I need the sum of all descendants' times for a certain node (including itself) when typ1=B.
I have the following solution but it is way too slow:
main query:
SELECT CONCAT(REPEAT(' ', level - 1), hi.id) AS treeitem, get_usertime_of_current_node_and_descendants(hi.id) as B_time,
hierarchy_sys_connect_by_path('/', hi.id) AS path,
parent, level
FROM (
SELECT hierarchy_connect_by_parent_eq_prior_id(id) AS id,
CAST(#level AS SIGNED) AS level
FROM (
SELECT #start_with := 0,
#id := #start_with,
#level := 0
) vars, t_hierarchy
WHERE #id IS NOT NULL
) ho
JOIN t_hierarchy hi
ON hi.id = ho.id
The function get_usertime_of_current_node_and_descendants(input int):
BEGIN
DECLARE _id INT;
DECLARE _desctime INT;
DECLARE _nodetime INT;
SET _id = input;
select COALESCE((select sum(time) from (
SELECT hi.id, time,typ1
FROM (
SELECT hierarchy_connect_by_parent_eq_prior_id_2(id) AS id, #levela AS level
FROM (
SELECT #start_witha := _id,
#ida := #start_witha,
#levela := 0,
) vars, t_hierarchy a
WHERE #ida IS NOT NULL
) ho
JOIN t_hierarchy hi
ON hi.id = ho.id
) q where typ1 = 'B'), 0) into _desctime;
select COALESCE((select time from t_hierarchy where id = _id and typ1='B'), 0) into _nodetime;
return _desctime + _nodetime;
END $$
The function hierarchy_connect_by_parent_eq_prior_id_2 is the same as in the article and as the one above hierarchy_connect_by_parent_eq_prior_id but it has differently named global variables so it won't interfere with the the ones used in the main query.
The above solution works as desired but it is way too slow (especially when working with large datasets). Can you offer a better solution or can you suggest how to improve the query? Thank you in advance for your time and help!

I solved the problem retrieving the time of the descendants outside of mysql (before inserting the entries into the table).

Related

How to split count() query into 10 groups in my sql

I am currently using mysql and I have to split the data into 10 groups.
For example, if the total count of data is 90, it should go like
1~9,
10~18,
19~27,
28~36,
37~45,
46~54,
55~63,
64~72,
73~81,
82~90.
if the total count of data is 100, it should go like
1~10,
11~20,
21~30,
31~40,
41~50,
51~60,
61~70,
71~80,
81~90,
91~100.
Can anyone give me a clue to split the data into 10 groups. I used rownum, but it did not work....
select total.row_num,
total.name,
total.reg,
total.id,
total.motspd
from(
select
(#row_num:=#row_num+1) AS row_num,
cg.group_name as name,
td.reg_date as reg,
td.car_id as id,
td.mcu_motspd as motspd
from
cartracker.tracker_data td
left join car c on (c.car_device_no = td.car_id)
left join car_group cg on (c.car_group_no = cg.car_group_no)
where cg.car_group_no = "1"
group by DATE_FORMAT(td.reg_date, "%Y%M%d%h%m")
)total
This is a result of a query, but it shows wrong row numbers.I want row_num goes from 0 to the end number of the data. but, in the picture, it starts from 44,713. can anyone help me to fix row num as it starts from 0 to the end number of the data.
attached image
I am going by the question in the title:
I am currently using mysql and I have to split the data into 10 groups.
That is exactly what the function ntile() does. So:
select t.*,
ntile(10) over (order by <whatever>) as tile
from t;
I have no idea what the query has to do with this question.
Haven't realize OP want with dynamic table BEFORE re-edit, this is not cleaver and require a lot of time to process though. switch a with OP table name and create rownumber first.
DECLARE #TotalNum INT;
DECLARE #Num INT;
DECLARE #NUm2 INT;
DECLARE #COUNTS INT;
SET #COUNTS = (select count(rownum) from a)/10
SET #TotalNum = 10
SET #Num =1
SET #Num2 =0
WHILE #Num <= #TotalNum
BEGIN
update a
set a.flag = #Num
where a.rownum >= (#COUNTS)*#NUM2+1 and a.rownum <= #NUM*(#COUNTS)
SET #Num = #NUM + 1
SET #Num2 = #NUM2 + 1
END

SQLserver Store Column as variable and loop through it

I am still pretty new to SQL server and I am not sure how to do this. I am first creating a table with just the IDs I need:
SELECT DISTINCT
ID_NUMBER
INTO
#IDlist
FROM
V_Rpt_IDs WITH (NOLOCK)
WHERE
ID_NUMBER in (
'1000764169'
,'1005870537'
,'1008053856'
,'1008054376'
,'1008410224'
,'1008411317'
,'1008465318'
,'1008466074'
,'1008492967'
,'1010546872'
,'1010554301')
Select * from #IDlist
And this works fine. But now I would like to declare a variable to represent this column, or each item in this column, so that I can then do a loop where it loops through each ID Number and returns information about each one and then presents all of that as a table. Here is my shot at that:
Declare #IDNumber as VARCHAR(10)
Set #IDNumber = #IDlist.ID_NUMBER
DECLARE #cnt INT = 0
WHILE #cnt < (Select Count(*) From #IDlist)
BEGIN
SELECT TOP 1
NAME
,MAILING_ADDRESS_1
,MAILING_ADDRESS_CITY
,MAILING_STATE
,MAILING_ZIP
from
V_Rpt_Info
WHERE
ID_NUMBER = #IDNumber
SET #cnt = #cnt + 1
END
DROP TABLE #IDlist
But when I Set the #IDNumber variable to #IDlist.ID_NUMBER, it says The multi-part identifier "#IDlist.ID_NUMBER" could not be bound.
How do I do this?
Thanks
The way you set the variable is not correct, SQL doesn't know which ID_NUMBER row it should assign to the #IDNumber variable.
You should do this with a SELECT, for example
SET #IDNumber = SELECT TOP 1 ID_NUMBER FROM #IDlist
But, why would you like to loop through this temporary table this way ? Isn't it possible to join the necessary data with this table instead of doing it one by one ?
Rather then loop through, you're going to want to join your ID table to your V_Rpt_Info view.
SELECT
NAME
, MAILING_ADDRESS_1
, MAILING_ADDRESS_CITY
, MAILING_STATE
, MAILING_ZIP
FROM V_Rpt_Info V
INNER JOIN #IDlist ID
ON V.ID_NUMBER = ID.ID_NUMBER

Arithmetic operation in MySQL through Procedure

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).

MySQL incrementing value

Is there a way to make a value increment with every insert if having multiple inserts? (I dont speak of the primary key that autoincrements)
Lets say I have a structure like this:
|ID_PRODUCT|ID_CATEGORY|NAME|POSITION|
So I have individual product ids, each produt belongs to a category and has a different position in this category. I want to do something like this:
INSERT INTO products
( SELECT id_product, id_category, name, MY_POSITION++
FROM db2.products WHERE id_category = xxx )
So there should be a variable MY_POSITION that starts with 1 and increments every insert.
It would be really easy to do this all just with a scripting-language like php or python, but I want to get better with SQL :)
Yes: Use a user defined variable:
SET #position := 0; -- Define a variable
INSERT INTO products
SELECT id_product, id_category, name, (#position := #position + 1)
FROM db2.products
WHERE id_category = xxx;
The result of increment to #position is the value used for the insert.
Edit:
You can skip the declaration of the variable by handling the initial value in-line:
...
SELECT ..., (#position := ifnull(#position, 0) + 1)
...
This can be particularly handy when executing the query using a driver that does not allow multiple commands (separated by semicolons).
You will need to ORDER BY id_category and use two user variables so you can track the previous category id -
SET #position := 0;
SET #prev_cat := 0;
INSERT INTO products
SELECT id_product, id_category, name, position
FROM (
SELECT
id_product,
id_category,
name,
IF(#prev_cat = id_category, #position := #position + 1, #position := 1) AS position,
#prev_cat := id_category
FROM db2.products
ORDER BY id_category ASC, id_product ASC
) AS tmp;
This will allow you to do all categories in one query instead of category by category.
Purely to add to #Bohemians answer - I wanted the counter to reset every so often and this can be done like such:
SELECT *,(#position := IF (#position >= 15,1,#position + 1))
Where 15 is obviously the maximum number before reset.
Try setting a value using a subquery like this
(SELECT MAX(position) FROM products AS T2)+1
Or
(SELECT MAX(position) FROM products AS T2 WHERE id_category = 'product category')+1
Hope this will work.
update <tbl_name> set <column_name>=<column_name>+1 where <unique_column/s>='1'";
For those who are looking for example of update query, here it is:
SET #i := 0;
UPDATE products SET id = (#i := #i + 1);

Sort table, using loop in MySQL with SELECT and UPDATE

Please help me to sort my table in MySQL. I need to make it only once, not for use in scripts or somewhere else dynamically.
Here's my table 'page':
| ... | title | ... | parent_id | ... | position | ... |
I need to update table with the rule: select all rows with the same parent_id, sort them by title DESC and set first row position = 0, second = 1 etc. And loop this for all parent_id.
I made a query to display what I need, but with no UPDATE and only for one parent_id. But as there are some thousands parent_id this query needs a loop. And after selecting, position must be updated to have the same value as #number for current row.
SET #number = -1;
SELECT #number:=#number+1 AS number, p.* FROM `page` AS p WHERE parent_id=1
ORDER BY title DESC;
Thanks for your help!
SELECT #p:=0, #parent:=0;
UPDATE page p1
JOIN
( SELECT title,
CASE WHEN #parent<>parent_id THEN #p:=0 ELSE #p:=#p+1 END as position,
#parent:=parent_id as parent_id
FROM page
ORDER BY parent_id, title DESC ) p2
ON p1.title = p2.title AND p1.parent_id = p2.parent_id
SET p1.position = p2.position
If your (parent_id,title) pairs aren't unique use your primary key as join condition - you will need to add it to select in parenthesis.
If you want a pure MySQL loop you'll have to create a stored procedure to do your looping. You should read up on MySQL cursors and then implement something like this:
DECLARE current_number INT;
DECLARE current_parent INT;
DECLARE current_id INT;
DECLARE my_cursor CURSOR FOR SELECT p.id FROM `page` AS p WHERE parent_id=current_parent;
SET current_parent := 1;
SET current_number := -1;
OPEN my_cursor;
read_loop: LOOP
SET current_number := current_number + 1;
FETCH my_cursor INTO current_id;
UPDATE page SET position = current_number WHERE id = current_id;
#whatever other logic you have
SET current_parent := current_id;
END LOOP;
CLOSE my_cursor;
Obviously I'm making lots of assumptions about your logic, but hopefully this will get your thought process moving ahead.