I have a problem that I believe has a perfectly elegant solution, but would like some help.
So I have a table of persons and a numerical value. Besides that, there is a table with the rules of division of that value (per person) to multiple accounts, rule can be either a max value or a percentage of the value.
This is a simplified version of these tables.
Persons(PersonID int, Value decimal)
Account(AccountID int, PersonID int)
Distribution(AccountID int, MaxValue decimal Null, Percentage decimal null)
At some point I need to divide those numerical values to a third table - that holds the account and value divided to that account.
AccountValues(AccountID int, AccountValue decimal)
The count of the accounts (per person) is not fixed. In the distribution table - if both of the distribution values are null - all the left over value goes to that account.
The order of distribution is by their ID's.
The data could look something like this.
Persons table
PersonID Value
1 1000,00
2 2000,00
3 5000,00
4 500,00
Accounts table
AccountID PersonID
1 1
2 1
3 2
4 2
5 2
6 3
7 3
8 4
9 4
10 4
Distribution table
AccountID MaxValue Percentage
1 500,00 null
2 null null
3 null 0,5
4 null 0,2
5 null null
6 1000,00 null
7 null null
8 2000,00 null
9 null 0,2
10 null null
Still a bit new to T-SQL so need help with the simplest and most efficient solution.
So for now I'm thinking of 3 possible solutions.
1. The least elegant - count the max number of accounts per person and do a loop that many times.
2. Cursors - the best way perhaps?
3. CTE recursion (about which I know nothing about)
I've used a CTE. There might be a smarter way to do the totalling, but I think this works.
Data setup:
declare #Persons table (PersonID int not null,Value decimal(18,4) not null)
insert into #Persons(PersonID,Value) values
(1,1000.00),
(2,2000.00),
(3,5000.00),
(4,500.00)
declare #Accounts table (AccountID int not null,PersonID int not null)
insert into #Accounts(AccountID,PersonID) values
(1,1),
(2,1),
(3,2),
(4,2),
(5,2),
(6,3),
(7,3),
(8,4),
(9,4),
(10,4)
declare #Distribution table (AccountID int not null,MaxValue decimal(18,4) null,Percentage decimal(6,5) null)
insert into #Distribution (AccountID,MaxValue,Percentage) values
(1,500.00,null),
(2,null,null),
(3,null,0.5),
(4,null,0.2),
(5,null,null),
(6,1000.00,null),
(7,null,null),
(8,2000.00,null),
(9,null,0.2),
(10,null,null)
declare #AccountValues table (AccountID int not null,Value decimal(18,4) null)
Actual query:
;With DisbValues as (
select
a.AccountID,
p.PersonID,
CASE
WHEN d.MaxValue is not null then d.MaxValue
WHEN d.Percentage is not null then d.Percentage * p.Value
END as Value,
p.Value as TotalAvailable
from
#Distribution d
inner join
#Accounts a
on
d.AccountID = a.AccountID
inner join
#Persons p
on
a.PersonID = p.PersonID
), CumulativeValues as (
select
AccountID,
PersonID,
Value,
COALESCE((select SUM(Value) from DisbValues d2 where d2.PersonID = d.PersonID and d2.AccountID < d.AccountID),0) as PrevValue,
TotalAvailable
from
DisbValues d
)
insert into #AccountValues (AccountID,Value)
select
AccountID,
CASE WHEN PrevValue < TotalAvailable THEN
CASE WHEN PrevValue + Value < TotalAvailable THEN Value --Entirely satisfied
ELSE TotalAvailable - PrevValue --Partially satisfied
END
ELSE
0 --Not satisfied
END
from CumulativeValues
The first CTE (DisbValues) eliminates the need to think in terms of percentages (I've assumed that we're working with a percentage of the total value available, not of the remainder when trying to satisfy a particular account). The second CTE (CumulativeValues) then adds up all of the values that earlier accounts would require to be filled.
We can then, in the final query, break things down into 3 cases, as indicated by the comments.
Related
I have a result set in a temp table that is the result of some complicated joins and need to know the best way to filter rows that have the same duplicate AccountId/HealthPlanId (shown below).
select * from #HealthPlans
And the contents are as follows:
AccountId MemberId HealthPlanId RankNo
101273 47570 5215 1
101273 47570 2187 2
101273 55551 5179 3
160026 48102 5620 1
160026 48446 5620 2
In this scenario RankNo, which is not a value computed by my original query, is a db column that ranks member/healthPlan where there is more than one member/healthPlan combination on a given account.
In the case of account 101273, I have the same member (47570) with 3 separate health plans (5215, 2187, 5179). That's fine. I want to rank the health plans.
However, for accountId 160026, I have healthPlanId: 5620 listed twice but with different memberId's. I need to keep either of these member id's and discard the other (it doesn't matter which I keep since I'm only interested in Ranking the HealthPlanId).
Basically, an account should only have a row for each unique health plan. However, duplicate memberId's is OK and should be ranked as long as the HealthPlanId differs.
In other words, select rows from #HealthPlans such that the following is the result set:
AccountId MemberId HealthPlanId RankNo
101273 47570 5215 1
101273 47570 2187 2
101273 55551 5179 3
160026 48102 5620 1
There's no need to show the original joins because this is basically a simplification of my original issue.
Thanks,
Sean
Another method using a window function:
DECLARE #tab TABLE (AccountId int, MemberId int, HealthPlanId int, RankNo int)
INSERT #tab VALUES
(101273,47570,5215,1),
(101273,47570,2187,2),
(101273,55551,5179,3),
(160026,48102,5620,1),
(160026,48446,5620,2)
SELECT *
FROM(
SELECT ROW_NUMBER() OVER(PARTITION BY t.AccountId, t.HealthPlanId ORDER BY t.RankNo) rn, t.*
FROM #tab t
) t2
WHERE t2.rn = 1
Your particular query might look like:
SELECT *
FROM(
SELECT ROW_NUMBER() OVER(PARTITION BY hp.AccountId, hp.HealthPlanId ORDER BY hp.RankNo) rn, hp.*
FROM #HealthPlans hp
) hp2
WHERE hp2.rn = 1
I want my Firebird SQL to loop through part of the code WHILE a condition is meet.
Initially I didn't even think it was possible. However I have done some reading and now believe that I can use WHILE loop.
I understand a FOR loop is not what I want as it applies to the whole code, not just part of it.
I am using this in Excel and could use some VBA code to do what I want, but it would be better if I can do it all via Firebird SQL as then I can apply it elsewhere.
SELECT
'1' as "Qty",
'of ' || ALP3.PROPERTYVALUE AS "Total Qty"
FROM ASSEMBLYLINES
LEFT JOIN ASSEMBLYLINEPROPS ALP1 ON ALP1.HEADERSYSUNIQUEID = ASSEMBLYLINES.SYSUNIQUEID AND ALP1.PROPERTYNAME = 'Process2'
LEFT JOIN ASSEMBLYLINEPROPS ALP2 ON ALP2.HEADERSYSUNIQUEID = ASSEMBLYLINES.SYSUNIQUEID AND ALP2.PROPERTYNAME = 'Process3'
LEFT JOIN ASSEMBLYLINEPROPS ALP3 ON ALP3.HEADERSYSUNIQUEID = ASSEMBLYLINES.SYSUNIQUEID AND ALP3.PROPERTYNAME = 'Job Quantity'
LEFT JOIN ASSEMBLYLINEPROPS ALP4 ON ALP4.HEADERSYSUNIQUEID = ASSEMBLYLINES.SYSUNIQUEID AND ALP4.PROPERTYNAME = 'Drawing No'
WHERE ASSEMBLYLINES.ORDERNUMBER='16708R01'
AND ASSEMBLYLINES.LINECODE='FABPART'
AND ASSEMBLYLINES.SYSUSERCREATED <> 'EXTERNAL USER'
ORDER BY ALP4.PROPERTYVALUE
My results using the code above is:
Qty Total Qty
1 4
However, what I want is:
My results using the code above is:
Qty Total Qty
1 4
2 4
3 4
4 4
I understand the While loop would be something like:
While Qty <= ALP3.PROPERTYVALUE Do
<<output>>
Loop
Qty Total Qty
1 4
2 4
3 4
4 4
I understand the While loop would be something like:
While Qty <= ALP3.PROPERTYVALUE Do
<<output>>
Loop
So, your "quantity" column is not actually a quantity of some real data (like quantity of containers in cargo ship), but a row number in some your output report/grid.
And then what you want is limiting the output "rowset" - matrix, table, grid - to some N first rows.
Well, that is exactly how it is done, asking for the first rows only.
Select FIRST(4) column1, column2, column3
From table 1
Where condition1 and condition2 or condition3
See the "first" clause in documentation: https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-select.html
Also see "Limiting result rows" chapyer in Wikipedia: https://en.wikipedia.org/wiki/Select_%28SQL%29#Limiting_result_rows
You can also use "window functions" starting with Firebird version 3, but they are somewhat overkill for the simple task of "only give me first N rows".
Now, there is one more method that provides for embedding a totally voluntary condition, but that is from "ugly hacks" toolsets and does not work in a typical situation when several simultaneous connections form different client programs are running. You can use a "generator" as part of the WHILE clause:
Select .....
Where (GEN_ID(cancel_generator_name, 0) = 0) AND ( ...you normal conditions...)
You set the generator value to 0 before the query, and your client evaluates some conditions of your choice while reading the data, and when it wants to - from some another SQL command library object it issues the generator change command, which would immediately skip the rest of the query. However while sometimes this is a useful technique, but only in very specific rare situations.
Since Mark seems to be better guessing than me, then some outlines for future guesswork.
SP is a standard abbreviation for SQL Stored Procedure. Firebird's Execute Block is essentially an anonymous non-persistent SP.
So, we start with a persistent and named SP.
create or alter procedure SEQ (
FROM_1_TO integer not null)
returns (
COUNTER integer)
as
begin
counter = 1;
while ( counter <= from_1_to ) do begin
suspend;
counter = counter + 1;
end
end
Select 1, s.counter from rdb$database, seq(5) s
CONSTANT COUNTER
1 1
1 2
1 3
1 4
1 5
The next question would be how to
join the table with SP (stored procedure) dependent upon specific table row values
avoid SP being executed with NULL parameter values
The answer is - LEFT JOIN, as shown in the FAQ: http://www.firebirdfaq.org/faq143/
CREATE TABLE T2 (
ID INTEGER NOT NULL PRIMARY KEY,
TITLE VARCHAR(10) NOT NULL,
QTY INTEGER NOT NULL
);
INSERT INTO T2 (ID, TITLE, QTY) VALUES (1, 'aaaa', 2);
INSERT INTO T2 (ID, TITLE, QTY) VALUES (2, 'bbbb', 5);
INSERT INTO T2 (ID, TITLE, QTY) VALUES (3, 'ccccc', 4);
Select * from t2 t
left join seq(t.qty) s on 1=1
ID TITLE QTY COUNTER
1 aaaa 2 1
1 aaaa 2 2
2 bbbb 5 1
2 bbbb 5 2
2 bbbb 5 3
2 bbbb 5 4
2 bbbb 5 5
3 ccccc 4 1
3 ccccc 4 2
3 ccccc 4 3
3 ccccc 4 4
If you would have many different queries on different tables/fields that would require this rows-cloning added then having a dedicated counter-generating SP makes sense.
However if you only need this rather exotic rows cloning once, then maybe polluting a global namespace with an SP you would never need again would be less of a good idea.
It seems one can not select from an EB, though: Select from execute block?
So you would have to make a specific ad-hoc EB exactly for your select statement. Which, arguably, might be the very reason d'etre for anonymous non-persistent EB.
execute block
returns (ID INTEGER, TITLE VARCHAR(10), QTY INTEGER, COUNTER INTEGER)
as
begin
for select
id, title, qty from t2
into :id, :title, :qty
do begin
counter = 1;
while
(counter <= qty)
do begin
suspend;
counter = counter + 1;
end
end
end
However the data access library your application uses to connect to Firebird should understand then that while this query is not SELECT-query it still returns the "rowset". Usually they do, but who knows.
Not sure if this is possible without some sort of Dynamic SQL or a Pivot (which I want to stay away from)... I have a report that displays total counts for various types/ various status combinations... These types and statuses are always going to be the same and present on the report, so returning no data for a specific combination yields a zero. As of right now there are only three caseTypes (Vegetation, BOA, and Zoning) and 8 statusTypes (see below).
I am first setting up the skeleton of the report using a temp table. I have been careful to name the temp table columns the same as what the "statusType" column will contain in my second table "#ReportData". Is there a way to update the different columns in "#FormattedData" based on the value of the "statusType" column in my second table?
Creation of Formatted Table (for report):
CREATE TABLE #FormattedReport (
caseType VARCHAR(50)
, underInvestigation INT NOT NULL DEFAULT 0
, closed INT NOT NULL DEFAULT 0
, closedDPW INT NOT NULL DEFAULT 0
, unsubtantiated INT NOT NULL DEFAULT 0
, currentlyMonitored INT NOT NULL DEFAULT 0
, judicialProceedings INT NOT NULL DEFAULT 0
, pendingCourtAction INT NOT NULL DEFAULT 0
, other INT NOT NULL DEFAULT 0
)
INSERT INTO #FormattedReport (caseType) VALUES ('Vegetation')
INSERT INTO #FormattedReport (caseType) VALUES ('BOA')
INSERT INTO #FormattedReport (caseType) VALUES ('Zoning')
Creation of Data Table (to populate #FormattedReport):
SELECT B.Name AS caseType, C.Name AS StatusType, COUNT(*) AS Amount
INTO #ReportData
FROM table1 A
INNER JOIN table2 B ...
INNER JOIN table3 C ...
WHERE ...
GROUP BY B.Name, C.Name
CURRENT Update Statement (Currently will be 1 update per column in #FormattedReport):
UPDATE A SET underInvestigation = Amount FROM #ReportData B
INNER JOIN #FormattedReport A ON B.CaseType LIKE CONCAT('%', A.caseType, '%')
WHERE B.StatusType = 'Under Investigation'
UPDATE A SET closed = Amount FROM #ReportData B
INNER JOIN #FormattedReport A ON B.CaseType LIKE CONCAT('%', A.caseType, '%')
WHERE B.StatusType = 'Closed'
...
REQUESTED Update Statement: Would like to have ONE update statement knowing which column to update when "#ReportData.statusType" is the same as a "#FormattedData" column's name. For my "other" column, I'll just do that one manually using a NOT IN.
Assuming I understand the question, I think you can use conditional aggregation for this:
;WITH CTE AS
(
SELECT CaseType
,SUM(CASE WHEN StatusType = 'Under Investigation' THEN Amount ELSE 0 END) As underInvestigation
,SUM(CASE WHEN StatusType = 'Closed' THEN Amount ELSE 0 END) As closed
-- ... More of the same
FROM #ReportData
GROUP BY CaseType
)
UPDATE A
SET underInvestigation = B.underInvestigation
,closed = b.closed
-- more of the same
FROM #FormattedReport A
INNER JOIN CTE B
ON B.CaseType LIKE CONCAT('%', A.caseType, '%')
I have been banging my head trying to come up with the correct logic (SQL Server 2012) needed to achieve something I would imagine would be fairly routine but I have been unable to find any examples of this anywhere. Basically, I have 3 columns in a table: product, flag, value. It is possible for a product to be listed multiple times within the table but only once with a unique flag (i.e. product1 can have flag1 or flag2 with different/identical but there will never be 2 records with product1 and flag1 and different/identical values).
The flag represents a pre-defined value (1,2,3,4) and the intention behind this field is to be able to assign a unique mathematical equation based on the value of the flag. The end result would yield a single product, the unique flag, and a new cumulative total based on the mathematical equation output. For instance, let's say product1 was listed 4 times with flag values of flag1, flag2, flag3, flag4 (see below):
Product-----Flag-----Value
Product1----Flag1----1.00
Product1----Flag2----3.00
Product1----Flag3----5.00
Product1----Flag4----7.00
Product-----Flag-----Value
Product1----Flag1----1.00 (flag1 value)
Product1----Flag2----4.00 (flag1+flag2 value)
Product1----Flag3----6.00 (flag1+flag3 value)
Product1----Flag4----10.00 (flag2+flag4 value)
Flag1 is defined as add flag1 only. Flag2 is defined as add flag1 and flag2. Flag 3 is defined as add flag1 and flag 3. Flag 4 is defined as add flag2 and flag4. the new output would be product1 listed four times with flag values of flag1, flag2, flag3, flag4 but new values as flag1, flag1_flag2, flag1+flag3, flag2+flag4.
I have tried to apply the logic via a case statement but I can't figure out how to traverse all the products for each condition and I have tried to go with a running totals solution but I am not sure how to incorporate the flag condition into it so it only performs a running total for when those conditions are true. Any assistance and/or article to help get me going down the right path would be greatly appreciated.
While I'm not sure I fully understand your question I think this might be what you want. For this to work it assumes flag1 is always present when flags 1 through 3 are and that flag2 is present when flag4 is.
;with cte as (
select
product,
max(case when flag = 'Flag1' then Value end) as f1Value,
max(case when flag = 'Flag2' then Value end) as f2Value,
max(case when flag = 'Flag3' then Value end) as f3Value,
max(case when flag = 'Flag4' then Value end) as f4Value
from flags group by Product
)
select
flags.Product,
flags.Flag,
flags.Value as "Org. value",
case flag
when 'Flag1' then f1Value
when 'Flag2' then f1Value + f2Value
when 'Flag3' then f1Value + f3Value
when 'Flag4' then f2Value + f4Value
else flags.Value -- take the present value when flag is not Flag1-4
end as "New value"
from flags
inner join cte on flags.Product = cte.Product
Take a look at this Sample SQL Fiddle to see it in action.
You can join a table to itself, and pick the conditions appropriately:
SELECT p1.product,p1.Flag,p1.Value + COALESCE(p2.Value,0)
FROM
Products p1
left join
Products p2
on
p1.Product = p2.Product and
p2.Flag = CASE p1.Flag
--1 doesn't need a previous value
WHEN 2 THEN 1
WHEN 3 THEN 1
WHEN 4 THEN 2
END
I assumed and tried on Range values.
CREATE TABLE #tmp (Product VARCHAR(10), flag VARCHAR(10),value numeric(13,2))
GO
INSERT INTO #tmp
SELECT 'Product1' , 'Flag1',1
UNION
SELECT 'Product1' , 'Flag2',3
UNION
SELECT 'Product1' , 'Flag3',5
UNION
SELECT 'Product1' , 'Flag4',7
GO
;WITH cte
AS
(
SELECT row_number () OVER(
ORDER BY flag) 'row',*
FROM #tmp
)
SELECT *,value 'RT'
FROM cte
WHERE row = 1
UNION
SELECT * ,(
SELECT cte.value
FROM cte
WHERE row = 1
) + value 'RT'
FROM cte
WHERE row BETWEEN 2
AND 3
UNION
SELECT * ,(
SELECT cte.value
FROM cte
WHERE row =2
) + value 'RT'
FROM cte
WHERE row >3
GO
DROP TABLE #tmp
I have witnessed a strange behaviour while trying to GROUP BY a VARCHAR field.
Let the following example, where I try to spot customers that have changed name at least once in the past.
CREATE TABLE #CustomersHistory
(
Id INT IDENTITY(1,1),
CustomerId INT,
Name VARCHAR(200)
)
INSERT INTO #CustomersHistory VALUES (12, 'AAA')
INSERT INTO #CustomersHistory VALUES (12, 'AAA')
INSERT INTO #CustomersHistory VALUES (12, 'BBB')
INSERT INTO #CustomersHistory VALUES (44, '444')
SELECT ch.CustomerId, count(ch.Name) AS cnt
FROM #CustomersHistory ch
GROUP BY ch.CustomerId HAVING count(ch.Name) != 1
Which oddly yields (as if 'AAA' from first INSERT was different from the second one)
CustomerId cnt // (I was expecting)
12 3 // 2
44 1 // 1
Is this behaviour specific to T-SQL?
Why does it behave in this rather counter-intuitive way?
How is it customary to overcome this limitation?
Note: This question is very similar to GROUP BY problem with varchar, where I didn't find the answer to Why
Side Note: Is it good practice to use HAVING count(ch.Name) != 1 instead of HAVING count(ch.Name) > 1 ?
The COUNT() operator will count all rows regardless of value. I think you might want to use a COUNT(DISTINCT ch.Name) which will only count unique names.
SELECT ch.CustomerId, count(DISTINCT ch.Name) AS cnt
FROM #CustomersHistory ch
GROUP BY ch.CustomerId HAVING count(DISTINCT ch.Name) > 1
For more information, take a look at the COUNT() article on book online