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
Related
There are two tables:
$inner_query =
"SELECT A.*, ROWNUM AS RN, TO_CHAR(A.last_newsletter_modify, 'DD/MM/YYYY') AS LAST_NEWSLETTER_MODIFY2
FROM ".$db_schema_name."newsletter_subscription A,
".$db_schema_name."newsletter_type B,
".$db_schema_name."newsletter_subtyp_profile C
WHERE A.id_subscription = C.id_subscription AND
C.id_type = B.id_type
ORDER BY e_mail";
If I run this query for id_subscription 734 it displays 3 times.
How can to display it just once?
Short answer: you are getting one row per newsletter_subtyp_profile. Subscription 734 is linked to three newsletter types, hence three rows of output.
You have three tables, not two, and the question would be clearer if you included full descriptions and sample data, and also got rid of the irrelevant PHP aspect and focussed on the SQL.
With some detective work, I make it this:
create table newsletter_subscription
( id_subscription integer primary key
, last_newsletter_modify date
, e_mail varchar2(50) not null );
create table newsletter_type
( id_type integer primary key
, description varchar2(40) not null unique );
create table newsletter_subtyp_profile
( id_subscription references newsletter_subscription
, id_type references newsletter_type
, constraint nsp_pk primary key (id_type,id_subscription) );
insert into newsletter_subscription values (600, date '2017-01-10', 'someone#somewhere.net');
insert into newsletter_subscription values (734, date '2017-02-05', 'someone#somewhereelse.net');
insert into newsletter_subscription values (800, date '2017-03-01', 'nobody#nowherewhere.net');
insert into newsletter_type values (1, 'Type One');
insert into newsletter_type values (2, 'Type Two');
insert into newsletter_type values (3, 'Type Three');
insert into newsletter_subtyp_profile values (734, 1);
insert into newsletter_subtyp_profile values (734, 2);
insert into newsletter_subtyp_profile values (734, 3);
Now run your query (I shortened the select list to simplify the output, and added b.description - a dummy column as I don't know what other columns you have on newsletter_type):
select a.id_subscription, a.e_mail
, to_char(a.last_newsletter_modify, 'DD/MM/YYYY') as last_newsletter_modify2
, b.description
from newsletter_subscription a,
newsletter_type b,
newsletter_subtyp_profile c
where a.id_subscription = c.id_subscription and
c.id_type = b.id_type
order by a.e_mail, c.id_type;
ID_SUBSCRIPTION E_MAIL LAST_NEWSLETTER_MODIFY2 DESCRIPTION
--------------- -------------------------- ----------------------- ---------------------
734 someone#somewhereelse.net 05/02/2017 Type One
734 someone#somewhereelse.net 05/02/2017 Type Two
734 someone#somewhereelse.net 05/02/2017 Type Three
btw the logic would be clearer if you used mnemonic aliases such as sub instead of a for newsletter_subscription, and also used standard ANSI joins and lost the uppercase:
select sub.id_subscription, sub.e_mail
, to_char(sub.last_newsletter_modify, 'DD/MM/YYYY') as last_newsletter_modify
, typ.description
from newsletter_subscription sub
join newsletter_subtyp_profile pro on pro.id_subscription = sub.id_subscription
join newsletter_type typ on typ.id_type = pro.id_type
where sub.id_subscription = 734
order by sub.e_mail, pro.id_type;
You can use window function row_number to get one row per id_subscription with max (or min id_type if you change the order by clause in the function to order by id_type):
$inner_query = "SELECT *
FROM (
SELECT A.*,
ROWNUM AS RN,
TO_CHAR(A.last_newsletter_modify, 'DD/MM/YYYY') AS LAST_NEWSLETTER_MODIFY2,
row_number() over (
partition by C.id_subscription
order by C.id_type desc nulls last
) as seqnum
FROM ".$db_schema_name."newsletter_subscription A
JOIN ".$db_schema_name."newsletter_subtyp_profile C
on A.id_subscription = C.id_subscription
JOIN ".$db_schema_name."newsletter_type B
on C.id_type = B.id_type
) t
WHERE seqnum = 1
ORDER BY e_mail";
Also notice that I have replace the old comma based join with widely recommended explicit JOIN syntax.
I have three tables viz. tb_filters, tb_products, and tb_products_to_filters. The structure of these tables along with some dummy data is given by:
tb_filters:
CREATE TABLE IF NOT EXISTS `tb_filters`
(
`filter_id` INT (11) AUTO_INCREMENT PRIMARY KEY,
`filter_name` VARCHAR (255)
);
INSERT INTO `tb_filters`
(`filter_name`)
VALUES ('USB'),
('High Speed'),
('Wireless'),
('Ethernet');
tb_products:
CREATE TABLE IF NOT EXISTS `tb_products`
(
`product_id` INT (11) AUTO_INCREMENT PRIMARY KEY,
`product_name` VARCHAR (255)
);
INSERT INTO `tb_products`
(`product_name`)
VALUES ('Ohm precision shunt resistor'),
('Orchestrator Libraries'),
('5cm scanner connection'),
('Channel isolated digital'),
('Network Interface Module');
tb_products_to_filters:
CREATE TABLE IF NOT EXISTS `tb_products_to_filters`
(
`id` INT (11) AUTO_INCREMENT PRIMARY KEY,
`product_id` INT (11),
`filter_id` INT (11)
);
INSERT INTO `tb_products_to_filters`
(`product_id`, `filter_id`)
VALUES (1, 1),
(2, 2),
(3, 3),
(4, 3),
(1, 3);
By looking into above "tb_products_to_filters" table, my required queries are:
When filter id = 1 and 3 are selected via checkbox on the page, all those products which belong to filter id 1 as well as filter id 3 must be fetched from the database. In this case, the product with id 1 should come.
Second, when only one filter (say id = 3) is checked, then all those products which fall under this id should be fetched. In this condition, the products id 1, 3 and 4 will come.
If filter id 2 is selected, then only one product with id = 2 will come.
If combination of filter (2 and 3) is selected, then no product will come because there is no product which belongs to both of them.
What is the way of writing queries to obtain above goal?
Please note that I want to include columns: product_id, product_name, filter_id and filter_name to display data in table result set.
EDIT:
The output should match below when filter ids 1 and 3 were checked:
EDIT 2:
I'm trying below query to fetch results when filter 1 and 3 were checked:
SELECT `p`.`product_id`, `p`.`product_name`,
GROUP_CONCAT(DISTINCT `f`.`filter_id` ORDER BY `f`.`filter_id` SEPARATOR ', ') AS filter_id, GROUP_CONCAT(DISTINCT `f`.`filter_name` ORDER BY `f`.`filter_name` SEPARATOR ', ') AS filter_name
FROM `tb_products` AS `p` INNER JOIN `tb_products_to_filters` AS `ptf`
ON `p`.`product_id` = `ptf`.`product_id` INNER JOIN `tb_filters` AS `f`
ON `ptf`.`filter_id` = `f`.`filter_id` GROUP BY `p`.`product_id`
HAVING GROUP_CONCAT(DISTINCT `ptf`.`filter_id` SEPARATOR ', ') = ('1,3')
ORDER BY `p`.`product_id`
But unfortunately, it returns an empty set. Why?
You can use the HAVING clause with GROUP_CONCAT :
SELECT t.product_id,tp.product_name,
GROUP_CONCAT(t.filter_id) as filter_id,
GROUP_CONCAT(tb.filter_name) as filter_name
FROM tb_products_to_filters t
INNER JOIN tb_filters tb ON(t.filter_id = tb.filter_id)
INNER JOIN tb_products tp ON(t.product_id = tp.product_id)
WHERE t.filter_id IN(1,3)
GROUP BY t.product_id
HAVING COUNT(distinct t.filter_id) = 2
You can adjust this any way you want. Note that the number of arguments placed inside the IN() should be the same as the COUNT(..) = X
EDIT:
A DISTINCT keyword is required in GROUP_CONCAT while fetching those columns otherwise all the filters would come in the list. I tried it by doing
SELECT t.product_id,tp.product_name,
GROUP_CONCAT(DISTINCT t.filter_id ORDER BY `t`.`filter_id` SEPARATOR ', ') as filter_id,
GROUP_CONCAT(DISTINCT tb.filter_name ORDER BY tb.filter_name SEPARATOR ', ') as filter_name
FROM tb_products_to_filters t
INNER JOIN tb_filters tb ON(t.filter_id = tb.filter_id)
INNER JOIN tb_products tp ON(t.product_id = tp.product_id)
WHERE t.filter_id IN(1,3)
GROUP BY t.product_id
HAVING COUNT(distinct t.filter_id) = 2
But still all the filter names (Ethernet, High Speed, USB, Wireless) are coming in the list. How to list only those filter names whose corresponding filter id (1, 3) are in the string?
I want to make a notifications page which shows notifications about a variety of things, like new followers, new likes, new comments etc. I want to display a list that shows all of these things in chronological order.
My tables look like this:
COMMENT
1 comment__id
2 comment__user_id
3 comment__snap__id
4 comment__text
5 comment_add_time
LIKE
1 like__id
2 like__user__id
3 like__snap__id
4 like__like_time
FOLLOW
1 follow__id
2 follower__user__id
3 followed__user__id
4 follow__follow_time
5 follow__request_status
I would load the followers of a user with a query like this:
try {
$select_followers_query = '
SELECT follow.follower__user__id, follow.followed__user__id, follow.follow__request_status, user.user__id, user.user__username, user.user__profile_picture, user.privacy
FROM follow
JOIN user ON(follow.follower__user__id = user.user__id)
WHERE followed__user__id = :followed__user__id';
$prep_select_followers = $conn->prepare($select_followers_query);
$prep_select_followers->bindParam(':followed__user__id', $get_user__id, PDO::PARAM_INT);
$prep_select_followers->execute();
$followers_result = $prep_select_followers->fetchAll();
$followers_count = count($followers_result);
}
catch(PDOException $e) {
$conn = null;
echo $error;
}
Next, I get the results like this:
foreach($followers_result AS $followers_row) {
$follower_user_id = $followers_row['follower__user__id'];
// the rest of the variables will come here...
}
I will have separate SQL queries like the one above which each load something. The example above loads the followers, another query will load the comments etc. I want to display the results of all of these queries and display them in chronological order, like this:
#user_1 liked your photo
#user_4 started following you
#user_2 commented on your photo
etc...
How can I achieve this? SQL UNION requires the tables to have the same number of columns and the selected columns must have the same name. I don't have all that. Moreover, every kind of result (follower, comment or like) will have different markups. A follower notification will have a follow button, a comment notification will have a button that redirects to the photo that was liked etc.
SQL UNION requires the tables to have the same number of columns and the selected columns must have the same name.
No, it doesn't. Here table "a" has two columns, integer and varchar.
create table a (
a_id integer,
a_desc varchar(10)
);
insert into a values (1, 'aaaaaaaa'), (2, 'bbbbbbbb');
Table "b" has three columns, varchar, date, and char.
create table b (
b_id varchar(10),
created_date date,
unused char(1)
);
insert into b values ('xyz', '2014-01-01', 'x'), ('tuv', '2014-01-13', 'x');
The SQL union operator only requires that the SELECT clauses (not tables) have the same number of columns, and that they be of compatible data types. You can usually cast incompatible types to something more useful.
-- SELECT clause has three columns, but table "a" has only two.
-- The cast is for illustration; MySQL can union an integer with a
-- varchar without a cast.
--
select cast(a_id as char) as col_1, a_desc as col_2, null as col_3
from a
union all
-- Note that these columns don't have the same names as the columns
-- above.
select b_id, null, created_date
from b;
You can use a single column for date and varchar, but it's usually not a good idea. (Mixing dates with something that's clearly not a date is usually not a good idea.)
select cast(a_id as char) as col_1, a_desc as col_2
from a
union all
select b_id, created_date
from b;
You can use UNION but you'll need to use 'AS' to give columns the same name.
You'll also need to add a line like this to each select:
, 'comment' as Type FROM comment
and:
, 'follow' as Type FROM follow
I want to execute a query where I can find one ID in a list of ID.
table user
id_user | name | id_site
-------------------------
1 | james | 1, 2, 3
1 | brad | 1, 3
1 | suko | 4, 5
and my query (doesn't work)
SELECT * FROM `user` WHERE 3 IN (`id_site`)
This query work (but doesn't do the job)
SELECT * FROM `user` WHERE 3 IN (1, 2, 3, 4, 6)
That's not how IN works. I can't be bothered to explain why, just read the docs
Try this:
SELECT * FROM `user` WHERE FIND_IN_SET(3,`id_site`)
Note that this requires your data to be 1,2,3, 1,3 and 4,5 (ie no spaces). If this is not an option, try:
SELECT * FROM `user` WHERE FIND_IN_SET(3,REPLACE(`id_site`,' ',''))
Alternatively, consider restructuring your database. Namely:
CREATE TABLE `user_site_links` (
`id_user` INT UNSIGNED NOT NULL,
`id_site` INT UNSIGNED NOT NULL,
PRIMARY KEY (`user_id`,`site_id`)
);
INSERT INTO `user_site_links` VALUES
(1,1), (1,2), (1,3),
(2,1), (2,3),
(3,4), (3,5);
SELECT * FROM `user` JOIN `user_site_links` USING (`id_user`) WHERE `id_site` = 3;
Try this: FIND_IN_SET(str,strlist)
NO! For relation databases
Your table doesn't comfort first normal form ("each attribute contains only atomic values, and the value of each attribute contains only a single value from that domain") of a database and you:
use string field to contain numbers
store multiple values in one field
To work with field like this you would have to use FIND_IN_SET() or store data like ,1,2,3, (note colons or semicolons or other separator in the beginning and in the end) and use LIKE "%,7,%" to work in every case. This way it's not possible to use indexes[1][2].
Use relation table to do this:
CREATE TABLE user_on_sites(
user_id INT,
site_id INT,
PRIMARY KEY (user_id, site_id),
INDEX (user_id),
INDEX (site_id)
);
And join tables:
SELECT u.id, u.name, uos.site_id
FROM user_on_sites AS uos
INNER JOIN user AS u ON uos.user_id = user.id
WHERE uos.site_id = 3;
This way you can search efficiently using indexes.
The problem is that you are searching within several lists.
You need something more like:
SELECT * FROM `user` WHERE id_site LIKE '%3%';
However, that will also select 33, 333 and 345 so you want some more advanced text parsing.
The WHERE IN clause is useful to replace many OR conditions.
For exemple
SELECT * FROM `user` WHERE id IN (1,2,3,4)
is cleaner than
SELECT * FROM `user` WHERE id=1 OR id=2 OR id=3 OR id=4
You're just trying to use it in a wrong way.
Correct way :
WHERE `field` IN (list_item1, list_item2 [, list_itemX])
I know this is quite complicated, but I sincerely hope someone will check this out.
I made short version (to better understand the problem) and full version (with original SQL)
Short version:
[TABLE A] [TABLE B]
|1|a|b| |1|x
|2|c|d| |1|y
|3| | | |2|z
|5| | | |2|v
|4|w
How can I make MySQL query to get rows like that:
1|a|b|x|y
2|c|d|z|v
2 columns from A and 2 rows from B as columns, only with keys 1 and 2, no empty results
Subquery?
Full version:
I tried to get from Prestashop db in one row:
product id
ean13 code
upc code
feature with id 24
feature with id 25
It's easy to get id_product, ean13 and upc, as it's one row in ps_product table. To get features I used subqueries (JOIN didn't work out).
So, I selected id_product, ean13, upc, (subquery1) as code1, (subquery2) as code2.
Then I needed to throw out empty rows. But couldn't just put code1 or code2 in WHERE.
To make it work I had to put everything in subquery.
This code WORKS, but it is terribly ugly and I bet this should be done differently.
How can I make it BETTER?
SELECT * FROM(
SELECT
p.id_product as idp, p.ean13 as ean13, p.upc as upc, (
SELECT
fvl.value
FROM
`ps_feature_product` fp
LEFT JOIN
`ps_feature_value_lang` fvl ON (fp.id_feature_value = fvl.id_feature_value)
WHERE fp.id_feature = 24 AND fp.id_product = idp
) AS code1, (
SELECT
fvl.value
FROM
`ps_feature_product` fp
LEFT JOIN
`ps_feature_value_lang` fvl ON (fp.id_feature_value = fvl.id_feature_value)
WHERE fp.id_feature = 25 AND fp.id_product = idp
) AS code2,
m.name
FROM
`ps_product` p
LEFT JOIN
`ps_manufacturer` m ON (p.id_manufacturer = m.id_manufacturer)
) mainq
WHERE
ean13 != '' OR upc != '' OR code1 IS NOT NULL OR code2 IS NOT NULL
create table tablea
( id int,
col1 varchar(1),
col2 varchar(1));
create table tableb
( id int,
feature int,
cola varchar(1));
insert into tablea (id, col1, col2)
select 1,'a','b' union
select 2,'c','d' union
select 3,null,null union
select 5,null,null;
insert into tableb (id, feature, cola)
select 1,24,'x' union
select 1,25,'y' union
select 2,24,'z' union
select 2,25,'v' union
select 4,24,'w';
select a.id, a.col1, a.col2, b1.cola b1a, b2.cola b2a
from tablea a
inner join tableb b1 on (b1.id = a.id and b1.feature = 24)
inner join tableb b2 on (b2.id = a.id and b2.feature = 25);
SQLFiddle here.
What you want to do is called a Pivot Query. MySQL has no native support for pivot queries, though other RDBMSen do.
You can simulate a pivot query with derived columns, but you must specify each derived column. That is, it is impossible in MySQL itself to have the number of columns match rows of another table. This has to be known ahead of time.
It would be much easier to query the results as rows and then use PHP to do the aggregation into columns. For example:
while ($row = $result->fetch()) {
if (!isset($table[$row->id])) {
$table[$row->id] = array();
}
$table[$row->id][] = $row->feature;
This is not a simple question because it's not a standard query, by the way if you can make use of views you can do the following procedure. Assuming you're starting from this tables:
CREATE TABLE `A` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`firstA` char(1) NOT NULL DEFAULT '',
`secondA` char(1) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
);
CREATE TABLE `B` (
`id` int(11) unsigned NOT NULL,
`firstB` char(1) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `A` (`id`, `firstA`, `secondA`)
VALUES (1, 'a', 'b'), (2, 'c', 'd');
INSERT INTO `B` (`id`, `firstB`)
VALUES (1, 'x'), (1, 'y'), (2, 'z'), (2, 'v'), (4, 'w');
First create a view that joins the two tables:
create or replace view C_join as
select A.firstA, A.secondA, B.firstB
from A
join B on B.id=A.id;
Create the view that groups the rows in table B:
create or replace view d_group_concat as
select firstA, secondA, group_concat(firstB) groupconcat
from c_join
group by firstA, secondA
Create the view that does what you need:
create or replace view e_result as
select firstA, secondA, SUBSTRING_INDEX(groupconcat,',',1) firstB, SUBSTRING_INDEX(SUBSTRING_INDEX(groupconcat,',',2),',',-1) secondB
from d_group_concat
And that's all. Hope this helps you.
If you can't create views, this could be the query:
select firstA, secondA, SUBSTRING_INDEX(groupconcat,',',1) firstB, SUBSTRING_INDEX(SUBSTRING_INDEX(groupconcat,',',2),',',-1) secondB
from (
select firstA, secondA, group_concat(firstB) groupconcat
from (
select A.firstA, A.secondA, B.firstB
from A
join B on B.id=A.id
) c_join
group by firstA, secondA
) d_group_concat
Big thanks to everyone for the answers. James's answer was first, simplest and works perfectly in my case. The query runs several times faster than mine, with subqueries. Thanks, James!
Just a few words why I needed that:
It's a part of integration component for Prestashop and wholesale exchange platform. There are 4 product code systems that wholesalers use on the platform (ean13, upc and 2 other systems). Those 2 other product codes are added as product feature in Prestashop. There are thousands of products on the shop and hundreds of thousands of products on the platform. Which is why speed is crucial.
Here is the code for full version of my question. Maybe someone will find this helpful.
Query to get Prestashop product codes and certain features in one row:
SELECT
p.id_product, p.ean13, p.upc, fvl1.value as code1, fvl2.value as code2
FROM `ps_product` p
LEFT JOIN
`ps_feature_product` fp1 ON (p.id_product = fp1.id_product and fp1.id_feature = 24)
LEFT JOIN
`ps_feature_value_lang` fvl1 ON (fvl1.id_feature_value = fp1.id_feature_value)
LEFT JOIN
`ps_feature_product` fp2 ON (p.id_product = fp2.id_product and fp2.id_feature = 25)
LEFT JOIN
`ps_feature_value_lang` fvl2 ON (fvl2.id_feature_value = fp2.id_feature_value)
WHERE
ean13 != '' OR upc != '' OR fvl1.value IS NOT NULL OR fvl2.value IS NOT NULL;