עסקתי לאחרונה בפיתוח מנגנון שמאפשר בצורה קלה לבצע SPLIT למילים מתוך טקסט .
יהיו אנשים שישאלו את עצמם למה זה טוב?
ובכן כאשר נתונה לנו עמודת תגיות אשר המילים בה משורשרות באמצעות פסיק או ברווח.
או לחלופין כאשר יש לנו עמודת תיאור מסוג טקסט ואנו רוצים ליצור טבלת מילות חיפוש לפי מילים.
לדוגמה ראו הטבלה הבאה:
המטרה הסופית היא ליצור טבלת חיפוש שתראה כך:
כאמור אני צריך לבצע מספר פעולות :
- לבטל את כל הסימנים המיוחדים (כגון $,%,תווי בקרה וכו)
- לבצע גזירה של המילים מתוך הטקסט.
- להשמיט מילות יחס (כגון I,AND , THE …)
- למנוע חזרה של מילים ברמת נשוא החיפוש GROUP BY)
לצורך ההדגמה אני משתמש בטבלה הבאה:
CREATE TABLE ##MoviesTable ( ID bigint NOT NULL IDENTITY(1, 1), Movies nvarchar(100) , Stars nvarchar(100))
INSERT INTO ##MoviesTable (Movies, Stars)
VALUES ('Pirates of the Caribbean: On Stranger Tides', 'Johnny Depp Penélope Cruz Ian McShane');
INSERT INTO ##MoviesTable (Movies, Stars)
VALUES ('Midnight in Paris', 'Owen Wilson Rachel McAdams Kathy Bates');
INSERT INTO ##MoviesTable (Movies, Stars)
VALUES ('Thor', 'Chris Hemsworth Anthony Hopkins Natalie Portman');
INSERT INTO ##MoviesTable (Movies, Stars)
VALUES ('Bridesmaids', 'Kristen Wiig Maya Rudolph Rose Byrne');
INSERT INTO ##MoviesTable (Movies, Stars)
VALUES ('Fast Five', 'Vin Diesel Paul Walker Dwayne Johnson')
כדי לבטל סימנים מיוחדים אני משתמש בפונקציות הבאות:
CREATE Function dbo.ReplaceWhiteWords(@Character nvarchar(max))
RETURNS nvarchar(max) As
BEGIN
Set @Character = Replace(@Character, ',', ' ')
Set @Character = Replace(@Character, '!', ' ')
Set @Character = Replace(@Character, '.', ' ')
Set @Character = Replace(@Character, '/', ' ')
Set @Character = Replace(@Character, '(', ' ')
Set @Character = Replace(@Character, ')', ' ')
Set @Character = Replace(@Character, '\', ' ')
Set @Character = Replace(@Character, ';', ' ')
Set @Character = Replace(@Character, '&', ' ')
Set @Character = Replace(@Character, '-', ' ')
Set @Character = Replace(@Character, ',', ' ')
Set @Character = Replace(@Character, '?', ' ')
Set @Character = Replace(@Character, '#', ' ')
Set @Character = Replace(@Character, '$', ' ')
Set @Character = Replace(@Character, '%', ' ')
Set @Character = Replace(@Character, '^', ' ')
Set @Character = Replace(@Character, '&', ' ')
Set @Character = Replace(@Character, '*', ' ')
Set @Character = Replace(@Character, '@', ' ')
Set @Character = Replace(@Character, '+', ' ')
Set @Character = Replace(@Character, '=', ' ')
Set @Character = Replace(@Character, '<', ' ')
Set @Character = Replace(@Character, '>', ' ')
return (dbo.Replace91013(@Character)) –- replace controle character
END
CREATE Function dbo.Replace91013(@Character nvarchar(max))
RETURNS nvarchar(max) As
BEGIN
Set @Character = Replace(@Character,char(9), ' ')
Set @Character = Replace(@Character,char(10), ' ')
Set @Character = Replace(@Character,char(13), ' ')
Set @Character = LTRIM(RTRIM(@Character))
Return (@Character)
End
עד כאן היה פשוט. עכשיו בשלב הבא אנו אמורים לגזור את המילים מתוך הטקסט
אני בחרתי בשיטה שלטעמי פשוטה ונוחה. אני פשוט ממיר את כל הטקסט לXML וממיר את הרווחים (או הפסיקים במידה ויש) לתגיות XML התוצאה של הפונקציה היא טבלה שמכילה שתי עמודות :
Item, מילה
records(r) זיהוי רשומה
CREATE FUNCTION dbo.Split
(
@delimited nvarchar(max), -- the original text
@delimiter nvarchar(100), -- the delimeter like [,] or [;] or [ ]
@IDRecourd bigint -- the ID of the recourd
) RETURNS @t TABLE
(
val nvarchar(max),
IDRecourd bigint
)
AS
BEGIN
declare @xml xml
set @xml = N'<root><r>' + replace(@delimited,@delimiter,'</r><r>') + '</r></root>' -- convert the text to xml
insert into @t(val,IDRecourd)
select
r.value('.','nvarchar(MAX)') as item, -- convert the word to item from xml
@IDRecourd -- give the ID record
from @xml.nodes('//root/r') as records(r)
RETURN
END
בשלב הסופי אני יצרתי פרוצדורה אשר משתמשת בפונקציות שתיארתי מקודם ליצירת הטבלה הסופית של המילים ממספר עמודות שונות.
גם כאן בחרתי להשתמש בXML אבל בשיטה קצת שונה. אני משתמש בביטוי FOR XML PATH בשני אופנים
פעם אחת בחלק הפנימי כדי לבנות את החלק הפנימי של הXML (‘Item’) לכל רשומה מקורית. ופעם שנייה לכלל הטבלה.
בסופו של התהליך אני מבצע שליפה מתוך הXML של המילים וזיהוי הרשומה.
CREATE PROCEDURE dbo.BuildSearchWord_sp1
AS
DECLARE @XML xml
SET @XML =
( SELECT
( SELECT ID, Val AS Word
FROM dbo.Split(dbo.ReplaceWhiteWords(Movies + ' ' + Stars), ' ', ID)
WHERE Val <> ''
AND LEN(Val) >= 2
FOR XML PATH('Item') , TYPE -- the internal XML
) AS 'Items'
FROM #MoviesTable
WHERE
LEN(Movies) > 2
AND LEN(Stars) > 2
FOR XML PATH
)
--SELECT @XML -- To get the XML opt.
SELECT
T.c.value ('ID[1]', 'bigint') AS ID, -- Select the ID from XML
T.c.value ('Word[1]', 'nvarchar(100)') AS Word -- Select the Word from XML
FROM
@xml.nodes('//row/Items/Item') AS T(c);
GO
להלן התוצאה:
exec BuildSearchWord_sp ID Word 1 Pirates 1 of 1 the 1 Caribbean: 1 On 1 Stranger 1 Tides 1 Johnny 1 Depp 1 Penélope 1 Cruz 1 Ian 1 McShane 2 Midnight 2 in 2 Paris 2 Owen 2 Wilson 2 Rachel 2 McAdams 2 Kathy 2 Bates 3 Thor 3 Chris 3 Hemsworth 3 Anthony 3 Hopkins 3 Natalie 3 Portman 4 Bridesmaids 4 Kristen 4 Wiig 4 Maya 4 Rudolph 4 Rose 4 Byrne 5 Fast 5 Five 5 Vin 5 Diesel 5 Paul 5 Walker 5 Dwayne 5 Johnson
לסיכום:
הבאתי כאן דוגמה לשימוש בXML לצורך גזירה פשוטה של מילים מתוך מספר עמודות טקסט.
לטעמי השיטה היא פשוטה יותר ודינמית מאשר שיטות של לולאות
נתראה ברשומה הבאה,
משה טיכר, DBA אפליקטיבי ומפתח אפליקציות בחברת CoolVision.
בעל וותק של 17 שנה בתחום מסדי נתונים ופיתוח .
ניתן לפנות לאלי במייל: moshe.taicher@gmail.com






שלום לכולם וברוכים הבאים לאתר קהילת בסיסי הנתונים של ישראל.
באתר תוכלו למצוא מאמרים וסרטונים טכנולוגיים רבים הקשורים לבסיסי הנתונים המובילים בישראל - Oracle, SQL Server, MySQL ועוד.
11 Responses to “שליפת מילים מתוך טקסט בTSQL”
Very nice post.
I can do it in Oracle with one query:
SQL> CREATE TABLE MoviesTable ( ID number not null, Movies varchar(100) , Stars
varchar(100));
Table created.
SQL> INSERT INTO MoviesTable (ID, Movies, Stars)
2 VALUES (1, ‘Pirates of the Caribbean: On Stranger Tides’, ‘Johnny Depp
Penélope Cruz Ian McShane’);
1 row created.
SQL> INSERT INTO MoviesTable (ID, Movies, Stars)
2 VALUES (2, ‘Midnight in Paris’, ‘Owen Wilson Rachel McAdams Kathy Bates’);
1 row created.
SQL> INSERT INTO MoviesTable (ID, Movies, Stars)
2 VALUES (3, ‘Thor’, ‘Chris Hemsworth Anthony Hopkins Natalie Portman’);
1 row created.
SQL> INSERT INTO MoviesTable (ID, Movies, Stars)
2 VALUES (4, ‘Bridesmaids’, ‘Kristen Wiig Maya Rudolph Rose Byrne’);
1 row created.
SQL> INSERT INTO MoviesTable (ID, Movies, Stars)
2 VALUES (5, ‘Fast Five’, ‘Vin Diesel Paul Walker Dwayne Johnson’);
1 row created.
SQL> commit;
Commit complete.
SQL> col word for a40
SQL> select * from (
2 select id,
3 regexp_substr(str,
4 ‘\S+’,
5 1,
6 row_number() over(partition by id order by i)
7 ) as word
8 from
9 (select id, Movies || ‘ ‘ || Stars as str from MoviesTable),
10 (select rownum i from dual connect by level <= (
11 select sum(regexp_count(Movies || ‘ ‘ || Stars,
12 ‘\S+’)) from MoviesTable)
13 )) where word is not null;
ID WORD
———- —————————————-
1 Pirates
1 of
1 the
1 Caribbean:
1 On
1 Stranger
1 Tides
1 Johnny
1 Depp
1 Penélope
1 Cruz
ID WORD
———- —————————————-
1 Ian
1 McShane
2 Midnight
2 in
2 Paris
2 Owen
2 Wilson
2 Rachel
2 McAdams
2 Kathy
2 Bates
ID WORD
———- —————————————-
3 Thor
3 Chris
3 Hemsworth
3 Anthony
3 Hopkins
3 Natalie
3 Portman
4 Bridesmaids
4 Kristen
4 Wiig
4 Maya
ID WORD
———- —————————————-
4 Rudolph
4 Rose
4 Byrne
5 Fast
5 Five
5 Vin
5 Diesel
5 Paul
5 Walker
5 Dwayne
5 Johnson
44 rows selected.
Amiel Davis
אהבתי את הפתרון של עמיאל דייויס ואני בתור אחד שעובד גם בסביבת אורקל וגם בסביבת SQLSERVER יכול רק לתהות למה לא מיישמים בTSQL פונקציות טרוויאליות שקיימות בPLSQL כגון פונקציות ממשפחת REGEXP . או הדבר הכי תמוה בעיני זה למה לא מיישמים את האפשרות של PARTITION WINDOW בפונקציות אנליטיות וזאת עוד במסד נתונים של חברה שניכסה לעצמה את המילה WINDOW כסימן רשום.
בכלל כאשר אני כותב בTSQL לעיתים אני מוצא את עצמי מקלל על הסירבול ועל המבחר המצומצם של TSQL לעומת PLSQL.
אבל איך אומרים “זה מה יש ואם זה צריך להסתדר”
You can use recursive sql to mimic oracle’s hierarchic sql:
Create the tables and insert rows as mentioned in the article.
Run the following block:
with MoviesTable as (
select ID, Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(Movies, ‘,’, ”), ‘!’, ”)
, ‘.’, ”)
, ‘/’, ”)
, ‘(‘, ”)
, ‘)’, ”)
, ‘\’, ”)
, ‘;’, ”)
, ‘&’, ”)
, ‘-’, ”)
, ‘,’, ”)
, ‘?’, ”)
, ‘#’, ”)
, ‘$’, ”)
, ‘%’, ”)
, ‘^’, ”)
, ‘&’, ”)
, ‘*’, ”)
, ‘@’, ”)
, ‘+’, ”)
, ‘=’, ”)
, ‘<’, ”)
, ‘>’, ”) Movies,
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(
Replace(Stars, ‘,’, ”), ‘!’, ”)
, ‘.’, ”)
, ‘/’, ”)
, ‘(‘, ”)
, ‘)’, ”)
, ‘\’, ”)
, ‘;’, ”)
, ‘&’, ”)
, ‘-’, ”)
, ‘,’, ”)
, ‘?’, ”)
, ‘#’, ”)
, ‘$’, ”)
, ‘%’, ”)
, ‘^’, ”)
, ‘&’, ”)
, ‘*’, ”)
, ‘@’, ”)
, ‘+’, ”)
, ‘=’, ”)
, ‘<’, ”)
, ‘>’, ”) Stars
from ##MoviesTable)
,MoviesTableMoviesSplit as (
select ID,1 LEVEL,Movies
from MoviesTable
union all
select ID,LEVEL + 1,Movies
from MoviesTableMoviesSplit
where LEVEL < len(Movies))
,MoviesTableStarsSplit as (
select ID,1 LEVEL,Stars
from MoviesTable
union all
select ID,LEVEL + 1,Stars
from MoviesTableStarsSplit
where LEVEL < len(Stars)),
ResultSplit as (
select ‘Movies’ TITLE,ID,LEVEL,
substring(Movies,case when CHARINDEX(‘ ‘,Movies,LEVEL) = LEVEL then LEVEL+1 else LEVEL end,
case when CHARINDEX(‘ ‘,Movies,LEVEL+1) <> 0 then CHARINDEX(‘ ‘,Movies,LEVEL+1) else LEN(Movies)+1 end-case when CHARINDEX(‘ ‘,Movies,LEVEL) = LEVEL then LEVEL+1 else LEVEL end) Text
from MoviesTableMoviesSplit
where (LEVEL = 1 or CHARINDEX(‘ ‘,Movies,LEVEL) = LEVEL)
and CHARINDEX(‘ ‘,Movies,LEVEL+1) <> LEVEL+1
union all
select ‘Stars’ TITLE,ID,LEVEL,
substring(Stars,case when CHARINDEX(‘ ‘,Stars,LEVEL) = LEVEL then LEVEL+1 else LEVEL end,
case when CHARINDEX(‘ ‘,Stars,LEVEL+1) <> 0 then CHARINDEX(‘ ‘,Stars,LEVEL+1) else LEN(Stars)+1 end-case when CHARINDEX(‘ ‘,Stars,LEVEL) = LEVEL then LEVEL+1 else LEVEL end) Text
from MoviesTableStarsSplit
where (LEVEL = 1 or CHARINDEX(‘ ‘,Stars,LEVEL) = LEVEL)
and CHARINDEX(‘ ‘,Stars,LEVEL+1) <> LEVEL+1)
select Text from ResultSplit
order by ID,TITLE,LEVEL
הפתרון של יונה הוא שונה
ואני מוצא בו שתי בעיות האחת היא זמני ביצוע ירודים
הרצתי את הפתרון של יונה לעומת הפתרון שלי והתוצאות הן
הפתרון של יונה
Total execution time 68
Bytes sent from client 5446
Bytes received from server 108086
הפתרון שלי
Total execution time 15
Bytes sent from client 226
Bytes received from server 80367
על פניו הפתרון שלי יותר יעיל.
הבעייה השנייה היא חוסר גמישות במידה ונרצה לחפש על עמודה נוספת לדוגמא director אזי בפתרון שלי כל השינוי יהיה בשתי שורות קוד המודגשות בכוכביות לפי הדוגמה הבאה
CREATE PROCEDURE dbo.BuildSearchWord_sp
AS
DECLARE @XML xml
SET @XML =
( SELECT
( SELECT ID, Val AS Word
FROM dbo.Split(dbo.ReplaceWhiteWords(Movies + ‘ ‘ + Stars + ‘ ‘ + Director), ‘ ‘, ID) —****
WHERE Val <> ”
AND LEN(Val) >= 2
FOR XML PATH(‘Item’) , TYPE
) AS ‘Items’
FROM #MoviesTable
WHERE
LEN(Movies) > 2
AND LEN(Stars) > 2
AND LEN(director) > 2 –****
FOR XML PATH
)
SELECT
T.c.value (‘ID[1]‘, ‘bigint’) AS ID,
T.c.value (‘Word[1]‘, ‘nvarchar(100)’) AS Word
FROM
@xml.nodes(‘//row/Items/Item’) AS T(c);
GO
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:”טבלה רגילה”;
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-priority:99;
mso-style-qformat:yes;
mso-style-parent:”";
mso-padding-alt:0cm 5.4pt 0cm 5.4pt;
mso-para-margin-top:0cm;
mso-para-margin-right:0cm;
mso-para-margin-bottom:10.0pt;
mso-para-margin-left:0cm;
line-height:115%;
mso-pagination:widow-orphan;
font-size:11.0pt;
font-family:”Calibri”,”sans-serif”;
mso-ascii-font-family:Calibri;
mso-ascii-theme-font:minor-latin;
mso-fareast-font-family:”Times New Roman”;
mso-fareast-theme-font:minor-fareast;
mso-hansi-font-family:Calibri;
mso-hansi-theme-font:minor-latin;
mso-bidi-font-family:Arial;
mso-bidi-theme-font:minor-bidi;}
משה תחילה אני אומר שגם אני חושב שפונקציה כמו SPLIT כמובן הייתה אמורה להיות מובנית בשרת. זו אינה הפונקציה היחידה שלטעמי חייבים להטמיע בכל שרת ואני למשל נוהג להטמיע מספר אלמנטים באופן קבוע בכל שרת כמו SPLIT,UNSPLIT, טבלת מספרים ועוד. “אבל” חשוב מאוד שיש כאן זה שאולי הסיבה שלא הוסיפו את זה ב SQL היא שלא ככה מומלץ לעבוד וזו דרך בזבזנית ולא טובה לבצע SPLIT (כתבתי בלוג בעבר עם דוגמאות ובדיקות בנושא וכמוני לפני ואחרי ניתן למצוא עשרות בלוגים בנושא ביצוע SPLIT ב SQL).
ב SQL יש לנו אופציה לעשות שימוש ב CLR. פעולות מסוג אלו הן פעולות מובהקות כמתאימות לשימוש ב CLR. ניתן לבנות בשורת קוד אחת פונקציה פשוטה ב CLR ולהטמיע אותה בשרת. אחרי הכול SPLIT היא פעולה הקיימת בכל שפת דוטנט בפקודה אחת.
תבדוק בלוגים ברשת ותבצע השוואות ותגיע בסיום לבד למסקנה שבשרתSQL הדרך הטובה היא שימוש בפונקצית SPLIT של CLR
אני לא רואה את התגובה ששמתי עם תגובה ללמה אין את זה ב SQL אבל בכל מקרה הנה קישור להשוואה יפה ושימוש ב CLR פשוט כאן ניתן לראות את הכוח של CLR שכן יש ב SQL http://sqlblog.com/blogs/aaron_bertrand/archive/2010/07/07/splitting-a-list-of-integers-another-roundup.aspx
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:”טבלה רגילה”;
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-priority:99;
mso-style-qformat:yes;
mso-style-parent:”";
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin-top:0in;
mso-para-margin-right:0in;
mso-para-margin-bottom:10.0pt;
mso-para-margin-left:0in;
line-height:115%;
mso-pagination:widow-orphan;
font-size:11.0pt;
font-family:”Calibri”,”sans-serif”;
mso-ascii-font-family:Calibri;
mso-ascii-theme-font:minor-latin;
mso-fareast-font-family:”Times New Roman”;
mso-fareast-theme-font:minor-fareast;
mso-hansi-font-family:Calibri;
mso-hansi-theme-font:minor-latin;
mso-bidi-font-family:Arial;
mso-bidi-theme-font:minor-bidi;}
אכן רעיון מעניין (שימוש ב- clr). כיוון דומה באורקל, יהיה על ידי כתיבת פונקציה חיצונית בשפת C (לחובבי ביצועים מצויינים) או ב- Java. ראוי לציין שהיכולת הזאת קיימת באורקל עוד מהמילניום הקודם…
[...] של משה טייכר בשם שליפת מילים מתוך טקסט בTSQL שהתפרסם לאחרונה עודד אותי לפרסם תגובה עקיפה בפוסט [...]
[...] של משה טייכר בשם שליפת מילים מתוך טקסט בTSQL שהתפרסם לאחרונה עודד אותי לפרסם תגובה עקיפה בפוסט [...]
[...] של משה טייכר בשם שליפת מילים מתוך טקסט בTSQL שהתפרסם לאחרונה עודד אותי לפרסם תגובה עקיפה בפוסט [...]
Can you please email me the code for this script or please let know me in detail in relation to this script?
Leave a Reply: