Re-format table, placing multiple column headers as rows - sql

I have a table of fishing catches, showing number of fish and total kg, for all the fishing days. Current format of the data is showing as below
In the other reference table is a list of all the official fish species with codes and names.
How can I re-format the first table so the rows are repeated for each day showing a certain species with the corresponding total catches and kgs in a row. So instead of the species kg and n having their different columns, I would have them in rows while there is only one n and kg column. I am thinking of looping through the list of all species and based on the numbers it will duplicate the rows in a way with the right values of n and kg of the species in the rows. This is the final format I need. My database is SQL Server.

You may use a union query here:
SELECT Day, 'Albacore' AS Species, ALB_n AS n, ALB_kg AS kg FROM yourTable
UNION ALL
SELECT Day, 'Big eye tuna', BET_n, BET_kg FROM yourTable
UNION ALL
SELECT Day, 'Sword fish', SWO_n, SWO_kg FROM yourTable
ORDER BY Day, Species;

You can also use a cross apply here, e.g.:
/*
* Data setup...
*/
create table dbo.Source (
Day int,
ALB_n int,
ALB_kg int,
BET_n int,
BET_kg int,
SWO_n int,
SWO_kg int
);
insert dbo.Source (Day, ALB_n, ALB_kg, BET_n, BET_kg, SWO_n, SWO_kg) values
(1, 10, 120, 4, 60, 2, 55),
(2, 15, 170, 8, 100, 1, 30);
create table dbo.Species (
Sp_id int,
Sp_name nvarchar(20)
);
insert dbo.Species (Sp_id, Sp_name) values
(1, N'Albacore'),
(2, N'Big eye tuna'),
(3, N'Sword fish');
/*
* Unpivot data using cross apply...
*/
select Day, Sp_name as Species, n, kg
from dbo.Source
cross apply dbo.Species
cross apply (
select
case
when Sp_name=N'Albacore' then ALB_n
when Sp_name=N'Big eye tuna' then BET_n
when Sp_name=N'Sword fish' then SWO_n
else null end as n,
case
when Sp_name=N'Albacore' then ALB_kg
when Sp_name=N'Big eye tuna' then BET_kg
when Sp_name=N'Sword fish' then SWO_kg
else null end as kg
) unpivotted (n, kg);

Related

How to specify a linear programming-like constraint (i.e. max number of rows for a dimension's attributes) in SQL server?

I'm looking to assign unique person IDs to a marketing program, but need to optimize based on each person's Probability Score (some people can be sent to multiple programs, some only one) and have two constraints such as budgeted mail quantity for each program.
I'm using SQL Server and am able to put IDs into their highest scoring program using the row_number() over(partition by person_ID order by Prob_Score), but I need to return a table where each ID is assigned to a program, but I'm not sure how to add the max mail quantity constraint specific to each individual program. I've looked into the Check() constraint functionality, but I'm not sure if that's applicable.
create table test_marketing_table(
PersonID int,
MarketingProgram varchar(255),
ProbabilityScore real
);
insert into test_marketing_table (PersonID, MarketingProgram, ProbabilityScore)
values (1, 'A', 0.07)
,(1, 'B', 0.06)
,(1, 'C', 0.02)
,(2, 'A', 0.02)
,(3, 'B', 0.08)
,(3, 'C', 0.13)
,(4, 'C', 0.02)
,(5, 'A', 0.04)
,(6, 'B', 0.045)
,(6, 'C', 0.09);
--this section assigns everyone to their highest scoring program,
--but this isn't necessarily what I need
with x
as
(
select *, row_number()over(partition by PersonID order by ProbabilityScore desc) as PersonScoreRank
from test_marketing_table
)
select *
from x
where PersonScoreRank='1';
I also need to specify some constraints: two max C packages, one max A & one max B package can be sent. How can I reassign the IDs to a program while also using the highest probability score left available?
The final result should look like:
PersonID MarketingProgram ProbabilityScore PersonScoreRank
3 C 0.13 1
6 C 0.09 1
1 A 0.07 1
6 B 0.045 2
You need to rethink your ROW_NUMBER() formula based on your actual need, and you should also have a table of Marketing Programs to make this work efficiently. This covers the basic ideas you need to incorporate to efficiently perform the filtering you need.
MarketingPrograms Table
CREATE TABLE MarketingPrograms (
ProgramID varchar(10),
PeopleDesired int
)
Populate the MarketingPrograms Table
INSERT INTO MarketingPrograms (ProgramID, PeopleDesired) Values
('A', 1),
('B', 1),
('C', 2)
Use the MarketingPrograms Table
with x as (
select *,
row_number()over(partition by ProgramId order by ProbabilityScore desc) as ProgramScoreRank
from test_marketing_table
)
select *
from x
INNER JOIN MarketingPrograms m
ON x.MarketingProgram = m.ProgramID
WHERE x.ProgramScoreRank <= m.PeopleDesired

Crosstab transpose query request

Using Postgres 9.3.4, I've got this table:
create table tbl1(country_code text, metric1 int, metric2 int, metric3 int);
insert into tbl1 values('us', 10, 20, 30);
insert into tbl1 values('uk', 11, 21, 31);
insert into tbl1 values('fr', 12, 22, 32);
I need a crosstab query to convert it to this:
create table tbl1(metric text, us int, uk int, fr int);
insert into tbl1 values('metric1', 10, 11, 12);
insert into tbl1 values('metric2', 20, 21, 22);
insert into tbl1 values('metric3', 30, 31, 32);
As an added bonus, I'd love a rollup:
create table tbl1(metric text, total int, us int, uk int, fr int);
insert into tbl1 values('metric1', 33, 10, 11, 12);
insert into tbl1 values('metric2', 63, 20, 21, 22);
insert into tbl1 values('metric3', 93, 30, 31, 32);
I'm done staring at the crosstab spec, I have it written with case statements but it's mad unruly and long, so can someone who's fluent in crosstab please whip up a quick query so I can move on?
The special difficulty is that your data is not ready for cross tabulation. You need data in the form row_name, category, value. You can get that with a UNION query:
SELECT 'metric1' AS metric, country_code, metric1 FROM tbl1
UNION ALL
SELECT 'metric2' AS metric, country_code, metric2 FROM tbl1
UNION ALL
SELECT 'metric3' AS metric, country_code, metric3 FROM tbl1
ORDER BY 1, 2 DESC;
But a smart LATERAL query only needs a single table scan and will be faster:
SELECT x.metric, t.country_code, x.val
FROM tbl1 t
, LATERAL (VALUES
('metric1', metric1)
, ('metric2', metric2)
, ('metric3', metric3)
) x(metric, val)
ORDER BY 1, 2 DESC;
Related:
What is the difference between LATERAL JOIN and a subquery in PostgreSQL?
SELECT DISTINCT on multiple columns
Using the simple form of crosstab() with 1 parameter with this query as input:
SELECT * FROM crosstab(
$$
SELECT x.metric, t.country_code, x.val
FROM tbl1 t
, LATERAL (
VALUES
('metric1', metric1)
, ('metric2', metric2)
, ('metric3', metric3)
) x(metric, val)
ORDER BY 1, 2 DESC
$$
) AS ct (metric text, us int, uk int, fr int);
List country names in alphabetically descending order (like in your demo).
This also assumes all metrics are defined NOT NULL.
If one or both are not the case, use the 2-parameter form instead:
PostgreSQL Crosstab Query
Add "rollup"
I.e. totals per metric:
SELECT * FROM crosstab(
$$
SELECT x.metric, t.country_code, x.val
FROM (
TABLE tbl1
UNION ALL
SELECT 'zzz_total', sum(metric1)::int, sum(metric2)::int, sum(metric3)::int -- etc.
FROM tbl1
) t
, LATERAL (
VALUES
('metric1', metric1)
, ('metric2', metric2)
, ('metric3', metric3)
) x(metric, val)
ORDER BY 1, 2 DESC
$$
) AS ct (metric text, total int, us int, uk int, fr int);
'zzz_total' is an arbitrary label, that must sort last alphabetically (or you need the 2-parameter form of crosstab()).
If you have lots of metrics columns, you might want to build the query string dynamically. Related:
How to perform the same aggregation on every column, without listing the columns?
Executing queries dynamically in PL/pgSQL
Also note that the upcoming Postgres 9.5 (currently beta) introduces a dedicated SQL clause for ROLLUP.
Related:
Spatial query on large table with multiple self joins performing slow

SQL select join the row with max (arithmatic(value1, value2))

I am trying to make a Trade system where people can make offer on the items they want. There are two currencies in the system, gold and silver. 100 silver = 1 gold. Note that people can make offers the same price as others, so there could be duplicate highest offer price.
Table structure looks roughly like this
Trade table
ID
TradeOffer table
ID
UserID
TradeID references Trade(ID)
GoldOffer
SilverOffer
I want to display to the user a list of trades sorted by the highest offer price whenever they do a search with constraint.
The Ideal output would be similar to this
Trade.ID TradeOffer.ID HighestGoldOffer HighestSilverOffer UserID
where HighestGoldOffer and HighestSilverOffer are the value of GoldOffer and SilverOffer column of the Offer with highest (GoldOffer * 100 + SilverOffer) and UserID is the user who made the offer
I know I can run 2 separate queries, one to retrieve all the Trades that satisfies all the constraint and extract all the ID to run another query to get the highest offer, but I am a perfectionist so I would prefer to do it with one sql instead of two.
I could just select all offers that are (GoldOffer * 100 + SilverOffer) = MAX (GoldOffer * 100 + SilverOffer) but this would possibly return duplicated Trade if there are multiple people offered the same price. Also there could be nobody offered on the Trade yet so GoldOffer and SilverOffer will be empty, I would still like to show the Trade as no offer when this happened.
Hope I made myself clear and thanks for any help
Model and test data
CREATE TABLE Trade (ID INT)
CREATE TABLE TradeOffer
(
ID INT,
UserID INT,
TradeID INT,
GoldOffer INT,
SilverOffer INT
)
INSERT Trade VALUES (1), (2), (3)
INSERT TradeOffer VALUES
(1, 1, 1, 10, 15),
(2, 2, 1, 11, 15),
(3, 1, 2, 10, 16),
(4, 2, 2, 10, 16)
Query
SELECT
[TradeID],
[TradeOfferID],
[HighestGoldOffer],
[HighestSilverOffer],
[UserID]
FROM (
SELECT
t.ID AS [TradeID],
tOffer.ID AS [TradeOfferID],
tOffer.GoldOffer AS [HighestGoldOffer],
tOffer.SilverOffer AS [HighestSilverOffer],
tOffer.[UserID],
RANK() OVER (
PARTITION BY t.ID
ORDER BY (([GoldOffer] * 100) + [SilverOffer]) DESC
) AS [Rank]
FROM Trade t
LEFT JOIN TradeOffer tOffer
ON tOffer.TradeID = t.ID
) x
WHERE [Rank] = 1
Result

Sql Delete Statement Trouble

I am having trouble writing a script which can delete all the rows which match on the first three columns and where the Quantities sum to zero?
I think the query needs to find all Products that match and then within that group, all the Names which match and then within that subset, all the currencies which match and then, the ones which have quantities netting to zero.
In the below example, the rows which would be deleted would be rows 1&2,4&6.
Product, Name, Currency, Quantity
1) Product A, Name A, GBP, 10
2) Product A, Name A, GBP, -10
3) Product A, Name B, GBP, 10
4) Product A, Name B, USD, 10
5) Product A, Name B, EUR, 10
6) Product A, Name B, USD, -10
7) Product A, Name C, EUR, 10
Hope this makes sense and appreciate any help.
Try this:
DELETE
FROM [Product]
WHERE Id IN
(
SELECT Id
FROM
(
SELECT Id, SUM(Quantity) OVER(PARTITION BY a.Product, a.Name, a.Currency) AS Sm
FROM [Product] a
) a
WHERE Sm = 0
)
You may want to break this problem into parts.
First create a view that lists those combinations which sum to zero
CREATE VIEW vw_foo AS
SELECT product,name, currency, sum(quantity) as net
FROM foo
GROUP BY product, name, currency
HAVING sum(quantity)=0;
At this point, you need to make sure this view has the data you expect to delete. In you example, the view should have only 2 records: ProductA/NameA/GBP and ProductA/NameB/USD
Step 2. Delete the data where the fields match:
DELETE FROM foo
WHERE EXISTS
(SELECT *
FROM vw_foo
WHERE vw_foo.product = product
AND vw_foo.name = name
AND vw_currency = currency);
One way to simplify the SQL is to just concatente the 3 columns into one and apply some grouping:
delete from product
where product + name + currency in (
select product + name + currency
from product
group by product + name + currency
having sum(quantity) = 0)
I am assuming this is a accounting problem with offsetting pairs of entries in the ledger.
If there are for instance three entries for combination (A, A, GBP) this code and some of the example above will not work.
I create a temporary test table, loaded it with your data, used a CTE - common table expression - to find the duplicate pattern and joined it to the table to select the rows.
Just change the 'select *' to 'delete'.
Again, this only works for equal offsetting pairs. It will cause havoc with odd number of entries.
Do you have only even number of entries?
Sincerely
John
-- create sample table
create table #products
(
product_id int identity(1,1),
product_txt varchar(16),
name_txt varchar(16),
currency_cd varchar(16),
quantity_num int
);
go
-- add data 2 table
insert into #products
(product_txt, name_txt, currency_cd, quantity_num)
values
('A', 'A', 'GBP', 10),
('A', 'A', 'GBP', -10),
('A', 'B', 'GBP', 10),
('A', 'B', 'USD', 10),
('A', 'B', 'EUR', 10),
('A', 'B', 'USD', -10),
('A', 'C', 'EUR', 10);
go
-- show the data
select * from #products;
go
-- use cte to find combinations
with cte_Ledger_Offsets (product_txt, name_txt, currency_cd)
as
(
select product_txt, name_txt, currency_cd
from #products
group by product_txt, name_txt, currency_cd
having sum(quantity_num) = 0
)
select * from #products p inner join cte_Ledger_Offsets c
on p.product_txt = c.product_txt and
p.name_txt = c.name_txt and
p.currency_cd = c.currency_cd;

Access 97 Outer join issue

I have two tables I want to join.
Table A has one column, named "Week", and contains 52 rows: 1,2,3,4,5,6 etc.
Table 2 has three columns, named "Name", "Week", and "Total", and contains 10 rows:
'Bob', 1, 1
'Bob', 3, 1
'Joe', 4, 1
'Bob', 6, 1
I want to join these together so that my data looks like:
NAME|WEEK|TOTAL
'Bob', 1, 1
'Bob', 2, 0
'Bob', 3, 1
'Bob', 4, 0
'Bob', 5, 0
'Bob', 6, 1
As you can see, a simple outer join. However, when I try to do this, I'm not getting the expected result, no matter what kind of join I use.
My query below:
SELECT a.WEEK, b.Total
FROM Weeks a LEFT JOIN Totals b ON (a.Week = b.Week and b.Name ='Bob')
The result of this query is
NAME|WEEK|TOTAL
'Bob', 1, 1
'Bob', 3, 1
'Bob', 6, 1
Thanks in advance for the help!
I know its access but your join is incorrect. Here we go in sql server..same concept just look at the join condition:
--dont worry about this code im just creating some temp tables
--table to store one column (mainly week number 1,2..52)
CREATE TABLE #Weeks
(
weeknumber int
)
--insert some test data
--week numbers...I'll insert some for you
INSERT INTO #Weeks(weeknumber) VALUES(1)
INSERT INTO #Weeks(weeknumber) VALUES(2)
INSERT INTO #Weeks(weeknumber) VALUES(3)
INSERT INTO #Weeks(weeknumber) VALUES(4)
INSERT INTO #Weeks(weeknumber) VALUES(5)
INSERT INTO #Weeks(weeknumber) VALUES(6)
--create another table with two columns storing the week # and a total for that week
CREATE TABLE #Table2
(
weeknumber int,
total int
)
--insert some data
INSERT INTO #Table2(weeknumber, total) VALUES(1, 100)
--notice i skipped week 2 on purpose to show you the results
INSERT INTO #Table2(weeknumber, total) VALUES(3, 100)
--here's the magic
SELECT t1.weeknumber as weeknumber, ISNULL(t2.total,0) as total FROM
#Weeks t1 LEFT JOIN #Table2 t2 ON t1.weeknumber=t2.weeknumber
--get rid of the temp tables
DROP TABLE #table2
DROP TABLE #Weeks
Results:
1 100
2 0
3 100
4 0
5 0
6 0
Take your week number table (the table that has one column:
SELECT t1.weeknumber as weeknumber
Add to it a null check to replace the null value with a 0. I think there is something in access like ISNULL:
ISNULL(t2.total, 0) as total
And start your join from your first table and left join to your second table on the weeknumber field. The result is simple:
SELECT t1.weeknumber as weeknumber, ISNULL(t2.total,0) as total FROM
#Weeks t1 LEFT JOIN #Table2 t2 ON t1.weeknumber=t2.weeknumber
Do not pay attention to all the other code I have posted, that is only there to create temp tables and insert values into the tables.
SELECT b.Name, b.Week, b.Total
FROM Totals AS b
WHERE b.Name ='Bob'
UNION
SELECT 'Bob' AS Name, a.Week, 0 AS Total
FROM Weeks AS a
WHERE NOT EXISTS ( SELECT *
FROM Totals AS b
WHERE a.Week = b.Week
AND b.Name ='Bob' );
You were on the right track, but just needed to use a left join. Also the NZ function will put a 0 if total is null.
SELECT Totals.Person, Weeks.WeekNo, Nz(Totals.total, 0) as TotalAmount
FROM Weeks LEFT JOIN Totals
ON (Weeks.WeekNo = Totals.weekno and Totals.Person = 'Bob');
EDIT: The query you now have won't even give the results you've shown because you left out the Name field (Which is a bad name for a field because it is a reserved word.). You're still not providing all the information. This query works.
*Another Approach: * Create a separate query on the Totals table having a where clause: Name = 'Bob'
Select Name, WeekNo, Total From Totals Where Name = 'Bob';
and substitute that query for the Totals table in this query.
Select b.Name, w.WeekNo, b.total
from Weeks as w
LEFT JOIN qryJustBob as b
on .WeekNo = b.WeekNo;