I would like to ask for a solution on how to insert n rows based on the values of a field in another table.
Consider the following tables:
CREATE TABLE Input (
ID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(128),
Ticket_Piece INT
);
CREATE TABLE Output (
Ticket_ID INT AUTO_INCREMENT PRIMARY KEY,
Transaction_ID INT,
Ticket_Number VARCHAR(23) UNIQUE,
FOREIGN KEY (Transaction_ID)
REFERENCES Input (ID)
ON UPDATE CASCADE
ON DELETE CASCADE
);
If a row from the Input table has n in the Ticket_Number column, then n rows should be inserted into the Output table, with Ticket_Number having values "ID-1" through "ID-n" (e.g. (4, "D", 5) in Input should result in rows with ticket numbers "4-1" through "4-5" being added to Output). How can rows for Output be generated in a range of numbers based on the Ticket_Piece column using PHP and MySQL?
For example, with the input:
INSERT INTO Input (ID, Name, Ticket_Piece)
VALUES
(1, 'A', 2),
(2, 'B', 1),
(3, 'C', 3)
;
the result should be:
Ticket_ID
Transaction_ID
Ticket_Number
1
1
1-1
2
1
1-2
3
2
2-1
4
3
3-1
5
3
3-2
6
3
3-3
For each row you fetch from the input table, use a for loop to insert multiple rows into the output table.
$res = $pdo->query("SELECT id, ticket_piece FROM Input_Table");
$insert_stmt = $pdo->prepare("INSERT INTO Output_Table (transaction_id, ticket_number) VALUES (:id, :ticket)");
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
$pieces = $row['ticket_piece'];
$id = $row['transaction_id'];
for ($i = 1; $i <= $pieces; $i++) {
$insert_stmt->execute([':id' => $id, ':ticket' => "$id-$i"]);
}
}
A solution in PHP or SQL will likely need to use a loop.
If this comes from the data model rather than business rules (and depending on other factors), a trigger might be a fairly simple option. The trigger body could have a WHILE or other loop to iterate over the ticket piece numbers, and CONCAT to combine the ID and piece number into a ticket number, inserting each in turn.
DELIMITER ;;
CREATE TRIGGER create_ticket_pieces
AFTER INSERT ON Input
FOR EACH ROW
BEGIN
DECLARE piece INT DEFAULT 1;
WHILE piece <= NEW.Ticket_Piece DO
INSERT INTO Output (Transaction_ID, Ticket_Number) Values (NEW.ID, Concat(NEW.ID, '-', piece));
SET piece := piece + 1;
END WHILE;
END;;
DELIMITER ;
An alternative some use is to pregenerate a table of numbers, then join with this table to generate rows:
INSERT INTO Output (Transaction_ID, Ticket_Number)
SELECT Input.ID, Concat(Input.ID, '-', Numbers.Number)
FROM Input
JOIN Numbers ON Numbers.Number <= Input.Ticket_Piece
WHERE ... -- select Input rows
It should be noted that by duplicating information (the transaction ID) in two different columns, the Output table isn't normalized. In particular, it violates 3rd normal form, due to a functional dependency of Transaction_ID on Ticket_Number. The way to resolve this is to leave the transaction ID out of the ticket number field (i.e. Output.Ticket_Number holds only the generated integer ≤ Input.Ticket_Piece). (See: "Third Normal Form: Composite PRIMARY KEY vs System-Generated Surrogate (IDENTITY)")
I have a condition like this:
0 < $var < (SELECT col FROM tableA ORDER BY col DESC LIMIT 1)
Also I have an INSERT query like this:
INSERT INTO tableB (col) VALUES (?) ON DUPLICATE KEY UPDATE col = VALUE(col)
Now I want to know, how can I implement this in MySQL:
If the above condition was true, then insert new row, otherwise do nothing or give me an error?
if (condition == true) then insert into table
SELECT MAX(col) FROM tableA gives you the maximum value in column col from table tableA.
I think what you're asking to do is:
If $val (a value from somewhere — presumably your PHP) is larger than 0 and smaller than the maximum value stored in the table tableA, then ensure that the value appears in the table tableB.
If so, you might be able to use:
INSERT INTO tableB(col)
SELECT $val
FROM dual
WHERE $val > 0
AND $val < (SELECT MAX(col) FROM tableA)
AND NOT EXISTS(SELECT col FROM tableB WHERE col = $val)
You now need to translate that into appropriate PHP.
(See also MySQL conditional insert.)
How can I add ON DUPLICATE KEY to my query?
Also you can remove the AND NOT EXISTS(SELECT col FROM tableB WHERE col = $val) because col is unique and it prevents inserting duplicate.
You don't need the ON DUPLICATE because the NOT EXISTS makes it irrelevant. And if you don't mind the error from an attempted duplicate insertion, you don't need any of this: you just do INSERT INTO tablaB(col) VALUES(?).
AFAICS, the ON DUPLICATE clause does a no-op UPDATE; this avoids the need to do that. I suppose it wouldn't be a no-op if there are triggers on the table such that the 'last modified' (time, or user, or both) columns will be changed, or if the key of the table isn't the col column — these are pretty esoteric possibilities, though. You could have helped by showing an outline schema of the table indicating such features.
If you really need the ON DUPLICATE clause, you can read the manual as well as I can and add it after the SELECT.
INSERT INTO tableB(col)
SELECT $val
FROM dual
WHERE $val > 0
AND $val < (SELECT MAX(col) FROM tableA)
AND NOT EXISTS(SELECT col FROM tableB WHERE col = $val)
ON DUPLICATE KEY UPDATE col = VALUE(col)
(And, of course, if you really don't want the NOT EXISTS clause, you can remove it.)
I'm not quite figuring out how to do what I'm after.So what I'm making is an online game.When a user buy's a new car I do an INSERT:
$c->query("INSERT INTO user_cars (carid, userid, acc, speed) VALUES ('$buyid','$id','$acc','$speed')");
Now I have another table that I need to insert info to right after the query above FROM the query above.What I need is the carid .The user can have more than 2 cars.What should I do now?
You have multiple options:
You can build a trigger to insert a new row in table2, when row is inserted in the cars table (Read more here http://dev.mysql.com/doc/refman/5.5/en/trigger-syntax.html)
There is this function mysql_insert_id() which returns the last inserted id ( Read more here http://php.net/manual/en/function.mysql-insert-id.php )
If you use PDO , there is a smillar command for it
etc.
This is a basic demonstration of the trigger you would want to create. For illustrative purposes I've also included the ddl and an example insert into your user_cars table to show that another table, which I've called "your_other_table" receives the insert (just the carid value) of an insert going into the user_cars table.
Fiddle:
http://sqlfiddle.com/#!9/f76a7/1/0
(notice how "your_other_tabe" has one row with the carid of the insert into "user_cars", despite having no direct inserts into itself)
delimiter //
create table user_cars
(
carid int,
userid int,
acc int,
speed int,
constraint id_pk primary key (carid, userid)
)//
create table your_other_table
(
carid int
)//
create trigger your_trigger_name before insert on user_cars
for each row begin
insert into your_other_table (carid)
values (new.carid);
end
//
insert into user_cars values
(1, 2, 3, 99)//
delimiter ;
select *
from your_other_table;
Output:
| CARID |
|-------|
| 1 |
This is the only portion of the above sql that creates the trigger:
delimiter //
create trigger your_trigger_name before insert on user_cars
for each row begin
insert into your_other_table (carid)
values (new.carid);
end
//
delimiter ;
I would like to copy all mysql rows with a matching field value into a new row and change one field.
So I have a mysql table with the following fields
`lessonplan`, `group`, `category`, `sort_id`, `item`
So I want to copy all rows that have lessonplan=10 and set change the value of lessonplan to 11 for all new rows. Lesson plan is not an auto increment field.
What is best practice for a scenario like this one?
INSERT INTO tableName( lessonplan, `group`, category, sort_id, item )
SELECT 11, `group`, category, sort_id, item
FROM tableName
WHERE lessonplan = 10
My PostgreSQL database contains a table to store instances of a registered entity. This table is populated via spreadsheet upload. A web interface allows an operator to modify the information presented. However, the original data is not modified. All changes are stored in a separate table changes with the columns unique_id, column_name, value and updated_at.
Once changes are made, they are presented to the operator by first querying the original table and then querying the change table (using instance ID and the latest change date, grouped by column name). The two results are merged in PHP and presented on the web interface. This is a rather rigid way of going about the task and I would like to keep all logic within SQL.
I can easily select the latest changes for the table using the following query:
SELECT fltr_chg.unique_id, fltr_chg.column_name, chg_val.value
FROM changes AS chg_val
JOIN (
SELECT chg_rec.unique_id, chg_rec.column_name, MAX( chg_rec.updated_at )
FROM information_schema.columns AS source
JOIN changes AS chg_rec ON source.table_name = 'instances'
AND source.column_name = chg_rec.column_name
GROUP BY chg_rec.unique_id, chg_rec.column_name
) AS fltr_chg ON fltr_chg.unique_id = chg_val.unique_id
AND fltr_chg.column_name = chg_val.column_name;
And selecting the entries from the instances table is just as easy:
SELECT * FROM instances;
Now, if there was only a way of transforming the former result and substituting the resulting values into the latter, based on the unique_id and column_name, and still retaining the result as a table, the problem would be solved. Is this possible to do?
I am sure that this is not the rarest of the problems and most likely, some systems do keep track of changes to the data in a similar way. How do they apply them back to the data if not through one of the the above described ways (current and sought solutions)?
Assuming Postgres 9.1 or later.
I simplified / optimized your basic query to retrieve the latest values:
SELECT DISTINCT ON (1,2)
c.unique_id, a.attname AS col, c.value
FROM pg_attribute a
LEFT JOIN changes c ON c.column_name = a.attname
AND c.table_name = 'instances'
-- AND c.unique_id = 3 -- uncomment to fetch single row
WHERE a.attrelid = 'instances'::regclass -- schema-qualify to be clear?
AND a.attnum > 0 -- no system columns
AND NOT a.attisdropped -- no deleted columns
ORDER BY 1, 2, c.updated_at DESC;
I query the PostgreSQL catalog instead of the standard information schema because that is faster. Note the special cast to ::regclass.
Now, that gives you a table. You want all values for one unique_id in a row.
To achieve that you have basically three options:
One subselect (or join) per column. Expensive and unwieldy. But a valid option for only a few columns.
A big CASE statement.
A pivot function. PostgreSQL provides the crosstab() function in the additional module tablefunc for that.
Basic instructions:
PostgreSQL Crosstab Query
Basic pivot table with crosstab()
I completely rewrote the function:
SELECT *
FROM crosstab(
$x$
SELECT DISTINCT ON (1, 2)
unique_id, column_name, value
FROM changes
WHERE table_name = 'instances'
-- AND unique_id = 3 -- un-comment to fetch single row
ORDER BY 1, 2, updated_at DESC;
$x$,
$y$
SELECT attname
FROM pg_catalog.pg_attribute
WHERE attrelid = 'instances'::regclass -- possibly schema-qualify table name
AND attnum > 0
AND NOT attisdropped
AND attname <> 'unique_id'
ORDER BY attnum
$y$
)
AS tbl (
unique_id integer
-- !!! You have to list all columns in order here !!! --
);
I separated the catalog lookup from the value query, as the crosstab() function with two parameters provides column names separately. Missing values (no entry in changes) are substituted with NULL automatically. A perfect match for this use case!
Assuming that attname matches column_name. Excluding unique_id, which plays a special role.
Full automation
Addressing your comment: There is a way to supply the column definition list automatically. It's not for the faint of heart, though.
I use a number of advanced Postgres features here: crosstab(), plpgsql function with dynamic SQL, composite type handling, advanced dollar quoting, catalog lookup, aggregate function, window function, object identifier type, ...
Test environment:
CREATE TABLE instances (
unique_id int
, col1 text
, col2 text -- two columns are enough for the demo
);
INSERT INTO instances VALUES
(1, 'foo1', 'bar1')
, (2, 'foo2', 'bar2')
, (3, 'foo3', 'bar3')
, (4, 'foo4', 'bar4');
CREATE TABLE changes (
unique_id int
, table_name text
, column_name text
, value text
, updated_at timestamp
);
INSERT INTO changes VALUES
(1, 'instances', 'col1', 'foo11', '2012-04-12 00:01')
, (1, 'instances', 'col1', 'foo12', '2012-04-12 00:02')
, (1, 'instances', 'col1', 'foo1x', '2012-04-12 00:03')
, (1, 'instances', 'col2', 'bar11', '2012-04-12 00:11')
, (1, 'instances', 'col2', 'bar17', '2012-04-12 00:12')
, (1, 'instances', 'col2', 'bar1x', '2012-04-12 00:13')
, (2, 'instances', 'col1', 'foo2x', '2012-04-12 00:01')
, (2, 'instances', 'col2', 'bar2x', '2012-04-12 00:13')
-- NO change for col1 of row 3 - to test NULLs
, (3, 'instances', 'col2', 'bar3x', '2012-04-12 00:13');
-- NO changes at all for row 4 - to test NULLs
Automated function for one table
CREATE OR REPLACE FUNCTION f_curr_instance(int, OUT t public.instances) AS
$func$
BEGIN
EXECUTE $f$
SELECT *
FROM crosstab($x$
SELECT DISTINCT ON (1,2)
unique_id, column_name, value
FROM changes
WHERE table_name = 'instances'
AND unique_id = $f$ || $1 || $f$
ORDER BY 1, 2, updated_at DESC;
$x$
, $y$
SELECT attname
FROM pg_catalog.pg_attribute
WHERE attrelid = 'public.instances'::regclass
AND attnum > 0
AND NOT attisdropped
AND attname <> 'unique_id'
ORDER BY attnum
$y$) AS tbl ($f$
|| (SELECT string_agg(attname || ' ' || atttypid::regtype::text
, ', ' ORDER BY attnum) -- must be in order
FROM pg_catalog.pg_attribute
WHERE attrelid = 'public.instances'::regclass
AND attnum > 0
AND NOT attisdropped)
|| ')'
INTO t;
END
$func$ LANGUAGE plpgsql;
The table instances is hard-coded, schema qualified to be unambiguous. Note the use of the table type as return type. There is a row type registered automatically for every table in PostgreSQL. This is bound to match the return type of the crosstab() function.
This binds the function to the type of the table:
You will get an error message if you try to DROP the table
Your function will fail after an ALTER TABLE. You have to recreate it (without changes). I consider this a bug in 9.1. ALTER TABLE shouldn't silently break the function, but raise an error.
This performs very well.
Call:
SELECT * FROM f_curr_instance(3);
unique_id | col1 | col2
----------+-------+-----
3 |<NULL> | bar3x
Note how col1 is NULL here.
Use in a query to display an instance with its latest values:
SELECT i.unique_id
, COALESCE(c.col1, i.col1)
, COALESCE(c.col2, i.col2)
FROM instances i
LEFT JOIN f_curr_instance(3) c USING (unique_id)
WHERE i.unique_id = 3;
Full automation for any table
(Added 2016. This is dynamite.)
Requires Postgres 9.1 or later. (Could be made out to work with pg 8.4, but I didn't bother to backpatch.)
CREATE OR REPLACE FUNCTION f_curr_instance(_id int, INOUT _t ANYELEMENT) AS
$func$
DECLARE
_type text := pg_typeof(_t);
BEGIN
EXECUTE
(
SELECT format
($f$
SELECT *
FROM crosstab(
$x$
SELECT DISTINCT ON (1,2)
unique_id, column_name, value
FROM changes
WHERE table_name = %1$L
AND unique_id = %2$s
ORDER BY 1, 2, updated_at DESC;
$x$
, $y$
SELECT attname
FROM pg_catalog.pg_attribute
WHERE attrelid = %1$L::regclass
AND attnum > 0
AND NOT attisdropped
AND attname <> 'unique_id'
ORDER BY attnum
$y$) AS ct (%3$s)
$f$
, _type, _id
, string_agg(attname || ' ' || atttypid::regtype::text
, ', ' ORDER BY attnum) -- must be in order
)
FROM pg_catalog.pg_attribute
WHERE attrelid = _type::regclass
AND attnum > 0
AND NOT attisdropped
)
INTO _t;
END
$func$ LANGUAGE plpgsql;
Call (providing the table type with NULL::public.instances:
SELECT * FROM f_curr_instance(3, NULL::public.instances);
Related:
Refactor a PL/pgSQL function to return the output of various SELECT queries
How to set value of composite variable field using dynamic SQL