In my MySQL database I have a table of products along the lines of
id | type | name | weight | base_price | [...]
where id is a primary key and type is an ENUM('personal','industrial'). Personal and industrial products both have some additional information stored in products_personal and products_industrial tables. Both are of the form
pid | additional info [...]
where pid is foreign keyed with products.id and additional info is different for products_personal and products_industrial. In my table I have two functions (through CREATE FUNCTION), PRICE_PERSONAL(...) and PRICE_INDUSTRIAL(...). These functions use the base_price and some of the additional info to compute a final price.
I wish to create a view of id | type | name | [...] | price for all of my products. My current candidate is
CREATE VIEW foo AS
SELECT id, type, name, [...], PRICE_PERSONAL(params) AS price
FROM products
INNER JOIN products_personal ON products.id = products_personal.pid
UNION
SELECT id, type, name, [...], PRICE_INDUSTRIAL(params) AS price
FROM products
INNER JOIN products_industrial ON products.id = products_industrial.pid
But this is a but bulky and seems to result in poor ORDER BY performance (as it needs to sort the un-indexed UNION results).
Is there a cleaner table or query structure to accomplish this type of query?
Didn't actually test it, but try something like this:
CREATE VIEW foo AS
SELECT id, type, name, [...], PRICE_PERSONAL(params) AS price
FROM products
INNER JOIN products_personal ON products.id = products_personal.pid
WHERE type = 'personal'
UNION ALL
SELECT id, type, name, [...], PRICE_INDUSTRIAL(params) AS price
FROM products
INNER JOIN products_industrial ON products.id = products_industrial.pid
WHERE type = 'industrial'
The idea is to cut-off "wrong" rows early by filtering on products.type. Also, I should give credit to #ypercube for spotting UNION ALL.
What are the fields under ORDER BY? They should probably be indexed.
Related
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
I have the following data structure (simplified), which includes a polymorphic association in the purchases table (MySql)
Purchases
product_type (this refers to a table, book, dvd, shirt,...)
product_id (this refers to the product's id in the table in product_type)
amount
Books
category_id
DVDs
category_id
Shirts
category_id
Categories
name
I would like to select categories from the db, ordered by total sales (purchases.amount). How can I do this with joins and aggregate functions?
I made a sqlfiddle for this:
http://sqlfiddle.com/#!2/74705
Polymorphic IDs are similar to inheritance structures in a DB, in that to query across all of them often you have to use a union.
However, if your Books/Shirts/DVDs tables have no common columns, it makes absolutely no sense to query across them. If they did have common columns, probably makes more sense to pull that out into a parent Product table that acts as a base for the others.
If you for example wanted total sales per product, you'd just do
Select sum(amount), product_id -- or product_type if you wanted sales per type
From Purchases
Group By product_id -- product_type
You could pull in product titles like this:
Select sum(p.amount), p.product_id, b.Title
From Purchases p
Inner Join Books b on b.category_id = p.product_id
Group By product_id
Union
Select sum(p.amount), p.product_id, s.Title
From Purchases p
Inner Join Shirts s on s.category_id = p.product_id
Group By product_id
It might be more performant to do the union first, then the group by outside of that.
My experience with inheritance hierarchies is that you should do whatever you can to avoid unions. They are a pain to query, result in non-DRY queries, and perform badly. This means pulling out commonalities such as Title into a proper normalized structure to avoid having to query across the concrete/derived types. This is essentially what Wrikken mention in comments "Usually, a better way for this would be a Products table with the shared information types"
Here's an example of how you could achieve this using temporary tables. The principle is that you create a "temporary" table and populate it with the data that you want, in the structure that you want, when your query is run. You can modify the data, and return it at the end and the table is then destroyed when the connection closes.
Anyway - you can set up your temporary table like with the schema combining a new unique, product id, product type, product name and category id. You then populate the table with a union query between all the records in your books, dvds and shirts tables to suit that format:
-- delete the temp table in case it's still there
DROP TEMPORARY TABLE IF EXISTS all_products;
-- create your new temp table
CREATE TEMPORARY TABLE IF NOT EXISTS all_products (
new_id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
product_id INT(11) NOT NULL,
product_type VARCHAR(50) NOT NULL,
product_name VARCHAR(128) NOT NULL,
category_id INT(11) NOT NULL
) AS (
-- populate it with unioned queries
SELECT * FROM (
(SELECT NULL AS new_id, id AS product_id, 'book' AS product_type, `name` AS product_name, category_id FROM books)
UNION ALL
(SELECT NULL AS new_id, id AS product_id, 'dvd' AS product_type, `name` AS product_name, category_id FROM dvds)
UNION ALL
(SELECT NULL AS new_id, id AS product_id, 'shirt' AS product_type, `name` AS product_name, category_id FROM shirts)
) AS temp -- an alias is required - doesn't matter what it is
);
Now you can access the data in this table as you normally would. The benefit of using temporary tables is that you don't have to change your database structure - in your case it's maybe not ideal to have it set up the way it is, but doing something like this could help you to select data efficiently without changing it.
To select data, you could use a query like this:
SELECT
purchase.*,
product.product_name,
category.name
FROM purchases AS purchase
-- get your product info from the temp table
INNER JOIN all_products AS product
ON (product.product_id = purchase.product_id AND product.product_type = purchase.product_type)
-- get the category name
INNER JOIN categories AS category
ON (category.id = product.category_id)
You said you want to order your results by amount, etc. Add your conditions and ordering to the query above as you want to.
Example output:
id product_id product_type amount product_name name
1 1 book 10 how to educational
2 1 dvd 10 video how to educational
3 1 shirt 10 tshirt clothes
These can actually be very efficient to run, for example running this on my local server is executing in 0.001s - it will scale very nicely. I would've set you up a fiddle for this, but SQLFiddle doesn't seem to support temporary tables very well. Just run the CREATE and SELECT statements at the same time on your local server to see.
I'm having trouble figuring out how to structure a SQL query. Let's say we have a User table and a Pet table. Each user can have many pets and Pet has a breed column.
User:
id | name
______|________________
1 | Foo
2 | Bar
Pet:
id | owner_id | name | breed |
______|________________|____________|_____________|
1 | 1 | Fido | poodle |
2 | 2 | Fluffy | siamese |
The end goal is to provide a query that will give me all the pets for each user that match the given where clause while allowing sort and limit parameters to be used. So the ability to limit each user's pets to say 5 and sorted by name.
I'm working on building these queries dynamically for an ORM so I need a solution that works in MySQL and Postgresql (though it can be two different queries).
I've tried something like this which doesn't work:
SELECT "user"."id", "user"."name", "pet"."id", "pet"."owner_id", "pet"."name",
"pet"."breed"
FROM "user"
LEFT JOIN "pet" ON "user"."id" = "pet"."owner_id"
WHERE "pet"."id" IN
(SELECT "pet"."id" FROM "pet" WHERE "pet"."breed" = 'poodle' LIMIT 5)
In Postgres (8.4 or later), use the window function row_number() in a subquery:
SELECT user_id, user_name, pet_id, owner_id, pet_name, breed
FROM (
SELECT u.id AS user_id, u.name AS user_name
, p.id AS pet_id, owner_id, p.name AS pet_name, breed
, row_number() OVER (PARTITION BY u.id ORDER BY p.name, pet_id) AS rn
FROM "user" u
LEFT JOIN pet p ON p.owner_id = u.id
AND p.breed = 'poodle'
) sub
WHERE rn <= 5
ORDER BY user_name, user_id, pet_name, pet_id;
When using a LEFT JOIN, you can't combine that with WHERE conditions on the left table. That forcibly converts the LEFT JOIN to a plain [INNER] JOIN (and possibly removes rows from the result you did not want removed). Pull such conditions up into the join clause.
The way I have it, users without pets are included in the result - as opposed to your query stub.
The additional id columns in the ORDER BY clauses are supposed to break possible ties between non-unique names.
Never use a reserved word like user as identifier.
Work on your naming convention. id or name are terrible, non-descriptive choices, even if some ORMs suggest this nonsense. As you can see in the query, it leads to complications when joining a couple of tables, which is what you do in SQL.
Should be something like pet_id, pet, user_id, username etc. to begin with.
With a proper naming convention we could just SELECT * in the subquery.
MySQL does not support window functions, there are fidgety substitutes ...
SELECT user.id, user.name, pet.id, pet.name, pet.breed, pet.owner_id,
SUBSTRING_INDEX(group_concat(pet.owner_id order by pet.owner_id DESC), ',', 5)
FROM user
LEFT JOIN pet on user.id = pet.owner_id GROUP BY user.id
Above is rough/untested, but this source has exactly what you need, see step 4. also you don't need any of those " 's.
This is my scenario. I have a table of events with a field type, with values 1 = food, 2 = recipe. Simplifying, my events table have this structure:
id | entity_id | user_id | type | timestamp | field1 | ... field n
Field "entity_id" refers to a unique autoincremental value from "Entities" table. Food and recipe table structure is very similar, with entity_id and user_id fields.
What I want is to get all the common data from the table events of last 10 registers, and fetch some needed fields of corresponding table based on type value of table events. By now I have achieved some quite similar, but not exactly what I want, with this query:
SELECT a.*, b.name, b.field1, b.field2, c.name, c.field1, c.field2
FROM events a
LEFT JOIN foods b ON b.entity_id = a.entity_id
LEFT JOIN recipes c ON c.entity_id = a.entity_id
ORDER BY timestamp DESC LIMIT 10
This allways returns all fields for all tables, with NULL values when the field is not of the type of this specific register.
So I want to get all fields of events table, and name, field1, field2 of the corresponding table.
EDIT:
Here is the sqlfiddle sqlfiddle.com/#!2/18d45/9 I'd like the query returned different field values based on the table. In the example table recipes has description field while foods not. Is it possible?
Please helpe me with this!
You might use a COALESCE to get the first not NULL column:
SELECT a.*,
COALESCE(b.name, c.name),
COALESCE(b.field1, c.field1),
COALESCE(b.field2, c.field2)
FROM events a
...
I'm trying to add a column to a Mysql result using a subquery,
Suppose i have a table 'Cars' and a table 'Cars usage'
Table Cars has a column "serial_number" and the Cars usage table has a column "serial_number" as well:
So i wanna generate a result with the car name as first column and car usage as second:
Table cars:
-model
-serial_number
Table carsusage
-date
-serial_number
What i would like to achieve would look like so:
Model | Usage count
--------------------
|bar | 1500 |
|foo | 700 |
Ideally i need a sub-query, that, while querying the cars table, spits a query to the Cars usage table counting:
SELECT model, serial_number AS sn,(SELECT COUNT(serial_number) FROM cars_usage WHERE serial_number=sn) FROM cars;
So basically i would like to use the serial_number AS sn as a local variable and add the result of the count operation as second column.
Any advice?
No that much experience with querying a db here.
Thx
If you want usage per serial number:
select model, serial_number, count(*) from cars inner join carsusage on cars.serial_number=carsusage.serial_number where cars.serial_number=$var group by 1,2
or if you want usage per model:
select model, count(*) from cars inner join carsusage on cars.serial_number=carsusage.serial_number where cars.serial_number=$var group by 1
Why not just try something like
SELECT model,
serial_number AS sn,
(SELECT COUNT(serial_number) FROM cars_usage cu WHERE cu.serial_number=c.serial_number)
FROM cars c;
Where you provide aliases for the tables, and then prefix the fields with the aliases.
You could also try a LEFT JOIN (do be carefull of inner join, because if there a no usages, it will not return a 0 entry for that car).
select c.model,
c.serialnumber sn,
count(cu.*)
from cars c LEFT join
carsusage cu on c.serial_number=cu.serial_number
GROUP BY c.model,
c.serialnumber