Convert MySQL JSON array with unpredictable keys to a table, and join - json

Given the following MySQL 8 table structures...
CREATE TABLE `products` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(1000) NOT NULL,
PRIMARY KEY (`id`)
);
/* the "names" of the custom attributes that could exist for any given product */
CREATE TABLE `custom_attributes` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
/* the custom attributes of products stored as json array, when "key" is FK to custom_attributes table, and "value" is the attribute value for the given product */
/* in an ideal design, this table would not exist as there is a one-to-one relationship with `product`, and so could have simply been an `attributes` column in the product table, but we are where we are... */
CREATE TABLE `product_custom_attributes` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`product_id` bigint unsigned NOT NULL,
`attribute_value` json DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `product_custom_attributes_product_id_foreign` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`)
);
...and the following data...
INSERT INTO products (`id`, `name`) VALUES (1, 'Big Blue Ball');
INSERT INTO products (`id`, `name`) VALUES (2, 'Small Wooden Bat');
INSERT INTO custom_attributes (`id`, `title`) VALUES (1, 'Size');
INSERT INTO custom_attributes (`id`, `title`) VALUES (2, 'Color');
INSERT INTO custom_attributes (`id`, `title`) VALUES (3, 'Material');
INSERT INTO custom_attributes (`id`, `title`) VALUES (4, 'Height');
INSERT INTO product_custom_attributes (`id`, `product_id`, `attribute_value`) VALUES (1, 1, '{"1": "Big", "2": "Blue"}');
INSERT INTO product_custom_attributes (`id`, `product_id`, `attribute_value`) VALUES (2, 2, '{"1": "Small", "3": "Wood"}');
...how can I write a query to return the following results...?
product_id
name
Size
Color
Material
Height
1
Big Blue Ball
Big
Blue
2
Small Wooden Bat
Small
Wood
I cannot predict, at runtime, what custom_attributes might exist for products because it's likely this list will change over time, e.g. new attributes added.
I'm pretty sure I'll need to use the JSON_TABLE() function, but I've struggled to get result with the unpredicatable attributes (and therefore unpredictable JSON "columns") in play.
Edit - Solution
SET SESSION group_concat_max_len=4294967295;
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'MAX(IF(ca.title = ''',
ca.title,
''', attribute_values.attribute_value, NULL)) AS `',
ca.title, '`'
)
) INTO #sql
FROM
products p
LEFT OUTER JOIN custom_attributes ca ON p.project_id = ca.project_id
LEFT OUTER JOIN product_custom_attributes pca ON p.id = pca.product_id
LEFT OUTER JOIN JSON_TABLE(
JSON_KEYS(pca.attribute_value),
'$[*]' COLUMNS (
rn FOR ORDINALITY,
attribute_key VARCHAR(255) PATH '$'
)
) attribute_keys ON ca.id = attribute_keys.attribute_key
LEFT OUTER JOIN JSON_TABLE(
JSON_EXTRACT(pca.attribute_value, '$.*'),
'$[*]' COLUMNS (
rn FOR ORDINALITY,
attribute_value VARCHAR(255) PATH '$'
)
) attribute_values ON attribute_values.rn = attribute_keys.rn;
-- if there are no custom attributes #sql will be NULL and when appended to the following query will make the whole thing NULL, so deal with this...
SET #sql = CASE WHEN ISNULL(#sql) THEN '' ELSE CONCAT(', ', #sql) END;
SET #sql = CONCAT('
SELECT
p.id
, p.name
', #sql, '
FROM
products p
LEFT OUTER JOIN custom_attributes ca ON p.project_id = ca.project_id
LEFT OUTER JOIN product_custom_attributes pca ON p.id = pca.product_id
LEFT OUTER JOIN JSON_TABLE(
JSON_KEYS(pca.attribute_value),
''$[*]'' COLUMNS (
rn FOR ORDINALITY,
attribute_key VARCHAR(255) PATH ''$''
)
) attribute_keys ON ca.id = attribute_keys.attribute_key
LEFT OUTER JOIN JSON_TABLE(
JSON_EXTRACT(pca.attribute_value, ''$.*''),
''$[*]'' COLUMNS (
rn FOR ORDINALITY,
attribute_value VARCHAR(255) PATH ''$''
)
) attribute_values ON attribute_values.rn = attribute_keys.rn
GROUP BY p.id, p.name');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

Related

MySQL query to get one item from a table an multiple items from another table

I have a MYSQL table called tbl_product
Another table called tb_opciones_productos
And a third table called tb_opciones
I need to show every item from tbl_product as follows:
How can I get one item from tbl_product and the needed rows from tb_opciones_producto to get the needed result?
EDIT:
This is my current query proposal:
SELECT tbl_product.*,
GROUP_CONCAT( (SELECT CONCAT(tb_opciones.nombre, "(+$", tb_opciones.precio, ")")
FROM tb_opciones WHERE tb_opciones.id_opcion = tb_opciones_productos.id_opcion) SEPARATOR "<br>" ) as options FROM tbl_product
INNER JOIN tb_opciones_productos ON tbl_product.id = tb_opciones_productos.producto
I've create a little sqlfiddle to test : http://sqlfiddle.com/#!9/fc3316/16
You can GROUP_CONCAT a sub-query. It may not be optimized, but it do the job.
PS: next time, can you provide a sample structure ?
Structure :
CREATE TABLE IF NOT EXISTS `products` (
`id` int(6) unsigned NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `products` (`id`, `name`) VALUES
(1, 'Product Lorem'),
(2, 'Product Ipsum');
CREATE TABLE IF NOT EXISTS `products_options` (
`id_product` int(6) unsigned NOT NULL,
`id_option` int(6) unsigned NOT NULL,
PRIMARY KEY (`id_product`, `id_option`)
) DEFAULT CHARSET=utf8;
INSERT INTO `products_options` (`id_product`, `id_option`) VALUES
(1, 1),
(1, 2),
(1, 3),
(2, 3);
CREATE TABLE IF NOT EXISTS `options` (
`id` int(6) unsigned NOT NULL,
`name` varchar(255) NOT NULL,
`value` double NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `options` (`id`, `name`, `value`) VALUES
(1, 'Option A', 42),
(2, 'Option B', 6),
(3, 'Option C', 12);
Request :
SELECT products.*,
GROUP_CONCAT(options.name, " (+$", options.value, ")" SEPARATOR "<br>")
FROM products
INNER JOIN products_options
ON products.id = products_options.id_product
INNER JOIN options
ON products_options.id_option = options.id
GROUP BY products.id
With your Structure, I think this one will work :
SELECT tbl_product.*,
GROUP_CONCAT(tb_opciones.nombre, " (+$", tb_opciones.precio, ")" SEPARATOR "<br>")
FROM tbl_product
INNER JOIN tb_opciones_productos
ON tbl_product.id = tb_opciones_productos.producto
INNER JOIN tb_opciones
ON tb_opciones_productos.opcion = tb_opciones.id
GROUP BY tbl_product.id

MySQL: Compare comma separated values with values in different rows

I have following requirements.
I have two tables T1 and T2 like
Table T1
Product Geography
P1 G1
P2 G1
P2 G2
Table T2
Product Geography
P1 G1, G2
P2 G1, G2
I want a query to get data from table T2 if comma separated Geography have exactly matching records in T1. If there are less or more geographies in any table than it should not return that row. Sequence of geographies in T2 is not fixed. So return of that query from above example will be:
Product Geography
P2 G1, G2
Join on concating values
SELECT t2.Product,t2.geography FROM t2
JOIN
(SELECT t1.Product,GROUP_CONCAT(t1.geography ORDER BY t1.Geography SEPARATOR ', ') as concatgeo FROM t1
GROUP BY t1.product)x
ON t2.Geography=x.concatgeo AND t2.Product=x.Product
SELECT
firstTable.Product,
firstTable.geographies
FROM
(
SELECT
Product,
REPLACE(GROUP_CONCAT(Geography),' ','') AS geographies
FROM T1
GROUP BY Product) firstTable
INNER JOIN
(
SELECT
Product,
REPLACE(Geography,' ','') AS geographies
FROM T2 ) secondTable
ON firstTable.Product = secondTable.Product
WHERE firstTable.geographies = secondTable.geographies;
Note: I've replaced the spaces using REPLACE function just to ensure the two queries from two tables generate the same string
SQL FIDDLE DEMO
TEST (If you cannot access sqlfiddle):
DROP TABLE IF EXISTS `t1`;
CREATE TABLE `t1` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`Product` varchar(50) CHARACTER SET utf8 NOT NULL,
`Geography` varchar(100) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`ID`)
);
INSERT INTO `t1` VALUES ('1', 'P1', 'G1');
INSERT INTO `t1` VALUES ('2', 'P2', 'G1');
INSERT INTO `t1` VALUES ('3', 'P2', 'G2');
DROP TABLE IF EXISTS `t2`;
CREATE TABLE `t2` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`Product` varchar(50) CHARACTER SET utf8 NOT NULL,
`Geography` varchar(100) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`ID`)
);
INSERT INTO `t2` VALUES ('1', 'P1', 'G1, G2');
INSERT INTO `t2` VALUES ('2', 'P2', 'G1, G2');
Running the above query on this test data you will get output like below:
Product geographies
P2 G1,G2

How to make use of index on LEFT OUTER JOIN

Here is a set of tables describing music composers :
CREATE TABLE IF NOT EXISTS `compositors` (
`id` int(11) NOT NULL,
`name` varchar(45) NOT NULL COMMENT 'Nom et Prenom',
`birth_date` varchar(45) DEFAULT NULL,
`death_date` varchar(45) DEFAULT NULL,
`birth_place` varchar(45) DEFAULT NULL,
`death_place` varchar(45) DEFAULT NULL,
`gender` enum('M','F') DEFAULT NULL,
`century` varchar(45) DEFAULT NULL,
`country` int(11) DEFAULT NULL
) ENGINE=InnoDB AUTO_INCREMENT=28741 DEFAULT CHARSET=latin1;
CREATE TABLE IF NOT EXISTS `compositor_biography` (
`index` int(11) NOT NULL,
`compositor_id` int(11) NOT NULL,
`url` varchar(255) DEFAULT NULL
) ENGINE=InnoDB AUTO_INCREMENT=15325 DEFAULT CHARSET=latin1;
CREATE TABLE IF NOT EXISTS `compositor_comments` (
`compositor_id` int(11) NOT NULL,
`comment` text NOT NULL,
`public` enum('Publique','Privé') NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE IF NOT EXISTS `compositor_country` (
`compositor_id` int(11) NOT NULL,
`country_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Here are my indexes :
--
-- Index pour la table `compositors`
--
ALTER TABLE `compositors` ADD PRIMARY KEY (`id`), ADD KEY `countries` (`country`);
ALTER TABLE `compositor_biography` ADD PRIMARY KEY (`index`), ADD KEY `index` (`compositor_id`);
ALTER TABLE `compositor_comments` ADD KEY `c_compositor_idx` (`compositor_id`);
And finally sample data :
INSERT INTO `compositors` (`id`, `name`, `birth_date`, `death_date`, `birth_place`, `death_place`, `gender`, `century`, `country`) VALUES
(1, 'Dummy Compositor', '1606', '1676', 'Bruxellesss', NULL, 'F', '17', 11);
INSERT INTO `compositor_biography` (`index`, `compositor_id`, `url`) VALUES
(15322, 1, 'Dummy Link 1'),
(15323, 1, 'Dummy Link 2'),
(15324, 1, 'Dummy Link 3');
INSERT INTO `compositor_comments` (`compositor_id`, `comment`, `public`) VALUES
(1, 'Dummy Comment', 'Privé');
Here is an example query that my PHP script generate :
SELECT DISTINCT compositors.id, compositors.name, compositors.birth_date, compositors.death_date, compositors.birth_place, compositors.death_place, compositors.gender, compositors.century, compositors.country,
GROUP_CONCAT( compositor_biography.url SEPARATOR ';') AS concat_compositor_biography_url,
GROUP_CONCAT( compositor_comments.comment SEPARATOR ';') AS concat_compositor_comments_comment,
GROUP_CONCAT( compositor_comments.public + 0 SEPARATOR ';') AS concat_compositor_comments_public
FROM compositors
LEFT JOIN compositor_biography ON compositors.id = compositor_biography.compositor_id
LEFT JOIN compositor_comments ON compositors.id = compositor_comments.compositor_id
GROUP BY compositors.id
However, this one has a problem, if you execute this query, you can see that in the concat_compositor_comments_comment column, you have that result :
Dummy Comment;Dummy Comment;Dummy Comment
but there is only one actual comment.
I didn't really understand what was the problem there, but it seemed like it was the GROUP BY. It should have one GROUP BY per JOIN - according to the second answer at Multiple GROUP_CONCAT on different fields using MySQL -- so I did it, and it worked, with this query :
SELECT DISTINCT compositors.id,
compositors.NAME,
compositors.birth_date,
compositors.death_date,
compositors.birth_place,
compositors.death_place,
compositors.gender,
compositors.century,
compositors.country,
concat_compositor_biography_url,
concat_compositor_comments_comment,
concat_compositor_comments_public
FROM compositors
LEFT JOIN (
SELECT compositor_id,
GROUP_CONCAT(compositor_biography.url SEPARATOR ';') AS concat_compositor_biography_url
FROM compositor_biography
GROUP BY compositor_biography.compositor_id
) compositor_biography ON compositors.id = compositor_biography.compositor_id
LEFT JOIN (
SELECT compositor_id,
GROUP_CONCAT(compositor_comments.comment SEPARATOR ';') AS concat_compositor_comments_comment,
GROUP_CONCAT(compositor_comments.PUBLIC + 0 SEPARATOR ';') AS concat_compositor_comments_public
FROM compositor_comments
GROUP BY compositor_comments.compositor_id
) compositor_comments ON compositors.id = compositor_comments.compositor_id
However, this query has huge performance problem, since it doesn't use INDEXES, or at least it seems to scan all the tables, and with 24000 composers, it takes approx 420 second for that query, while the other ( which gives wrong results on GROUP BY ) takes 1 second.
How can I change the second query, so it uses correctly the index and doesn't scan all the tables ?
Here is a link to a SQL-Fiddle the database schema : http://sqlfiddle.com/#!2/6b0132
UPDATE
According to #phil_w, and after further testing, this query seems to work with very good performance :
SELECT a.id,
a.name,
a.concat_compositor_biography_url,
b.concat_compositor_aliases_data,
GROUP_CONCAT(compositor_comments.comment SEPARATOR ';') as concat_compositor_comments_comment,
GROUP_CONCAT(compositor_comments.public + 0 SEPARATOR ';') as concat_compositor_comments_public
FROM (
SELECT b.id,
b.name,
b.concat_compositor_biography_url,
GROUP_CONCAT(compositor_aliases.data SEPARATOR ';') as concat_compositor_aliases_data
FROM (
SELECT compositors.id,
compositors.name,
GROUP_CONCAT(compositor_biography.url SEPARATOR ';') AS concat_compositor_biography_url
FROM compositors
LEFT JOIN compositor_biography ON compositors.id = compositor_biography.compositor_id
GROUP BY compositors.id
) b
LEFT JOIN compositor_aliases ON b.id = compositor_aliases.compositor_id
GROUP BY b.id
) a
LEFT JOIN compositor_comments ON a.id = compositor_comments.compositor_id
GROUP BY a.id
However, how would it be possible to have the same result in a more compact query ? ( by the way, shall I create a new question for that and make this one resolved ? )
This question has nothing to do with "indexes". The problem is that you have two joins and every combination of rows will be returned (ie you have 3 matching rows in the other join to compositor_biography).
The fix is simple - just add DISTINCT to the GROUP_CONCAT() function:
...
GROUP_CONCAT( DISTINCT compositor_comments.comment SEPARATOR ';') AS concat_compositor_comments_comment,
...
It's normal that you have the entry 3 times, because you have 3 rows in compositor_biography...
perhaps you could go step by step,
first gathering the bio only:
SELECT compositors.id, compositors.name,
GROUP_CONCAT( compositor_biography.url SEPARATOR ';') AS concat_compositor_biography_url
FROM compositors
LEFT JOIN compositor_biography ON compositors.id = compositor_biography.compositor_id
GROUP BY compositors.id
then join the rest
select t.id, t.name,t.concat_compositor_biography_url,
GROUP_CONCAT( compositor_comments.comment SEPARATOR ';') AS concat_compositor_comments_comment
from (
SELECT compositors.id, compositors.name,
GROUP_CONCAT( compositor_biography.url SEPARATOR ';') AS concat_compositor_biography_url
FROM compositors
LEFT JOIN compositor_biography ON compositors.id = compositor_biography.compositor_id
GROUP BY compositors.id
) t
LEFT JOIN compositor_comments ON t.id = compositor_comments.compositor_id
and so on...
I don't see why it would not use the index unless the table is small.
Try 'explain select...' to confirm that.

How to delete mysql rows using join?

I have three tables:
"products" - contains products
"location" - store location
"product_orders" - joins data from products and location
SQL DUMP:
--
-- Table structure for table `location`
--
CREATE TABLE IF NOT EXISTS `location` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=77 ;
--
-- Dumping data for table `location`
--
INSERT INTO `location` (`id`, `name`) VALUES
(1, 'Miami'),
(2, 'Denver');
-- --------------------------------------------------------
--
-- Table structure for table `products`
--
CREATE TABLE IF NOT EXISTS `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`type` varchar(255) NOT NULL DEFAULT '',
`product` text NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=6014552 ;
--
-- Dumping data for table `products`
--
INSERT INTO `products` (`id`, `type`, `product`) VALUES
(1, 'shirt', 'red shirt'),
(2, 'shirt', 'red shirt'),
(3, 'pants', 'blue pants'),
(4, 'pants', 'blue pants');
-- --------------------------------------------------------
--
-- Table structure for table `product_orders`
--
CREATE TABLE IF NOT EXISTS `product_orders` (
`product_id` int(11) NOT NULL,
`location_id` int(11) NOT NULL,
`status` varchar(255) NOT NULL,
PRIMARY KEY (`product_id`,`location_id`),
KEY `ix_product_orders` (`location_id`),
KEY `ix_product_orders_1` (`product_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- Dumping data for table `product_orders`
--
INSERT INTO `product_orders` (`product_id`, `location_id`, `status`) VALUES
(1, 1, 'sold'),
(2, 2, 'sold'),
(1, 2, 'available'),
(2, 1, 'sold');
If I want to select all sold products from store 'miami - this query works:
SELECT * FROM `products`
INNER JOIN `product_orders`
ON `product_orders`.`product_id`= `products`.`id`
WHERE `product_orders`.`location_id` = '1'
AND
status = 'sold'
How would you rewrite to delete all sold products from the miami store? My query is not working:
DELETE `products` FROM `products`
INNER JOIN `product_orders`
ON `product_orders`.`location_id` = `products`.`id`
WHERE `product_orders`.`location_id` = '2'
AND
status = 'sold'
DELETE FROM products WHERE id IN (SELECT location_id FROM products_orders WHERE location = 'miami' AND status = 'sold');
Try this one,
DELETE `products` FROM `products` INNER JOIN `products_orders`
ON `products_orders`.`location_id` = `products`.`id`
WHERE `products_orders`.`location` = 'miami'
AND
status = 'sold'
OR
DELETE prod FROM `products` AS prod INNER JOIN `products_orders`
ON `products_orders`.`location_id` = `products`.`id`
WHERE `products_orders`.`location` = 'miami'
AND
status = 'sold'
I am not sure how you have set foreign keys in your tables. But looking at it with the most primary assumptions, product table holds products data and location holds location data. In your common table product orders you are having one foreign key to products table by product id and one another relation to location table by location.
So to back my comment, I am posting the following query. It is your query with a change to a field that I ASSUME WAS A TYPO....in your query. :-) Again as foampile said, I might be doing it with consistency...
DELETE FROM `products`
INNER JOIN `products_orders`
ON `products_orders`.`product_id` = `products`.`id`
WHERE `products_orders`.`location` = 'miami'
AND `products_orders`.status = 'sold'
;

Complicated SELECT query

I have a table which defines what things another table can have, for example:
CREATE TABLE `objects` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50) NOT NULL
);
INSERT INTO `objects` (`name`) VALUES ('Test');
INSERT INTO `objects` (`name`) VALUES ('Test 2');
CREATE TABLE `properties` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50) NOT NULL
);
INSERT INTO `properties` (`name`) VALUES ('colour');
INSERT INTO `properties` (`name`) VALUES ('size');
CREATE TABLE `objects_properties` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`object_id` INT UNSIGNED NOT NULL,
`property_id` INT UNSIGNED NOT NULL,
`value` VARCHAR(50) NOT NULL,
FOREIGN KEY (`object_id`)
REFERENCES `objects` (`id`),
FOREIGN KEY (`property_id`)
REFERENCES `properties` (`id`)
);
INSERT INTO `objects_properties` (`object_id`, `property_id`, `value`) VALUES 1, 1, 'red');
INSERT INTO `objects_properties` (`object_id`, `property_id`, `value`) VALUES 1, 2, 'small');
INSERT INTO `objects_properties` (`object_id`, `property_id`, `value`) VALUES 2, 1, 'blue');
INSERT INTO `objects_properties` (`object_id`, `property_id`, `value`) VALUES 2, 2, 'large');
Hopefully this makes sense. Basically instead of having columns for colour, size etc. in the objects table, I have two other tables, one that defines the properties any object can have, and another that links objects to some or all of these properties.
My question is if there's some way to retrieve this information like this:
+--------+------------+------------+
| object | colour | size |
+--------+------------+------------+
| Test | red | small |
| Test 2 | blue | large |
+--------+------------+------------+
So you can see the column headings are actually row values. I'm not sure if it's possible or how costly it would be compared to doing a few separate queries and putting everything together in PHP.
SELECT o.name, c.colour, s.size
FROM objects o
LEFT JOIN (SELECT op.object_id, op.value colour
FROM objects_properties op
join properties p on op.property_id = p.id and p.name = 'colour') c
ON o.id = c.object_id
LEFT JOIN (SELECT op.object_id, op.value size
FROM objects_properties op
join properties p on op.property_id = p.id and p.name = 'size') s
ON o.id = s.object_id
The keyword here is "pivot table" "crosstab" (but a "pivot table" lies also in that direction) and no, MySQL cannot do this directly. You can create a query that will select this, but you will have to explicitly define the columns yourself in the query. No fetching of columns from another table. Other RDBMS may have capabilities for this.
pivot (or something like that) could be useful. In MS SQL Server you can use it BUT the values to pivot the table must be constant or you can use a stored procedure to calculate it.
Here you can find more info.
Have a nice day!
SELECT o.*,
(
SELECT *
FROM object_properties op
WHERE op.object_id = o.object_id
AND op.property_id = $prop_color_id
) AS color,
(
SELECT *
FROM object_properties op
WHERE op.object_id = o.object_id
AND op.property_id = $prop_size_id
) AS size
FROM objects o
Substitute the $prop_color_id and $prop_size_id with the color and size property id's.
For this query to be efficient, make (object_id, property_id) a PRIMARY KEY in the object_properties and get rid of the surrogate key.