I have a pl/pgsql script that needs to check if a word/sentence is in a string, and it must take care of word boundaries, and case insenstive.
Example:
String: "my label xx zz yy", Pattern: "my label", MATCH
String: "xx my label zz", Pattern: "my label", MATCH
String: "my labelxx zz", Pattern: "my label", NO MATCH
So the obvious solution is to use a regex, like this:
select _label ~* (E'\\y' || _pattern || E'\\y') into _match;
It works but is slow, compared to a simple
select _label ilike '%' || _pattern || '%' into _match;
This is wrapped in a function that my script calls A LOT (in the tens of millions, I do a lot of recursion), and with this requirement the overall runtime doubled.
Now my question is, is there a faster way to implement this ?
Thanks.
EDIT: ended up using this:
if _label ilike '%' || _pattern || '%' then
select _label ~* (E'\\m' || _pattern || E'\\M') into _match;
end if;
and it is significantly faster.
I would consider the full text search capabilities, but from what you're describing, I'd likely implement this using PostgreSQL arrays.
First: define a function that takes a label, lowercases it (or uppercase if you prefer), splits it on word boundaries, and returns an array. Say:
CREATE OR REPLACE FUNCTION label_to_array(text) RETURNS text[] AS $$
SELECT regexp_split_to_array(lower($1), E'\\W');
$$ LANGUAGE sql IMMUTABLE;
$ select label_to_array('my label xx zz yy');
label_to_array
---------------------
{my,label,xx,zz,yy}
Now, create a GIN index over this function:
CREATE INDEX sometable_label_array_key ON sometable
USING GIN((label_to_array(label));
From here, PostgreSQL can use this index for many queries involving array operators, such as "contains":
SELECT *
FROM sometable
WHERE label_to_array(label) #> label_to_array('my label');
This query would split 'my label' into {my,label}, and would then use the index to find a list of rows containing my, intersect that with the list of rows containing label, and then return the result. This isn't exactly equivalent to your original query (since it doesn't check their order), but since it uses an index to eliminate most of the rows in the table, adding the original check on the end would work just fine:
SELECT *
FROM sometable
WHERE label_to_array(label) <# label_to_array('my label')
AND label ~* (E'\\y' || 'my label' || E'\\y');
Related
Hope one can help me and explain this query for me,
why the first query return result but the second does not:
EDIT:
first query:
select name from Items where name like '%abc%'
second Query:
select name from Items where name like substring('''%abc%''',1,10)
why the first return result but the second return nothing while
substring('''%abc%''',1,10)='%abc%'
If there are a logic behind that, Is there another approach to do something like the second query,
my porpuse is to transform a string like '''abc''' to 'abc' in order to use like statement,
You can concatenate strings to form your LIKE string. To trim the first 3 and last 3 characters from a string use the SUBSTRING and LEN functions. The following example assumes your match string is called #input and starts and ends with 3 quote marks that need to be removed to find a match:
select name from Items where name like '%' + SUBSTRING(#input, 3, LEN(#input) - 4) + '%'
I have a problem where I think you can also use when needed. I wish to compare word sets on separate tables identified by their Item No and Order (ordinality) and the word or value.
Here is a snapshot of the table:
Then the result I wish to accomplish was like this:
The comparison is LIKE rather than equality.
You can do a join and have like in predicate:
select * from table1 t1
inner join table2 t2 on t1.ValueA like '%' + t2.ValueB + '%' or
t2.ValueB like '%' + t1.ValueA + '%'
You may need use * instead of % and & instead of +. I don't remember the correct syntax for Access, but it is along the lines.
I need to create a subquery (or view) column with values pulled from part of a long string. Values will appear like this:
"Recruiter: Recruiter Name Date:..."
I need to select the recruiter name after : and end with the space after the recruiter name. I understand that normalizing would be better, but we only have query access not database setup access in this case.
Ideas appreciated!
You can use a regex for this. A regex will let you express that you want to search for the text Recruiter followed by a colon, a space, and a series of characters followed by a space, and that you want it to extract those characters.
The expression might look a bit like this (untested)
Recruiter: (.+) Date:
This would look for 'Recruiter: ' literally, followed by a string of any characters (.) of length 1 or larger (+), which is to be extracted (the brackets), followed by the literal string ' Date:'.
How you use this with SQL depends on your vendor.
I would create a function that pulls out the value for a given key. You would use it like:
select [dbo].[GetValue]('recruiter',
'aKey: the a value Recruiter: James Bond cKey: the c value')
This returns 'James Bond'
Here is the function:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
create function [dbo].[GetValue](#Key varchar(50), #Line varchar(max))
returns varchar(max)
as
begin
declare #posStart int, #posEnd int
select #posStart=charindex(#Key, #Line) -- the start of the key
if(#posStart = 0)
return '' -- key not found
set #posStart = #posStart + len(#Key) + 1 -- the start of the value
select #line = substring(#line, #posStart, 1000) -- start #Line at the value
select #posEnd=charindex(':', #line) -- find the next key
if(#posEnd > 0)
begin
-- shorten #line to next ":"
select #line = substring(#line, 0, #posEnd)
-- take off everything after the value
select #posEnd = charindex(' ', reverse(#line));
if(#posEnd > 0)
select #line = substring(#line, 0, len(#line) - #posEnd + 1)
end
return rtrim(ltrim(#line))
end
go
I am trying to find if a string exist in a word and extract it. I have uses the instr() function but this works as the LIKE function: if part or the whole word exists it returns it.
Here I want to get the string 'Services' out, it works but if I change 'Services' to 'Service' it still works. I don't want that. If 'Service' is entered it should return null and not 'Services'
Modified:
What I am trying to do here is abbreviate certain parts of the company name.
This is what my database table looks like :
Word | Abb
---------+-----
Company | com
Limited | ltd
Service | serv
Services | servs
Here is the code:
Declare
Cursor Words Is
SELECT word,abb
FROM abbWords
processingWord VARCHAR2(50);
abbreviatedName VARCHAR(120);
fullName = 'A.D Company Services Limited';
BEGIN
FOR eachWord IN Words LOOP
--find the position of the word in name
wordPosition := INSTR(fullName, eachWord.word);
--extracts the word form the full name that matches the database
processingWord := Substr(fullName,instr(fullName,eachWord.word), length(eachWord.word));
--only process words that exist in name
if wordPosition > 0 then
abbreviatedName = replace(fullName, eachWord.word,eachWord.abb);
end if;
END lOOP;
END;
So if the user enters 'Service' I don't want 'Services' to be returned. By this I mean word position should be 0 if the word 'Service' in not found instead of returning the position for the word 'Services'
One way of doing it:
DECODE(INSTR('A.D Company Seervices Limited','Services'),
0,
NULL,
SUBSTR('A.D Company Services Limited',
INSTR('A.D Company Services Limited','Services'),
length('Services')))
INSTR() will return 0 if text is not found. DECODE() will evaluate the first argument, compare to the second, if match, return third argument, if not, return fourth argument. (sqlfiddle link)
Arguably not the most elegant way, but matches your requirement.
I think you're over-complicating this. You can do everything with regular expressions. For instance; given the following table:
create table names ( name varchar2(100));
insert into names values ('A.D Company Services Limited');
insert into names values ('A.D Company Service Limited');
This query will only return the name 'A.D Company Services Limited'.
select *
from names
where regexp_like( name
, '(^|[[:space:]])services($|[[:space:]])'
, 'i' )
This means match the beginning of the string, ^, or a space followed by services followed the end of the string, $, or a space. This is what differentiates regular expressions from using instr etc. You can make your matches easily conditional on other factors.
However, though this seems to be your question I don't think this is what you're trying to do. You're trying to replace the string 'serv' in your wider string without replacing 'services' or 'service'. For this you need to use regexp_replace().
If I add the following row to the table:
insert into names values ('A.D Company Serv Limited');
and run this query:
select regexp_replace( name
, '(^|[[:space:]])serv($|[[:space:]])'
, ' Services '
, 1, 0, 'i' )
from names
The only thing that will change is ' Serv ', which in this newest line, will be replaced with ' Services '. Note the spaces; as you don't want to replace 'Services' with 'ServServices' these are very important.
Here's a little SQL Fiddle to demonstrate.
Another alternative is to use something like:
select replace(name,' serv ', ' Services ')
from names;
This will replace only the word 'Serv' situated between 2 spaces.
Thank you,
Alex.
INSTR returns a number: the index of the first occurrence of the matching string. You should use regexp_substr instead (10g+):
SQL> select regexp_substr('A.D Company Services Limited', 'Services') match,
2 regexp_substr('A.D Company Service Limited', 'Services') unmatch
3 from dual;
MATCH UNMATCH
-------- -------
Services
I am rolling up a huge table by counts into a new table, where I want to change all the empty strings to NULL, and typecast some columns as well. I read through some of the posts and I could not find a query, which would let me do it across all the columns in a single query, without using multiple statements.
Let me know if it is possible for me to iterate across all columns and replace cells with empty strings with null.
Ref: How to convert empty spaces into null values, using SQL Server?
To my knowledge there is no built-in function to replace empty strings across all columns of a table. You can write a plpgsql function to take care of that.
The following function replaces empty strings in all basic character-type columns of a given table with NULL. You can then cast to integer if the remaining strings are valid number literals.
CREATE OR REPLACE FUNCTION f_empty_text_to_null(_tbl regclass, OUT updated_rows int)
LANGUAGE plpgsql AS
$func$
DECLARE
_typ CONSTANT regtype[] := '{text, bpchar, varchar}'; -- ARRAY of all basic character types
_sql text;
BEGIN
SELECT INTO _sql -- build SQL command
'UPDATE ' || _tbl
|| E'\nSET ' || string_agg(format('%1$s = NULLIF(%1$s, '''')', col), E'\n ,')
|| E'\nWHERE ' || string_agg(col || ' = ''''', ' OR ')
FROM (
SELECT quote_ident(attname) AS col
FROM pg_attribute
WHERE attrelid = _tbl -- valid, visible, legal table name
AND attnum >= 1 -- exclude tableoid & friends
AND NOT attisdropped -- exclude dropped columns
AND NOT attnotnull -- exclude columns defined NOT NULL!
AND atttypid = ANY(_typ) -- only character types
ORDER BY attnum
) sub;
-- RAISE NOTICE '%', _sql; -- test?
-- Execute
IF _sql IS NULL THEN
updated_rows := 0; -- nothing to update
ELSE
EXECUTE _sql;
GET DIAGNOSTICS updated_rows = ROW_COUNT; -- Report number of affected rows
END IF;
END
$func$;
Call:
SELECT f_empty2null('mytable');
SELECT f_empty2null('myschema.mytable');
To also get the column name updated_rows:
SELECT * FROM f_empty2null('mytable');
db<>fiddle here
Old sqlfiddle
Major points
Table name has to be valid and visible and the calling user must have all necessary privileges. If any of these conditions are not met, the function will do nothing - i.e. nothing can be destroyed, either. I cast to the object identifier type regclass to make sure of it.
The table name can be supplied as is ('mytable'), then the search_path decides. Or schema-qualified to pick a certain schema ('myschema.mytable').
Query the system catalog to get all (character-type) columns of the table. The provided function uses these basic character types: text, bpchar, varchar, "char". Only relevant columns are processed.
Use quote_ident() or format() to sanitize column names and safeguard against SQLi.
The updated version uses the basic SQL aggregate function string_agg() to build the command string without looping, which is simpler and faster. And more elegant. :)
Has to use dynamic SQL with EXECUTE.
The updated version excludes columns defined NOT NULL and only updates each row once in a single statement, which is much faster for tables with multiple character-type columns.
Should work with any modern version of PostgreSQL. Tested with Postgres 9.1, 9.3, 9.5 and 13.