How to re-key a hierarchy in a table? - sql

I have a two tables
cars: contains hierarchy data about cars
+-----+-------------+-----------+
| id | description | parent_id |
+-----+-------------+-----------+
| 1 | All cars | 1 |
| 30 | Toyota | 1 |
| 34 | Yaris | 30 |
| 65 | Yaris | 30 |
| 87 | Avensis | 30 |
| 45 | Avensis | 30 |
| 143 | Skoda | 1 |
| 199 | Octavia | 143 |
| 12 | Yeti | 143 |
+-----+-------------+-----------+
car_mapping: contains mapping data where duplicate cars (with different ids) are mapped to one id.
+--------+----------+--------+
| car_id | car_name | map_id |
+--------+----------+--------+
| 34 | Yaris | 1 |
| 65 | Yaris | 1 |
| 87 | Avensis | 2 |
| 45 | Avensis | 2 |
| 199 | Octavia | 3 |
| 12 | Yeti | 4 |
| 30 | Toyota | 5 |
| 143 | Skoda | 6 |
| 1 | All cars | 0 |
+--------+----------+--------+
Now, the idea is to create a third table, cars_new, based on cars and car_mapping which removes duplicates and re-keys the hierarchy in the cars table based on the map_id field in the car_mapping table. Here is the resulting cars_new:
+--------+----------+---------------+
| map_id | car_name | parent_map_id |
+--------+----------+---------------+
| 0 | All | 0 |
| 1 | Yaris | 5 |
| 2 | Avensis | 5 |
| 3 | Octavia | 6 |
| 4 | Yeti | 6 |
| 5 | Toyota | 0 |
| 6 | Skoda | 0 |
+--------+----------+---------------+
Here is the SQL Fiddle for this question. Any ideas how to re-key this hiearchy?

select distinct cm.map_id, cm.car_name, cm2.map_id parent_map_id
from cars c, car_mapping cm, car_mapping cm2
where c.id = cm.car_id
and c.parent_id = cm2.car_id(+)
order by cm.map_id;
PS: in your car_mapping table, you need one extra line (first one below) to get exactly the result you want:
+--------+----------+--------+
| car_id | car_name | map_id |
+--------+----------+--------+
| 1 | All | 0 |
| 34 | Yaris | 1 |
| 65 | Yaris | 1 |
Etc..

Based on #Majid LAISSI's accepted answer, this seems to work both in Oracle and SQL Server:
select distinct cm.map_id, cm.car_name, cm2.map_id as parent_map_id
from cars c
left outer join car_mapping cm on c.id = cm.car_id
left outer join car_mapping cm2 on c.parent_id = cm2.car_id
order by cm.map_id;

You don't have a hierarchy, and you're better off not creating one. Observe that your "cars" table doesn't describe cars; it merely assigns a string to a number (and another number to that number). Right from the get-go, "all cars" isn't a car, and "Toyota" is a car manufacturer, not a car.
The solution -- which would help with your uniqueness issue and simplify your queries -- is to use one table for each distinct thing:
manufactures { mfg_id, name } -- e.g. GM, Ford
makes { make_id, name, mfg_id } -- e.g. Chevrolet, Lincoln; links to manufactures
models { name, make_id } -- e.g. Yaris, etc.; links to makes.
Be sure to make "name" unique in each of the tables to prevent spurious IDs from being created.
This will let you assign new attributes to these things as they arise, such as the years they were made or how many were sold, or how many doors each model comes in. It will also let you prevent "relations" of Ford to GM or, say, making Yaris the parent of "all cars".
(BTW, I suggest you eschew "map" or "mapping" in a table name, because it doesn't say anything. Every table relates the elements in the row to each other. Every table maps the key to its values. The good news is that your car_mapping table disappears in the new design.)
As for how to convert the existing cars table, it will be a nuisance. Assuming cars_mapping is right, you'll be able to insert into each table, joining to it and taking the min(id) while grouping by name. You'll need three such queries, followed by some careful eyeballing to check for, er, misalignment.

Related

For each unique entry, include all rows from another list

I have 2 tables as such:
cars: contains price of some parts for each car
| Car | Parts | Price |
| -------- | -------------- | -------|
| A | Windshield | 100 |
| A | Rims | 50 |
| B | Bumper | 200 |
| B | Rims | 60 |
parts: contains all possible parts for a car
| Parts |
|--------------|
| Windshield |
| Rims |
| Bumper |
| Headlights |
I want each car in cars to have every entry in parts. The end result should look like this:
| Car | Parts | Price |
| -------- | -------------- | -------|
| A | Windshield | 100 |
| A | Rims | 50 |
| A | Bumper | 0 |
| A | Headlights | 0 |
| B | Bumper | 200 |
| B | Rims | 60 |
| B | Windshield | 0 |
| B | Headlights | 0 |
Any ideas on how I could do this?
PS: The order matters less
You may use a calendar table approach:
SELECT c.Car, p.Parts, COALESCE(t.Price, 0) AS Price
FROM (SELECT DISTINCT Car FROM cars) c
CROSS JOIN parts p
LEFT JOIN cars t
ON t.Car = c.Car AND t.Parts = p.Parts
ORDER BY c.Car, p.Parts;
But as #Larnu has correctly pointed out in his comment, your schema should have a separate table containing all cars. This would avoid the distinct select I have in my answer above.
If you want to insert the additional rows into cars, then the code would look like:
insert into cars (car, parts, price)
select c.car, p.part, 0
from (select distinct car from cars) c cross join
parts p
where not exists (select 1
from cars c2
where c2.car = c.car and c2.part = c.part
);
This generates all combinations of cars and parts. It then filters out the ones that don't already exist in cars.
Note that left join and not exists are pretty much the same in this context. In an insert query, though, I usually use not exists because I think the intention is clearer.

SQL - joining 3 tables and choosing newest logged entry per id

I got rather complicated riddle to solve. So far I'm unlocky.
I got 3 tables which I need to join to get the result.
Most important is that I need highest h_id per p_id. h_id is uniqe entry in log history. And I need newest one for given point (p_id -> num).
Apart from that I need ext and name as well.
history
+----------------+---------+--------+
| h_id | p_id | str_id |
+----------------+---------+--------+
| 1 | 1 | 11 |
| 2 | 5 | 15 |
| 3 | 5 | 23 |
| 4 | 1 | 62 |
+----------------+---------+--------+
point
+----------------+---------+
| p_id | num |
+----------------+---------+
| 1 | 4564 |
| 5 | 3453 |
+----------------+---------+
street
+----------------+---------+-------------+
| str_id | ext | name |
+----------------+---------+-------------+
| 15 | | Mein st. 33 | - bad name
| 11 | | eck st. 42 | - bad name
| 62 | abc | Main st. 33 |
| 23 | efg | Back st. 42 |
+----------------+---------+-------------+
EXPECTED RESULT
+----------------+---------+-------------+-----+
| num | ext | name |h_id |
+----------------+---------+-------------+-----+
| 3453 | efg | Back st. 42 | 3 |
| 4564 | abc | Main st. 33 | 4 |
+----------------+---------+-------------+-----+
I'm using Oracle SQL. Tried using query below but result is not true.
SELECT num, max(name), max(ext), MAX(h_id) maxm FROM history
INNER JOIN street on street.str_id = history._str_id
INNER JOIN point on point.p_id = history.p_id
GROUP BY point.num
In Oracle, you can use keep:
SELECT p.num,
MAX(h.h_id) as maxm,
MAX(s.name) KEEP (DENSE_RANK FIRST ORDER BY h.h_id DESC) as name,
MAX(s.ext) KEEP (DENSE_RANK FIRST ORDER BY h.h_id DESC) as ext
FROM history h INNER JOIN
street s
ON s.str_id = h._str_id INNER JOIN
point p
ON p.p_id = h.p_id
GROUP BY p.num;
The keep syntax allows you to do "first()" and "last()" for aggregations.

Select all rows where rows in another joined table match condition

So I want to select all rows where a subset of rows in another table match the given values.
I have following tables:
Main Profile:
+----+--------+---------------+---------+
| id | name | subprofile_id | version |
+----+--------+---------------+---------+
| 1 | Main 1 | 4 | 1 |
| 2 | Main 1 | 5 | 2 |
| 3 | Main 2 | ... | 1 |
+----+--------+---------------+---------+
Sub Profile:
+---------------+----------+
| subprofile_id | block_id |
+---------------+----------+
| 4 | 6 |
| 4 | 7 |
| 5 | 8 |
| 5 | 9 |
+---------------+----------+
Block:
+----------+-------------+
| block_id | property_id |
+----------+-------------+
| 7 | 10 |
| 7 | 11 |
| 7 | 12 |
| 7 | 13 |
| 8 | 14 |
| 8 | 15 |
| 8 | 16 |
| 8 | 17 |
| ... | ... |
+----------+-------------+
Property:
+----+--------------------+--------------------------+
| id | name | value |
+----+--------------------+--------------------------+
| 10 | Description | XY |
| 11 | Responsible person | Mr. Smith |
| 12 | ... | ... |
| 13 | ... | ... |
| 14 | Description | XY |
| 15 | Responsible person | Mrs. Brown |
| 16 | ... | ... |
| 17 | ... | ... |
+----+--------------------+--------------------------+
The user can define multiple conditions on the property table. For example:
Description = 'XY'
Responsible person = 'Mr. Smith'
I need all 'Main Profiles' with the highest version which have ALL matching properties and can have more of course which do not match.
It should be doable in JPA because i would translate it into QueryDSL to build typesafe, dynamic queries with the users input.
I already searched trough all questions regarding similar problems but couldn't project the answer onto my problem.
Also, I've already tried to write a query which worked quite good but retrieved all rows with at least one matching condition. Therefore i need all properties in my set but it only fetched (fetch join, which is missing in my code examplte) the matching ones.
from MainProfile as mainProfile
left join mainProfile.subProfile as subProfile
left join subProfile.blocks as block
left join block.properties as property
where mainProfile.version = (select max(mainProfile2.version)from MainProfile as mainProfile2 where mainProfile2.name = mainProfile.name) and ((property.name = 'Description' and property.value = 'XY') or (property.name = 'Responsible person' and property.value = 'Mr. Smith'))
Running my query i got two rows:
Main 1 with version 2
Main 2 with version 1
I would have expected to get only one row due to mismatch of 'responsible person' in 'Main 2'
EDIT 1:
So I found a solution which works but could be improved:
select distinct mainProfile
from MainProfile as mainProfile
left join mainProfile.subProfile as subProfile
left join subProfile.blocks as block
left join block.properties as property
where mainProfile.version = (select max(mainProfile2.version)from MainProfile mainProfile2 where mainProfile2.name = mainProfile.name)
and ((property.name = 'Description' and property.content = 'XY') or (property.name = 'Responsible person' and property.content = 'Mr. Smith'))
group by mainProfile.id
having count (distinct property) = 2
It actually retrieves the right 'Main Profiles'. But the problem is, that only the two found properties are getting fetched. I need all properties though because of further processing.

SQL: Transfer from column to row, size systems, fashion

I'm struggling, hope you can help me out! The application is an ERP system in MS ACCESS for fashion retailing.
The question: How can I add a row in Table3 with the article's corresponding SizeID1, SizeID2 etc filled according to it's size system of Table2?
Table1: Article details containing size system
ArticleID | SizeType
--------------------
1 | US
2 | EU
Table2: Different size systems for different regions
SizeID | Size | SizeType
------------------------
1 | S | US
2 | M | US
3 | L | US
4 | XL | US
5 | 36 | EU
6 | 38 | EU
7 | 40 | EU
Table3: Order details
OrderID | ArticleID | Size1 | Amount1 | Size2 | Amount2 | Size3 | Amount3
-------------------------------------------------------------------------
1 | 1 | S | 1 | M | 3 | L | 1
2 | 2 | 36 | 2 | 38 | 1 | 40 | 3
3 | 2 | 36 | | 38 | | 40 |
The row with OrderID = 3 is the goal of the insery query for ArticleID 2. I can then enter the amount to the corresponding size. Thanks for your help!!
It's not entirely clear what you're trying to achieve here, but I think that might be because your database design is a bit broken.
You're probably used to working with spreadsheets, becase you seem to have designed your Table3 as if it were a spreadsheet. Any time you end up with repeating fields in a database table (eg. Size1, Size2, Size3....) then it's a sign that you need to normalise more.
Please read up on database normalisation: http://www.studytonight.com/dbms/database-normalization.php (and also google generally for database normalisation for more info).
I suspect you'll need something more like this:
Article table:
ArticleID | ArticleName | SizeType(FK to SizeType table)
1 | Blue T-shirt | 1
2 | Red T-shirt | 2
SizeType table:
SizeTypeID | SizeTypeDescription
1 | US
2 | EU
Sizes table:
SizeID | SizeType(FK) | SizeDescription
1 | 1 | 38
2 | 1 | 40
3 | 2 | M
4 | 2 | L
Items table:
ItemID | ArticleID(FK) | SizeID(FK)
1 | 1 | 1 'Blue T-shirt size 38
2 | 1 | 2 'Blue T-shirt size 40
3 | 2 | 3 'Red T-shirt size M
4 | 2 | 4 'Red T-shirt size L
Orders table:
OrderID | CustomerID (FK) | OrderDate | etc. (other info you need to store about an order)
1 | 2458 | 01/01/2001|
2 | 3452 | 02/02/2002|
Order Details table:
OrderDetailsID | OrderID(FK) | ItemID (FK) | Quantity
1 | 1 | 3 | 6
2 | 1 | 4 | 3
3 | 2 | 1 | 1
So here linking everything back, Customer with ID 2458 has ordered 6x Size M Red T-shirts and 3x Size L Red T-shirts, and Customer with ID 3452 has ordered 1x size 38 Blue T-shirt.
Note You'd also realistically have a PRICE field in either the Items table (if the price varies with the size of an item) or in the Article table (if the price is the same for every size). CustomerID would also link back to a table containing a customer's details (Name, address, username/password etc).
You might also have other things that I've not shown here, for example in either your Articles table or your Items table you'll probably have a supplierID which tells you who you buy that item from, as well as a supplierItemNo which would be a unique code (barcode?) which you use to order that item.
Once your database is structured properly it will be much easier to write queries to insert records etc.

Create a pivot table from two tables based on dates

I have two MS Access tables sharing a one to many relationship. Their structures are like the following:
tbl_Persons
+----------+------------+-----------+
| PersonID | PersonName | OtherData |
+----------+------------+-----------+
| 1 | PersonA | etc. |
| 2 | PersonB | |
| 3 | PersonC | |
tbl_Visits
+----------+------------+------------+-----------------------
| VisitID | PersonID | VisitDate | dozens of other fields
+----------+------------+------------+-----------
| 1 | 1 | 09/01/13 |
| 2 | 1 | 09/02/13 |
| 3 | 2 | 09/03/13 |
| 4 | 2 | 09/04/13 | etc...
I wish to create a new table based on the VisitDate field, the column headings of which are Visit-n where n is 1 to the number of visits, Visit-n-Data1, Visit-n-Data2, Visit-n-Data3 etc.
MergedTable
+----------+----------+---------------+-----------------+----------+----------------+
| PersonID | Visit1 | Visit1Data1 | Visit1Data2... | Visit2 | Visit2Data1... |
+----------+----------+---------------+-----------
| 1 | 09/01/13 | | | 09/02/13 |
| 2 | 09/03/13 | | | 09/04/13 |
| 3 | etc. | |
I am really not sure how to do this. Whether SQL query or using DAO then looping through records and columns. It is essential that there is only 1 PersonID per row and all his data appears chronologically into columns.
Start of by ranking the visits with something like
SELECT PersonID, VisitID,
(SELECT COUNT(VisitID) FROM tbl_Visits AS C
WHERE C.PersonID = tbl_Visits.PersonID
AND C.VisitDate < tbl_Visits.VisitDate) AS RankNumber
FROM tbl_Visits
Use this query as a base for the 'pivot'
Since you seem to have some visits of persons on the same day (visit 1 and 2) the WHERE clause needs to be a bit more sophisticated. But I hope you get the basic concept.
Pivoting can be done with multiple LEFT JOINs.
I question if my solution will have a high performance, since I did not test it. It is easier in SQL Server than in MS Access to accomplish.