I have a database column which is used as a grouping for variations for products and it is not in the First Normal Form. We don't need the Width, Height and the measurements to be in separate columns.
We have various attributes in this one database column, for example, product sizes like 1200*400mm and sometimes meters like 1m or even colors like red, blue and green.
The issue is that the CASE statement is being ignored and SQL is trying to Convert Black to an Int in the Case statement.
Currently the code needs to Sort the sizes correctly which the SQL now does. The issue comes in when it fetches a product with only colour attributes and the CASE statement is ignored near the Order By section.
I have created this SQL Fiddle to showcase the DB column with Sizes and Colors.
http://sqlfiddle.com/#!18/4e58e/1
This is my current SQL:
DECLARE #StockID int = 684
DECLARE #VariationParent int = (SELECT TOP 1 StockParent_ParentId FROM
StockVariations SV INNER JOIN FinGoodsParent FGP ON FGP.Id =
SV.StockParent_ChildId WHERE StockParent_ChildId = #StockID AND
SV.IsDeleted = 0 AND FGP.IsDeleted = 0 AND FGP.Publish = 1)
SELECT AV.ID, AV.AttrValue, AV.AttributeTypes_Id 'AttributeTypeID',
CAST(CASE WHEN SA.StockParent_Id = #StockID THEN 1 ELSE 0 END as BIT)
'IsDefault'
FROM AttributeTypes AT
INNER JOIN AttributeValues AV ON AV.AttributeTypes_Id = AT.Id
INNER JOIN StockParent_AttributeValues SA on SA.AttributeValue_Id = AV.Id
INNER JOIN FinGoodsParent FGP ON FGP.Id = SA.StockParent_Id AND
FGP.IsDeleted = 0 AND FGP.Publish = 1
WHERE SA.StockParent_Id IN (SELECT SV.StockParent_ChildId FROM
StockVariations SV INNER JOIN FinGoodsParent FGP ON FGP.Id =
SV.StockParent_ChildId AND FGP.IsDeleted = 0 AND FGP.Publish = 1 WHERE
SV.StockParent_ParentId = #VariationParent AND SV.IsDeleted = 0)
AND SA.IsDeleted = 0 AND AT.IsDeleted = 0 AND AV.IsDeleted = 0
ORDER BY
CASE WHEN (CHARINDEX('*', AV.AttrValue) > 0
AND CHARINDEX('mm', AV.AttrValue) > 0)
THEN
CONVERT(int, (select top 1 value from STRING_SPLIT(AV.AttrValue, '*'))) *
CONVERT(int, (select top 1 LEFT(value, LEN(value) - 2) from
STRING_SPLIT(AV.AttrValue, '*') where value LIKE '%mm'))
ELSE
AV.AttrValue
END
ASC
This is the error I am getting when trying to fetch a product with only color attributes and sorting it by the Attribute name with the CASE Statement.
So basically I got this SQL code to do the sorting from How to match a width and height size with a Regex expression and use Sort By in SQL or C# to build a drop-down?
I have added the CASE statement to try and only do the sorting when there is an Asterisk * and mm in the column, otherwise, I would just like to use Order By attrValue normally when the column contains, for example, red or black instead of numerics.
Updated
I have updated the SQL Fiddle as the data Output should only be either Sizes or Colours. So I have added an additional column with a type for this example. The SQL retrieving the data near the WHERE Clause should be either 0 for sizes or 1 for just colors and the SQL should still be able to work and order the Output of the sizes correctly.
http://sqlfiddle.com/#!18/56b3e/1
Explanations:
Consider the following:
CASE evaluates a list of conditions and returns the highest precedence type from the set of types. In your case you have int and varchar as possible return values and this explains your Conversion failed when converting the varchar value 'Black' to data type int error.
do not use SELECT TOP 1 FROM STRING_SPLIT() because STRING_SPLIT() doesn't guarantee that the order of the splitted substrings matches their positions in the original string. In your case you may try to use simple string parsing with LEFT, RIGHT and CHARINDEX functions. After that you need to convert the result from calculations to varchar.
Example:
Table:
CREATE TABLE attributes
([AttrValue] varchar(13))
;
INSERT INTO attributes
([AttrValue])
VALUES
('900*900mm'),
('1200*900mm'),
('1200*1200mm'),
('1200*6000mm'),
('1500*3000mm'),
('Red'),
('Green'),
('Black'),
('Purple')
;
Statement:
SELECT
AttrValue
FROM
attributes
ORDER BY
CASE
WHEN (CHARINDEX('*', AttrValue) > 0 AND CHARINDEX('mm', AttrValue) > 0) THEN
RIGHT(
REPLICATE('0', 13) +
CONVERT(
varchar(13),
CONVERT(int, LEFT(REPLACE(AttrValue, 'mm', ''), CHARINDEX('*', REPLACE(AttrValue, 'mm', '')) - 1)) *
CONVERT(int, RIGHT(REPLACE(AttrValue, 'mm', ''), LEN(REPLACE(AttrValue, 'mm', '')) - CHARINDEX('*', REPLACE(AttrValue, 'mm', ''))))
),
13
)
ELSE AttrValue
END ASC
Output:
--------------
AttrValue
--------------
900*900mm
1200*900mm
1200*1200mm
1500*3000mm
1200*6000mm
Black
Green
Purple
Red
Related
Due to a few limitations (which I won't go into here).. our architecture is using queries in Access running via ODBC SQL Server driver.
The following query produces 2 errors:
SELECT Tbl2.columnid,
Tbl2.label,
Tbl2.group1,
Tbl2.group2,
Count(Tbl2.columnid) AS Total
FROM (SELECT scanned AS Group1,
false AS Group2,
scanned AS Label,
scanned AS ColumnID
FROM (SELECT *,
( quantity - productqty ) AS Variance
FROM order_line
WHERE processed = false) AS Tbl1
WHERE wsid = 1 ) AS Tbl2
WHERE Tbl2.columnid = false
GROUP BY Tbl2.group1,
Tbl2.group2,
Tbl2.columnid,
Tbl2.label
ORDER BY Tbl2.group1 DESC,
Tbl2.group2
Error 1: Each GROUP BY Expression must contain at least one column that is an outer reference: (#164)
Error 2: The ORDER BY position number 0 is out of range of the number of items in the Select list (#108)
Its important to note that "scanned" is a BIT field in SQL Server (and therefore Group1, Label, ColumnId are also bits). I believe this is the reason why GROUP BY and ORDER BY are treating it as a constant (value=0), resulting in these errors.
But I do not know how to resolve these issues. Any suggestions would be great!
PS - The reason why 2 sub queries are being used is due to other constraints, where we are trying to get ID, Label, Counts for a column in Kanban.
Based on DRapp's comment and suggestion.. the following works:
SELECT Tbl2.columnid,
Tbl2.label,
Tbl2.group1,
Tbl2.group2,
Count(Tbl2.columnid) AS Total
FROM (SELECT IIf(scanned=True, 'Y', 'N') AS Group1,
'N' AS Group2,
IIf(scanned=True, 'Y', 'N') AS Label,
IIf(scanned=True, 'Y', 'N') AS ColumnID
FROM (SELECT *,
( quantity - productqty ) AS Variance
FROM order_line
WHERE processed = false) AS Tbl1
WHERE wsid = 1 ) AS Tbl2
WHERE Tbl2.columnid = 'N'
GROUP BY Tbl2.group1,
Tbl2.group2,
Tbl2.columnid,
Tbl2.label
ORDER BY Tbl2.group1 DESC,
Tbl2.group2
Not ideal (as the first subquery is generated dynamically, and now needs extra handling if group field is bit. But works! Still open to any other solutions.
Environment: MS SQL Server 2016.
I have a table which contains (Jasper Reports) layout representations like this (only relevant fields shown for brevity):
ID Name Key Version
1 CoverLetter <guid1> 1.00.00
2 Contract <guid2> 1.00.00
3 CoverLetter <guid1> 1.00.01
Goal:
I need an additional calculated field which is set to true or false according to
whether the record is the highest version of any given Layout or not (Same layout but different versions have same key, different layouts have different key).
Like this:
ID: Name: Key: Version: isHighestVersion: (calculated field)
1 CoverLetter <guid1> 1.00.00 false
2 Contract <guid2> 1.00.00 true
3 CoverLetter <guid1> 1.00.01 true
The SQL query which shows only the highest versions of each Layout is like this:
( SELECT TACMasterlayouts.*
FROM
(SELECT
TACMasterLayoutKey, MAX(TACMasterLayoutVersion) as TACMasterLayoutVersion
FROM
TACMasterlayouts
GROUP BY
TACMasterLayoutKey) AS latest_TACMasterLayouts
INNER JOIN
TACMasterlayouts
ON
TACMasterlayouts.TACMasterLayoutKey = latest_TACMasterLayouts.TACMasterLayoutKey AND
TACMasterlayouts.TACMasterLayoutVersion = latest_TACMasterLayouts.TACMasterLayoutVersion
)
But I need all records - the ones with highest version number per same key flagged with true and the rest flagged with false.
What I already did:
Searched google and SO but didn't find anything similar which I could transform into what I need.
Just change your INNER JOIN To a LEFT OUTER JOIN
and use a case in your
Select
EG
CASE WHEN latest_TACMasterLayouts.TACMasterLayoutKey IS NOT NULL THEN 1 ELSE 0 END as isHighestVersion
Thanks John,
this has pointed me into the right direction.
It has to be a RIGHT OUTER JOIN - otherwise only the records with highest version are shown.
As reference here the fully working code:
SELECT TACMasterlayouts.*, CASE WHEN latest_TACMasterLayouts.TACMasterLayoutKey IS NOT NULL THEN 1 ELSE 0 END as isHighestVersion
FROM
(SELECT TACMasterLayoutKey, MAX(TACMasterLayoutVersion) as TACMasterLayoutVersion
FROM
TACMasterlayouts
GROUP BY
TACMasterLayoutKey) AS latest_TACMasterLayouts
RIGHT OUTER JOIN
TACMasterlayouts
ON
TACMasterlayouts.TACMasterLayoutKey = latest_TACMasterLayouts.TACMasterLayoutKey AND
TACMasterlayouts.TACMasterLayoutVersion = latest_TACMasterLayouts.TACMasterLayoutVersion
)
You need to do some parsing in order to get desired result.
First, you split your version numbers into separate ints, then assign row_number based on them, and then based on row number, you put 1-true or 0-false in a extra column, which I called IsLatest.
In SQL Server there is no true or false, you can use BIT datatype, which has two values (just like boolean): 1 and 0.
Try this query:
declare #tbl table(ID int,Name varchar(20),[Key] varchar(10),Version varchar(10));
insert into #tbl values
(1,'CoverLetter','<guid1>','1.00.00'),
(2,'Contract','<guid2>','1.00.00'),
(3,'CoverLetter','<guid1>','1.00.01');
select ID, [Key], [version],
case when rn = 1 then 1 else 0 end IsLatest
from (
select *,
row_number() over (order by
cast(substring([version], 1, FirstDot - 1) as int) desc,
cast(substring([version], FirstDot + 1, SecondDot - FirstDot - 1) as int) desc,
cast(substring([version], SecondDot + 1, 100) as int) desc) rn
from (
select ID, [Key], [version],
charindex('.', [version]) FirstDot,
charindex('.', [version], charindex('.', [version]) + 1) SecondDot
from #tbl
) a
) a
What I'm looking for is a way in MSSQL to create a complex IN or LIKE clause that contains a SET of values, some of which will be ranges.
Sort of like this, there are some single numbers, but also some ranges of numbers.
EX: SELECT * FROM table WHERE field LIKE/IN '1-10, 13, 24, 51-60'
I need to find a way to do this WITHOUT having to specify every number in the ranges separately AND without having to say "field LIKE blah OR field BETWEEN blah AND blah OR field LIKE blah.
This is just a simple example but the real query will have many groups and large ranges in it so all the OR's will not work.
One fairly easy way to do this would be to load a temp table with your values/ranges:
CREATE TABLE #Ranges (ValA int, ValB int)
INSERT INTO #Ranges
VALUES
(1, 10)
,(13, NULL)
,(24, NULL)
,(51,60)
SELECT *
FROM Table t
JOIN #Ranges R
ON (t.Field = R.ValA AND R.ValB IS NULL)
OR (t.Field BETWEEN R.ValA and R.ValB AND R.ValB IS NOT NULL)
The BETWEEN won't scale that well, though, so you may want to consider expanding this to include all values and eliminating ranges.
You can do this with CTEs.
First, create a numbers/tally table if you don't already have one (it might be better to make it permanent instead of temporary if you are going to use it a lot):
;WITH Numbers AS
(
SELECT
1 as Value
UNION ALL
SELECT
Numbers.Value + 1
FROM
Numbers
)
SELECT TOP 1000
Value
INTO ##Numbers
FROM
Numbers
OPTION (MAXRECURSION 1000)
Then you can use a CTE to parse the comma delimited string and join the ranges with the numbers table to get the "NewValue" column which contains the whole list of numbers you are looking for:
DECLARE #TestData varchar(50) = '1-10,13,24,51-60'
;WITH CTE AS
(
SELECT
1 AS RowCounter,
1 AS StartPosition,
CHARINDEX(',',#TestData) AS EndPosition
UNION ALL
SELECT
CTE.RowCounter + 1,
EndPosition + 1,
CHARINDEX(',',#TestData, CTE.EndPosition+1)
FROM CTE
WHERE
CTE.EndPosition > 0
)
SELECT
u.Value,
u.StartValue,
u.EndValue,
n.Value as NewValue
FROM
(
SELECT
Value,
SUBSTRING(Value,1,CASE WHEN CHARINDEX('-',Value) > 0 THEN CHARINDEX('-',Value)-1 ELSE LEN(Value) END) AS StartValue,
SUBSTRING(Value,CASE WHEN CHARINDEX('-',Value) > 0 THEN CHARINDEX('-',Value)+1 ELSE 1 END,LEN(Value)- CHARINDEX('-',Value)) AS EndValue
FROM
(
SELECT
SUBSTRING(#TestData, StartPosition, CASE WHEN EndPosition > 0 THEN EndPosition-StartPosition ELSE LEN(#TestData)-StartPosition+1 END) AS Value
FROM
CTE
)t
)u INNER JOIN ##Numbers n ON n.Value BETWEEN u.StartValue AND u.EndValue
All you would need to do once you have that is query the results using an IN statement, so something like
SELECT * FROM MyTable WHERE Value IN (SELECT NewValue FROM (/*subquery from above*/)t)
I created the following select statement:
SELECT x.Code, y.AttributeCode, y.Value
FROM x INNER JOIN
y ON x.Id = y.ItemCodeId
WHERE (AttributeCode = 'Length' or AttributeCode = 'Width' or AttributeCode = 'Height')
The results display like this:
Code AttributeCode Value
1000165 Width 4
1000165 Length 19.5
1000165 Height 3.8
1000173 Length 3
1000173 Height 8
1000173 Width 5
And I'd like them to display as follows:
100165 Width 4 Length 19.5 Height 3.8
100173 Width 5 Length 3 Height 8
I apologize if this is a repeat but I looked through several other answers to try to answer this question (MS SQL is new to me so I may just not be using the right language when I'm searchign).
Tsk, tsk, someone is going EAV (Entity-Attribute-Value). Anyway, PIVOT can be used in this case to shred a finite set of values back into columns. It's an SQL Server extension to standard SQL - but it's a very useful extension for such cases:
PIVOT rotates a table-valued expression by turning the unique values from one column in the expression into multiple columns in the output, and performs aggregations where they are required on any remaining column values that are wanted in the final output.
Here is a SQL Fiddle showing PIVOT:
-- SETUP
create table x (entity int, attribute varchar(20), value float);
insert into x (entity, attribute, value) values
(1000165, 'Width', 4),
(1000165, 'Length', 19.5),
(1000165, 'Height', 3.8),
(1000173, 'Length', 3),
(1000173, 'Height', 8),
(1000173, 'Width', 5)
-- QUERY
SELECT pvt.*
FROM (SELECT entity, attribute, value FROM x) AS src
PIVOT (
-- Have to use an aggregate, but we have multiplicity of one as
-- presented so that's not an issue: MAX of any single value is itself.
-- Note that there is an implicit GROUP BY on columns NOT in
-- the aggregate ([value]) or used for the pivot ([attribute]) which
-- leaves only [entity] as the grouping column.
MAX(value)
FOR attribute IN ([Width], [Length], [Height])
) AS pvt
-- RESULT
ENTITY WIDTH LENGTH HEIGHT
1000165 4 19.5 3.8
1000173 5 3 8
The input to PIVOT (src) and the result from PIVOT can be filtered or joined as needed. The only real "gotcha" to watch out for with PIVOT is accidentally carrying over an extra (and incorrectly anticipated) grouping column
SELECT x.Code, a.AttributeCode, a.Value, b.AttributeCode, b.Value, c.AttributeCode, c.Value
FROM x
INNER JOIN y a ON x.Id = a.ItemCodeId
INNER JOIN y b ON x.Id = b.ItemCodeId
INNER JOIN y c ON x.Id = c.ItemCodeId
WHERE (a.AttributeCode = 'Width') and (b.AttributeCode = 'Length') and (c.AttributeCode = 'Height')
I want to select values from table in range.
Something like this:
SELECT
date_values.date_from,
date_values.date_to,
sum(values.value)
FROM values
inner join date_values on values.id_date = date_values.id
inner join date_units on date_values.id_unit = date_units.id
WHERE
date_values.date_from >= '14.1.2012' AND
date_values.date_to <= '30.1.2012' AND
date_units.id = 4
GROUP BY
date_values.date_from,
date_values.date_to
ORDER BY
date_values.date_from,
date_values.date_to;
But this query give me back only range of days, where is any value. Like this:
14.01.12 15.01.12 66
15.01.12 16.01.12 4
17.01.12 18.01.12 8
...etc
(Here missing 16.01.12 to 17.01.12)
But I want to select missing value too, like this:
14.01.12 15.01.12 66
15.01.12 16.01.12 4
16.01.12 17.01.12 0
17.01.12 18.01.12 8
...etc
I can't use PL/SQL and if can you advise more general solution which can I expand for use on Hours, Months, Years; will be great.
I'm going to assume you're providing date_from and date_to. If so, you can generate your list of dates first and then join to it to get the remainder of your result. Alternatively, you can union this query to your date_values table as union does a distinct this will remove any extra data.
If this is how the list of dates is generated:
select to_date('14.1.2012','dd.mm.yyyy') + level - 1 as date_from
, to_date('14.1.2012','dd.mm.yyyy') + level as date_to
from dual
connect by level <= to_date('30.1.2012','dd.mm.yyyy')
- to_date('14.1.2012','dd.mm.yyyy')
Your query might become
with the_dates as (
select to_date('14.1.2012','dd.mm.yyyy') + level - 1 as date_from
, to_date('14.1.2012','dd.mm.yyyy') + level as date_to
from dual
connect by level <= to_date('30.1.2012','dd.mm.yyyy')
- to_date('14.1.2012','dd.mm.yyyy')
)
SELECT
dv.date_from,
dv.date_to,
sum(values.value)
FROM values
inner join ( select the_dates.date_from, the_dates.date_to, date_values.id
from the_dates
left outer join date_values
on the_dates.date_from = date_values.date_from ) dv
on values.id_date = dv.id
inner join date_units on date_values.id_unit = date_units.id
WHERE
date_units.id = 4
GROUP BY
dv.date_from,
dv.date_to
ORDER BY
dv.date_from,
dv.date_to;
The with syntax is known as sub-query factoring and isn't really needed in this case but it makes the code cleaner.
I've also assumed that the date columns in date_values are, well, dates. It isn't obvious as you're doing a string comparison. You should always explicitly convert to a date where applicable and you should always store a date as a date. It saves a lot of hassle in the long run as it's impossible for things to be input incorrectly or to be incorrectly compared.