How to design db for holding Dominion cards? - sql

I'd like to store some information about games of the card game Dominion. You don't need to know much about this game, except that:
There are around 200 unique cards
Each game includes only ten of these cards, or on occasion eleven
I'll be tracking lots more about each game (who played, who won, etc), but what I'm having trouble with is working with the "supply" (the ten included cards for this game).
I'm thinking I want three tables, something like card_name, supply, and game:
card_name supply game
id | name supply | card game | supply | player1 | player2 | ...
----+--------- --------+------ ------+--------+---------+---------+-----
1 | Village 1 | 1 301 | 1 | 'Mike' | 'Tina' | ...
2 | Moat 1 | 3
3 | Witch 1 | 200
... | ... ... | ...
200 | Armory
I think this is a reasonable way to represent "Mike and Tina played a game which contained Village, Witch, Armory, and some other cards I didn't bother typing into this example". Given this structure (or some other one, if you think mine is no good), I'd like to run queries like "which games had Witch and Village, but not Moat?" That is, I want to specify some arbitrary number of "these X cards included, these Y cards excluded" and search the game table for games satisfying the criteria.
I think this is a classic one-to-many relation, where a supply has multiple cards, but I don't understand the right way to search for a supply by multiple cards.

Your data structure is reasonable. I might suggest that you would want a game_users table as well, so the users are not listed in separate columns. This would be particularly important if games had different numbers of users. However, this aspect is not relevant to your question.
You want to solve "set-within-sets" subqueries. Your structure is useful and the supply table provides the basic information needed for this.
So, a query to get the appropriate "supply" records for "Witch", "Village", and not "Moat" would look like:
select supplyid
from supplies s join
cards c
on s.cardid = c.cardid
group by supplyid
having sum(case when cardname = 'Witch' then 1 else 0 end) > 0 and
sum(case when cardname = 'Village' then 1 else 0 end) > 0 and
sum(case when cardname = 'Moat' then 1 else 0 end) = 0;
First note that I changed the name, so the id columns contain the word "id" and the table names are in plural.
Each condition in the having clause is representing one condition on the cards. You can tweak this to get the game information by joining in games:
select g.gameid
from supplies s join
cards c
on s.cardid = c.cardid join
games g
on g.supplyid = s.gameid
group by g.gameid
having sum(case when cardname = 'Witch' then 1 else 0 end) > 0 and
sum(case when cardname = 'Village' then 1 else 0 end) > 0 and
sum(case when cardname = 'Moat' then 1 else 0 end) = 0;

Related

Case statement logic and substring

Say I have the following data:
Passes
ID | Pass_code
-----------------
100 | 2xBronze
101 | 1xGold
102 | 1xSilver
103 | 2xSteel
Passengers
ID | Passengers
-----------------
100 | 2
101 | 5
102 | 1
103 | 3
I want to count then create a ticket in the output of:
ID 100 | 2 pass (bronze)
ID 101 | 5 pass (because it is gold, we count all passengers)
ID 102 | 1 pass (silver)
ID 103 | 2 pass (steel)
I was thinking something like the code below however, I am unsure how to finish my case statement. I want to substring pass_code so that we get show pass numbers e.g '2xBronze' should give me 2. Then for ID 103, we have 2 passes and 3 customers so we should output 2.
Also, is there a way to firstly find '2xbronze' if the pass_code contained lots of other things such as '101001, 1xbronze, FirstClass' - this may change so i don't want to substring, could we search for '2xbronze' and then pull out the 2??
SELECT
CASE
WHEN Passes.pass_code like '%gold%' THEN Passengers.passengers
WHEN Passes.pass_code like '%steel%' THEN SUBSTRING(passes.pass_code, 1,1)
WHEN Passes.pass_code like '%bronze%' THEN SUBSTRING(passes.pass_code, 1,1)
WHEN Passes.pass_code like '%silver%' THEN SUBSTRING(passes.pass_code, 1,1)
else 0 end as no,
Passes.ID,
Passes.Pass_code,
Passengers.Passengers
FROM Passes
JOIN Passengers ON Passes.ID = Passengers.ID
https://dbfiddle.uk/?rdbms=oracle_18&fiddle=db698e8562546ae7658270e0ec26ca54
So assuming you are indeed using Oracle (as your DB fiddle implies).
You can do some string magic with finding position of a splitter character (in your case the x), then substringing based on that. Obviously this has it's problems, and x is a bad character seperator as well.. but based on your current set.
WITH PASSCODESPLIT AS
(
SELECT PASSES.ID,
TO_Number(SUBSTR(PASSES.PASS_CODE, 0, (INSTR(PASSES.PASS_CODE, 'x')) - 1)) AS NrOfPasses,
SUBSTR(PASSES.PASS_CODE, (INSTR(PASSES.PASS_CODE, 'x')) + 1) AS PassType
FROM Passes
)
SELECT
PASSCODESPLIT.ID,
CASE
WHEN PASSCODESPLIT.PassType = 'gold' THEN Passengers.Passengers
ELSE PASSCODESPLIT.NrOfPasses
END AS NrOfPasses,
PASSCODESPLIT.PassType,
Passengers.Passengers
FROM PASSCODESPLIT
INNER JOIN Passengers ON PASSCODESPLIT.ID = Passengers.ID
ORDER BY PASSCODESPLIT.ID ASC
Gives the result of:
ID NROFPASSES PASSTYPE PASSENGERS
100 2 bronze 2
101 5 gold 5
102 1 silver 1
103 2 steel 3
As can also be seen in this fiddle
But I would strongly advise you to fix your table design. Having multiple attributes in the same column leads to troubles like these. And the more variables/variations you start storing, the more 'magic' you need to keep doing.
In this particular example i see no reason why you don't simply have the 3 columns in Passes, also giving you the opportunity to add new columns going forward. I.e. to keep track of First class.
You can extract the numbers using regexp_substr(). So I think this does what you want:
SELECT (CASE WHEN p.pass_code LIKE '%gold%'
THEN TO_NUMBER(REGEXP_SUBSTR(p.pass_code, '^[0-9]+'))
ELSE pp.passengers
END) as num,
p.ID, p.Pass_code, pp.Passengers
FROM Passes p JOIN
Passengers pp
ON p.ID = pp.ID;
Here is a db<>fiddle.
This converts the leading digits in the code to a number. Also note the use of table aliases to simplify the query.

How to create two JOIN-tables so that I can compare attributes within?

I take a Database course in which we have listings of AirBnBs and need to be able to do some SQL queries in the Relationship-Model we made from the data, but I struggle with one in particular :
I have two tables that we are interested in, Billing and Amenities. The first one have the id and price of listings, the second have id and wifi (let's say, to simplify, that it equals 1 if there is Wifi, 0 otherwise). Both have other attributes that we don't really care about here.
So the query is, "What is the difference in the average price of listings with and without Wifi ?"
My idea was to build to JOIN-tables, one with listings that have wifi, the other without, and compare them easily :
SELECT avg(B.price - A.price) as averagePrice
FROM (
SELECT Billing.price, Billing.id
FROM Billing
INNER JOIN Amenities
ON Billing.id = Amenities.id
WHERE Amenities.wifi = 0
) A, (
SELECT Billing.price, Billing.id
FROM Billing
INNER JOIN Amenities
ON Billing.id = Amenities.id
WHERE Amenities.wifi = 1) B
WHERE A.id = B.id;
Obviously this doesn't work... I am pretty sure that there is a far easier solution to it tho, what do I miss ?
(And by the way, is there a way to compute the absolute between the difference of price ?)
I hope that I was clear enough, thank you for your time !
Edit : As mentionned in the comments, forgot to say that, but both tables have idas their primary key, so that there is one row per listing.
Just use conditional aggregation:
SELECT AVG(CASE WHEN a.wifi = 0 THEN b.price END) as avg_no_wifi,
AVG(CASE WHEN a.wifi = 1 THEN b.price END) as avg_wifi
FROM Billing b JOIN
Amenities a
ON b.id = a.id
WHERE a.wifi IN (0, 1);
You can use a - if you want the difference instead of the specific values.
Let's assume we're working with data like the following (problems with your data model are noted below):
Billing
+------------+---------+
| listing_id | price |
+------------+---------+
| 1 | 1500.00 |
| 2 | 1700.00 |
| 3 | 1800.00 |
| 4 | 1900.00 |
+------------+---------+
Amenities
+------------+------+
| listing_id | wifi |
+------------+------+
| 1 | 1 |
| 2 | 1 |
| 3 | 0 |
+------------+------+
Notice that I changed "id" to "listing_id" to make it clear what it was (using "id" as an attribute name is problematic anyways). Also, note that one listing doesn't have an entry in the Amenities table. Depending on your data, that may or may not be a concern (again, refer to the bottom for a discussion of your data model).
Based on this data, your averages should be as follows:
Listings with wifi average $1600 (Listings 1 and 2)
Listings without wifi (just 3) average 1800).
So the difference would be $200.
To achieve this result in SQL, it may be helpful to first get the average cost per amenity (whether wifi is offered). This would be obtained with the following query:
SELECT
Amenities.wifi AS has_wifi,
AVG(Billing.price) AS avg_cost
FROM Billing
INNER JOIN Amenities ON
Amenities.listing_id = Billing.listing_id
GROUP BY Amenities.wifi
which gives you the following results:
+----------+-----------------------+
| has_wifi | avg_cost |
+----------+-----------------------+
| 0 | 1800.0000000000000000 |
| 1 | 1600.0000000000000000 |
+----------+-----------------------+
So far so good. So now we need to calculate the difference between these 2 rows. There are a number of different ways to do this, but one is to use a CASE expression to make one of the values negative, and then simply take the SUM of the result (note that I'm using a CTE, but you can also use a sub-query):
WITH
avg_by_wifi(has_wifi, avg_cost) AS
(
SELECT Amenities.wifi, AVG(Billing.price)
FROM Billing
INNER JOIN Amenities ON
Amenities.listing_id = Billing.listing_id
GROUP BY Amenities.wifi
)
SELECT
ABS(SUM
(
CASE
WHEN has_wifi = 1 THEN avg_cost
ELSE -1 * avg_cost
END
))
FROM avg_by_wifi
which gives us the expected value of 200.
Now regarding your data model:
If both your Billing and Amenities table only have 1 row for each listing, it makes sense to combine them into 1 table. For example: Listings(listing_id, price, wifi)
However, this is still problematic, because you probably have a bunch of other amenities you want to model (pool, sauna, etc.) So you might want to model a many-to-many relationship between listings and amenities using an intermediate table:
Listings(listing_id, price)
Amenities(amenity_id, amenity_name)
ListingsAmenities(listing_id, amenity_id)
This way, you could list multiple amenities for a given listing without having to add additional columns. It also becomes easy to store additional information about an amenity: What's the wifi password? How deep is the pool? etc.
Of course, using this model makes your original query (difference in average cost of listings by wifi) a bit tricker, but definitely still doable.

Trying to count total active, inactive, and specialized memberships

I'm trying to get a count of active vs inactive memberships for classic and basic membership types -- Of the classic memberships, there are a subset of members that signed up through a different company but those memberships would also be considered classic.
So far, I have been able to select all memberships that are either classic or basic and show whether they are active or not. However, I have not had success in COUNTing how many active and inactive memberships there are for each and of the classic memberships, which were done through the different company.
So far, I have this:
SELECT
m.membershipID as MembershipID,
m.Level,
s.subInactive,
CASE
WHEN s.subInactive = 0 THEN s.subInactive + 1
WHEN s.subInactive = 1 THEN s.subInactive - 1
END AS oneMeansActive
FROM dbo.membershipsStartsAndDrops m
INNER JOIN dbo.Subscriptions s on m.membershipID = s.subSubscription_ID
I know I'm probably on the wrong track but would appreciate if someone could point me in the right direction. Thanks!
Sample Data:
membershipID | Level | subInactive | OneMeansActive
1 Classic 0 1
2 Basic 0 1
3 Classic 0 0
Desired Result:
ActClassic | ActBasic | InaClassic | InaBasic | ActSpecial
83079 5607 12658 3403 1270
Columns = Active classic memberships, active basic memberships, inactive classic memberships, inactive basic memberships, active classic memberships that signed up through 3rd party company.
I can't figure out how to count all of these values and also group them like this. I'm pulling data from 2 different databases as well. The classic memberships that signed up through the 3rd party company come from same DB as tbl_subscription. The table it comes from is company_members
You seem to want conditional aggregation. Something like this:
select sum(case when level = 'Classic' then OneMeansActive else 0 end) as numClassicActives,
sum(case when level = 'Basic' then OneMeansActive else 0 end) as numBasicActives,
sum(case when level = 'Classic' then subInactive else 0 end) as numClassicSubinactives,
sum(case when level = 'Classic' then subInactive else 0 end) as numBasicSubinactives
from company_members m

Using a field to filter a selection on a second field in SQL Server

I have a table ClientContacts, which holds basic information about a pairing of clients. Some of the details held in this table include P1Title, P2Title, P1FirstName, P2FirstName. For each row in this table there may be details of one or two clients, with a CustomerId that represents the pairing. Within this table is also ContactId, which is used to link to the table described below.
In a second table ContactDetails which contains rows that hold a specific contact detail that is associated with a client. Each client may have a number of rows in this table, each row holding a different detail such as HomeNumber, MobileNumber and Email. This table also contains a Type field which represents the type of contact detail held in the row. 1 = Home number, 2 = Mobile number and 3 = email. The Note field is also included, which may hold either Mr or Mrs denoting whether the mobile number held belongs to Person1 or Person2 in the client pairing.
Here is a visual structure of the tables.
ClientContacts
CustomerId ContactId Person1Title Person1FirstName Person1LastName Person2Title Person2FirstName Person2LastName
1 100 Mr Bob BobLastname Mrs Bobette BobetteLastname
2 101 Mr John JohnLastname Mrs Johnette JohnetteLastname
ContactDetails
ContactId Detail Type Note
100 012345 1
100 077777 2 P1
100 012333 1
100 088888 2 P2
101 099999 1
101 012211 1
101 066666 2
101 email#email.com 3
I want to construct a query that allows me to pull back the information of both of the clients, as well as figure out whether any of the mobile numbers stored in the ContactDetails table belongs to either of the two clients, if it does, I need to be able to tell which belongs to Person1 or Person2 in the pairing.
In addition, if the note field is null for a particular mobile number (type = 2), the first mobile number should be used for Person1 and the second should be used for Person2.
Below is my desired output:
Output
CustomerId Person1Firstname
Person1Lastname Person2Firstname Person2Lastname Home Person1Mobile Person2Mobile Person2Email
1 Bob BobLastname Bobette BobetteLastname 012211 077777 088888 null
I have a partially working query that manages to extract the mobile numbers and relates them to P1 or P2, however this only works if the Note field is not null.
select
cc.CustomerId,
cc.Person1Forename,
cc.Person1Surname,
cc.Person2Forename,
cc.Person2Surname,
max(case when cd.Type = 3 then cd.Detail end) as 'Home',
max(case when cd.Type = 4 and cd.Note = cc.P1Title then cd.Detail end) as 'Person1Mobile',
max(case when cd.Type = 4 and cd.Note = cc.P2Title then cd.Detail end) as 'Person2Mobile',
max(case when cd.Type = 5 then cd.Detail end) as 'Email'
from ClientContacts cc join
ContactDetails
cd on cc.ContactId = cd.ContactId
I'm unsure how to proceed from here. Any help would be appreciated.

Sql Server Query To Group By Name

I have the following data from 2 tables Notes (left) and scans (right) :
Imagine the picker and packers were all varying, like you can have JOHN, JANE etc.
I need a query that outputs like so :
On a given date range :
Name - Picked (units) - Packed (units)
MASI - 15 - 21
JOHN - 21 - 32
etc.
I can't figure out how to even start this, any tips will be helpful thanks.
Without a "worker" take that lists each Picker/Packer individually, I think you'd need something like this...
SELECT
CASE WHEN action.name = 'Picker' THEN scans.Picker ELSE scans.Packer END AS worker,
SUM(CASE WHEN action.name = 'Picker' THEN notes.Units ELSE 0 END) AS PickedUnits,
SUM(CASE WHEN action.name = 'Packer' THEN notes.Units ELSE 0 END) AS PackedUnits
FROM
notes
INNER JOIN
scans
ON scans.PickNote = notes.Number
CROSS JOIN
(
SELECT 'Picker' AS name
UNION ALL SELECT 'Packer' AS name
)
AS action
GROUP BY
CASE WHEN action.name = 'Picker' THEN scans.Picker ELSE scans.Packer END
(This is actually just an algebraic re-arrangement of the answer that #RaphaƫlAlthaus posted at the same time as me. Both use UNION to work out the Picker values and the Packer values separately. If you have separate indexes on scans.Picker and scans.Packer then I would expect mine MAY be slowest. If you don't have those two indexes then I would expect mine to be fastest. I recommend creating the indexes and testing on a realtisic data set.)
EDIT
Actually, what I would recommend is a change to scans table completely; normalise it.
Your de-normalised set has one row per PickNote, with fields picker and packer.
A normalised set would have two rows per PickNote with fields role and worker.
id | PickNote | Role | Worker
------+----------+------+--------
01 | PK162675 | Pick | MASI
02 | PK162675 | Pack | MASI
03 | PK162676 | Pick | FRED
04 | PK162676 | Pack | JOHN
This allows you to create simple indexes and simple queries.
You may initially baulk at the extra unecessary rows, but it will yield simpler queries, faster queries, better maintainability, increased flexibility, etc, etc.
In short, this normalisation may cost a little extra space, but it pays back dividends forever.
SELECT name, SUM(nbPicked) Picked, SUM(nbPacked) Packed
FROM
(SELECT n.Picker name, SUM(n.Units) nbPicked, 0 nbPacked
FROM Notes n
INNER JOIN scans s ON s.PickNote = n.Number
--WHERE s.ProcessedOn BETWEEN x and y
GROUP BY n.Picker
UNION ALL
SELECT n.Packer name, 0 nbPicked, SUM(n.Units) nbPacked
FROM Notes n
INNER JOIN scans s ON s.PickNote= n.Number
--WHERE s.ProcessedOn BETWEEN x and y
GROUP BY n.Packer)
GROUP BY name;