Эффективный запрос SQL 2000 для выбора предпочтительной конфеты - PullRequest
3 голосов
/ 28 июня 2009

(хотелось бы придумать более описательный заголовок ... предложите один или отредактируйте этот пост, если вы можете назвать тип запроса, о котором я спрашиваю)

База данных: SQL Server 2000

Пример данных (предположим, 500 000 строк):

<b>Name   Candy       PreferenceFactor</b>
Jim    Chocolate   1.0
Brad   Lemon Drop   .9
Brad   Chocolate    .1
Chris  Chocolate    .5
Chris  Candy Cane   .5
<i>499,995 more rows...</i>

Обратите внимание, что число строк с данным «Именем» не ограничено.

Желаемые результаты запроса:

Jim    Chocolate   1.0
Brad   Lemon Drop   .9
Chris  Chocolate    .5
<i>~250,000 more rows...</i>

(Поскольку Крис одинаково предпочитает Candy Cane и Chocolate, последовательный результат является адекватным).

Вопрос: Как выбрать Имя, Candy из данных, где каждая результирующая строка содержит уникальное имя, так что выбранный Candy имеет самый высокий PreferenceFactor для каждого имени. (быстрые эффективные ответы предпочтительнее).

Какие индексы требуются для таблицы? Имеет ли значение, если Name и Candy являются целочисленными индексами в другой таблице (кроме необходимости некоторых объединений)?

Ответы [ 11 ]

6 голосов
/ 30 июня 2009

Вы обнаружите, что следующий запрос превосходит все остальные ответы, так как он работает с одним сканированием. Это имитирует первую и последнюю агрегатные функции MS Access, что в основном и делает.

Конечно, вы, вероятно, будете иметь внешние ключи вместо имен в таблице CandyPreference. Чтобы ответить на ваш вопрос, на самом деле было бы лучше, если бы Candy и Name были внешними ключами в другой таблице.

Если в таблице CandyPreferences есть другие столбцы, то наличие покрывающего индекса, который включает в себя задействованные столбцы, приведет к еще большей производительности. Сделав столбцы как можно меньше, вы увеличите количество строк на странице и снова увеличите производительность. Если вы чаще всего выполняете запрос с условием WHERE для ограничения строк, тогда становится важным индекс, охватывающий условия WHERE.

Питер был на правильном пути для этого, но имел некоторую ненужную сложность.

CREATE TABLE #CandyPreference (
   [Name] varchar(20),
   Candy varchar(30),
   PreferenceFactor decimal(11, 10)
)
INSERT #CandyPreference VALUES ('Jim', 'Chocolate', 1.0)
INSERT #CandyPreference VALUES ('Brad', 'Lemon Drop', .9)
INSERT #CandyPreference VALUES ('Brad', 'Chocolate', .1)
INSERT #CandyPreference VALUES ('Chris', 'Chocolate', .5)
INSERT #CandyPreference VALUES ('Chris', 'Candy Cane', .5)

SELECT
   [Name],
   Candy = Substring(PackedData, 13, 30),
   PreferenceFactor = Convert(decimal(11,10), Left(PackedData, 12))
FROM (
   SELECT
      [Name],
      PackedData = Max(Convert(char(12), PreferenceFactor) + Candy)
   FROM CandyPreference
   GROUP BY [Name]
) X

DROP TABLE #CandyPreference

Я на самом деле не рекомендую этот метод, если производительность не критична. «Каноническим» способом сделать это является стандартная производная таблица OrbMan Max / GROUP BY, а затем соединение с ней для получения выбранной строки. Тем не менее, этот метод начинает становиться трудным, когда есть несколько столбцов, которые участвуют в выборе Max, и окончательная комбинация селекторов может дублироваться, то есть, когда нет столбца для обеспечения произвольной уникальности, как в случае здесь, где мы используем имя, если PreferenceFactor совпадает.

Редактировать: вероятно, лучше дать еще несколько замечаний по использованию, чтобы улучшить ясность и помочь людям избежать проблем.

  • Как общее практическое правило, при попытке повысить производительность запросов вы можете сделать МНОГО дополнительной математики, если это сэкономит вам ввод-вывод. Сохранение поиска или сканирования всей таблицы существенно ускоряет запрос, даже с учетом всех преобразований и подстрок и т. Д.
  • Из-за проблем точности и сортировки использование этого типа данных с плавающей запятой, вероятно, является плохой идеей. Хотя, если вы не имеете дело с очень большими или маленькими числами, вам все равно не следует использовать float в вашей базе данных.
  • Лучшими типами данных являются те, которые не упакованы и сортируются в том же порядке после преобразования в двоичный или символьный тип. Datetime, smalldatetime, bigint, int, smallint и tinyint преобразуются напрямую в двоичный файл и сортируются правильно, поскольку они не упакованы. С двоичным, избегайте left () и right (), используйте substring (), чтобы получить значения, надежно возвращенные в их оригиналы.
  • Я воспользовался преимуществом Preference, в котором в этом запросе была только одна цифра перед десятичной запятой, что позволяло преобразовывать прямо в символ, поскольку перед десятичной запятой всегда есть как минимум 0. Если возможно больше цифр, вам нужно выровнять десятичное число, чтобы все было правильно отсортировано. Проще всего было бы умножить свой рейтинг предпочтений, чтобы не было десятичной части, преобразовать в bigint, а затем преобразовать в двоичный (8). Как правило, преобразование между числами происходит быстрее, чем преобразование между символами и данными другого типа, особенно с датой математики.
  • Остерегайтесь нулей. Если они есть, вы должны преобразовать их во что-то, а затем обратно.
4 голосов
/ 29 июня 2009
select c.Name, max(c.Candy) as Candy, max(c.PreferenceFactor) as PreferenceFactor
from Candy c
inner join (
    select Name, max(PreferenceFactor) as MaxPreferenceFactor
    from Candy
    group by Name
) cm on c.Name = cm.Name and c.PreferenceFactor = cm.MaxPreferenceFactor
group by c.Name
order by PreferenceFactor desc, Name
2 голосов
/ 30 июня 2009

Комментарий к решению Emtucifor (поскольку я не могу делать регулярные комментарии)

Мне нравится это решение, но есть некоторые комментарии, как его можно улучшить (в данном конкретном случае).

Это невозможно сделать, если у вас все в одной таблице, но наличие нескольких таблиц, как в решении Джона Сандерса, изменит ситуацию к лучшему.

Поскольку мы имеем дело с числами в таблице [CandyPreferences], мы можем использовать математическую операцию вместо конкатенации, чтобы получить максимальное значение.

Я предлагаю PreferenceFactor быть десятичным, а не вещественным, так как я считаю, что здесь нам не нужен размер реального типа данных, и еще дальше я бы предложил десятичное число (n, n), где n <10, чтобы хранить только десятичную часть в 5 байтов. Предположим, что десятичного числа (3,3) достаточно (1000 уровней коэффициента предпочтения), мы можем сделать простое </p>

PackedData = Max (PreferenceFactor + CandyID)

Далее, если мы знаем, что у нас меньше 1 000 000 CandyID, мы можем добавить приведение как:

PackedData = Max (приведение (PreferenceFactor + CandyID как десятичное число (9,3)))

разрешение серверу sql использовать 5 байтов во временной таблице

Распаковка легко и быстро с использованием функции пола.

Niikola

- ДОБАВЛЕНО ПОЗЖЕ ---

Я протестировал оба решения, Джона и Эмтуцифора (модифицированные для использования структуры Джона и использования моих предложений). Я тестировал также с и без соединений.

Решение Emtucifor явно выигрывает, но поля не велики. Может быть иначе, если SQL-серверу нужно было выполнить некоторые физические чтения, но во всех случаях они были равны 0.

Вот вопросы:

    SELECT
   [PersonID],
   CandyID = Floor(PackedData),
   PreferenceFactor = Cast(PackedData-Floor(PackedData) as decimal(3,3))
FROM (
   SELECT
      [PersonID],
      PackedData = Max(Cast([PrefernceFactor] + [CandyID] as decimal(9,3)))
   FROM [z5CandyPreferences] With (NoLock)
   GROUP BY [PersonID]
) X

SELECT X.PersonID,
        (
                SELECT TOP 1 CandyID
                FROM z5CandyPreferences
                WHERE PersonID=X.PersonID AND PrefernceFactor=x.HighestPreference
        ) AS TopCandy,
                    HighestPreference as PreferenceFactor
FROM 
(
        SELECT PersonID, MAX(PrefernceFactor) AS HighestPreference
        FROM z5CandyPreferences
        GROUP BY PersonID
) AS X


Select p.PersonName,
       c.Candy,
       y.PreferenceFactor
  From z5Persons p
 Inner Join (SELECT [PersonID],
                    CandyID = Floor(PackedData),
                    PreferenceFactor = Cast(PackedData-Floor(PackedData) as decimal(3,3))
                    FROM ( SELECT [PersonID],
                                  PackedData = Max(Cast([PrefernceFactor] + [CandyID] as decimal(9,3)))
                             FROM [z5CandyPreferences] With (NoLock)
                            GROUP BY [PersonID]
                         ) X
            ) Y on p.PersonId = Y.PersonId
 Inner Join z5Candies c on c.CandyId=Y.CandyId

Select p.PersonName,
       c.Candy,
       y.PreferenceFactor
  From z5Persons p
 Inner Join (SELECT X.PersonID,
                    ( SELECT TOP 1 cp.CandyId
                        FROM z5CandyPreferences cp
                       WHERE PersonID=X.PersonID AND cp.[PrefernceFactor]=X.HighestPreference
                    ) CandyId,
                    HighestPreference as PreferenceFactor
               FROM ( SELECT PersonID, 
                             MAX(PrefernceFactor) AS HighestPreference
                        FROM z5CandyPreferences
                       GROUP BY PersonID
                    ) AS X
            ) AS Y on p.PersonId = Y.PersonId
 Inner Join z5Candies as c on c.CandyID=Y.CandyId

И результаты:

 TableName          nRows
 ------------------ -------
 z5Persons          200,000
 z5Candies          150,000
 z5CandyPreferences 497,445


Query                       Rows Affected CPU time Elapsed time
--------------------------- ------------- -------- ------------
Emtucifor     (no joins)          183,289   531 ms     3,122 ms
John Saunders (no joins)          183,289 1,266 ms     2,918 ms
Emtucifor     (with joins)        183,289 1,031 ms     3,990 ms
John Saunders (with joins)        183,289 2,406 ms     4,343 ms


Emtucifor (no joins)
--------------------------------------------
Table               Scan count logical reads
------------------- ---------- -------------
z5CandyPreferences           1         2,022 


John Saunders (no joins)
--------------------------------------------
Table               Scan count logical reads
------------------- ---------- -------------
z5CandyPreferences     183,290       587,677

Emtucifor (with joins)
--------------------------------------------
Table               Scan count logical reads
------------------- ---------- -------------
Worktable                    0             0
z5Candies                    1           526
z5CandyPreferences           1         2,022
z5Persons                    1           733

John Saunders (with joins) 
--------------------------------------------
Table               Scan count logical reads
------------------- ---------- -------------
z5CandyPreferences      183292       587,912
z5Persons                    3           802
Worktable                    0             0
z5Candies                    3           559
Worktable                    0             0
2 голосов
/ 28 июня 2009

Я пытался:

SELECT X.PersonName,
    (
        SELECT TOP 1 Candy
        FROM CandyPreferences
        WHERE PersonName=X.PersonName AND PreferenceFactor=x.HighestPreference
    ) AS TopCandy
FROM 
(
    SELECT PersonName, MAX(PreferenceFactor) AS HighestPreference
    FROM CandyPreferences
    GROUP BY PersonName
) AS X

Кажется, это работает, хотя я не могу говорить об эффективности без реальных данных и реальной нагрузки.

Я создал первичный ключ для PersonName и Candy. Использование SQL Server 2008 и отсутствие дополнительных индексов показывает, что он использует два сканирования кластерных индексов, так что это может быть хуже.


Я играл с этим немного больше, потому что мне нужно было оправдание, чтобы поиграть с возможностью «Генерация данных» в плане генерации данных. Во-первых, я реорганизовал одну таблицу, чтобы иметь отдельные таблицы для имен конфет и имен людей. Я сделал это в основном потому, что это позволило мне использовать генерацию тестовых данных без необходимости читать документацию. Схема стала:

CREATE TABLE [Candies](
    [CandyID] [int] IDENTITY(1,1) NOT NULL,
    [Candy] [nvarchar](50) NOT NULL,
 CONSTRAINT [PK_Candies] PRIMARY KEY CLUSTERED 
(
    [CandyID] ASC
),
 CONSTRAINT [UC_Candies] UNIQUE NONCLUSTERED 
(
    [Candy] ASC
)
)
GO

CREATE TABLE [Persons](
    [PersonID] [int] IDENTITY(1,1) NOT NULL,
    [PersonName] [nvarchar](100) NOT NULL,
 CONSTRAINT [PK_Preferences.Persons] PRIMARY KEY CLUSTERED 
(
    [PersonID] ASC
)
)
GO

CREATE TABLE [CandyPreferences](
    [PersonID] [int] NOT NULL,
    [CandyID] [int] NOT NULL,
    [PrefernceFactor] [real] NOT NULL,
 CONSTRAINT [PK_CandyPreferences] PRIMARY KEY CLUSTERED 
(
    [PersonID] ASC,
    [CandyID] ASC
)
)
GO

ALTER TABLE [CandyPreferences]  
WITH CHECK ADD  CONSTRAINT [FK_CandyPreferences_Candies] FOREIGN KEY([CandyID])
REFERENCES [Candies] ([CandyID])
GO

ALTER TABLE [CandyPreferences] 
CHECK CONSTRAINT [FK_CandyPreferences_Candies]
GO

ALTER TABLE [CandyPreferences]  
WITH CHECK ADD  CONSTRAINT [FK_CandyPreferences_Persons] FOREIGN KEY([PersonID])
REFERENCES [Persons] ([PersonID])
GO

ALTER TABLE [CandyPreferences] 
CHECK CONSTRAINT [FK_CandyPreferences_Persons]
GO

Запрос стал:

SELECT P.PersonName, C.Candy
FROM (
    SELECT X.PersonID,
        (
            SELECT TOP 1 CandyID
            FROM CandyPreferences
            WHERE PersonID=X.PersonID AND PrefernceFactor=x.HighestPreference
        ) AS TopCandy
    FROM 
    (
        SELECT PersonID, MAX(PrefernceFactor) AS HighestPreference
        FROM CandyPreferences
        GROUP BY PersonID
    ) AS X
) AS Y
INNER JOIN Persons P ON Y.PersonID = P.PersonID
INNER JOIN Candies C ON Y.TopCandy = C.CandyID

При 150 000 конфет, 200 000 человек и 500 000 CandyPreferences запрос занимал около 12 секунд и выдавал 200 000 строк.


Следующий результат меня удивил. Я изменил запрос, чтобы удалить последние "красивые" соединения:

SELECT X.PersonID,
    (
        SELECT TOP 1 CandyID
        FROM CandyPreferences
        WHERE PersonID=X.PersonID AND PrefernceFactor=x.HighestPreference
    ) AS TopCandy
FROM 
(
    SELECT PersonID, MAX(PrefernceFactor) AS HighestPreference
    FROM CandyPreferences
    GROUP BY PersonID
) AS X

Теперь это занимает две или три секунды для 200 000 строк.

Теперь, чтобы было ясно, ничто из того, что я здесь сделал, не должно было улучшить производительность этого запроса: я считал, что 12 секунд - это успех. Теперь он говорит, что проводит 90% своего времени в поиске кластерного индекса.

1 голос
/ 28 июня 2009
SELECT Name, Candy, PreferenceFactor
  FROM table AS a
 WHERE NOT EXISTS(SELECT * FROM table AS b
                   WHERE b.Name = a.Name
                     AND (b.PreferenceFactor > a.PreferenceFactor OR (b.PreferenceFactor = a.PreferenceFactor AND b.Candy > a.Candy))
1 голос
/ 28 июня 2009

вы можете использовать следующие операторы выбора

select Name,Candy,PreferenceFactor
from candyTable ct 
where PreferenceFactor = 
    (select max(PreferenceFactor) 
     from candyTable where ct.Name = Name)

но с этим выбором вы получите "Крис" 2 раза в наборе результатов.

, если вы хотите получить наиболее предпочтительную еду для пользователя, чем использовать

select top 1 Name,Candy,PreferenceFactor
from candyTable ct
where name = @name
and PreferenceFactor= 
    (select max([PreferenceFactor]) 
     from candyTable where name = @name )

Я думаю, что изменение имени и конфет на целочисленные типы может помочь вам повысить производительность. Вы также должны вставить индексы в оба столбца.

[Изменить] изменено! до @

0 голосов
/ 30 июня 2009

Я также тестировал ROW_NUMBER () версию + добавлен дополнительный индекс

Создание индекса IX_z5CandyPreferences на z5CandyPreferences (PersonId, PrefernceFactor, CandyID)

Время отклика между версией Emtucifor и ROW_NUMBER () (с индексом на месте) минимально (если есть - тест должен повторяться несколько раз и принимать средние значения, но я ожидаю, что не будет существенной разницы)

Вот запрос:

Select p.PersonName,
       c.Candy,
       y.PrefernceFactor
  From z5Persons p
 Inner Join (Select * from (Select cp.PersonId,
       cp.CandyId,
       cp.PrefernceFactor,
       ROW_NUMBER() over (Partition by cp.PersonId Order by cp.PrefernceFactor, cp.CandyId ) as hp
  From z5CandyPreferences cp) X
   Where hp=1) Y on p.PersonId = Y.PersonId
 Inner Join z5Candies c on c.CandyId=Y.CandyId

и результаты с новым индексом и без него:

                           |     Without index    |      With Index
                           ----------------------------------------------
Query (Aff.Rows 183,290)   |CPU time Elapsed time | CPU time Elapsed time
-------------------------- |-------- ------------ | -------- ------------
Emtucifor     (with joins) |1,031 ms     3,990 ms |   890 ms     3,758 ms
John Saunders (with joins) |2,406 ms     4,343 ms | 1,735 ms     3,414 ms
ROW_NUMBER()  (with joins) |2,094 ms     4,888 ms |   953 ms     3,900 ms.


Emtucifor (with joins)         Without index |              With Index
-----------------------------------------------------------------------
Table              |Scan count logical reads | Scan count logical reads
-------------------|---------- ------------- | ---------- -------------
Worktable          |         0             0 |          0             0
z5Candies          |         1           526 |          1           526
z5CandyPreferences |         1         2,022 |          1           990
z5Persons          |         1           733 |          1           733

John Saunders (with joins)     Without index |              With Index
-----------------------------------------------------------------------
Table              |Scan count logical reads | Scan count logical reads
-------------------|---------- ------------- | ---------- -------------
z5CandyPreferences |    183292       587,912 |    183,290       585,570
z5Persons          |         3           802 |          1           733
Worktable          |         0             0 |          0             0
z5Candies          |         3           559 |          1           526
Worktable          |         0             0 |          -             -


ROW_NUMBER() (with joins)      Without index |              With Index 
-----------------------------------------------------------------------
Table              |Scan count logical reads | Scan count logical reads
-------------------|---------- ------------- | ---------- -------------
z5CandyPreferences |         3          2233 |          1           990
z5Persons          |         3           802 |          1           733
z5Candies          |         3           559 |          1           526
Worktable          |         0             0 |          0             0
0 голосов
/ 28 июня 2009

Примерно так будет работать:

select name
, candy  = substring(preference,7,len(preference))
  -- convert back to float/numeric
, factor = convert(float,substring(preference,1,5))/10
from (
  select name, 
    preference = (
      select top 1 
           -- convert from float/numeric to zero-padded fixed-width string
           right('00000'+convert(varchar,convert(decimal(5,0),preferencefactor*10)),5)
         + ';' + candy
       from candyTable b
       where a.name = b.name
       order by 
         preferencefactor desc
       , candy
       )
  from (select distinct name from candyTable) a
  ) a

Производительность должна быть достойной при использовании метода. Проверьте свой план запроса.

TOP 1 ... ORDER BY в коррелированном подзапросе позволяет нам указать произвольные правила, для которых мы хотим вернуть строку для каждой строки во внешнем запросе. В этом случае нам нужен самый высокий коэффициент предпочтения для каждого имени с конфетами для тай-брейков.

Подзапросы могут возвращать только одно значение, поэтому мы должны объединить конфеты и коэффициент предпочтения в одном поле. Точка с запятой здесь только для удобства чтения, но в других случаях вы можете использовать ее для анализа комбинированного поля с CHARINDEX во внешнем запросе.

Если вы хотите получить полную точность в выводе, вы можете использовать это вместо этого (при условии, что preferencefactor - это float):

convert(varchar,preferencefactor) + ';' + candy

А затем проанализируйте его с помощью:

factor = convert(float,substring(preference,1,charindex(';',preference)-1))
candy = substring(preference,charindex(';',preference)+1,len(preference))
0 голосов
/ 28 июня 2009
SELECT d.Name, a.Candy, d.MaxPref
FROM myTable a, (SELECT Name, MAX(PreferenceFactor) AS MaxPref FROM myTable) as D
WHERE a.Name = d.Name AND a.PreferenceFactor = d.MaxPref

Это должно дать вам строки с соответствующим PrefFactor для данного Имени. (например, если Джон в качестве HighPref равен 1 для лимона и шоколада).

Извините за ответ, который я пишу без SQL Query Analyzer.

0 голосов
/ 28 июня 2009

Я изменил имя вашего столбца на PersonName, чтобы избежать любых распространенных конфликтов зарезервированных слов.

SELECT     PersonName, MAX(Candy) AS PreferredCandy, MAX(PreferenceFactor) AS Factor
FROM         CandyPreference
GROUP BY PersonName
ORDER BY Factor DESC
...