I'm trying to implement a price fallback system in SQL server. I'd like to have a set of increasingly specific prices (eg: by region, store, warehouse, etc.) for a product, that may or may not be defined, and be able to select the most specific prices (ie: the one with most parameters defined) in a report.
For example, I might have the following data:
Region
--------
1
2
Store
--------
1
2
3
Product | Region | Store | Price
--------------------------------
Foo | NULL | NULL | 1.0
Foo | 1 | NULL | 2.0
Foo | 1 | 1 | 2.5
Foo | 1 | 2 | 2.3
So if I wanted to know the price for product Foo...
in Region 1, Store 1 = 2.5
in Region 1, Store 3 = 2.0 (Store 3 is not defined explicitly in the data, so the result comes from the NULL store for Region 1)
in Region 2, Store 4 = 1.0 (Region 2 is not defined explicitly in the data, so the result comes from the NULL region)
For the sake of simplicity, I can assume that the Store is always more specific than the region, and a store can only exist in one region.
A schema for this data would be something like this:
CREATE TABLE Prices(
ID int IDENTITY(1,1) NOT NULL,
Product int NOT NULL,
Region int NULL,
Store int NULL,
Price money NOT NULL,
CONSTRAINT PK_Prices PRIMARY KEY CLUSTERED (ID ASC),
CONSTRAINT IX_Prices UNIQUE NONCLUSTERED (Product ASC, Region ASC, Store ASC)
)
Aside from crunching this data in code, how can I query this table for a list of effective prices for every product, based on (Region, Store)?
try this:
EDIT for all products, each listed once, even where given region and store do not exist...
CREATE PROCEDURE GetPrice
#Region int = null
,#Store int = null
AS
SELECT
Product
,Region
,Store
,Price
FROM (SELECT
Product
,Region AS Region
,Store As Store
,Price
,Row_Number() OVER(PARTITION BY Product ORDER BY SortBy,Product,Region,Store,Price) AS RowNumber
FROM (SELECT 1 AS SortBy,* FROM Prices WHERE (Region = #Region OR #Region IS NULL) AND (Store = #Store OR #Store IS NULL)
UNION
SELECT 2 AS SortBy,* FROM Prices WHERE (Region = #Region OR #Region IS NULL)
UNION
SELECT 3 AS SortBy,* FROM Prices
) Prices
) dt
WHERE RowNumber=1
ORDER BY Product
GO
Wow, you guys are trying way too hard. Here's the easy way: use the COALESCE command, which takes the first non-null value.
SELECT COALESCE(st.Price, rg.Price, gn.Price) AS Price
FROM dbo.Prices gn /*General price*/
LEFT OUTER JOIN dbo.Prices rg /*Regional price*/
ON rg.Product = #Product AND rg.Region = #Region AND rg.Store IS NULL
LEFT OUTER JOIN dbo.Prices st /*Store price*/
ON rg.Product = #Product AND rg.Region = #Region AND rg.Store = #Store
WHERE gn.Product = #Product
AND gn.Region IS NULL AND gn.Store IS NULL
That way, you'll get the store price if that's not null, or the regional price if that's not null, or the general price if all else fails.
I think your data model might be a little screwy. Is the prices table being populated just for the report? I might review my tables, but if i had to use this setup, I would do something like this:
For product prices in a specified region (NOT STORE). Use this select in a sproc that accepts region as a parameter. If I did it right, it will return the region price, if there is one, and the product price, if there isn't.
Select Product, Price from dbo.prices where (Region = #region) or (region is NULL and product not in (select product from prices where region = #Region))
For product prices in a specified store. Use this select in a sproc that accepts store as a parameter. You will also need to pass in region or figure it out from the store. If I did it right, it will return the store price, if there is one, then he region price, then the product price.
Select product, price from dbo.prices where (store = #store) or (store is NULL AND region = #Region and product not in (select product from prices where store = #store) or (store is NULL and region is NULL and product not in (select product from prices where region = #Region))
To get prices from multiple regions or stores, just execute one of the above sprocs multiple times (in a loop or cursor).
I guess this must work (didn't try it, though):
select top 1 price
from prices
where product = #product
and isnull(region, #region) = #region
and isnull(store, #store) = #store
order by region desc, store desc
A product must have least 1 Prices record. The ORDER BY .. DESC sorts original NULL values last.
Related
Hi All i am unable to create a query in sql i want to get all employee who's contain the product which i passed in parameter
Ex, i if passed product id 11,12 then its should be return only DeliveryBoyId1 and 2 and if passed the product id 11 then its should be return only DeliveryBoyId 1,2,3 and if i passed productid 11,12,16 then its should return 0 record because there is no delivery boy assigned the product id 11,12,16
I don't know what your table is called so I'm calling it dbo.Delivery. Try this out:
;with CTE as (
select distinct DeliveryBoyId --get all the boys who delivered product 11
from dbo.Delivery
where ProductId = 11
union all --combine the above results with the below
select distinct DeliveryBoyId --get all the boys who delivered product 12
from dbo.Delivery
where ProductId = 12
)
select DeliveryBoyId
from CTE
group by DeliveryBoyId
having count(1) = 2 --get all the boys who appear twice in the above table, once for each product
If you want to match additional products, you can add to the CTE section for other product IDs.
If you want a single solution for any number of products, you can use this instead (which may be a little less readable but more concise and easier to maintain):
select DeliveryBoyId
from (
select distinct DeliveryBoyId, ProductId
from dbo.Delivery
where ProductId in (11, 12)
) s
group by DeliveryBoyId
having count(1) = 2 --number matches number of products you need to match
I am using PostgreSQL 9.3 version database.
I have a situation where I want to count the number of products sales and sum the amount of product and also want to show the cities in a column where the product have sale.
Example
Setup
create table products (
name varchar(20),
price integer,
city varchar(20)
);
insert into products values
('P1',1200,'London'),
('P1',100,'Melborun'),
('P1',1400,'Moscow'),
('P2',1560,'Munich'),
('P2',2300,'Shunghai'),
('P2',3000,'Dubai');
Crosstab query:
select * from crosstab (
'select name,count(*),sum(price),city,count(city)
from products
group by name,city
order by name,city
'
,
'select distinct city from products order by 1'
)
as tb (
name varchar(20),TotalSales bigint,TotalAmount bigint,London bigint,Melborun bigint,Moscow bigint,Munich bigint,Shunghai bigint,Dubai bigint
);
Output
name totalsales totalamount london melborun moscow munich shunghai dubai
---------------------------------------------------------------------------------------------------------
P1 1 1200 1 1 1
P2 1 3000 1 1 1
Expected Output:
name totalsales totalamount london melborun moscow munich shunghai dubai
---------------------------------------------------------------------------------------------------------
P1 3 2700 1 1 1
P2 3 6860 1 1 1
Your first mistake seems to be simple. According to the 2nd parameter of the crosstab() function, 'Dubai' must come as first city (sorted by city). Details:
PostgreSQL Crosstab Query
The unexpected values for totalsales and totalamount represent values from the first row for each name group. "Extra" columns are treated like that. Details:
Pivot on Multiple Columns using Tablefunc
To get sums per name, run window functions over your aggregate functions. Details:
Get the distinct sum of a joined table column
select * from crosstab (
'select name
,sum(count(*)) OVER (PARTITION BY name)
,sum(sum(price)) OVER (PARTITION BY name)
,city
,count(city)
from products
group by name,city
order by name,city
'
-- ,'select distinct city from products order by 1' -- replaced
,$$SELECT unnest('{Dubai,London,Melborun
,Moscow,Munich,Shunghai}'::varchar[])$$
) AS tb (
name varchar(20), TotalSales bigint, TotalAmount bigint
,Dubai bigint
,London bigint
,Melborun bigint
,Moscow bigint
,Munich bigint
,Shunghai bigint
);
Better yet, provide a static set as 2nd parameter. Output columns are hard coded, it may be unreliable to generate data columns dynamically. If you a another row with a new city, this would break.
This way you can also order your columns as you like. Just keep output columns and 2nd parameter in sync.
Honestly I think your database needs some drastic normalization and your results in several columns (one for each city name) is not something I would do myself.
Nevertheless if you want to stick to it you can do it this way.
For the first step you need get the correct amounts. This would do the trick quite fast:
select name, count(1) totalsales, sum(price) totalAmount
from products
group by name;
This will be your result:
NAME TOTALSALES TOTALAMOUNT
P2 3 6860
P1 3 2700
You would get the Products/City this way:
select name, city, count(1) totalCityName
from products
group by name, city
order by name, city;
This result:
NAME CITY TOTALCITYNAME
P1 London 1
P1 Melborun 1
P1 Moscow 1
P2 Dubai 1
P2 Munich 1
P2 Shunghai 1
If you really would like a column per city you could do something like:
select name,
count(1) totalsales,
sum(price) totalAmount,
(select count(1)
from Products a
where a.City = 'London' and a.name = p.name) London,
...
from products p
group by name;
But I would not recommend it!!!
This would be the result:
NAME TOTALSALES TOTALAMOUNT LONDON ...
P1 3 2700 1
P2 3 6860 0
Demonstration here.
Here are the key fields I'm working with: (They come from a join on two other tables and there are also some extra fields like date I'll be filtering on, but that stuff I can handle.)
Customer Type (text), Item ID (number), Sales $s (number), Customer ID (number)
I want to answer two questions with one query, if possible:
1) For a given list of customer type(s), what were the top 25 items (by sum of sales)
2) Using the list of 25 items generated in step 1, did a given list of customer IDs purchase each of the specified items?
So my final result would look something like this:
(header) Item # | Customer Purchased?
(row 01) Item 1123 | Yes
(row 02) Item 2452 | Yes
(row 03) Item 3354 | No
...
(row 25) Item 2554 | No
The item numbers would be listed in decreasing sales volume (within the specified customer category/categories) and I'd be testing whether sum of sales > 0 to trip the Yes / No flag on customer(s) purchased.
Thanks!
Assuming you have your columns in a table #Orders, and the "list of customer Ids" in a table #CustomerIds:
create table #Orders (CustomerId int, CustomerType varchar(10), ItemId int, Sales decimal);
create table #MyCustomers (CustomerId int);
... you could try something like this:
declare #CustomerType varchar(10) = 'Ugly';
with MarkedOrders as (
select
o.ItemId,
o.Sales,
case when mc.CustomerId is not null then 1 else 0 end IsMyCustomer
from
Orders o
left join #MyCustomers mc
on mc.CustomerId = o.CustomerId
where
o.CustomerType = #CustomerType
)
select top 25
o.ItemId,
max(IsMyCustomer) IsPurchasedByMyCustomer
from MarkedOrders o
group by o.ItemId
order by sum(o.Sales) desc
Here is a SQL Function - sorry about the complexity;
SELECT [Codes].[Description], RawData.FS, COUNT(*) As 'Total Units'
FROM RawData, Codes, Categories
WHERE RawData.ACR = Codes.Name
AND Codes.CategoryName = 'ACR'
GROUP BY [Codes].[Description], [RawData].[FS]
ORDER BY [RawData].[FS]
In description - there is a Codes table that contains codes that are used in the table RawData for each of the columns. A second table called Categories keeps track of all these columns and Codes.CategoryName is a FK to Categories.Name. Basically creating a single lookup table for each of the coded values in RawData.
The field RawData.FS has 3 values NULL, 1, and 2. The RawData.ACR has 3 values corresponding to the descriptions of less than 1 acre, 1-10 acres, > 10 acres. The query above gives the correct results
Description FS Total Units
House on less than one acre 57080
House on one to less than ten acres 4760
House on ten acres or more 880
House on less than one acre 1 31496
House on one to less than ten acres 1 4312
House on ten acres or more 1 360
House on less than one acre 2 594404
House on one to less than ten acres 2 74688
House on ten acres or more 2 9104
The challenge here is to redo the SQL so that instead of 3 sets of 3 rows, theirs is a column corresponding to values of FS. In other words the header would be (for the first row);
Description FS=NULL FS=1 FS=2
House on less than one acre 57080 31496 594404
As a little bit more help - here is the SQL to create the Category and Code structure
CREATE TABLE Categories (
[Name] NVARCHAR(50) PRIMARY KEY,
[Description] NVARCHAR(200)
)
CREATE TABLE Codes (
[Name] NVARCHAR(50),
[CategoryName] NVARCHAR(50) FOREIGN KEY REFERENCES Categories(Name),
[Description] NVARCHAR(200) )
Every field in RawData is coded (in fact the data dictionary is at http://www.census.gov/acs/www/Downloads/data_documentation/pums/DataDict/PUMS_Data_Dictionary_2009-2011.pdf ). This is one of those classic SQL puzzles.
It sounds like you want to do the following which will pivot the FS values into columns:
SELECT [Codes].[Description],
sum(case when RawData.FS is null then 1 else 0 end) FS_null,
sum(case when RawData.FS = 1 then 1 else 0 end) FS_1,
sum(case when RawData.FS = 2 then 1 else 0 end) FS_2
FROM RawData
INNER JOIN Codes
ON RawData.ACR = Codes.Name
INNER JOIN Categories
ON Codes.CategoryName = Categories.Name
WHERE Codes.CategoryName = 'ACR'
GROUP BY [Codes].[Description]
Untested but it seems a case(s) and a subselect would work.
Select description,
case when fs is null then total_units end as 'FS=Null',
CASE WHEN fs = 1 then total_units end as 'FS=1',
case when fs = 2 then total_units end as 'FS=2'
FROM (
SELECT [Codes].[Description],
RawData.FS, COUNT(*) As 'Total_units'
FROM RawData, Codes, Categories
WHERE RawData.ACR = Codes.Name
AND Codes.CategoryName = 'ACR'
GROUP BY [Codes].[Description])
ORDER BY description
Given the following (heavily simplified) tables:
create table Tags (
TagId int Primary Key
)
create table OrderLines (
Branch int,
Station int,
TransNo int,
TagId int foreign key references Tags,
primary key (Branch, Station, TransNo)
)
I need a list of Tags along with an OrderLine which references each Tag. I am expecting zero or one OrderLines to reference each Tag, but there is nothing in the database constraints to enforce this.
Given input like this:
OrderLines Tags
Branch Station TransNo TagId TagId
1 100 2345 1 1
1 100 2346 1 2
1 101 5223 2 3
3 100 6677 4 4
I want to get output like this:
TagId Branch Station TransNo
1 1 100 2345 <-- it could list 2346 here, don't care
2 1 101 5223
3 null null null
4 3 100 6677
Note that although TagId 1 is referenced twice, my output only contains one of it. It doesn't matter which OrderLine is listed alongside it, but there must be only one instance of each tag in the output.
What is the most efficient way to do this?
I'm not able to alter the database schema.
You'd have to look at the execution plan to gauge efficiency
;WITH O AS
(
SELECT Branch, Station, TransNo, TagId,
ROW_NUMBER() OVER (PARTITION BY TagId ORDER BY TagId ) AS RN
FROM OrderLines
)
SELECT T.TagID, O.Branch, O.Station, O.TransNo
FROM Tags T
LEFT JOIN O ON T.TagID = O.TagID and RN=1
select t.TagId, t.Station, t.Branch, t.TransNo
from (
SELECT Station, Branch, TransNo, TagId, ROW_NUMBER() OVER (partition by TagId order by TagId) r
From OrderLines) t
WHERE r = 1
UNION ALL
SELECT TagId, NULL, NULL, NULL
from Tags
WHERE NOT EXISTS (Select 1 from OrderLines ol Where ol.TagId = Tags.Id)
SELECT Tags.TagID, Branch, Station, TransNo
FROM Tags
LEFT JOIN OrderLines ON Tags.TagID = OrderLines.TagID
ORDER BY Tags.TagID
The left join will make sure that all tags get listed, even those that don't have any order line attached to them.
The only flaw is that if more than one order line references a tag, the tag will be listed once for each order line. Some DBMS's (e.g. MySQL) allow you to solve this using a GROUP BY and still select columns you don't group by, but it's nonstandard and there is no guarantee which OrderLine you'll get. If you want this, you'll have to resort to subqueries, unions, temporary tables, or views (but since you can't change the schema, that last option is out).
select
T.tagid,
O.branch,
O.station,
O.transno
from orderlines O
right join tags T on (t.tagid=O.tagid)
group by t.tagid
Result:
tagid branch station transno
1 1 100 2345
2 1 101 5223
3 NULL NULL NULL
4 3 100 6677