Is there any way to output multiple table rows if a certain field in the table is greater than 1.
Here's my example:
I'm building an auction website, where we sell tickets for a raffle.
The tickets are stored in a table like so:
id, order_id, product_id, qty, price
When the time comes to print the tickets, I want to dump all of it into a CSV.
So far, I'm doing this query (simplifying, omitting INNER JOIN):
SELECT id, order_id, product_id, qty, price FROM order_details
And then running something like the following loop on it:
foreach($rows as $row) {
for($i = 0; $i < $row['qty']; $i++) {
$tickets[] = $row;
}
}
so that I get a separate entry for each qty (so that people get the correct amount of entries...).
Is there any way to accomplish this in SQL itself, so that each row is multiplied x times, where x is a certain field in the table (qty in this example)?
You can accomplish this purely in MySQL using a blackhole table and a trigger
Set up tables
First create the blackhole table you're going to insert to and the memory (or temporary table) the blackhole will reroute to.
CREATE TABLE Blackhole1 LIKE order_details ENGINE = BLACKHOLE;
CREATE TABLE temp_order_results LIKE order_details ENGINE = MEMORY;
Set up trigger
Now create a trigger on the blackhole table that will reroute the insert to the memory table, duplicating the rows with qty > 1.
DELIMITER $$
CREATE TRIGGER ai_Blackhole1_each AFTER INSERT ON blackhole1 FOR EACH ROW
BEGIN
DECLARE ACount INTEGER;
SET ACount = new.qty;
WHILE ACount > 1 DO BEGIN
INSERT INTO temp_order_results
VALUES (new.id, new.order_id, new.product_id, 1, new.price)
SET ACount = ACount - 1;
END; END WHILE;
END $$
DELIMITER ;
Statements to do the query
Now do a insert .. select into the blackhole
INSERT INTO blackhole1
SELECT id, order_id, product_id, qty, price FROM order_details;
And a select on temp_order_results.
SELECT id, order_id, product_id, qty, price FROM order_details;
To expand on #zdennis' answer, you could do this in MySQL:
SELECT order_details.*
FROM order_details
INNER JOIN kilo
ON kilo.i < order_details.qty;
where the "kilo" relation has the integers 0 - 999, a contrivance adapted from a post by xaprb:
CREATE TABLE deca (i integer not null);
INSERT INTO deca (i) VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9);
CREATE VIEW kilo (i) AS
SELECT iii.i * 100 + ii.i * 10 + i.i
FROM deca iii
CROSS JOIN deca ii
CROSS JOIN deca i;
There's not really a performance reason to. MySQL has a couple of strong suits: sorting, indexing, searching, storing, etc. You might as well do this in PHP.
The appropriate response is likely to use dual connect by level. See this question for related information: How can I return multiple identical rows based on a quantity field in the row itself?
Although this doesn't work in MySQL, see: How do I make a row generator in MySQL?
If you're using MySQL you'll need to be content with doing it in PHP or doing something gross (like the trigger that Johan posted). I'd vote to simply do it in PHP if that was the case.
I think this might be possible in Sql Server or Oracle by using a recursive common table expression (CTE) that joins the original table to itself and includes Qty-1 as an expression in place of Qty in the select list of the CTE. Sadly, last I heard MySql doesn't support CTEs yet.
Another option is to build a simple sequence table that just includes a numeric column and rows that start with 1 and end with the largest number you'll realistically have in the Qty column of your original table. You can join this to your orders table with a WHERE clause limiting the digits results to less than the Qty field and duplicate the rows this way. To quickly build the sequence table, create a digits table with records for 0 through 9 and cross join it to itself once for each power of 10.
I was required to do the same thing in order to avoid a cursor. My solution is for SQL Server and is really simple because for my case, qty is never greater than 99, so here is a sample using temporary tables:
create table #t (
id int
,qty int
)
insert into #t values (1,2)
insert into #t values (2,3)
create table #n (
id int
)
insert into #n values (1)
insert into #n values (2)
insert into #n values (3)
insert into #n values (4)
insert into #n values (5)
select t.*
from #t t
inner join #n n on
n.id <= t.qty
You just need to insert into #n the max qty you expect (in my case 99).
Related
Okay here is the situation:
I the following data in a table.
PAIR_NO NO NO2
3 5678EFGH 1234ABCD
4 1111BBBB 0000AAAA
1 1234ABCD 5678EFGH
2 0000AAAA 1111BBBB
The constraints are if no = no2 in another row skip that row.
So in this sample data the only rows that would be selected should be pair no 3 and 4.
I have tried to merge and inner join with self but I just keep getting all 4 rows back.
I have tried to insert into a table where not exists but again I get 4 rows inserted.
SELECT a.* from PAIRS a
inner join PAIRS b on a.no=b.no2 and a.no2=b.no;
I was thinking maybe selecting distinct number from column 1 and then check those in column 2 but I think that would yield the same four rows.
I may be over thinking this problem and maybe some here can look at this and see where the solution is hiding.
I am currently testing this on MySQL but it should run on SQLServer 2008. I have searched but all the questions didn't seem to match my data set issue.
Taking you at your word, meaning selecting all records where the value of no column does not appear anywhere in no2 column in the same table, try this:
SELECT A.PAIR_NO, A.NO, A.NO2
FROM PAIRS A
LEFT JOIN PAIRS B ON(A.NO = B.NO2)
WHERE B.PAIR_NO IS NULL -- assuming this column is not nullable
Another option is to use NOT EXISTS:
SELECT PAIR_NO, NO, NO2
FROM PAIRS A
WHERE NOT EXISTS(
SELECT 1
FROM PAIRS B
WHERE B.NO2 = A.NO
)
I personally prefer the LEFT JOIN option since it's shorter and more readable.
Both of these statement should work on both MySql and Sql Server.
Okay fellas I want to thank you all for helping, but I think I solved my issue. Took me a second but I believe this is what I am after (SQL Server 2008):
if OBJECT_ID('tempdb..#pairs') is not null drop table #pairs
if OBJECT_ID('tempdb..#pairs_final') is not null drop table #pairs_final
create table #pairs(pair_no int, a_no varchar(17),a_no2 varchar(17))
create table #pairs_final(pair_no int Identity(1,1), a_no varchar(17),a_no2 varchar(17))
insert into #PAIRS values(1,'1234ABCD','5678EFGH');
insert into #PAIRS values(1,'1234ABCD','XXXX9999');
insert into #PAIRS values(2,'0000AAAA','1111BBBB');
insert into #PAIRS values(3,'5678EFGH','1234ABCD');
insert into #PAIRS values(4,'1111BBBB','0000AAAA');
insert into #PAIRS values(1,'XXXX9999','1234ABCD');
insert into #pairs_final
select a.a_no,a.a_no2 from #pairs a
join (
select distinct a_no_p from(
select pair_no,a_no_p,
ROW_NUMBER() over (partition by pair_no order by a_no_p) as RN
from #pairs
unpivot(
a_no_p for clms in (a_no2,a_no)
) as umpvt
) as mypairs
where RN = 1
) as my_pairs on my_pairs.a_no_p=a.a_no
select * from #pairs_final
This will give me the following results:
pair_no a_no a_no2
1 1234ABCD 5678EFGH
2 1234ABCD XXXX9999
3 0000AAAA 1111BBBB
Hope this might help someone else.
Enjoy.
DECLARE #TBL AS TABLE
(
[NO] INT,
[CODE] VARCHAR(50),
[AREA] VARCHAR(50)
)
/* EXAMPLE 1 */
INSERT INTO #TBL([NO],[CODE],[AREA]) VALUES (1,'001','A00')
INSERT INTO #TBL([NO],[CODE],[AREA]) VALUES (2,'001','A00')
INSERT INTO #TBL([NO],[CODE],[AREA]) VALUES (3,'001','B00')
INSERT INTO #TBL([NO],[CODE],[AREA]) VALUES (4,'001','C00')
INSERT INTO #TBL([NO],[CODE],[AREA]) VALUES (5,'001','C00')
INSERT INTO #TBL([NO],[CODE],[AREA]) VALUES (6,'001','A00')
INSERT INTO #TBL([NO],[CODE],[AREA]) VALUES (7,'001','A00')
/* EXAMPLE 2 */
/* ***** USE THIS CODE TO ENTER DATA FROM DIRECT TABLE *****
SELECT
ROW_NUMBER() OVER(ORDER BY [FIELD_DATE]) AS [NO]
,[FIELD_CODE] AS [CODE]
,[FIELD_AREA] AS [AREA]
FROM TABLE_A
WHERE CAST([FIELD_DATE] AS DATE) >= CAST('20200307' AS DATE)
ORDER BY [FIELD_DATE],[FIELD_CODE]
*/
SELECT
A.NO AS ANO
,A.CODE AS ACODE
,A.AREA AS AAREA
,B.NO AS BNO
,B.CODE AS BCODE
,B.AREA AS BAREA
,CASE WHEN A.AREA=B.AREA THEN 'EQUAL' ELSE 'NOT EQUAL' END AS [COMPARE AREA]
FROM #TBL A
LEFT JOIN #TBL B
ON A.NO=B.NO+1
I have a table and want to add another field identity
id | name | identity
1 | sam |
2 | joe |
3 | jen |
Right now there is no data for identity. I will have a string of 5 random character (ex: kdU3k) populate each row.
What is the best way to alter/update the table in this manner?
Since I have a PHP backend, I could technically loop through a SQL statement where identity = null, but I want to know how to do this with just SQL.
While I do not recommend doing this, primarily because MySQL makes certain aspects less fun, this can be done entirely in MySQL DML without even the use of user-defined procedures. Procedures would allow the use of procedural while loops, etc.
I've created an sqlfiddle. The first step is to create the random values; in this case they are also ensured to be distinct in the table afterwards, which ensures there is one less thing to worry about.
-- Create lots of random values without using a proceure and loop.
-- There may be duplicates created. Could be a temporary table.
-- Would be much simplified if there was already a numbers table.
create table idents (value char(5));
insert into idents (value) values (left(md5(rand()), 5)); -- 1
insert into idents (value) select (left(md5(rand()), 5)) from idents; -- 2
insert into idents (value) select (left(md5(rand()), 5)) from idents; -- 4
insert into idents (value) select (left(md5(rand()), 5)) from idents;
insert into idents (value) select (left(md5(rand()), 5)) from idents;
insert into idents (value) select (left(md5(rand()), 5)) from idents;
insert into idents (value) select (left(md5(rand()), 5)) from idents; -- 64
-- Delete duplicate values. While there may be a rare duplicate we will
-- still be left with a good many random values. A similar process
-- could also be used to weed out existing used values.
delete from idents
where value in (
-- The select * is for another MySQL quirk
select value from (select * from idents) i
group by value
having count(value) > 1);
Then the random values have to be associated with each person. This is done with a horrid simulation of a "ROW_NUMBER" on derived relations and a join.
set #a = 0;
set #b = 0;
-- Now here is UGLY MYSQL MAGIC, where variables are used to simulate
-- ROW_NUMBER. YMMV, it "Works Here, Now". Note the very suspicious
-- hack to assign #b back to 0 "for each" joined item.
update people p2
join (select p.id, i.value
-- Give each person record a row number
from (select #a := #a + 1 as rn1, id, #b := 0 as hack from people) p
-- Give each random number a row number
join (select #b := #b + 1 as rn2, value from idents) i
-- And join on row number
on p.rn1 = i.rn2) pv
on p2.id = pv.id
set p2.identity = pv.value
Again, YMMV.
I'm pretty stuck on a Mysql query.
I have a table with three columns;
user_id | person_id | score.
The table is going to be used to store top 5 highscores for each person.
I need at query that checks if there is less than five rows for a specific person.
Is there is less, insert new row. But if there is five rows I have to replace the lowest score with the new one.
It is for a webservice written in PHP and the data about the new score is posted to the method as params.
Been stuck for some hours now — is it even possible to make this happen in one query ?
You can use stored procedure in mysql. I dont know the names of the tables but if you look closer you will understand how it works.
DELIMITER $$
DROP PROCEDURE IF EXISTS test $$
CREATE PROCEDURE test( IN testparam VARCHAR(22) )
BEGIN
DECLARE count INT(11);
SET count = (SELECT COUNT(*) FROM persons );
IF count < 5 THEN
insert into table_needed_for_insert values(testparam);
ELSE
update table_needed_for_insert where score=(select min(score) from table_needed_for_insert);
END IF;
select * from table_needed_for_insert
END $$
DELIMITER;
And how to execute this thing CALL test(1); 1 is the parameter, you can create as many as you need.
And from php you can call directly as like
$result = mysql_query("call test(".$param.")");
And here you can check a tutorial on mysql stored procedures:
http://www.mysqltutorial.org/mysql-stored-procedure-tutorial.aspx
It might be possible if you have a unique key which identifies the lowest score. Then you could use the
INSERT ... ON DUPLICATE KEY construct. But you would have to install a trigger which keeps explicit track of the lowest score.
I would propose this scenario (I have not tried it, it is just an idea):
as I understand, you only need 5 ids. you can run a subqueries like these
SELECT MAX(id) AS last_id FROM table
SELECT MIN(score), id AS lowest_id FROM table
then
insert or replace into table (id, ...) values ( MIN(last_id+1, lowest_id), ... )
there are possible mistakes and also only one subquery is possible, but I hope you get the main idea
The simplest way imo is to insert data,
INSERT INTO top_scores (user_id, person_id, score_id) VALUES (1,2,3)
then delete inappropriate rows
DELETE top_scores FROM top_scores
INNER JOIN
(SELECT * FROM top_scores WHERE person_id = 2 ORDER BY score ASC LIMIT 5, 1000000) AS inappropriate_rows
USING (user_id, person_id, score)
I have a table (ft_ttd) and want to sort it descending (num) and insert rating numbers into rating column.
Initial Table http://dl.dropbox.com/u/3922390/2.png
Something like that:
Result Table http://dl.dropbox.com/u/3922390/1.png
I've created a procedure.
CREATE PROCEDURE proc_ft_ttd_sort
BEGIN
CREATE TEMPORARY TABLE ft_ttd_sort
(id int (2),
num int (3),
rating int (2) AUTO_INCREMENT PRIMARY KEY);
INSERT INTO ft_ttd_sort (id, num) SELECT id, num FROM ft_ttd ORDER BY num DESC;
TRUNCATE TABLE ft_ttd;
INSERT INTO ft_ttd SELECT * FROM ft_ttd_sort;
DROP TABLE ft_ttd_sort;
END;
When I call it - it works great.
CALL proc_ft_ttd_sort;
After that I've created trigger calling this procedure.
CREATE TRIGGER au_ft_ttd_fer AFTER UPDATE ON ft_ttd FOR EACH ROW
BEGIN
CALL proc_ft_ttd_sort();
END;
Now every time when I update ft_ttd table I've got a error.
UPDATE ft_ttd SET num = 9 WHERE id = 3;
ERROR 1422 (HY000): Explicit or implicit commit is not allowed in stored function ortrigger.
Any ideas how to make it work? Maybe this process can be optimized?
Thank you!
The create table statement is an implicit commit, since it's DDL. Basically, the answer is you can't create a table in a trigger.
http://dev.mysql.com/doc/refman/5.0/en/stored-program-restrictions.html
Triggers can't do it
DDL aside, your trigger-based approach has a few difficulties. First, you want to modify the very table that's been updated, and that's not permitted in MySQL 5.
Second, you really want a statement-level trigger rather than FOR EACH ROW — no need to re-rank the whole table for every affected row — but that's not supported in MySQL 5.
Dynamically compute "rating"
So ... is it enough to just compute rating dynamically using a MySQL ROW_NUMBER() workaround?
-- ALTER TABLE ft_ttd DROP COLUMN rating; -- if you like
SELECT id,
num,
#i := #i + 1 AS rating
FROM ft_ttd
CROSS JOIN (SELECT #i := 0 AS zero) d
ORDER BY num DESC;
Unfortunately, you cannot wrap that SELECT in a VIEW (since a view's "SELECT statement cannot refer to system or user variables"). However, you could hide that in a selectable stored procedure:
CREATE PROCEDURE sp_ranked_ft_ttd () BEGIN
SELECT id, num, #i := #i + 1 AS rating
FROM ft_ttd CROSS JOIN (SELECT #i := 0 AS zero) d
ORDER BY num DESC
END
Or UPDATE if you must
As a kluge, if you must store rating in the table rather than compute it, you can run this UPDATE as needed:
UPDATE t
CROSS JOIN ( SELECT id, #i := #i + 1 AS new_rating
FROM ft_ttd
CROSS JOIN (SELECT #i := 0 AS zero) d
ORDER BY num DESC
) ranked
ON ft_ttd.id = ranked.id SET ft_ttd.rating = ranked.new_rating;
Now instruct your client code to ignore rows where rating IS NULL — those haven't been ranked yet. Better, create a VIEW that does that for you.
Kluging further, you can likely regularly UPDATE via CREATE EVENT.
First, here's the concise summary of the question:
Is it possible to run an INSERT statement conditionally?
Something akin to this:
IF(expression) INSERT...
Now, I know I can do this with a stored procedure.
My question is: can I do this in my query?
Now, why would I want to do that?
Let's assume we have the following 2 tables:
products: id, qty_on_hand
orders: id, product_id, qty
Now, let's say an order for 20 Voodoo Dolls (product id 2) comes in.
We first check if there's enough Quantity On Hand:
SELECT IF(
( SELECT SUM(qty) FROM orders WHERE product_id = 2 ) + 20
<=
( SELECT qty_on_hand FROM products WHERE id = 2)
, 'true', 'false');
Then, if it evaluates to true, we run an INSERT query.
So far so good.
However, there's a problem with concurrency.
If 2 orders come in at the exact same time, they might both read the quantity-on-hand before any one of them has entered the order.
They'll then both place the order, thus exceeding the qty_on_hand.
So, back to the root of the question:
Is it possible to run an INSERT statement conditionally, so that we can combine both these queries into one?
I searched around a lot, and the only type of conditional INSERT statement that I could find was ON DUPLICATE KEY, which obviously does not apply here.
INSERT INTO TABLE
SELECT value_for_column1, value_for_column2, ...
FROM wherever
WHERE your_special_condition
If no rows are returned from the select (because your special condition is false) no insert happens.
Using your schema from question (assuming your id column is auto_increment):
insert into orders (product_id, qty)
select 2, 20
where (SELECT qty_on_hand FROM products WHERE id = 2) > 20;
This will insert no rows if there's not enough stock on hand, otherwise it will create the order row.
Nice idea btw!
Try:
INSERT INTO orders(product_id, qty)
SELECT 2, 20 FROM products WHERE id = 2 AND qty_on_hand >= 20
If a product with id equal to 2 exists and the qty_on_hand is greater or equal to 20 for this product, then an insert will occur with the values product_id = 2, and qty = 20. Otherwise, no insert will occur.
Note: If your product ids are note unique, you might want to add a LIMIT clause at the end of the SELECT statement.
Not sure about concurrency, you'll need to read up on locking in mysql, but this will let you be sure that you only take 20 items if 20 items are available:
update products
set qty_on_hand = qty_on_hand - 20
where qty_on_hand >= 20
and id=2
You can then check how many rows were affected. If none were affected, you did not have enough stock. If 1 row was affected, you have effectively consumed the stock.
You're probably solving the problem the wrong way.
If you're afraid two read-operations will occur at the same time and thus one will work with stale data, the solution is to use locks or transactions.
Have the query do this:
lock table for read
read table
update table
release lock
I wanted to insert into a table using values so I found this solution to insert the values using the IF condition
DELIMITER $$
CREATE PROCEDURE insertIssue()
BEGIN
IF (1 NOT IN (select I.issue_number from issue as I where I.series_id = 1)) THEN
INSERT IGNORE INTO issue ( issue_number, month_published, year_published, series_id, mcs_issue_id) VALUES (1, 1, 1990, 1, 1);
END IF;
END$$
DELIMITER ;
If you later on want to call the procedure it's as simple as
CALL insertIssue()
You can find more information about PROCEDURES and if conditions in this site