How to find value from comma-separated column - sql-server-2008

I am wondering how to write a SQL statement for SQL Server 2008 that selects entries where a column contains a comma-delimited value (usually - there could only be one entry (and no leading comma)) for instance:
Column1
---------------------------
10-15,20-30,31-97,104-187
Values in the column represents comma delimited ranges.
In this case I want to find 25.

Please consider normalizing your database. You are adding insult to injury by using a single column to keep multiple ranges data. A normalized database would have another table for the ranges, with a start value and an end value, along with a foreign key to the original table.
Something like this:
CREATE TABLE tblRange
(
Range_itemId int, --(fk to the original table)
Range_From int,
Range_To int
)
Should you do this, your query would be simply:
SELECT i.*
FROM tblItems
INNER JOIN tblRange ON(Item_Id = Range_ItemId)
WHERE Range_From <= 25
AND Range_To >= 25
However, if you can't normalize your database then you would have to use a split string function to create rows from your comma delimited column, and then parse the text of each row to find what is the range of each row.
Here is an example:
create demo table
CREATE TABLE ZZZItems
(
ItemId int identity(1,1),
ItemRanges varchar(500)
)
populate demo table
INSERT INTO ZZZItems VALUES
('10 - 20, 20 - 30, 30 - 40'),
('40 - 50, 50 - 60, 60 - 70'),
('70 - 80, 80 - 90, 90 - 100'),
('10 - 20, 20 - 30, 30 - 40')
using CROSS APPLY to a split function table and extracting the ranges from the splitted data
;WITH CTE AS
(
SELECT ItemId,
CAST(LEFT(Data, CHARINDEX('-', Data) - 1) As Int) As RangeFrom,
CAST(RIGHT(Data, LEN(Data) - CHARINDEX('-', Data)) As Int) As RangeTo
FROM ZZZItems
CROSS APPLY dbo.Split(ItemRanges, ',')
)
-- select the item ids where the requested number fits in the range.
SELECT ItemId
FROM CTE
WHERE RangeFrom < 25
AND RangeTo > 25
clean up
DROP TABLE ZZZItems
Results:
ItemId
-----------
1
4
I didn't add the split function, you should choose your own from the article I've linked to.

Related

I need to create a query with a dynamic range of value in mysql

I have a table with a column called "Points", the value of this column have a min value of 0, and a max value of 100.000. I have to do an analysis per range of this column, so I wrote a query like this:
select case when Points between 0 and 5000 then '0 - 5000'
when Points between 5001 and 20000 then '50001 - 20000'
when Points > 20000 then '> 20000'
else 0 end RangeP
from Sales
The problem is that the customer wants to see for each 2.000 points from 0 to 100.000
If I wrote the query using the case, the query will get really big, so I'd like one way to get dynamically this range.
It is possible? Thank you very much
You may create a table which contains the ranges, and their text labels, and then join to it, e.g.
SELECT
s.Points,
COALESCE(r.label, 'range not found') AS range
FROM Sales s
LEFT JOIN
(
SELECT 0 AS start, 2000 AS end, '0 - 2000' AS label UNION ALL
SELECT 2001, 4000, '2001 - 4000' UNION ALL
...
SELECT 98000, 100000, '98000 - 100000'
) r
ON s.Points BETWEEN r.start AND r.end;
I have inlined the table of ranges, but you may create a formal table (or maybe a temp table) instead, and then replace the above subquery with just a reference to that table.

Get value between from to dataset columns ssrs

I have a data set like that:
Data Set Contents
From To Comment
----+---+--------
0 50 Bad
50 70 Good
70 100 Excellent
If I have a value of 75, I need to get Excellent by searching the Dataset.
I know about the lookup function but it is not what I want. How can I do that?
The values should be in percentage.
Note : the value (75) is Average of a column (Calculated) it
calculate student grade from max and student mark Version SQL Server
2016
Note 2 : the dataset is from database not static values
Thank You
Assuming you only ever have a fixed number of 'grades' then this will work. However, I would strongly recommend doing this type of work on the server where possible.
Here we go...
I created two datasets
dsGradeRange with the following sql to recreate your example (more or less)
DECLARE #t TABLE (low int, high int, comment varchar(20))
INSERT INTO #t VALUES
(0,49,'Bad'),
(50,69,'Good'),
(70,100, 'Excellent')
SELECT * FROM #t
dsRandomNumbers This just creates 30 random numbers between 0 and 100
SELECT *
FROM (SELECT top 30 ABS(CHECKSUM(NEWID()) % 100) as myNumber FROM sys.objects) x
ORDER BY myNumber
I added a table to the report to show the grades (just for reference).
I then added a table to show the dsRandomNumbers
Finally I set the expression of the 2nd column to the following expression.
=SWITCH
(
Fields!myNumber.Value < LOOKUP("Bad", Fields!comment.Value, Fields!high.Value, "dsGradeRange"), "Bad",
Fields!myNumber.Value < LOOKUP("Good", Fields!comment.Value, Fields!high.Value, "dsGradeRange"), "Good",
True, "Excellent"
)
This gives the following results
As you can see we only need to compare to the high value of each case, the first match will return the correct comment.
Right click on your dataset and add a calculated field. Go to Field Properties > Fields > Add and add the following expression, which descripes your scenario:
=IIF(Fields!Number.Value < 50, "Bad", "Good")

How to INSERT a value based on the current date and a generated sequence number in MySQL?

I have this MySQL table:
CREATE TABLE bills
(
id_interess INT UNSIGNED NOT NULL,
id_bill VARCHAR(30) NULL,
PRIMARY KEY (id_interess)
) ENGINE=InnoDB;
And now I want to be able to manually insert unique integer for id_interess and automatically generate id_bill so that it consists of a current date and an integer (integer resets on a new year using trigger) like this:
id_interess |id_bill |
------------+-----------+
1 |20170912-1 |
2 |20171030-2 |
6 |20171125-3 |
10 |20171231-4 |
200 |20180101-1 |
3 |20180101-2 |
8 |20180102-3 |
If anyone has direct solution to this using only one query, I would be very glad! I only came up with a solution that uses three queries, but I still get some errors...
My newbie attempt: I created an additional column id_bill_tmp which holds integer part of id_bill like this:
CREATE TABLE bill
(
id_interess INT UNSIGNED NOT NULL,
id_bill_tmp INT UNSIGNED NULL,
id_bill VARCHAR(30) NULL,
PRIMARY KEY (id_interess)
) ENGINE=InnoDB;
Table from above would in this case look like this (note that on new year id_bill_tmp is reset to 1 and therefore I can't use AUTO_INCREMENT which can only be used on keys and keys need unique values in a column):
id_interess |id_bill_tmp |id_bill |
------------+--------------+-----------+
1 |1 |20170912-1 |
2 |2 |20171030-2 |
6 |3 |20171125-3 |
10 |4 |20171231-4 |
200 |1 |20180101-1 |
3 |2 |20180101-2 |
6 |3 |20180102-3 |
So for example to insert 1st row from the above table, table would have to be empty, and I would insert a value in three queries like this:
1st query:
INSERT INTO racuni (id_interess) VALUES (1);
I do this first because I don't know how to increment a nonexistent value for id_bill_tmp and this helped me to first get id_bill_tmp = NULL:
id_interess |id_bill_tmp |id_bill |
------------+--------------+-----------+
1 |[NULL] |[NULL] |
2nd query
Now I try to increment id_bill_tmp to become 1 - I tried two queries both fail saying:
table is specified twice both as a target for 'update' and as a separate source for data
This are the queries I tried:
UPDATE bills
SET id_bill_tmp = (SELECT IFNULL(id_bill_tmp, 0)+1 AS id_bill_tmp FROM bills)
WHERE id_interess = 1;
UPDATE bills
SET id_bill_tmp = (SELECT max(id_bill_tmp)+1 FROM bills)
WHERE id_interess = 1;
3rd query:
The final step would be to reuse id_bill_tmp as integer part of id_bill like this:
UPDATE bills
SET id_bill = concat(curdate()+0,'-',id_bill_tmp)
WHERE id_interess = 1;
so that I finally get
id_interess |id_bill_tmp |id_bill |
------------+--------------+-----------+
1 |1 |20170912-1 |
So if anyone can help me with the 2nd query or even present a solution with a single query or even without using column id_bill_tmp it would be wonderful.
Solution #1 - with the extra column
Demo
http://rextester.com/GOTPA70741
SQL
INSERT INTO bills (id_interess, id_bill_tmp, id_bill) VALUES (
1, -- (Change this value appropriately for each insert)
IF(LEFT((SELECT id_bill FROM
(SELECT MAX(CONCAT(LEFT(id_bill, 8),
LPAD(SUBSTR(id_bill, 10), 10, 0))) AS id_bill
FROM bills) b1), 4) = DATE_FORMAT(CURDATE(),'%Y'),
IFNULL(
(SELECT id_bill_tmp
FROM (SELECT id_bill_tmp
FROM bills
WHERE CONCAT(LEFT(id_bill, 8),
LPAD(SUBSTR(id_bill, 10), 10, 0)) =
(SELECT MAX(CONCAT(LEFT(id_bill, 8),
LPAD(SUBSTR(id_bill, 10), 10, 0)))
FROM bills)) b2),
0),
0)
+ 1,
CONCAT(DATE_FORMAT(CURDATE(),'%Y%m%d'), '-' , id_bill_tmp));
Notes
The query looks slightly more complicated that it actually is because of the issue that MySQL won't let you directly use a subselect from the same table that's being inserted into. This is circumvented using the method of wrapping another subselect around it as described here.
Solution #2 - without the extra column
Demo
http://rextester.com/IYES40010
SQL
INSERT INTO bills (id_interess, id_bill) VALUES (
1, -- (Change this value appropriately for each insert)
CONCAT(DATE_FORMAT(CURDATE(),'%Y%m%d'),
'-' ,
IF(LEFT((SELECT id_bill
FROM (SELECT MAX(CONCAT(LEFT(id_bill, 8),
LPAD(SUBSTR(id_bill, 10), 10, 0))) AS id_bill
FROM bills) b1), 4) = DATE_FORMAT(CURDATE(),'%Y'),
IFNULL(
(SELECT id_bill_tmp
FROM (SELECT SUBSTR(MAX(CONCAT(LEFT(id_bill, 8),
LPAD(SUBSTR(id_bill, 10), 10, 0))), 9)
AS id_bill_tmp
FROM bills) b2),
0),
0)
+ 1));
Notes
This is along the same lines as above but gets the numeric value that would have been in id_bill_tmp by extracting from the right part of id_bill from the 10th character position onwards via SUBSTR(id_bill, 10).
Step by step breakdown
CONCAT(...) assembles the string by concatenating its parts together.
DATE_FORMAT(CURDATE(),'%Y%m%d') formats the current date as yyyymmdd (e.g. 20170923).
The IF(..., <x>, <y>) is used to check whether the most recent date that is already present is for the current year: If it is then the numeric part should continue by incrementing the sequence, otherwise it is reset to 1.
LEFT(<date>, 4) gets the year from the most recent date - by extracting from the first 4 characters of id_bill.
SELECT MAX(...) AS id_bill FROM bills gets the most recent date + sequence number from id_bill and gives this an alias of id_bill. (See the notes above about why the subquery also needs to be given an alias (b1) and then wrapped in another SELECT). See the two steps below for how a string is constructed such that MAX can be used for the ordering.
CONCAT(LEFT(id_bill, 8), ...) is constructing a string that can be used for the above ordering by combining the date part with the sequence number padded with zeros. E.g. 201709230000000001.
LPAD(SUBSTR(id_bill, 10), 10, 0) pads the sequence number with zeros (e.g. 0000000001 so that MAX can be used for the ordering. (See the comment by Paul Spiegel to understand why this needs to be done - e.g. so that sequence number 10 is ordered just after 9 rather than just after 1).
DATE_FORMAT(CURDATE(),'%Y') formats the current date as a year (e.g. 2017) for the IF comparison mentioned in (3) above.
IFNULL(<x>, <y>) is used for the very first row since no existing row will be found so the result will be NULL. In this case the numeric part should begin at 1.
SELECT SUBSTR(MAX(...), 9) AS id_bill_tmp FROM bills selects the most recent date + sequence number from id_bill (as described above) and then extracts its sequence number, which is always from character position 9 onwards. Again, this subquery needs to be aliased (b2) and wrapped in another SELECT.
+ 1 increments the sequence number. (Note that this is always done since 0 is used in the cases described above where the sequence number should be set to 1).
If you are certain to be inserting in chronological order, then this will both bump the number and eliminate the need for the annual trigger:
DROP FUNCTION fcn46309431;
DELIMITER //
CREATE FUNCTION fcn46309431 (_max VARCHAR(22))
RETURNS VARCHAR(22)
DETERMINISTIC
SQL SECURITY INVOKER
BEGIN
RETURN
CONCAT(DATE_FORMAT(CURDATE(), "%Y%m%d"), '-',
IF( LEFT(_max, 4) = YEAR(CURDATE()),
SUBSTRING_INDEX(_max, '-', -1) + 1,
1 ) );
END
//
DELIMITER ;
INSERT INTO se46309431 (id_interess, id_bill)
SELECT 149, fcn46309431(MAX(id_bill)) FROM se46309431;
SELECT * FROM se46309431;
(If you might insert out of date order, then the MAX(..) can mess up.)
A similar solution is shown here: https://www.percona.com/blog/2008/04/02/stored-function-to-generate-sequences/
What you could do is to create a sequence with table, as shown there:
delimiter //
create function seq(seq_name char (20)) returns int
begin
update seq set val=last_insert_id(val+1) where name=seq_name;
return last_insert_id();
end
//
delimiter ;
CREATE TABLE `seq` (
`name` varchar(20) NOT NULL,
`val` int(10) unsigned NOT NULL,
PRIMARY KEY (`name`)
)
Then you need to populate the sequence values for each year, like so:
insert into seq values('2017',1);
insert into seq values('2018',1);
insert into seq values('2019',1);
...
(only need to do this once)
Finally, this should work:
insert into bills (id_interess, id_bill)
select
123,
concat(date_format(now(), '%Y%m%d-'), seq(date_format(now(), '%Y')));
Just replace 123 with some real/unique/dynamic id and you should be good to go.
I think you should redesign your approach to make life easier.
I would design your table as follows:
id_interess |id_counter |id_bill |
------------+--------------+-----------+
1 |1 |20170912 |
2 |2 |20171231 |
3 |1 |20180101 |
Your desired output for the first row would be "20170912-1", but you would merge id_counter and id_bill in your SQL-Query or in your application logic, not directly in a table (here is why).
Now you can write your SQL-Statements for that table.
Furthermore, I would advise not to store the counter in the table. You should only read the records' id and date from your database and calculate the id_counter in your application (or even in your SQL-Query).
You could also declare your column id_counter as auto_increment and reset it each time, see here.
One approach to do in single query would be just save the date in your table when ever you update any record. For id_bill no., generate a sequence when you want to display the records.
Schema
CREATE TABLE IF NOT EXISTS `bill` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
`bill_date` date NULL
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
Query
select a.id,concat(DATE_FORMAT(a.bill_date,"%Y%m%d"),'-',a.no) id_bill
from(
select b.*,count(b2.bill_date) no
from bill b
join bill b2 ON (EXTRACT(YEAR FROM b.bill_date) = EXTRACT(YEAR FROM b2.bill_date)
and b.bill_date >= b2.bill_date)
group by b.id
order by b.bill_date,no
) a
Inner query will return you the rank of each record per year by joining the same table outer query just format the data as per your desired view
DEMO
If for same date there can be more than 1 entries then in inner query the id column which is set to auto_increment can be used to handle this case
Updated Query
select a.id,concat(DATE_FORMAT(a.bill_date,"%Y%m%d"),'-',a.no) id_bill
from(
select b.*,count(b2.bill_date) no
from bill b
join bill b2 ON (EXTRACT(YEAR FROM b.bill_date) = EXTRACT(YEAR FROM b2.bill_date)
and b.id >= b2.id)
group by b.id
order by b.bill_date,no
) a
Updated Demo
The following solution requires generated (virtual) columns (available in MySQL 5.7 and MariaDB).
CREATE TABLE bills (
id_interess INT UNSIGNED NOT NULL,
bill_dt DATETIME DEFAULT CURRENT_TIMESTAMP,
bill_year YEAR AS (year(bill_dt)),
year_position INT UNSIGNED NULL,
id_bill VARCHAR(30) AS (concat(date_format(bill_dt, '%Y%m%d-'), year_position)),
PRIMARY KEY (id_interess),
INDEX (bill_year, year_position)
) ENGINE=InnoDB;
bill_year and id_bill are not stored in the table. They are derived from other columns. However - bill_year is stored in the index, which we need to get the last position for a specific year efficiently (it would also work without the index).
To insert a new row with the current timestamp:
insert into bills(id_interess, year_position)
select 1, coalesce(max(year_position), 0) + 1
from bills
where bill_year = year(now());
You can also use a custom timestamp or date:
insert into bills(id_interess, bill_dt, year_position)
select 10, '2016-01-01', coalesce(max(year_position), 0) + 1
from bills
where bill_year = year('2016-01-01')
Demo: https://www.db-fiddle.com/f/8pFKQb93LqNPNaD5UhzVwu/0
To get even simpler inserts, you can create a trigger which will calculate year_postion:
CREATE TRIGGER bills_after_insert BEFORE INSERT ON bills FOR EACH ROW
SET new.year_position = (
SELECT coalesce(max(year_position), 0) + 1
FROM bills
WHERE bill_year = year(coalesce(new.bill_dt, now()))
);
Now your insert statement would look like:
insert into bills(id_interess) values (1);
or
insert into bills(id_interess, bill_dt) values (11, '2016-02-02');
And the select statements:
select id_interess, id_bill
from bills
order by id_bill;
Demo: https://www.db-fiddle.com/f/55yqMh4E1tVxbpt9HXnBaS/0
Update
If you really, really need to keep your schema, you can try the following insert statement:
insert into bills(id_interess, id_bill)
select
#id_interess,
concat(
date_format(#date, '%Y%m%d-'),
coalesce(max(substr(id_bill, 10) + 1), 1)
)
from bills
where id_bill like concat(year(#date), '%');
Replace #id_interess and #date accordingly. For #date you can use CURDATE() but also any other date you want. There is no issue inserting dates out of order. You can even insert dates from 2016 when entries for 2017 already exist.
Demo: http://rextester.com/BXK47791
The LIKE condition in the WHERE clause can use an index on id_bill (if you define it), so the query only need to read the entries from the same year. But there is no way to determine the last counter value efficiently with this schema. The engine will need to read all rows for the cpecified year, extract the counter and search for the MAX value. Beside the complexity of the insert statement, this is one more reason to change the schema.

Finding count of unique value before a character

I have a some entries in database table rows as follows.
101 - 1
101 - 2
101 - 3
102 - 1
102 - 2
102 - 3
103
I need to get the result of SELECT Query for count as '3' since there are 101 and 102 are the only number before the -.
So is there any way to find the unique value in db table columns before a character?
EDIT : I have entries even without the - .
In case your entries have always the format you have provided us, you just have to find the position of the '-' character, split the values, get the first n characters and count the distinct values
This works for SQL Server, otherwise informs us about what DBMS you are using or replace the functions with the ones of your DBMS on your own
SELECT COUNT(DISTINCT SUBSTRING(val,0,CHARINDEX('-', val))) from YourTable
create table T1
(
id int primary key identity,
col1 varchar(20)
)
insert into T1 values('101 - 1'),('101 - 2'),('101 - 3'),('102 - 1'),('102 - 2'),('102 - 3')
select SUBSTRING(col1,0,CHARINDEX(' ',col1)) as 'Value',count(*) as 'Count' from T1 group by SUBSTRING(col1,0,CHARINDEX(' ',col1))

Break Numbers List Into Min and Max Ranges

Brain is not working today and my google skills are failing me.
I have a column of numbers ranging from 1 - 1000. I want to dump the min and max values for 100 (or whatever I chose) record ranges into a temp table. The plan is to use this temp table to process ranges of records (in this example 100 at a time) in a larger table.
Swear I have done this before with a CTE but then I had something to group on. Here I just want to break up a single list of numbers into ranges of X.
The output from the temp table should look like:
Min Max
0 99
100 199
200 299
300 399
etc.
Thanks!
You can use this trick from Stuart Ainsworth:
http://codegumbo.com/index.php/2009/01/25/building-ranges-using-a-dynamically-generated-numbers-table/
Numbers tables are awesome, but he uses a dynamically generated numbers table, which is even awesome...r.
If you know all numbers are present in the source table, you can use a recursive CTE to generate the number ranges:
; with numbers as
(
select 0 as a
, 99 as b
union all
select a+100
, b+100
from numbers
where a < 900
)
select *
from numbers
If the source table is sparsely populated, you can limit it to numbers that are actually present like:
... insert CTE from above here ...
select min(ot.NumberColumn)
, max(ot.NumberColumn)
from numbers
left join
OtherTable ot
on ot.NumberColumn between numbers.a and numbers.b
group by
numbers.a
enter code hereI have been having a play with a CTE after you posted this and came up with the following, I would be interested to hear if it works for you at all.
DECLARE #segment int = 100
;
WITH _CTE
(rowNum, value)
AS
(
SELECT ROW_NUMBER() OVER(ORDER BY col01) -1, col01
FROM dbo.testTable
)
SELECT rowNum/#segment AS Bucket, MIN(Value) AS MinVal, MAX(Value) AS MaxVal
FROM _CTE
group by rowNum/#segment
ORDER BY Bucket
;
col01 in this case is the column that you want the min/max range values from, as is TestTable.