I have two tables in my MySql database:
Employee Table:
id | name | projectcount
---------------------------------
5 | john | 1
7 | mike | 1
8 | jane | 0
Project Table:
id | name | employeeId
---------------------------------
1 | pro1 | 5
2 | pro2 | 7
3 | pro3 | 8
CREATE TABLE EmployeeTable (
id INT,
name VARCHAR(30),
projectcount int
);
CREATE TABLE ProjectTable (
id INT,
name VARCHAR(30),
employeeId INT
);
INSERT INTO EmployeeTable (id, name, projectcount) VALUES
(5, "john", 1),
(7, "mike", 1),
(8, "jane", 0);
INSERT INTO ProjectTable (id, name, employeeId) VALUES
(1, "pro1", 5),
(2, "pro2", 7),
(3, "pro3", 8);
I would like to select 3 random records from the projects table and update the projectcount in employees table and return those records using select query.
My Approach using Stored Procedure:
DECLARE userId1 int DEFAULT 0;
DECLARE userId2 int DEFAULT 0;
DECLARE userId3 int DEFAULT 0;
DECLARE projectId1 int DEFAULT 0;
DECLARE projectId2 int DEFAULT 0;
DECLARE projectId3 int DEFAULT 0;
select id, employeeid into projectId1, userId1 from project order by RAND() LIMIT 1;
select id, employeeid into projectId2, userId2 from project order by RAND() LIMIT 1;
select id, employeeid into projectId3, userId3 from project order by RAND() LIMIT 1;
update employee set projectcount = projectcount + 1 where id = userId1;
update employee set projectcount = projectcount + 1 where id = userId2;
update employee set projectcount = projectcount + 1 where id = userId3;
select * from project where id in (projectId1, projectId2, projectId3);
The above code works but written in more static way. Is there any improvements done to this to look more cleaner. Thanks.
PLan A
"Is there any improvements done to this to look more cleaner." Do only one request at a time.
Since that probably does not satisfy your intended meaning of "cleaner", I will continue:
Plan B
Flip things around. And use a transaction.
This runs 3 times as fast, and fixes one potential bug. Does that qualify as "cleaner"?
BEGIN;
SELECT #ids := GROUP_CONCAT(id)
-- I assume that `id` is the PRIMARY KEY of `project`?
from project
order by RAND() LIMIT 3;
-- That will run 3 times as fast as 3 separate SELECTs.
-- It avoids tallying the same id 3 times
-- but it may hit the same employee 3 times
SET #sql = CONCAT("UPDATE employee
SET projectcount = projectcount + 1
WHERE id IN (", #ids, ")");
PREPARE _sql FROM #sql;
EXECUTE _sql;
DEALLOCATE PREPARE _sql;
... -- similar execute to get the `SELECT *`
COMMIT;
If you have millions of rows you are picking from, then be aware that ORDER BY RAND() always requires a full table scan and a sort. Here are some kludges to speed that up: http://mysql.rjweb.org/doc.php/random
Plan C
Tighten up the specs (no dup projects, double-assigning a user, define "cleaner", etc) Then we can look for other ways.
Related
I have multiple queries like
query1=select StudentName from student limit 1;
query2=select StudentAge from student limit 2;
I want something like
category value
StudentName query1 result
StudentAge query2 result
Just to illustrate how far away from a good question you are here's an answer which fully complies with your question but may be absolute rubbish.
drop table if exists student;
drop table if exists t;
create table student(id int, studentname varchar(3), studentage int);
create table t(val varchar(4));
insert into student values
(1,'aaa',19),(2,'bbb',19),(3,'ccc',30),(5,'ddd',20);
insert into t
select * from
(
select StudentName from student limit 1
) a
union all
(
select StudentAge from student limit 2
) ;
select * from t;
+------+
| val |
+------+
| aaa |
| 19 |
| 19 |
+------+
3 rows in set (0.001 sec)
I have a list of strings. Each of them have categories which are separated out by a '/'.
For example:
animals/domestic/dog
animals/domestic/cat
What I want to do with these categories is to insert into a MySql categories table.
The table has 4 columns:
id (int auto increment), category_name (nvarchar), parent_id (int), is_active (bit)
The logic around inserting these should be as follows:
The main categories (animals) should have a parent_id of 0.
The child categories will have the id of their parent as parent_id.
There cannot be two active categories with the same category name.
I have tried to implement the following logic:
Get a distinct list of strings.
From these, get a distinct list of main categories.
Insert the distinct main categories to the categories table with a parent ID of 0.
Organise each of the categories in pairs and get distinct pairs:
(animals, domestic)
(domestic, dog)
(domestic, cat)
Get the matching id for each of the parent categories and insert in to the child's parent_id
SQL:
/*INSERT ALL THE FIRST PARENT CATEGORIES WITH A PARENT ID OF 0*/
INSERT INTO categories (category_name, parent_id, is_active)
VALUES ('animals', 0, 1);
/*INSERT ALL THE CATEGORIES IN PAIRS TO TEMP TABLE*/
CREATE TEMPORARY TABLE tempcat(parent nvarchar(256), child nvarchar(256));
INSERT INTO tempcat
VALUES ('animals', 'domestic'),('domestic', 'dog'),('domestic','cat');
/*INSERT INTO THE CATEGORIES TABLE*/
INSERT INTO categories(category_name, parent_id, is_active)
SELECT tempcat.child, categories.id, 1
FROM categories
INNER JOIN tempcat
ON categories.category_name = tempcat.parent;
WHERE categories.is_active = 1;
/*DISPOSE THE TEMPORARY TABLE*/
DROP TEMPORARY TABLE tempcat;
Issue:
After the query is run I expect 4 entries in the categories table.
But I only get 2.
I can see that the temp table has correct entries before doing the last inner join.
I can't seem to figure out why the categories table wouldn't have the other two rows.
Any guidance in the right direction is highly appreciated.
Update #1
Suppose the specifications said 'There cannot be two active categories with the same category name that had the same parent IDs'.
For example, if there were two strings as (animals/domestic/cat), (animals/outdoor/cat) there should be two entries for cat with IDs of domestic and outdoor as parent_id's.
CREATE TABLE categories (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
category_name VARCHAR(64),
parent_id INT UNSIGNED NOT NULL DEFAULT 0,
is_active CHAR(1) NULL,
UNIQUE INDEX idx_name_active (category_name));
CREATE TABLE source_data (path TEXT);
INSERT INTO source_data VALUES ('animals/domestic/dog'), ('animals/domestic/cat');
CREATE PROCEDURE update_categories_table()
BEGIN
DECLARE cnt INT DEFAULT 0;
INSERT IGNORE INTO categories (category_name, parent_id, is_active)
SELECT SUBSTRING_INDEX(path, '/', 1), 0, '1'
FROM source_data;
iteration: LOOP
SELECT COUNT(*) INTO cnt
FROM source_data
WHERE LOCATE('/', path);
IF NOT cnt THEN
LEAVE iteration;
END IF;
INSERT IGNORE INTO categories (category_name, parent_id, is_active)
SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(source_data.path, '/', 2), '/', -1),
categories.id,
'1'
FROM source_data, categories
WHERE SUBSTRING_INDEX(source_data.path, '/', 1) = categories.category_name;
UPDATE source_data
SET path = SUBSTRING(path FROM 1 + LOCATE('/', path));
END LOOP iteration;
TRUNCATE source_data;
END
call update_categories_table;
SELECT * FROM categories;
id | category_name | parent_id | is_active
-: | :------------ | --------: | :--------
1 | animals | 0 | 1
4 | domestic | 1 | 1
7 | dog | 4 | 1
8 | cat | 4 | 1
db<>fiddle here
In MySQL 8, you can do this with a single query:
with splits as (
select 1 as n, substring_index(cats, '/', 1) as cat, cats
from strings union all
select 2 as n, substring_index(substring_index(cats, '/', 2), '/', -1) as cat, cats
from strings
where cats like '%/%' union all
select 3 as n, substring_index(substring_index(cats, '/', 3), '/', -1) as cat, cats
from strings
where cats like '%/%/%'
),
splits_n as (
select s.*, dense_rank() over (order by n, cat) as new_id
from splits s
),
splits_np as (
select s.*, sp.new_id as parent_id
from splits_n s left join
splits_n sp
on sp.cats = s.cats and sp.n = s.n - 1
)
select distinct new_id as id, cat, parent_id, 1 as is_active
from splits_np s;
Here is a db<>fiddle.
Unfortunately, this is much more painful in earlier versions.
Say I have customers who can be awarded certain prizes:
SELECT gs.claimed_by AS consumer_id, p.prize_id AS prize_id FROM
awarded_prizes
And right now, customer 1 has three prizes and customer 2 has a single prize
+-------------+----------+
| consumer_id | prize_id |
+-------------+----------+
| 1 | 45 |
| 1 | 46 |
| 1 | 47 |
| 2 | 66 |
+-------------+----------+
Say we also have collections, and if you collect all the members to that collectible, you now have a collectable set:
SELECT set_id, member_prize_id AS prize_id FROM collectable_set_members;
+--------+----------+
| set_id | prize_id |
+--------+----------+
| 1 | 45 |
| 1 | 46 |
| 1 | 47 |
| 2 | 65 |
| 2 | 66 |
+--------+----------+
With the above table and the previous query, we can see customer 1 has completed set 1 once (they have 45, 46, 47) and customer 2 has completed nothing.
There are cases where a customer can complete a set multiple times (customer could have 45, 46, 47, 45, 46, 47 in the awarded_prize table.
I've been looking at the pantry problem and its variations (like the bartender problem), have been playing with cross joins and groupings and can't seem to find what I want.
I'm trying to get a result for a given customer, showing all the set_ids they own and the number of sets they've completed:
+-------------+---------------+--------+
| consumer_id | completed_set | count |
+-------------+---------------+--------+
| 1 | 1 | 1 |
+-------------+---------------+--------+
I'm on mariadb:5.5
See here SqlFiddle
My tables have different names than yours, but it proves the point:
select sets_x_consumers.consumer_id, sets_x_consumers.set_id,
set_summary.items_in_set = consumer_summary.items_per_set_per_consumer as set_is_complete
from (
-- build a cross-product of sets and consumers
select distinct set_id, consumer_id
from sets join consumers -- no join condition -> cross product
) sets_x_consumers
inner join
( -- the total number of items in each set per set_id
select set_id, count(*) items_in_set
from sets
group by set_id
) set_summary on sets_x_consumers.set_id = set_summary.set_id
inner join
( -- the total number of items per set and customer
select set_id, consumer_id, count(*) items_per_set_per_consumer
from sets
inner join consumers on sets.prize_id = consumers.prize_id
group by consumer_id, set_id
) consumer_summary on sets_x_consumers.set_id = consumer_summary.set_id and sets_x_consumers.consumer_id = consumer_summary.set_id
My basic idea is to sum up the number of items in each set and the number of items per set each consumer has claimed. As long as there are no duplicate entries for the pair of consumer and prize, this should work (if duplicates were allowed, I would use count distinct(prize_id) for the consumer_summary).
The output of the query above is:
| consumer_id | set_id | set_is_complete |
|-------------|--------|-----------------|
| 1 | 1 | 1 |
| 2 | 2 | 0 |
This lists each pair of consumers and set for which a consumer has at least one prize. (to change this to list every consumer-set combination, use outer join)
Listing only complete sets or summarizing the number of complete sets should be easy on this basis ;-)
Can't really figure out what your last column 'count' is supposed to mean,
but here is a solution that lists users and their sets completed.
demo Link
The whole idea is to count the number of prizes required for each set, and count the collected prizes per customer per set, and thus you can join the two.
I know it's mssql, but I did not manage to make mysql ctes work in sqfiddle.
CTE-s is nothing more than a subquery basically. If your server does not support CTE-s you could use normal subqueries or temp tables instead.
For what it's worth I came up with a nice routine for this in Sql Server. This works even if there are overlapping prize_id values in each set (will default to higher setid if ambiguous). Assume all temp tables are original data:
declare #awarded_prize table (rowid int identity, consumer_id int, prize_id int )
insert #awarded_prize
select * from #awarded_prizes
declare #collections table ( set_id int, prize_id int, rownumber int , filled int)
insert #collections
select *, row_number() over(partition by set_id order by set_id, prize_id) , null
from #collections
declare #todelete table (rowid int)
declare #scorecard table (consumer_id int, set_id int)
declare #iterator int=1
declare #prize_id int
declare #set_id int = (Select min(set_id) from #collections)
declare #consumer_id int = (Select min(consumer_id) from #awarded_prize)
while #consumer_id<=(select max(consumer_id) from #awarded_prize)
begin
while #set_id<=(select max(set_id) from #collections)
begin
while 1=1
begin
select #prize_id=prize_id
from #collections
where set_id=#set_id and rownumber=#iterator
if (select max(rowid) from #awarded_prize where prize_id=#prize_id and consumer_id=#consumer_id and rowid not in (select rowid from #todelete)) is null break
insert #todelete
select max(rowid) from #awarded_prize where prize_id=#prize_id and consumer_id=#consumer_id and rowid not in (select rowid from #todelete)
update #collections set filled=1
where rownumber=#iterator and set_id=#set_id
if not exists(select 1 from #collections where set_id=#set_id and filled is null)
begin
insert #scorecard
select #consumer_id, #set_id
delete #awarded_prize where rowid in (Select rowid from #todelete)
delete #todelete
update #collections set filled=null where filled=1
end
set #iterator=case when #iterator=(Select max(rownumber) from #collections where set_id=#set_id) then
(select min(rownumber) from #collections where set_id=#set_id) else #iterator+1 end
end
delete #todelete
set #iterator=1
set #set_id=#set_id+1
end
set #iterator=1
select #set_id=min(set_id) from #collections
select #consumer_id=min(consumer_id) from #awarded_prize where consumer_id>#consumer_id
end
select consumer_id, set_id, count(*) complete_sets
from #scorecard
group by consumer_id, set_id
order by consumer_id, set_id
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
I have seen this issue in SF, but me being a noob I just can't get my fried brain around them. So please forgive me if this feels like repetition.
My Sample Table
--------------------------
ID | Supplier | QTY
--------------------------
1 1 2
2 1 2
3 2 5
4 3 2
5 1 3
6 2 4
I need to get the rows "UNTIL" the cumulative total for "QTY" is equal or greater than 5 in descending order for a particular supplier id.
In this example, for supplier 1, it will be rows with the ids of 5 and 2.
Id - unique primary key
Supplier - foreign key, there is another table for supplier info.
Qty - double
It ain't pretty, but I think this does it and maybe it can be the basis of something less cumbersome. Note that I use a "fake" INNER JOIN just to get some variable initialized for the first time--it serves no other role.
SELECT ID,
supplier,
qty,
cumulative_qty
FROM
(
SELECT
ID,
supplier,
qty,
-- next line keeps a running total quantity by supplier id
#cumulative_quantity := if (#sup <> supplier, qty, #cumulative_quantity + qty) as cumulative_qty,
-- next is 0 for running total < 5 by supplier, 1 the first time >= 5, and ++ after
#reached_five := if (#cumulative_quantity < 5, 0, if (#sup <> supplier, 1, #reached_five + 1)) as reached_five,
-- next takes note of changes in supplier being processed
#sup := if(#sup <> supplier, supplier, #sup) as sup
FROM
(
--this subquery is key for getting things in supplier order, by descending id
SELECT *
FROM `sample_table`
ORDER BY supplier, ID DESC
) reverse_order_by_id
INNER JOIN
(
-- initialize the variables used to their first ever values
SELECT #cumulative_quantity := 0, #sup := 0, #reached_five := 0
) only_here_to_initialize_variables
) t_alias
where reached_five <= 1 -- only get things up through the time we first get to 5 or above.
How about this? Using two variables.
SQLFIDDLE DEMO
Query:
set #tot:=0;
set #sup:=0;
select x.id, x.supplier, x.ctot
from (
select id, supplier, qty,
#tot:= (case when #sup = supplier then
#tot + qty else qty end) as ctot,
#sup:=supplier
from demo
order by supplier asc, id desc) x
where x.ctot >=5
;
| ID | SUPPLIER | CTOT |
------------------------
| 2 | 1 | 5 |
| 1 | 1 | 7 |
| 3 | 2 | 5 |
Standard SQL has no concept of 'what row number am I up to', so this can only be implemented using something called a cursor. Writing code with cursors is something like writing code with for loops in other languages.
An example of how to use cursors is here:
http://dev.mysql.com/doc/refman/5.0/en/cursors.html
Here is a rough demo about cursor, may be it's helpful.
CREATE TABLE #t
(
ID INT IDENTITY,
Supplier INT,
QTY INT
);
TRUNCATE TABLE #t;
INSERT INTO #t (Supplier, QTY)
VALUES (1, 2),
(1, 2),
(2, 5),
(3, 2),
(1, 3);
DECLARE #sum AS INT;
DECLARE #qty AS INT;
DECLARE #totalRows AS INT;
DECLARE curSelectQTY CURSOR
FOR SELECT QTY
FROM #t
ORDER BY QTY DESC;
OPEN curSelectQTY;
SET #sum = 0;
SET #totalRows = 0;
FETCH NEXT FROM curSelectQTY INTO #qty;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #sum = #sum + #qty;
SET #totalRows = #totalRows + 1;
IF #sum >= 5
BREAK;
END
SELECT TOP (#totalRows) *
FROM #t
ORDER BY QTY DESC;
CLOSE curSelectQTY;
DEALLOCATE curSelectQTY;
SELECT x.*
FROM supplier_stock x
JOIN supplier_stock y
ON y.supplier = x.supplier
AND y.id >= x.id
GROUP
BY supplier
, id
HAVING SUM(y.qty) <=5;