SQL join or merge results? - mysql

Look at this DB schema:
The users can order products. For any order I add a new record in the orders table and a new record in orders-products N:M table for any single product ordered by the user (and fill pieces field). But, when a user creating an order, I need to show the list of all products and fill the pieces field with the quantity ordered for any product.
Usually, to do it, I use two queries.
The first query get all products in the products table:
SELECT * FROM products;
The second query gets the products ordered in the orders-products table filtering them by orders-idorder FK:
SELECT orders_idorder AS idorder, products_idproduct AS idproduct, pieces FROM orders_products WHERE orders_idorder=1;
Then I merge the results in a single array and use it to display the complete list of products with the pieces ordered for each one. The final result is something like this:
+---------+-----------+--------------+-------+--------+
| idorder | idproduct | description | price | pieces |
+---------+-----------+--------------+-------+--------+
| 1 | 1 | Product 1 | 10.20 | 2 |
| 1 | 2 | Product 22 | 11.00 | NULL |
| 1 | 3 | Product 333 | 19.22 | NULL |
| 1 | 4 | Product 4444 | 9.20 | NULL |
+---------+-----------+--------------+-------+--------+
Note: In the above example there are 4 records in the products table and just 1 record in the orders-products table (it has idorder=1, idproduct=1 and pieces=2).
-> Here you can find the SQL Dump to test the queries.
Merging arrays built from 2 queries is the best way?
I can do it with a single query?
What do you think about the performance?

Generally letting the databse optimize the merge will be better than what you can do in code. The db is usually better at sorting too.
How you structure the query depends on your desired result set. If you want all products regardless of whether they appear in the orders table then you'd use an OUTER JOIN otherwise an INNER JOIN would filter out products that have never been ordered.
If you give us your desired results for some sample data we might be able to help you with the query, but give it a shot yourself first.
set #searchOrderId = 1;
select ifnull(op.orders_idorder, #searchOrderId) AS idOrder,
p.idproduct,
p.description,
p.price,
op.pieces
from products p
left join orders_products op on op.products_idproduct = p.idproduct
where ifnull(op.orders_idorder, #searchOrderId) = #searchOrderId ;
SQL Fiddle I was playing with to test it.

Sure you can get the information using a single query by JOINing the tables together. Is it what you're having troubles with?

I think that in general, executing a single query to retrieve the results will perform better than executing two queries and merging the results. It is impossible to say what the ideal plan for execution is, given the information provided. Table sizes, architecture, etc will play a role.
For retrieving all of the products from a single order, try:
select
o.idorder,
p.idproduct,
p.description,
p.price,
op.pieces
from
orders o
inner join orders_products op
on o.idorder = op.orders_idorder
inner join products p
on op.products_idproduct = p.idproduct
where
o.idorder = 1

I see, you wanted a LEFT JOIN from Products table into Orders_Products. This will give you result you're looking for.

Related

Mysql subquery in where clause that returns comma separated value

I'm not really good at subqueries, here's the sample tables that I have.
table customers
=====================
id | name | order_ids
1 | John | 1,2
table orders
=====================
id | name
1 | apple
2 | orange
I'm trying to get the order names using this query, but I'm only getting one result. I'm not sure if this is possible.
select o.name
from orders o
where o.id IN(
select c.order_ids
from customers c
where c.id=1
)
Your primary effort should go into fixing your design. You should not be storing several integer values in a string column. If each order belongs to a single customer, then the customer id should be stored in the orders table. If an order may belong to multiple customers at once, then you need a bridge table, with one row per customer/order tuple.
That said: for you current design, you can use find_in_set():
select o.*
from orders o
inner join customers c on find_in_set(o.id, c.order_ids)
where c.id = 1

SELECT using three tables w/ 1000+ entries

For transaction listing I need to provide the following columns:
log_out.timestamp
items.description
log_out.qty
category.name
storage.name
log_out.dnr ( Representing the users id )
Table structure from log_out looks like this:
| id | timestamp | storageid | itemid | qty | categoryid | dnr |
| | | | | | | |
| 1 | ........ | 2 | 23 | 3 | 999 | 123 |
As one could guess, I only store the corresponding ID's from other tables in this table. Note: log_out.id is the primary key in this table.
To get the the corresponding strings, int's or whatever back, I tried two queries.
Approach 1
SELECT i.description, c.name, s.name as sname, l.*
FROM items i, categories c, storages s, log_out l
WHERE l.itemid = i.id AND l.storageid = s.id AND l.categoryid = c.id
ORDER BY l.id DESC
Approach 2
SELECT log_out.id, items.description, storages.name, categories.name AS cat, timestamp, dnr, qty
FROM log_out
INNER JOIN items ON log_out.itemid = items.id
INNER JOIN storages ON log_out.storageid = storages.id
INNER JOIN categories ON log_out.categoryid = categories.id
ORDER BY log_out.id DESC
They both work fine on my developing machine, which has approx 99 dummy transactions stored in log_out. The DB on the main server got something like 1100+ tx stored in the table. And that's where trouble begins. No matter which of these two approaches I run on the main machine, it always returns 0 rows w/o any error *sigh*.
First I thought, it's because the main machine uses MariaDB instead of MySQL. But after I imported the remote's log_out table to my dev-machine, it does the same as the main machine -> return 0 rows w/o error.
You guys got any idea what's going on ?
If the table has the data then it probably has something to do with JOIN and related records in corresponding tables. I would start with log_out table and incrementally add the other tables in the JOIN, e.g.:
SELECT *
FROM log_out;
SELECT *
FROM log_out
INNER JOIN items ON log_out.itemid = items.id;
SELECT *
FROM log_out
INNER JOIN items ON log_out.itemid = items.id
INNER JOIN storages ON log_out.storageid = storages.id;
SELECT *
FROM log_out
INNER JOIN items ON log_out.itemid = items.id
INNER JOIN storages ON log_out.storageid = storages.id
INNER JOIN categories ON log_out.categoryid = categories.id;
I would execute all the queries one by one and see which one results in 0 records. Additional join in that query would be the one with data discrepancy.
You're queries look fine to me, which makes me think that it is probably something unexpected with the data. Most likely the ids in your joins are not maintained right (do all of them have a foreign key constraint?). I would dig around the data, like SELECT COUNT(*) FROM items WHERE id IN (SELECT itemid FROM log_out), etc, and seeing if the returns make sense. Sorry I can't offer more advise, but I would be interested in hearing if the problem is in the data itself.

How to add columns to another table based on one column?

I have my shop database and I want to join two tables together.
id_order | reference | id_shop_group | id_shop | id_carrier | id_lang | id_customer | id_cart
This the header row of my orders table and below is the header of customers table.
id_customer | id_shop_group | id_shop | id_gender | firstname | lastname
What I want to do is to join them based on id_customer column. More specifically I want to add all columns of customers except the ones that are already there to orders table based onid_customer. After joining the tables should look like this:
id_order|reference|id_shop_group|id_shop|id_carrier|id_lang|id_customer|id_cart|id_gender|firstname|lastname
When searching for a solution I found INNER JOIN keyword, but I'm not sure how to use it the way I want.
We don't "Add columns to a table". We, instead, submit SQL to the database that returns the result set that we want. In your case we want to Join the two tables and we can do that using an INNER JOIN on your id_customer field that is common between the two tables. We can turn that into it's own table if you want to hold, permanently, those results. It would look something like
SELECT
orders.id_order,
orders.reference,
orders.id_shop_group,
orders.id_shop,
orders.id_carrier,
orders.id_lang,
orders.id_customer,
orders.id_cart,
customer.id_gender,
customer.firstname,
customer.lastname
FROM orders INNER JOIN customer on orders.id_customer = customer.id_customer;
You can tweak the list of fields to be returned from the joining of these tables to suit your needs.
The fact that id_shop and id_shop_group are in both tables suggests they are part of a composite key. You may need to join using all three shared columns to guarantee unique rows. Otherwise you may retrieve duplicate order rows where the customer belongs to more than one shop.
e.g.
SELECT
...
FROM orders INNER JOIN customer on orders.id_customer = customer.id_customer
and orders.id_shop_group = customer.id_shop_group
and orders.id_shop = customer.id_shop

MySQL Multiple Join with delimiting via FINDINSET

I am attempting to JOIN onto two different columns in the first table below from columns in the second and third tables.
I wish to JOIN users.id to job_listings.id to return users.username, and to also JOIN and delimit job_listings.categories to job_categories.id to return job_categories.description via FIND_IN_SET
job_listings
id | employer_id | categories
1 | 1 | 1,2
2 | 1 | 2
users
id | username | type
1 | foo | employer
2 | wat | employer
job_categories
id | description
1 | fun
2 | hak
I desire output that is of the following format:
output
username | type | category | description
foo | employer | 1 | fun
foo | employer | 2 | hak
foo | employer | 2 | hak
I have tried using various permutations of the following code:
SELECT users.username, users.type, job_listings.categories FROM users
JOIN job_listings ON users.id
JOIN job_listings AS category ON FIND_IN_SET(category.categories, job_categories.id)
ORDER BY users.username, category.categories
I know from other answers that I need to use an alias in order to use multiple JOIN operations with the same table, but despite adapting other answers I keep receiving errors related to declaring an alias, or returning output that has a column with the alias but no data returned in that column.
First, you should normalize your design. You should not store integer values in strings. You should not have foreign key references that you cannot declare as such. You should not store lists in strings. Is that enough reasons? You want a junction table for JobCategories with one row per job and one row per category.
Sometimes, we are stuck with other peoples lousy decisions and cannot readily change them. In that case, you want a query like:
SELECT u.username, u.type, jc.id, jc.category
FROM users u JOIN
job_listings jl
ON u.id = jl.employer_id and u.type = 'employer' join
job_categories jc
ON FIND_IN_SET(jc.id, j.categories) > 0
ORDER BY u.username, jc.category;
This query cannot take advantage of indexes for the category joins. That means that it will be slow. The proper data structure -- a junction table -- would fix this performance problem.

Laravel count categories join

I have three tables that I need to ultimately query from, and I am looking for the most efficient way of going about it.
The tables are: products, product_colors, and categories.
The relationships are products.category_id => categories.id and product_colors.product_id => products.id (filtered by product_colors.color_id)
First I have a product table, and that product has a one to many relationship with a color table, so I need to filter that based on the color.
Second, I need to filter it by the category, and return the amount of products in each category it fits.
I have the following query which will ultimately count the products in each of the categories that I want, but it doesn't filter based on the first filter of colors.
SELECT categories.name, SUM(IF(category_id IS NULL, 0, 1)) AS categories
FROM products JOIN categories ON (categories.id = products.category_id)
WHERE(products.gender = 1) GROUP BY categories.id;
I'm not too familiar with what is efficient or not in terms of queries or joins, but what I was thinking of maybe getting a list of product IDs which match the color being filtered, and then somehow doing a join on just those IDs.
Edit: in my answer below, I've come up with a solution, but it is using DB:raw -- is there any way around that?
I've managed to come up with the following query that will effectively return what I need, with the option of sorting by gender as well.
mysql> SELECT categories.name, SUM(IF(category_id IS NULL, 0, 1)) AS
categories FROM products LEFT JOIN product_colors ON
(product_colors.product_id = products.id) LEFT OUTER JOIN categories
ON (categories.id = products.category_id) WHERE(products.gender = 1 AND
product_colors.color_id = 1) GROUP BY categories.id;
+--------------+------------+
| name | categories |
+--------------+------------+
| トップス | 35 |
| アウター | 1 |
| ボトムス | 7 |
| シューズ | 21 |
| 帽子 | 2 |
+--------------+------------+
5 rows in set (0.00 sec)
Edit: I've rewritten in it Eloquent.
$categories = Category::select('categories.name',DB::raw('SUM(IF(category_id IS NULL, 0, 1)) AS categories'))
->join('products','categories.id','=','products.category_id')
->join('product_colors','product_colors.product_id','=','products.id','left outer')
->where('products.gender','=',1)
->where('product_colors.color_id','=',1)
->groupBy('categories.id')
->get();
The solution over all I feel isn't too bad, but I don't like the fact it is using DB:raw. If anyone has an alternative to that, I would appreciate it.