Optimize while loop - mysql

I am using this procedure that I created to create a member's downline.
PROCEDURE get_downline(IN id INT)
BEGIN
declare cur_depth int default 1;
-- Create the structure of the final table
drop temporary table if exists tmp_downline;
create temporary table tmp_downline (
member_id int unsigned,
referrer_id int unsigned,
depth tinyint unsigned
);
-- Create a table for the previous list of users
drop temporary table if exists tmp_members;
create temporary table tmp_members(
member_id int unsigned
);
-- Make a duplicate of tmp_members so we can select on both
drop temporary table if exists tmp_members2;
create temporary table tmp_members2(
member_id int unsigned
);
-- Create the level 1 downline
insert into tmp_downline select id, member_id, cur_depth from members where referrer_id = id;
-- Add those members into the tmp table
insert into tmp_members select member_id from members where referrer_id = id;
myLoop: while ((select count(*) from tmp_members) > 0) do
-- Set next level of users
set cur_depth = cur_depth + 1;
-- Insert next level of users into table
insert into tmp_downline select id, member_id, cur_depth from members where referrer_id in(select member_id from tmp_members);
-- Re-fill duplicate temporary table
truncate table tmp_members2;
insert into tmp_members2 select member_id from tmp_members;
-- Reset the default temporary table
truncate table tmp_members;
insert into tmp_members select member_id from members where referrer_id in(select member_id from tmp_members2);
end while;
-- Get the final list of results
select * from tmp_downline order by depth;
END
Here are my results:
Found rows: 424,097; Duration for 1 query: 12.438 sec.
All the queries look like they are using optimized indexes, but it is still taking a while to run. Is there a better way to run my while loop? I feel that making 2 temporary tables might be part of the issue, but when running my last insert query I can't reopen the temporary table which is why I made a duplicate table.
Here is a slimmed down version of the original table (original has 50 cols):
CREATE TABLE `members` (
`member_id` INT(11) NOT NULL AUTO_INCREMENT,
`referrer_id` INT(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`member_id`),
INDEX `referrer_id_idx` (`referrer_id`)
);
What I am trying to achieve is to get an MLM downline.
Here is a picture that shows a downline where the number shows the level and you are the main circle at the top.
Level 1: People you referred to the program
Level 2: People your referrals referred to the program
Level 3: People your referrals, referrals referred to the program
Level 4: ...
Level 5: ....
Level ........

Re-evaluating select count(*) from tmp_members for each iteration is expensive and unnecessary. Find the initial count then deduct mysql_affected_rows() after each operation. Doing a proper join in the the insert....select...in (select...) will likely result in better plans.
You've not shown the structure of the members table nor the EXPLAIN plans for the queries, so its impossible to say if there's any scope for optimising there, but you've probably get a big performance uplift by using a proper graph database rather than a relational one for this task. Or maintain a denormalised view of the results populated when you add a new record to the base table. Or use a more appropriate tree representation such as an adjacency list.

I think you might be able to do away with the second tmp_members table...
PROCEDURE get_downline(IN id INT)
BEGIN
declare cur_depth int default 1;
-- Create the structure of the final table
drop temporary table if exists tmp_downline;
create temporary table tmp_downline (
member_id int unsigned,
referrer_id int unsigned,
depth tinyint unsigned
);
-- Create a table for the previous list of users
drop temporary table if exists tmp_members;
create temporary table tmp_members(
member_id int unsigned
);
-- Create the level 1 downline
insert into tmp_downline
select id, member_id, cur_depth
from members
where referrer_id = id
;
myLoop: while (mysql_affected_rows()>1) do
-- Load tmp_members with previously added referred members
-- to use as the next potential referrers
truncate tmp_members;
insert into tmp_members
select referrer_id
from tmp_downline
where depth = cur_depth
;
-- Set next level of users
set cur_depth = cur_depth + 1;
-- Insert next level of users into table
insert into tmp_downline
select id, m.member_id, cur_depth
from members AS m
INNER JOIN tmp_members AS t ON m.referrer_id = t.member_id
;
end while;
-- Get the final list of results
select * from tmp_downline order by depth;
END
In this case, tmp_members could be more appropriately named tmp_candidate_referrers, but I left it as it was to show how little of a change it was.
Also, for this version, you might consider adding indexes for tmp_downline.cur_depth and tmp_members.member_id since they are repeatedly used.
--- Edit ---
I made a correction in the first insert within the loop above. referrer_id should be selected from tmp_downline, not member_id; which brings me to a question...
Why bother with the tmp_downline.member_id field when it always contains the value of the argument passed?
If you need it in the final results, you can always finish with
SELECT id AS member_id, referrer_id, depth FROM tmp_downline ORDER BY depth;

Related

How to get last inserted rows in MySQL from different tables?

I have a lot of different tables in my database, and I need somehow to get last inserted rows from those tables. Like social networks feeds. Also, those tables are have not random, but unknown names, because they all generated by users.
In example:
I have tables: A,B,C and D with 5k rows in each table.
I need somehow to get last rows from those tables and make it ordered by id, like we do in a simple query: "SELECT * FROM table A ORDER BY id DESC", but I'm looking for something like: "SELECT * FROM A,B,C,D ORDER BY id DESC".
Tables have same structure.
You can use union and order by if your tables have the same structure. Something like:
select *
from (
select * from A
union all
select * from B
union all
select * from C
) order by id desc
If the tables don't have the same structure then you cannot select * from all and order them and you might want to do two queries. First would be:
select id, tableName
from (
select id, 'tableA' as tableName from A
union all
select id, 'tableB' as tableName from B
union all
select id, 'tableC' as tableName from C
) order by id desc
Which will give you the last IDs and the tables where they are inserted. And then you need to get the rows from each respective table.
With pure Mysql it will be a bit hard. You can select the table names like:
SELECT table_name FROM information_schema.tables; but still how to use them in the statement? You will need to generate it dynamically
A procedure to generate the query dynamically can be something like ( I haven't tested it but I believe with some debugging it should work) :
DELIMITER $$
CREATE PROCEDURE buildQuery (OUT v_query VARCHAR)
BEGIN
DECLARE v_finished INTEGER DEFAULT 0;
DECLARE v_table_count INTEGER DEFAULT 0;
DECLARE v_table varchar(100) DEFAULT "";
-- declare cursor for tables (this is for all tables but can be changed)
DEClARE table_cursor CURSOR FOR
SELECT table_name FROM information_schema.tables;
-- declare NOT FOUND handler
DECLARE CONTINUE HANDLER
FOR NOT FOUND SET v_finished = 1;
OPEN table_cursor;
SET v_query="select * from ( ";
get_table: LOOP
FETCH table_cursor INTO v_table;
SET v_table_count = v_table_count + 1;
IF v_finished = 1 THEN
LEAVE get_table;
END IF;
if v_table_count>1 THEN
CONCAT(vquery, " UNION ALL ")
END IF;
SET v_query = CONCAT(vquery," select * from ", v_table );
END LOOP get_table;
SET v_query = CONCAT(vquery," ) order by id desc " );
-- here v_query should be the created query with UNION_ALL
CLOSE table_cursor;
SELECT #v_query;
END$$
DELIMITER ;
If each table's id is counted seperatly you can't order by ID, so you'll need to calculate a global id and use it on all of your tables.
You can do it as follows:
Assuming you have 2 tables A,B:
Create Table A(id int NOT NULL auto_increment, name varchar(max), value varchar(max), PRIMARY_KEY(id));
Create Table B(id int NOT NULL auto_increment, name varchar(max), value varchar(max), PRIMARY_KEY(id));
Add another table IDS with id as auto increment primary key.
Create table IDS (id int NOT NULL auto_increment, ts Timestamp default CURRENT_TIMESTAMP, PRIMARY_KEY(id));
For all your tables id column should use now the id from the IDS table as foreign key instead of auto increment.
Create Table A(id int NOT NULL auto_increment, name varchar(max), value varchar(max), PRIMARY_KEY(id),CONSTRAINT fk_A_id FOREIGN KEY(id) REFERENCE IDS(id) ON DELETE CASCADE ON UPDATE CASCADE);
Create Table B(id int NOT NULL auto_increment, name varchar(max), value varchar(max), PRIMARY_KEY(id),CONSTRAINT fk_A_id FOREIGN KEY(id) REFERENCE IDS(id) ON DELETE CASCADE ON UPDATE CASCADE);
for each table add before insert trigger, the trigger should first insert row to the IDS table and insert the LAST_INSERT_ID the table.
Create TRIGGER befor_insert_A BEFORE INSERT On A
FOR EACH ROW
BEGIN
insert into IDS() values ();
set new.id = LAST_INSERT_ID();
END
Create TRIGGER befor_insert_B BEFORE INSERT On B
FOR EACH ROW
BEGIN
insert into IDS() values ();
set new.id = LAST_INSERT_ID();
END
Now you can create view from all tables with union all, the rows of v can be sorted now by the id and give the cronlogic order of insertion.
Create view V AS select * from A UNION ALL select * from B
For example you can query on V the latest 10 ids:
select * from V Order by id desc LIMIT 10
Other option is to add timestamp for each table and sort the view by the timestamp.
Hi are you looking for this? However the id is not a good column to see the last updated among different tables.
select *
from A
join B
on 1=1
join C
on 1=1
join D
on 1=1
order by A.id desc

MySQL loop for every row and update

I have table called users and for example it looks like:
Name ID
Tom 1
Al 55
Kate 22
...
The problem is: the IDs are not in sequence.
I would like to give them new IDs from 1 to length of users. I would like to declare some var=1 and make UPDATE in loop and give them new ID = var, and later do var=var+1 until var <= users length
How can I do this?
Thank you very much!
Here is how you would do that in MySQL. Just run this:
set #newid=0;
update users set ID = (#newid:=#newid+1) order by ID;
If the ID in the Users table is not referenced by other tables by FK, the following query can update the ID in the table to have new consecutive values:
CREATE TABLE IF NOT EXISTS tmpUsers (
ID int not null,
newID int not null auto_increment primary key
) engine = mysisam;
INSERT INTO tmpUsers (ID,newID)
SELECT ID,NULL
FROM users
ORDER BY ID;
UPDATE users u INNER JOIN tmpUsers t
ON u.ID=t.ID
SET u.ID=t.NewID;
DROP TABLE IF EXISTS tmpUsers;
Test script:
CREATE TABLE users (ID int not null, name nvarchar(128) not null);
INSERT users(ID,name)
VALUES (1,'aaa'),(4,'bbb'),(7,'ggg'),(17,'ddd');
SELECT * FROM users;

Recursive mysql select?

I saw this answer and i hope he is incorrect, just like someone was incorrect telling primary keys are on a column and I can't set it on multiple columns.
Here is my table
create table Users(id INT primary key AUTO_INCREMENT,
parent INT,
name TEXT NOT NULL,
FOREIGN KEY(parent)
REFERENCES Users(id)
);
+----+--------+---------+
| id | parent | name |
+----+--------+---------+
| 1 | NULL | root |
| 2 | 1 | one |
| 3 | 1 | 1down |
| 4 | 2 | one_a |
| 5 | 4 | one_a_b |
+----+--------+---------+
I'd like to select user id 2 and recurse so I get all its direct and indirect child (so id 4 and 5).
How do I write it in such a way this will work? I seen recursion in postgresql and sqlserver.
CREATE DEFINER = 'root'#'localhost'
PROCEDURE test.GetHierarchyUsers(IN StartKey INT)
BEGIN
-- prepare a hierarchy level variable
SET #hierlevel := 00000;
-- prepare a variable for total rows so we know when no more rows found
SET #lastRowCount := 0;
-- pre-drop temp table
DROP TABLE IF EXISTS MyHierarchy;
-- now, create it as the first level you want...
-- ie: a specific top level of all "no parent" entries
-- or parameterize the function and ask for a specific "ID".
-- add extra column as flag for next set of ID's to load into this.
CREATE TABLE MyHierarchy AS
SELECT U.ID
, U.Parent
, U.`name`
, 00 AS IDHierLevel
, 00 AS AlreadyProcessed
FROM
Users U
WHERE
U.ID = StartKey;
-- how many rows are we starting with at this tier level
-- START the cycle, only IF we found rows...
SET #lastRowCount := FOUND_ROWS();
-- we need to have a "key" for updates to be applied against,
-- otherwise our UPDATE statement will nag about an unsafe update command
CREATE INDEX MyHier_Idx1 ON MyHierarchy (IDHierLevel);
-- NOW, keep cycling through until we get no more records
WHILE #lastRowCount > 0
DO
UPDATE MyHierarchy
SET
AlreadyProcessed = 1
WHERE
IDHierLevel = #hierLevel;
-- NOW, load in all entries found from full-set NOT already processed
INSERT INTO MyHierarchy
SELECT DISTINCT U.ID
, U.Parent
, U.`name`
, #hierLevel + 1 AS IDHierLevel
, 0 AS AlreadyProcessed
FROM
MyHierarchy mh
JOIN Users U
ON mh.Parent = U.ID
WHERE
mh.IDHierLevel = #hierLevel;
-- preserve latest count of records accounted for from above query
-- now, how many acrual rows DID we insert from the select query
SET #lastRowCount := ROW_COUNT();
-- only mark the LOWER level we just joined against as processed,
-- and NOT the new records we just inserted
UPDATE MyHierarchy
SET
AlreadyProcessed = 1
WHERE
IDHierLevel = #hierLevel;
-- now, update the hierarchy level
SET #hierLevel := #hierLevel + 1;
END WHILE;
-- return the final set now
SELECT *
FROM
MyHierarchy;
-- and we can clean-up after the query of data has been selected / returned.
-- drop table if exists MyHierarchy;
END
It might appear cumbersome, but to use this, do
call GetHierarchyUsers( 5 );
(or whatever key ID you want to find UP the hierarchical tree for).
The premise is to start with the one KEY you are working with. Then, use that as a basis to join to the users table AGAIN, but based on the first entry's PARENT ID. Once found, update the temp table as to not try and join for that key again on the next cycle. Then keep going until no more "parent" ID keys can be found.
This will return the entire hierarchy of records up to the parent no matter how deep the nesting. However, if you only want the FINAL parent, you can use the #hierlevel variable to return only the latest one in the file added, or ORDER BY and LIMIT 1
I know there is probably better and more efficient answer above but this snippet gives a slightly different approach and provides both - ancestors and children.
The idea is to constantly insert relative rowIds into temporary table, then fetch a row to look for it's relatives, rinse repeat until all rows are processed. Query can be probably optimized to use only 1 temporary table.
Here is a working sqlfiddle example.
CREATE TABLE Users
(`id` int, `parent` int,`name` VARCHAR(10))//
INSERT INTO Users
(`id`, `parent`, `name`)
VALUES
(1, NULL, 'root'),
(2, 1, 'one'),
(3, 1, '1down'),
(4, 2, 'one_a'),
(5, 4, 'one_a_b')//
CREATE PROCEDURE getAncestors (in ParRowId int)
BEGIN
DECLARE tmp_parentId int;
CREATE TEMPORARY TABLE tmp (parentId INT NOT NULL);
CREATE TEMPORARY TABLE results (parentId INT NOT NULL);
INSERT INTO tmp SELECT ParRowId;
WHILE (SELECT COUNT(*) FROM tmp) > 0 DO
SET tmp_parentId = (SELECT MIN(parentId) FROM tmp);
DELETE FROM tmp WHERE parentId = tmp_parentId;
INSERT INTO results SELECT parent FROM Users WHERE id = tmp_parentId AND parent IS NOT NULL;
INSERT INTO tmp SELECT parent FROM Users WHERE id = tmp_parentId AND parent IS NOT NULL;
END WHILE;
SELECT * FROM Users WHERE id IN (SELECT * FROM results);
END//
CREATE PROCEDURE getChildren (in ParRowId int)
BEGIN
DECLARE tmp_childId int;
CREATE TEMPORARY TABLE tmp (childId INT NOT NULL);
CREATE TEMPORARY TABLE results (childId INT NOT NULL);
INSERT INTO tmp SELECT ParRowId;
WHILE (SELECT COUNT(*) FROM tmp) > 0 DO
SET tmp_childId = (SELECT MIN(childId) FROM tmp);
DELETE FROM tmp WHERE childId = tmp_childId;
INSERT INTO results SELECT id FROM Users WHERE parent = tmp_childId;
INSERT INTO tmp SELECT id FROM Users WHERE parent = tmp_childId;
END WHILE;
SELECT * FROM Users WHERE id IN (SELECT * FROM results);
END//
Usage:
CALL getChildren(2);
-- returns
id parent name
4 2 one_a
5 4 one_a_b
CALL getAncestors(5);
-- returns
id parent name
1 (null) root
2 1 one
4 2 one_a

how to update rows of temporary table in stored procedure in mysql

i wrote a stored procedure(getAllInitializedContact())to get data from diffrent tables.getAllInitializedContact()
CREATE DEFINER=`root`#`192.168.1.15` PROCEDURE `getAllInitializedContactNext`()
BEGIN
DROP TEMPORARY TABLE IF EXISTS temp_users;
CREATE TEMPORARY TABLE temp_users (
SELECT
c.id as contactId,
c.first_name as contactName,
c.email_address as contactEmailAddress,
sl.id as subscriberListId,
sl.name as subscriberListName,
sl.display_name as subscriberListDisplayName,
sl.from_email_address,<br/>
sl.opt_in_msg_subject as subject,
sl.opt_in_msg_content as content,
sl.opt_in_msg_signature as signature,
csl.identifier
FROM contact c
INNER JOIN contact_subscriber_list csl ON csl.contact_id=c.id
INNER JOIN subscriber_list sl ON sl.id=csl.sub_list_id
INNER JOIN contact_sub_list_status csls ON csls.id=csl.status_id where csls.description='initialized');
END
but now i want to update that resultset.so i create a temporary table(temp_users) and i need to do some update to some column in temporary table(temp_users).but i cant understand how can i iterate over that and update the temp_table.i tried using while loop and dont know how it apply(while loop).can i do this using while loop? and how can i apply it?need help
regards
kosala
Not sure why you want to use a cursor to update the rows in your tmp table when you can simply just update them - see following example:
drop table if exists foo;
create table foo
(
foo_id int unsigned not null auto_increment primary key,
value int unsigned not null default 0
)
engine=innodb;
insert into foo (value) values (10),(20),(30);
drop procedure if exists bar;
delimiter #
create procedure bar()
begin
create temporary table tmp engine=memory select * from foo;
update tmp set value = value + 100;
select * from tmp;
drop temporary table if exists tmp;
end #
delimiter ;
call bar();
I was making a mistake using the update so it was not working. I though it might help someone so am sharing:
For the update to work with a temp table, data to be updated must match the column type you want to update. You can't update a string/text/varchar column with a number. This would just not work.

Optimization of SQL query regarding pair comparisons

I'm working on a pair comparison site where a user loads a list of films and grades from another site. My site then picks two random movies and matches them against each other, the user selects the better of the two and a new pair is loaded. This gives a complete list of movies ordered by whichever is best.
The database contains three tables;
fm_film_data - this contains all imported movies
fm_film_data(id int(11),
imdb_id varchar(10),
tmdb_id varchar(10),
title varchar(255),
original_title varchar(255),
year year(4),
director text,
description text,
poster_url varchar(255))
fm_films - this contains all information related to a user, what movies the user has seen, what grades the user has given, as well as information about each film's wins/losses for that user.
fm_films(id int(11),
user_id int(11),
film_id int(11),
grade int(11),
wins int(11),
losses int(11))
fm_log - this contains records of every duel that has occurred.
fm_log(id int(11),
user_id int(11),
winner int(11),
loser int(11))
To pick a pair to show the user, I've created a mySQL query that checks the log and picks a pair at random.
SELECT pair.id1, pair.id2
FROM
(SELECT part1.id AS id1, part2.id AS id2
FROM fm_films AS part1, fm_films AS part2
WHERE part1.id <> part2.id
AND part1.user_id = [!!USERID!!]
AND part2.user_id = [!!USERID!!])
AS pair
LEFT JOIN
(SELECT winner AS id1, loser AS id2
FROM fm_log
WHERE fm_log.user_id = [!!USERID!!]
UNION
SELECT loser AS id1, winner AS id2
FROM fm_log
WHERE fm_log.user_id = [!!USERID!!])
AS log
ON pair.id1 = log.id1 AND pair.id2 = log.id2
WHERE log.id1 IS NULL
ORDER BY RAND()
LIMIT 1
This query takes some time to load, about 6 seconds in our tests with two users with about 800 grades each.
I'm looking for a way to optimize this but still limit all duels to appear only once.
The server runs MySQL version 5.0.90-community.
i think you are better off creating a stored procedure/function which will return a pair as soon as it found a valid one.
make sure there are proper indexes:
fm_films.user_id (try including the film_id also)
fm_log.user_id (try including the winner and loser)
DELIMITER $$
DROP PROCEDURE IF EXISTS spu_findPair$$
CREATE PROCEDURE spu_findPair
(
IN vUserID INT
)
BEGIN
DECLARE done BOOLEAN DEFAULT FALSE;
DECLARE vLastFilmID INT;
DECLARE vCurFilmID INT;
DECLARE cUserFilms CURSOR FOR
SELECT id
FROM fm_films
WHERE user_id = vUserID
ORDER BY RAND();
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=TRUE;
OPEN cUserFilms;
ufLoop: LOOP
FETCH cUserFilms INTO vCurFilmID;
IF done THEN
CLOSE cUserFilms;
LEAVE ufLoop;
END IF;
IF vLastFilmID IS NOT NULL THEN
IF NOT EXISTS
(
SELECT 1
FROM fm_log
WHERE user_id = vUserID
AND ((winner = vCurFilmID AND loser = vLastFilmID) OR (winner = vLastFilmID AND loser = vCurFilmID))
) THEN
CLOSE cUserFilms;
LEAVE ufLoop;
#output
SELECT vLastFilmID, vCurFilmID;
END IF;
END IF;
END LOOP;
END$$
DELIMITER ;
Have you tried applying any indexes to the tables?
The user_id columns would be a good start. The id field that is also used in the WHERE clause would be another index that might be worth adding.
Benchmakr to make sure the addition of the indices do result in speedups and do not slow other code (eg. insertions).
However, I have found that simple indexes on short tables like these can still result in some huge speed ups when they apply to fields in the WHERE clauses of SELECT and UPDATE statements.