I have tables products and column_names as follows:
products
+----+------+-----------+--------------+-------+
| id | code | category | description | link |
+----+------+-----------+--------------+-------+
| 1 | 1111 | category1 | description1 | link1 |
| 2 | 2222 | category1 | description2 | link2 |
| 3 | 3333 | category1 | description3 | link3 |
| 4 | 4444 | category2 | description4 | link4 |
| 5 | 5555 | category2 | description5 | link5 |
| 6 | 6666 | category3 | description6 | link6 |
+----+------+-----------+--------------+-------+
column_names
+----+-------------+-------+
| id | column | type |
+----+-------------+-------+
| 1 | id | type1 |
| 2 | code | type1 |
| 3 | category | type2 |
| 4 | description | type2 |
| 5 | link | type3 |
+----+-------------+-------+
I can make this statement:
SELECT ( SELECT `column` FROM `column_names` WHERE `column_id` = 3) FROM `products` WHERE `id` = 1
while I cannot get this statement:
SELECT ( SELECT `column` FROM `column_names` WHERE `type` = 'type2') FROM `products` WHERE `id` = 1
It gives me error #1242 - Subquery returns more than 1 row
But is it possible to perform query like that? Namely, I would like to extract data just of certain columns in the products table that have certain type in the column_names table.
Is this the right design of the tables or should there be another approach? Of course category should be in another table but this is not what I am asking.
Thank you very much!
So, thanks to Gordon Linoff and A Paul I was able to do what I wanted. I know this is probably a clumsy solution but it works. Anyone who would like to point out clumsiness is welcome.
So, first I created a user defined procedure GetMyColumns(). I cannot tell the exact purpose of the first two lines. It is just what the phpMyAdmin editor for functions added when I chose option in the editor.
DELIMITER $$
CREATE DEFINER=`geonextp`#`localhost` FUNCTION `GetMyColumns`(`type` VARCHAR(258)) RETURNS VARCHAR(4096) CHARSET latin1 NOT DETERMINISTIC READS SQL DATA SQL SECURITY DEFINER BEGIN
DECLARE rownum INT DEFAULT 1;
DECLARE counter INT DEFAULT 1;
DECLARE columns_string VARCHAR(4096) DEFAULT '';
DECLARE col_string VARCHAR(512);
SET rownum = (SELECT COUNT(*) FROM column_names);
SET columns_string = '';
WHILE counter <= rownum DO
SET col_string = (
SELECT `column_name`
FROM `column_names`
WHERE
`column_id` = counter AND
`column_type` = type
);
IF col_string IS NULL
THEN
SET col_string = '';
END IF;
IF columns_string = '' THEN
SET columns_string = col_string;
ELSE
IF NOT (col_string = '')
THEN
SET columns_string = CONCAT(CONCAT(columns_string, ', '), col_string);
END IF;
END IF;
SET counter = counter + 1;
END WHILE;
RETURN columns_string;
END$$
DELIMITER ;
Then I added a little piece of code A Paul suggested:
SET #inner_sql = GetColumns('type2');
SET #sql = CONCAT(CONCAT('SELECT ', #inner_sql), ' FROM products WHERE id = 1');
PREPARE stmt FROM #sql;
EXECUTE stmt;
The result is exactly:
+-----------+--------------+
| category | description |
+-----------+--------------+
| category1 | description1 |
+-----------+--------------+
It's been an experience :)
I have a table with the following structure
CREATE TABLE `data` (
`type` varchar(64) DEFAULT NULL,
`subscr_id` varchar(64) DEFAULT NULL
)
In this table, there are many records with subscr_id of id100. I would like to select a record with subscr_id of id100, that was added to the table most recently.
How can I do that?
You add an ID - Indentify column. It's best performance in this/your situation.
ALTER TABLE data ADD COLUMN id INT NULL AUTO_INCREMENT FIRST, ADD KEY(id);
Run the below SQL, you will receive the record with subscr_id of id100, that was added to the table most recently most recently.
SELECT * FROM `data` WHERE subscr_id = 'id100' ORDER BY id DESC LIMIT 1;
I think you have to improve your table design and add auto-inctemental primary key or created_at field.
But if you can't do it or you need run query just once, you can try this approach (it's a bit tricky but it works 😉).
In general recent record will be present at the end of the table. For example we have table like this:
+------+-----------+
| type | subscr_id |
+------+-----------+
| a | id100 |
| b | id100 |
| c | id100 |
| a | id200 |
| b | id200 |
| d | id100 |
| c | id200 |
| e | id100 |
+------+-----------+
And here wee need calculate total count of interesting rows and use it for offset, like this:
set #offset = (select count(*) from data where subscr_id = 'id100') - 1;
set #sql = concat(
"select * from data where subscr_id = 'id100' limit 1 offset ",
#offset
);
prepare stmt1 from #sql;
execute stmt1;
The result will look like this:
+------+-----------+
| type | subscr_id |
+------+-----------+
| e | id100 |
+------+-----------+
Puzzle puzzle, riddle me functional (MySQL query/Search Experiment)
Stored Table
--------------------------------------------
| id | namespace | key | value |
--------------------------------------------
| 1 | page | item.id | test1 |
| 1 | page | content.title | page2 |
| 1 | trigger | tag | val1 |
| 2 | page | item.id | t1 |
| 2 | page | content.title | page3 |
| 2 | trigger | tag | val2 |
| 2 | oddball | num | in |
| 3 | truck | plate | 12345 |
--------------------------------------------
Search parameter: "page" can be anywhere but not in id
Desired Request output:
---------------------------------------------------------------------
|id | page.item.id | page.content.title | trigger.tag | oddball.num |
---------------------------------------------------------------------
|1 | test1 | page2 | val1 | NULL |
|2 | t1 | page3 | val2 | in |
---------------------------------------------------------------------
Hints:
ok solution: Solution with backend language (ex: php) + SQL queries
better solution: Solution with stored procedures
best solution: Solution with single SQL query, (pivot table?, temporary table?)
Fastest solution wins! (50 bounty points)
Cheers!
Goal is to have dynamic columns from agregated rows.
To get it working as pivot table you must run two queries:
Get the columns to be used
select distinct concat(namespace,'.',`key`) as `column`,
namespace,`key` from your_table;
+--------------------+-----------+---------------+
| column | namespace | key |
+--------------------+-----------+---------------+
| page.item.id | page | item.id |
| page.content.title | page | content.title |
| trigger.tag | trigger | tag |
| oddball.num | oddball | num |
| truck.plate | truck | plate |
+--------------------+-----------+---------------+
Combine with unique ids and get the each value as sub-query, to prevent sub-query more than one result it must contain aggregate function, I used max().
I created a stored procedure:
DELIMITER $$
DROP PROCEDURE IF EXISTS `get_pivot_table`$$
CREATE PROCEDURE `get_pivot_table`()
BEGIN
declare done int default 0;
declare v_sql text;
declare v_column varchar(100);
declare v_namespace varchar(100);
declare v_key varchar(100);
-- (1) getting the columns with this cursor
declare c_columns cursor for
select distinct concat(namespace,'.',`key`) as `column`
, namespace
,`key`
from your_table;
declare continue handler for not found set done = 1;
open c_columns;
-- (2) now creating the sub-queries based on cursor results
set v_sql = "select p.id ";
read_loop: loop
fetch c_columns into v_column, v_namespace, v_key;
if done then
leave read_loop;
end if;
set v_sql = concat(v_sql,", (select max(t.`value`) from your_table t
where t.id = p.id
and t.namespace = '", v_namespace ,"'
and t.`key` = '", v_key ,"') as `", v_column,"` ");
end loop;
close c_columns;
-- now run the entire query
set #sql = concat(v_sql," from (select distinct id from your_table) as p");
prepare stmt1 from #sql;
execute stmt1;
deallocate prepare stmt1;
END$$
DELIMITER ;
Then you can call the stored procedure:
mysql> call get_pivot_table();
+------+--------------+--------------------+-------------+-------------+-------------+
| id | page.item.id | page.content.title | trigger.tag | oddball.num | truck.plate |
+------+--------------+--------------------+-------------+-------------+-------------+
| 1 | test1 | page2 | val1 | NULL | NULL |
| 2 | t1 | page3 | val2 | in | NULL |
| 3 | NULL | NULL | NULL | NULL | 12345 |
+------+--------------+--------------------+-------------+-------------+-------------+
3 rows in set (0.00 sec)
The speed of that query will depend on the indexes of your_table and the amount of data.
It is based on An approach to mysql dynamic cross reference article.
Here's my solution using pivot table. Not in a single query though...
USE tempdb
GO
CREATE TABLE _temp ([id] int, [namespace] varchar(20), [key] varchar(20), [value] varchar(20))
INSERT INTO _temp VALUES (1, 'page', 'content.title', 'page2')
INSERT INTO _temp VALUES (1, 'page', 'item.id', 'test1')
INSERT INTO _temp VALUES(1, 'trigger', 'tag', 'val1')
INSERT INTO _temp VALUES (2, 'oddball', 'num', 'in')
INSERT INTO _temp VALUES (2, 'page', 'content.title', 'page3')
INSERT INTO _temp VALUES (2, 'page', 'item.id', 't1')
INSERT INTO _temp VALUES (2, 'trigger', 'tag', 'val2')
INSERT INTO _temp VALUES (3, 'truck', 'plate', '12345')
DECLARE #param AS varchar(15)
SET #param = 'page'
DECLARE #c AS nvarchar(100)
DECLARE #sql AS nvarchar(max)
SELECT #c =
ISNULL(
#c + ',[' + c + ']',
'[' + c + ']'
)
FROM (SELECT DISTINCT [namespace] + '.' + [key] AS c FROM _temp WHERE id IN (SELECT id FROM _temp WHERE ISNULL([namespace], '') + ISNULL([key], '') + ISNULL([value], '') LIKE '%' + #param + '%') ) AS col
SET #sql = N'
SELECT *
FROM
(
SELECT id,
namespace + ''.'' + [key] AS [column],
value
FROM _temp
WHERE id IN (SELECT id FROM _temp WHERE ISNULL([namespace], '''') + ISNULL([key], '''') + ISNULL([value], '''') LIKE ''%' + #param + '%'')
) AS src
PIVOT
(
MAX(value)
FOR [column]
IN (' + #c + ')
) AS piv'
EXECUTE (#sql)
DROP TABLE _temp
The nature of pivot tables in SQL is that it takes two queries.
The first to discover the set of distinct values and build a dynamic SQL query with one column per distinct value.
The second query to run the the dynamic query to get the pivot table result.
The reason for this is that SQL requires that you define the select-list columns before it accesses any data. There is no SQL query that can dynamically expand the columns of the select-list based on the distinct data values it discovers as it scans the table.
In other words: you can't pivot in a single SQL query.
Even in SQL implementations that have a built-in PIVOT operation, like Microsoft SQL Server, you still have to name the columns in the query syntax before you run it. Which means you need to know the distinct values you want to represent in the columns before that.
You would discover the distinct values with a simple query like this:
SELECT DISTINCT namespace, `key` FROM NoOneEverNamesTheirTableInSqlQuestions;
Then use the result of that to build a dynamic SQL query.
$sql = "SELECT DISTINCT namespace, `key` FROM NoOneEverNamesTheirTableInSqlQuestions";
$stmt = $pdo->query($sql);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
$select_list = [];
foreach ($results as $row) {
$select_list[] = sprintf(
"MAX(CASE WHEN namespace=%s AND `key`=%s THEN value END) AS `%s.%s`",
$pdo->quote($row['namespace']), $pdo->quote($row['key']),
$row['namespace'], $row['key']);
}
$dynamic_sql = sprintf(
"SELECT id, %s FROM NoOneEverNamesTheirTableInSqlQuestions GROUP BY id",
implode(', ', $select_list));
You could also use SQL to do both at the same time, by returning the result of the first query in the form of a new SQL query to do the actual pivot.
SELECT CONCAT('SELECT id, ', GROUP_CONCAT(DISTINCT CONCAT(
'MAX(CASE WHEN namespace=', QUOTE(namespace), ' AND `key`=', QUOTE(`key`),
' THEN value END) AS `', CONCAT_WS('.', namespace, `key`), '`')),
' FROM NoOneEverNamesTheirTableInSqlQuestions GROUP BY id;') AS _sql
FROM NoOneEverNamesTheirTableInSqlQuestions;
The output of the query above is the real dynamic SQL for the pivot query, with each respective column of the select-list populated:
SELECT id,
MAX(CASE WHEN namespace='page' AND `key`='content.title' THEN value END) AS `page.content.title`,
MAX(CASE WHEN namespace='page' AND `key`='item.id' THEN value END) AS `page.item.id`,
MAX(CASE WHEN namespace='trigger' AND `key`='tag' THEN value END) AS `trigger.tag`,
MAX(CASE WHEN namespace='oddball' AND `key`='num' THEN value END) AS `oddball.num`,
MAX(CASE WHEN namespace='truck' AND `key`='plate' THEN value END) AS `truck.plate`
FROM NoOneEverNamesTheirTableInSqlQuestions GROUP BY id;
Then you run the dynamic query and you get the result you asked for:
+----+--------------------+--------------+-------------+-------------+-------------+
| id | page.content.title | page.item.id | trigger.tag | oddball.num | truck.plate |
+----+--------------------+--------------+-------------+-------------+-------------+
| 1 | page2 | test1 | val1 | NULL | NULL |
| 2 | page3 | t1 | val2 | in | NULL |
| 3 | NULL | NULL | NULL | NULL | 12345 |
+----+--------------------+--------------+-------------+-------------+-------------+
Here's both steps implemented as a MySQL stored procedure:
DELIMITER ;;
CREATE PROCEDURE PivotProc()
BEGIN
SELECT CONCAT('SELECT id, ', GROUP_CONCAT(DISTINCT CONCAT(
'MAX(CASE WHEN namespace=', QUOTE(namespace), ' AND `key`=', QUOTE(`key`),
' THEN value END) AS `', CONCAT_WS('.', namespace, `key`), '`')),
' FROM NoOneEverNamesTheirTableInSqlQuestions GROUP BY id;') AS _sql
FROM NoOneEverNamesTheirTableInSqlQuestions
INTO #sql;
PREPARE stmt FROM #sql;
EXECUTE stmt;
END;;
So what's the alternative if you don't want to run two queries?
The alternative is to run a single simple query to fetch the data as it exists in the database, with multiple rows per id. Then fix it up by post-processing it your application.
$sql = "SELECT id, namespace, `key`, value FROM NoOneEverNamesTheirTableInSqlQuestions";
$stmt = $pdo->query($sql);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
$pivot_results = [];
foreach ($results as $row) {
if (!array_key_exists($row['id'], $pivot_results)) {
$pivot_results[$row['id']] = ['id' = $row['id']];
}
$field = sprintf("%s.%s", $row['namespace'], $row['key']);
$pivot_results[$row['id']][$field] = $row['value'];
}
Once you're done post-processing, you'll have a hash array with one row per id, each pointing to a hash array of fields indexed as the namespace.key names you described.
I have two models one is student_profile where I have university field to show university name. I have a list of universities where I need to update another table called Package only if one of the university exists in table. Table has 1000 records and I need to update all the entries with one query.
If university a, b, c, d exists in student_profile.
Update few "Package" table fields.
My tables:
+---------------------------+
| student_profile |
+---------------------------+
| id | int(11) |
| first_name | varchar(45) |
| last_name | varchar(45) |
| university | varchar(45) |
+---------------------------+
+---------------------------+
| package |
+---------------------------+
| student_id | int(11) |
| is_active | tinyint |
| package_type| varchar(45) |
+---------------------------+
ForeignKeys in StudentProfile Table:
name = student_package
schema = mydb
Column = student_id
reference Schema = mydb
referenced table = student_profile
referenced column= id
If university exists I need to set is_active=True and set package.student_id as student_profile.id and package.package_type as 'UniverityEnrolled'.
To figure something like this out, start with a SELECT that outputs the records to be updated.
Then when it is working, convert to an update statement.
SELECT *
FROM `StudentProfile` a
JOIN `Package` b
ON a.`id` = b.`student_id`
WHERE `university` in ('a','b','c');
UPDATE `StudentProfile` a
SET `is_active` = 1
JOIN `Package` b
ON a.`id` = b.`student_id`
WHERE `university` in ('a','b','c');
Based on what I understand of the question, this may be your solution:
UPDATE package
SET is_active = 1,package_type = "UniversityEnrolled"
WHERE student_id IN
(SELECT id FROM student_profile WHERE university IN ("a","b","c","d"))
I have a problem, for example in my system I have the next table:
CREATE TABLE `sales` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`amount` FLOAT NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
-- is more complex table
With content:
+-----+-------+
| id | amount|
+-----+-------+
|2023 | 100 |
|2024 | 223 |
|2025 | 203 |
|... |
|2505 | 324 |
+-----+-------+
I don't know the current id(There are sales every day). I'm trying to normalize the table.
UPDATE sales SET id=id - 2022;
Result:
+-----+-------+
| id | amount|
+-----+-------+
| 1 | 100 |
| 2 | 223 |
| 3 | 203 |
|... |
| 482 | 324 |
+-----+-------+
The problem
My problem was trying to change the AUTO_INCREMENT, f.e.:
ALTER TABLE sales AUTO_INCREMENT = 483;
Its correct but I don't know the current id :(, I try the following query:
ALTER TABLE sales AUTO_INCREMENT = (SELECT MAX(id) FROM sales );
This causes me a error(#1064). Reading the documentation tells me:
In MySQL, you cannot modify a table and select from the same table in a subquery.
http://dev.mysql.com/doc/refman/5.7/en/subqueries.html
I try whit variables:
SET #new_index = (SELECT MAX(id) FROM sales );
ALTER TABLE sales AUTO_INCREMENT = #new_index;
But, this causes a error :(.
ALTER TABLE must have literal values in it by the time the statement is parsed (i.e. at prepare time).
You can't put variables or parameters into the statement at parse time, but you can put variables into the statement before parse time. And that means using dynamic SQL:
SET #new_index = (SELECT MAX(id) FROM sales );
SET #sql = CONCAT('ALTER TABLE sales AUTO_INCREMENT = ', #new_index);
PREPARE st FROM #sql;
EXECUTE st;
Thanks to Bill Karwin, my query was:
SET #sales_init = 2022;
DELETE FROM `sales` WHERE `sales`.`id` <= #sales_init;
UPDATE sales SET id=id - #sales_init;
-- set new index for sales
SET #new_init = (SELECT MAX(id) + 1 FROM sales );
SET #query = CONCAT("ALTER TABLE sales AUTO_INCREMENT = ", #new_init);
PREPARE stmt FROM #query;
EXECUTE stmt;