SQL Conditional relationships - sql

I've come up with two approaches to the same idea and would like to avoid any obvious pitfalls by using one over the other. I have a table (tbl_post) where a single row can have many relationships to other tables (tbl_category, tbl_site, tbl_team). I have a relationship table to join these but don't know which structure to go with, conditional or direct? Hopefully the following will explain...
tbl_post (simple post, can be associated with many categories, teams and sites)
* id
* title
* content
tbl_category
* id
* title
* other category only columns
tbl_team
* id
* title
* other team only columns
tbl_site
* id
* title
* other site only columns
----------------------------------------------------------
tbl_post_relationship
* id (pk)
* post_id (fk tbl_post)
* related_id (fk, dependant on related_type to either tbl_category, tbl_site or tbl_team)
* related_type (category, site or team)
____________________________________
|id|post_id|related_id|related_type|
|--|-------|----------|------------|
| 1| 1| 6| category|
| 2| 1| 4| site|
| 3| 1| 9| category|
| 4| 1| 3| team|
------------------------------------
SELECT c.*
FROM tbl_category c
JOIN tbl_relationship r ON
r.post_id = 1
AND r.related_type = 'category'
AND c.id = r.related_id
------------- OR ---------------
tbl_post_relationship
* id (pk)
* post_id (fk tbl_post)
* category_id (fk tbl_category)
* site_id (fk tbl_site)
* team_id (fk tbl_team)
________________________________________
|id|post_id|category_id|site_id|team_id|
|--|-------|-----------|-------|-------|
| 1| 1| 6| NULL| NULL|
| 2| 1| NULL| 4| NULL|
| 3| 1| 9| NULL| NULL|
| 4| 1| NULL| NULL| 3|
----------------------------------------
SELECT c.*
FROM tbl_category c
JOIN tbl_relationship r ON
r.post_id = 1
AND r.category_id = c.id
So with the one approach I'll end up with lots of columns (there might be more tables) with NULL's. Or I end up with one simple table to maintain it, but every join is based on a "type". I also know I could have a table per relationship, but again that feels like too many tables. Any ideas / thoughts?

You are best out with one table per relationship. You should not worry about the amount of tables. The drawbacks of a single relationship table are several, and quite risky:
1) You cannot enforce foreign keys if the related tables vary from row to row, so your data integrity is at risk... and sooner or later you will have orphaned data.
2) Queries are more complex because you have to use the related_type to filter out the relations in many places.
3) Query maintenance is more costly, for the same reasons of 2), and because you have to explicitly use the related_type constants in many places... it'll be hell when you need to change them or add some.
I'd suggest you use the orthodox design... just got with 3 distinct relationship tables: post_category, post_team, post_site.

Related

SQL: Merge localized version of a table to the main one

Imagine I have a main table like:
Table guys
|id| name|profession|
|--|------|----------|
| 1| John| developer|
| 2| Mike| boss|
| 3| Roger| fireman|
| 4| Bob| policeman|
I also have a localized version which is not complete (the boss is missing):
Table guys_bg
|id| name | profession|
|--|------|-----------|
| 1| Джон|разработчик|
| 3|Роджър| пожарникар|
| 4| Боб| полицай|
I want to prioritize guys_bg results while still showing all the guys (The boss is still a guy, right?).
This is the desired result:
|id| name | profession|
|--|------|-----------|
| 1| Джон|разработчик|
| 2| Mike| boss|
| 3|Роджър| пожарникар|
| 4| Боб| полицай|
Take into consideration that both tables may have a lot of (100+) columns so joining the tables and using CASE for every column will be very tedious.
What are my options?
Here is one way using union all:
select gb.*
from guys_bg gb
union all
select g.*
from guys g
where not exists (select 1 from guys_bg gb where gb.id = g.id);
You can also make it with using FULL JOIN.
SELECT
ISNULL(b.id,g.id) id
, ISNULL(b.name, g.name) name
, ISNULL(b.profession, g.profession) profession
FROM
guys g
FULL JOIN guys_bg b ON g.id = b.id

Taking the most distant value from the root in a hierarchy

I'm trying to model a hierarchy of "settings", where values are defined at a root level and can be overridden by more specific children. If a child does not specify a value, its parents value should be used.
I'll use a trite example to illustrate the problem. The hierarchy here is only three levels deep, but I would like a solution that works for N levels.
Given that I am storing the information as follows:
id| parent_id| setting
----------------------
1| NULL| false
2| 1| true
3| 2| NULL
What I want, from a procedural perspective, is to get a child node in a tree and if its "settings" value is NULL look at its parent for a value, recursively, until a value is found, or the root is reached. Essentially, for the information provided, I want to produce the following set, so I can attach a simple WHERE clause to get the applicable settings for any given id.
id| setting
-----------
1| false
2| true
3| true
I've have a view which "flattens" the hierarchy into ancestors and descendants:
ancestor| descendant| ancestor_setting| descendant_setting
----------------------------------------------------------
1| 2| false| true
1| 3| false| NULL
2| 3| true| NULL
NULL| 1| NULL| false
NULL| 2| NULL| true
NULL| 3| NULL| NULL
In this way, you can query all levels of the hierarchy as a set, which I hoped would be useful in getting an answer.
So far, I've only been able to select a "branch" from the tree using this view:
SELECT COALESCE(ancestor, descendent) id,
CASE WHEN ancestor IS NULL THEN descendant_setting
ELSE ancestor_setting
END setting
FROM hierarchy
WHERE descendant = 3
id| setting
-----------
1| false
2| true
3| NULL
I've tried to think up ways to use this "flattened" structure to form a simple set of joins, and whilst I can get all the records back this way (and then filter them procedurally on a client), I want to know if there's a way to produce the expected set so that I could get back the expected settings for a single ID.
WITH RECURSIVE
q AS
(
SELECT id, parent_id, id ancestor_id, setting
FROM mytable
WHERE parent_id IS NULL
UNION ALL
SELECT m.id, q.id, ancestor_id, COALESCE(m.setting, q.setting)
FROM q
LEFT JOIN
mytable m
ON m.parent_id = q.id
WHERE q.id IS NOT NULL
)
SELECT *
FROM q
WHERE id IS NULL

Finding the intersection of tables that use a many-to-many relationship in SQL

I need to get an intersection of two tables that uses two many-to-many tables to relate each other. Example tables as follows:
**Discount** **DiscountRef** **ProductCat** **Product**
|DisId| Discount|Amount| |DisId|RefType|RefId|IsActive| |ProdId|CatId| |ProdId| ProdName|ProdPrice|
+-----+---------+------+ +-----+-------+-----+--------+ +------+-----+ +------+--------------+---------+
| 1| 2% Off| 0.02| | 1|Product| 9004| 0| | 9001| 3456| | 9001| 9" Nail| 0.50|
| 2| 10% Off| 0.10| | 2|Product| 9002| 0| | 9002| 3456| | 9002| 2"x4" Stud| 2.50|
| 3| 25% Off| 0.25| | 2| PCat| 3456| 1| | 9005| 3456| | 9003| Claw Hammer| 5.99|
| 4| 2 for 1| 0.50| | 3| PCat| 7346| 1| | 9001| 7346| | 9004| Wood Glue| 1.20|
| 5|Clearance| 0.75| | 3| PCat| 4455| 1| | 9003| 7346| | 9005|6'x4' Dry Wall| 10.39|
| 5|Product| 9004| 0| | 9003| 4455| | 9006| Screwdriver| 4.25|
| 9006| 4455|
With these tables I need to get the intersection of Product Categories if there under the same Discount Id. The below table is what I need to get:
|DisId|ProdId|DisPrice|
+-----+------+--------+
| 2| 9001| 0.45|
| 2| 9002| 2.25|
| 2| 9005| 9.36|
| 3| 9003| 4.50|
I have tried a few different ways but can't seem to get to that table. The below SQL returns me the discounts that have more then one category applied to it.
SELECT DR.DisId, PC.CatId
FROM DiscountRef DR
INNER JOIN (
SELECT DisId
FROM DiscountRef
GROUP BY DisId
HAVING COUNT(DisId) > 1
) SDR ON SDR.DisId = DR.DisId
INNER JOIN ProductCat PC ON PC.CatId = DR.RefId AND DR.RefType = 'PCat'
GROUP BY DR.DisId, PC.CateId
Table Returned:
|DisId|CatId|
+-----+-----+
| 3| 7346|
| 3| 4455|
Then using the Product Categories Id's with an intersect of Product tables I get the correct amount of product Ids.
SELECT P1.ProdId
FROM Product P1
INNER JOIN ProdCat PC1 ON PC1.ProdId = P1.ProdId AND PC1.CategoryId = 7346
INTERSECT
SELECT P2.ProdId
FROM Product P2
INNER JOIN ProdCat PC2 ON PC2.ProdId = P2.ProdId AND PC2.CategoryId = 4455
Also a discount can have more then two categories (Narrows down the number of products), and some times there's more then one discount active (discount data is omitted for this but a check will be done).
Any help on how I can get my desired table above?
EDIT: If there are multiple DisIds on the DiscountRef table and they happen to be the PCat type they are products that shared in all the categories. Like how Claw Hammer is the only item that appears in both CatId 7346 AND CatId 4455.

Retrieving rows that share multiple ID's in SQL

I am stuck on how to narrow down a selection of rows that are related by multiple ID's. Here is my problem with the data as follows:
|Widget | |Widget Category | |Part Category | |Part |
+---------+ +--------------------+ +--------------+ +-------------+
|Id|Name | |WidId|CatId|CatName | |PartId| CatId | |Id|Name |
+---------+ +-----+-----+--------+ +------+-------+ +--+----------+
| 1|item01| | 1| 1|Windows | | 1| 1| | 1|Glass |
| 2|item02| | 2| 1|Windows | | 1| 2| | 2|Door Frame|
| 3|item03| | 3| 1|Windows | | 2| 2| | 3|Wheel |
| 4|item04| | 1| 2|Door | | 4| 2| | 4|Handle |
| 5|item05| | 5| 2|Door |
| 6|item06| | 6| 3|Trunk |
One or more widgets can be in a Widget Category. Many widget categories can have many part Categories. Many Parts can be part of many part categories. I need to know what Parts are linked to what Widgets. So we know that Item01 has parts "Glass" and Item05 has Parts "Glass, Door Frame, and Handle".
Here is my SQL I have so far but I need it to be dynamic so it can run once a week on a stored procedure.
---- This gives me the Correct number of Widgets to Parts based on set of 2 category ID's as a quick and static hack
SELECT W.Id
FROM Widget W
INNER JOIN dbo.[WidgetCategory] WC1 ON WC1.WidId = W.Id
INNER JOIN dbo.[WidgetCategory] WC2 ON WC2.WidId = W.Id
WHERE WC1.CatId = 1 AND WC2.CatId = 2
GROUP BY W.Id
The reason for the above query is to get a table structure that is grouped by PartId's to WidgetId's as an intersection of the two related categories and all the widgets that are related to parts. The below table is what I am trying to get so that I can aggregate how many widgets are in a part (COUNT(WidId) GROUP BY PartId):
|WidId|PartId|WidgetName|
+-----+------+----------+
| 1| 1| Item01|
| 2| 1| Item02|
| 3| 1| Item03|
| 1| 2| Item01|
| 5| 2| Item05|
Updated question: How can I get this response from the tables above with only returning the intersection of the two categories?
|WidId|PartId|WidgetName|
+-----+------+----------+
| 1| 1| Item01|
| 1| 2| Item01|
Any help would be greatly appreciated! Sorry for the sloppiness, had to post quickly before I left for weekend.
EDIT: Sorry, about the ProductId, was left over from some SQL that I was using. Should be Widget Id. Added more clarity to the problem and added an addition problem I was having.
I think you need a query like this.
SELECT DISTINCT w.WidId, p.ParId, w.Name
FROM Widget w
JOIN WidgetCategory wc ON wc.WidId=w.Id
JOIN PartCategory pc ON pc.CatId=wc.CatId
JOIN Part p ON p.Id=pc.ParId
I don't see why you would need to join twice on the WidgetCategory table. What you need is to reach the Part table by joining the PartCategory table.
And why are you grouping? If you want all the parts, then you can't group, unless you use some specific SQL feature to concatenate all the parts in a single row. This may or may not be possible, depending on which database engine you are using.
I added the DISTINCT, just in case you have more than one ways to get from Widget X to Part Y... that is enough to remove duplicates. There is no need for a GROUP BY unless you need to COUNT or do something else with the aggregation.

SQL needed to get most popular product based also off a quantity

I'm currently trying to get the most popular productID from my MSSQL Database. This is what the table looks like (With a bit of dummy data):
OrderItems:
+--+-------+--------+---------+
|ID|OrderID|Quantity|ProductID|
+--+-------+--------+---------+
| 1| 1| 1| 1|
| 2| 1| 1| 2|
| 3| 2| 1| 1|
| 4| 2| 50| 2|
The OrderID field can be ignored, but I need to find the most popular ProductID's from this table, ordering them by how often they occur. The results set should look something like this:
+--------+
|PoductID|
+--------+
| 2|
| 1|
As ProductID 2 has a total quantity of 51, it needs to come out first, followed by ProductID 1 which only has a total quantity of 2.
(Note: Query needs to be compatible back to MSSQL-2008)
SELECT
productID
FROM
yourTable
GROUP BY
productID
ORDER BY
SUM(Quantity) DESC
GROUP BY allows SUM(), but you don't have to use it in the SELECT to be allowed to use it in the ORDER BY.
select ProductID
from OrderItems
group by ProductId
order by sum(Quantity) desc;