【问题标题】:What query is needed to determine if any of a set of strings are contained in a column that also contains multiple values?需要什么查询来确定一组字符串中的任何一个是否包含在还包含多个值的列中?
【发布时间】:2021-02-16 15:36:27
【问题描述】:

也许这对我来说是一个不可靠的设计决定,但我创建了一个表,其中有一列包含 1 到 N 个逗号分隔值,我需要查询这些值中是否包含多个值中的任何一个。

为了更清楚,我允许用户选择一系列电影类型(喜剧、戏剧等),并有一个“电影”表,这些类型都包含在一个列中。例如,电影“公主新娘”的流派列包含复合 csv 值“冒险、家庭、奇幻”

因此,如果用户至少选择了其中一种类型,则该电影应包含在根据他们选择的搜索条件返回的结果集中。但电影应该只返回一次,即使用户选择了不止一种类型来搜索。

数据库的前一个化身包含查找表和多对多表,它们避免了这个问题,但为了通过将其设为一个大(宽)表来使其“更容易”和更直接,我运行了进入这个难题(关于如何制作查询)。

查询目前是以这种方式动态构建的:

string baseQuery = "SELECT * FROM MOVIES_SINGLETABLE ";
string imdbRatingFilter = "WHERE IMDBRating >= @IMDBMinRating ";
string yearRangeFilter = "AND YearReleased BETWEEN @EarliestYear AND @LatestYear ";
string genreFilterBase = "AND GENRE IN ({0}) ";
string mpaaRatingFilterBase = "AND MPAARating IN ({0}) ";
string orderByPortion = "ORDER BY IMDBRating DESC, YearReleased DESC ";

...以及包含所选标准的字符串,如下所示:

if (filterGenres)
{
    if (ckbxAction.Checked) genresSelected = "\'Action\',"; // changed to "@" hereafter:
    if (ckbxAdventure.Checked) genresSelected = genresSelected + @"'Adventure',";
        . . .
    if (ckbxWar.Checked) genresSelected = genresSelected + @"'War',";
    if (ckbxWestern.Checked) genresSelected = genresSelected + @"'Western',";
    LastCommaIndex = genresSelected.LastIndexOf(',');
    genresSelected = genresSelected.Remove(LastCommaIndex, 1)
}
// the same situation holds for mpaaRatings as for Genres:
if (filterMPAARatings)
{
    if (ckbxG.Checked) mpaaRatingsSelected = @"'G',";
    if (ckbxPG.Checked) mpaaRatingsSelected = mpaaRatingsSelected + @"'PG',";
    if (ckbxPG13.Checked) mpaaRatingsSelected = mpaaRatingsSelected + @"'PG13',";
    if (ckbxNR.Checked) mpaaRatingsSelected = mpaaRatingsSelected + @"'NR',";
    LastCommaIndex = mpaaRatingsSelected.LastIndexOf(',');
    mpaaRatingsSelected = mpaaRatingsSelected.Remove(LastCommaIndex, 1);
}

. . .

//string genreFilterBase = "AND GENRES IN ({0}) ";
if (filterGenres)
{
    genreFilter = string.Format(genreFilterBase, genresSelected);
    completeQuery = completeQuery + genreFilter;
}
//string mpaaRatingFilterBase = "AND MPAARating IN ({0}) ";
if (filterMPAARatings)
{
    mpaaRatingFilter = string.Format(mpaaRatingFilterBase, mpaaRatingsSelected);
    completeQuery = completeQuery + mpaaRatingFilter;
}

我的设计可以挽救吗? IOW,鉴于这些公认的有问题的表设计决策,我可以检索适当的数据吗?

更新

我通过将 GMB 的 SQL 合并到我的 SQL 中来测试它,但我可能做错了什么,因为它无法编译:

我不知道为什么会有这些逗号,但我认为 GMB 比我更擅长 SQL...

尽管如此,这确实工作(没有逗号和管道):

更新 2

我也尝试过使用 CONTAINS:

从 MOVIES_SINGLETABLE 中选择 * WHERE IMDBRating >= 7.5 和年份在 '1980' 和 '2020' 之间发布 并包含(类型,“冒险”) 或包含(流派、“家庭”) 或包含(类型,“奇幻”) 并包含(MPAARating,'G') 或包含(MPAARating,'PG') 或包含(MPAARating,'PG-13') ORDER BY IMDBRating DESC, YearReleased DESC

...但是得到“不能在表或索引视图 'MOVIES_SINGLETABLE' 上使用 CONTAINS 或 FREETEXT 谓词,因为它不是全文索引。”

Alex Aza 的答案 [https://stackoverflow.com/questions/6003240/cannot-use-a-contains-or-freetext-predicate-on-table-or-indexed-view-because-it]提供了一个解决方案,但显然它不适用于 SQL Server Express:

...此外,无论如何,这最终(很快)将被迁移到 SQLite 表,我怀疑如果在 SQL Server(尽管 Express)中执行它需要,即使可能,SQLite 是否支持 CONTAINS通过篮球。

更新 3

我结合了 Lukasz 对 SQLite 的想法(这就是我现在正在查询的内容),查询字符串最终是:

选择电影标题、MPAARating、IMDBRating、DurationInMinutes、 上映年份、流派、演员、导演、编剧 FROM MOVIES_SINGLETABLE WHERE IMDBRating >= @IMDBMinRating AND (在@EarliestYear 和@LatestYear 之间发布的年份)和 (INSTR(genres, @genre1) > 0 或 INSTR(genres, @genre2) > 0) AND (MPAARating = @mpaaRating1) 按 IMDBRating DESC 排序,发布年份 DESC 限制 1000

...但仍然没有结果。

【问题讨论】:

  • 这里有什么不使用LINQ的理由吗?
  • 没有“也许”。 :) 您成为classic blunders 之一的受害者——其中最著名的是“永远不要卷入亚洲的陆战”——但鲜为人知的是:“永远不要将 csv 值放在表格列中!”
  • 我猜这已经回答了?设计不好,如果数据库很小或不需要性能,您可以摆脱类似的条件,因为所有此类查询都会执行全表扫描。如果那是例如 Postgres 数据库 - 您可以将其存储在数组列(可索引)中,但不是在这里。另外,为什么 year 是 nchar(4) 而不是数字类型?
  • 在该屏幕截图上,您正在执行年份之间的查询,您假设进行数值比较。
  • @B.ClayShannon 您能否为某些参数值添加 3 或 4 行示例数据和您的预期结果集。这将帮助我验证您的查询并提出建议。

标签: c# sql sqlite csv where-clause


【解决方案1】:

使用 SQL Server CHARINDEX:

SELECT * 
FROM MOVIES_SINGLETABLE 
WHERE IMDBRating >= 7.5 AND YearReleased BETWEEN '1980' AND '2020' 
AND (
    CHARINDEX ('Adventure',genres) > 0
 OR CHARINDEX ('Family',genres) > 0
 OR CHARINDEX ( 'Fantasy',genres)  > 0
)
AND (
    CHARINDEX ('G', MPAARating) > 0
 OR CHARINDEX ('PG', MPAARating) > 0
 OR CHARINDEX ('PG-13', MPAARating) > 0
)
ORDER BY IMDBRating DESC, YearReleased DESC

或 SQLite INSTR:

SELECT * 
FROM MOVIES_SINGLETABLE 
WHERE IMDBRating >= 7.5 AND YearReleased BETWEEN '1980' AND '2020' 
  AND (
      INSTR (genres, 'Adventure') > 0
   OR INSTR (genres,'Family') > 0
   OR INSTR (genres, 'Fantasy') > 0
  )
  AND (
      INSTR (MPAARating, 'G') > 0
   OR INSTR (MPAARating, 'PG') > 0
   OR INSTR (MPAARating, 'PG-13') > 0
  )
ORDER BY IMDBRating DESC, YearReleased DESC;

db<>fiddle demo

注意事项:

  1. OR 条件周围添加了括号
  2. 以 CSV 格式存储数据并不是最好的设计(列不包含原子值)。

【讨论】:

  • 请看我的更新;当我将数据库转换为 SQLite 时,我尝试了上面的第二个想法。
  • @B.ClayShannon 请先尝试查询本身。 SQL 代码应该可以工作(提供现场演示),然后是 C# 代码/参数绑定(适当的数据类型以避免隐式转换)
【解决方案2】:

这确实是糟糕的设计。您应该首先努力修复它:CSV 列表中的每个元素都应该存储在单独的行中。见:Is storing a delimited list in a database column really that bad?

一种解决方法是使用一系列like 条件,如下所示:

and (
       ',' || mpaarating || ',' like '%,' || {0} || ',%'
    or ',' || mpaarating || ',' like '%,' || {1} || ',%'
    or ',' || mpaarating || ',' like '%,' || {2} || ',%'
)

【讨论】:

  • 我更新了我的问题,试图合并您的 SQL 解决方法。
【解决方案3】:

这可能就是你要找的东西

DECLARE @TempTable TABLE (FilmName VARCHAR(128), GENRE VARCHAR(128))
INSERT INTO @TempTable VALUES ('Film 1', '1,2,5')
INSERT INTO @TempTable VALUES ('Film 2', '1,3,4')
INSERT INTO @TempTable VALUES ('Film 3', '6')

DECLARE @SearchGenre VARCHAR(128) = '3,4,2'
DECLARE @SearchGenreT TABLE (gens VARCHAR(8))
INSERT INTO @SearchGenreT SELECT * FROM STRING_SPLIT(@SearchGenre, ',')

SELECT * FROM @TempTable
WHERE 
(
SELECT COUNT(a.value) from STRING_SPLIT(GENRE, ',') a
JOIN /*STRING_SPLIT(@SearchGenre, ',')*/ @SearchGenreT b ON a.value = b.gens
) > 0

【讨论】:

    【解决方案4】:

    是的,可以在您的限制范围内工作。它不漂亮,但它很实用。以下所有内容都在 SQL Server T-SQL 语法中。

    1. 查找或创建可以将逗号分隔的值拆分为表格的函数

    2. 通过您的函数运行流派列,以便将流派列表拆分为单个值的微型表;我们通过 CROSS APPLY 来做到这一点

    3. 在您的 WHERE 子句中,通过您的函数运行用户的首选项,以将其拆分为单独的值;查看代码以获得更清晰的信息。

    CREATE TABLE MOVIES_SINGLETABLE (
        MovieID nvarchar(10) NOT NULL,
        MovieTitle nvarchar(100) NOT NULL,
        Genres nvarchar(100) null
    )
    GO
    
    INSERT INTO MOVIES_SINGLETABLE VALUES ('M001', 'The Princess Bride', 'Fantasy, Action, Comedy')
    INSERT INTO MOVIES_SINGLETABLE VALUES ('M002', 'Die Hard', 'Action')
    INSERT INTO MOVIES_SINGLETABLE VALUES ('M003', 'Elf', 'Christmas, Holiday, Comedy')
    INSERT INTO MOVIES_SINGLETABLE VALUES ('M004', 'Percy Jackson and the Lightning-Thief', 'Fantasy')
    go
    
    DECLARE @genreList varchar(150) = 'Comedy, Action'
    
    -- IN SQL 2016 onward
    SELECT DISTINCT MovieTitle, Genres 
    FROM MOVIES_SINGLETABLE m
    CROSS APPLY STRING_SPLIT(m.genres, ',') mgenres
    WHERE TRIM(mgenres.value) IN (SELECT TRIM(value) FROM string_split(@genreList, ','))
    
    
    -- Before 2016, using a function dbo.stringSplit we create beforehand
    -- notice the syntax is nearly identical
    SELECT DISTINCT MovieTitle, Genres 
    FROM MOVIES_SINGLETABLE m
    CROSS APPLY dbo.stringSplit(m.genres, ',') mgenres
    WHERE LTRIM(RTRIM((mgenres.value))) IN (SELECT LTRIM(RTRIM(value)) FROM dbo.stringSplit(@genreList, ','))
    

    SQL Server 2016 及更高版本具有内置功能,并且似乎存在于 Express 许可证中。但是,这里是我使用的 dbo.stringSplit 函数代码,如果您想要它的不同变体,请提供源代码:

    /* SOURCE: https://stackoverflow.com/a/19935594/14443733 */
    CREATE FUNCTION dbo.stringSplit (
        @list NVARCHAR(max),
        @delimiter NVARCHAR(255)
        )
    RETURNS TABLE
    AS
    RETURN (
            SELECT [Value]
            FROM (
                SELECT [Value] = LTRIM(RTRIM(SUBSTRING(@List, [Number], CHARINDEX(@delimiter, @List + @delimiter, [Number]) - [Number])))
                FROM (
                    SELECT Number = ROW_NUMBER() OVER (
                            ORDER BY name
                            )
                    FROM sys.all_columns
                    ) AS x
                WHERE Number <= LEN(@List)
                    AND SUBSTRING(@delimiter + @List, [Number], DATALENGTH(@delimiter) / 2) = @delimiter
                ) AS y
            );
    GO
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-11-22
      • 2021-07-10
      • 1970-01-01
      • 2018-04-27
      • 1970-01-01
      • 2015-12-21
      • 2021-12-11
      相关资源
      最近更新 更多