Как избежать многих обращений к базе данных и большого количества ненужных данных? - PullRequest
7 голосов
/ 18 августа 2011

Я работал с различными приложениями и сталкивался с этой ситуацией много раз. До сих пор я не понял, какой подход лучше.

Вот сценарий:

  • У меня есть приложение на рабочем столе или в Интернете
  • Мне нужно получить простые документы из базы данных. Документ содержит общие сведения и сведения об элементе, поэтому база данных:

GeneralDetails таблица:

| DocumentID | DateCreated | Owner     |
| 1          | 07/07/07    | Naruto    |
| 2          | 08/08/08    | Goku      |
| 3          | 09/09/09    | Taguro    |

ItemDetails таблица

| DocumentID | Item        | Quantity  |
| 1          | Marbles     | 20        |
| 1          | Cards       | 56        |
| 2          | Yo-yo       | 1         |
| 2          | Chess board | 3         |
| 2          | GI Joe      | 12        |
| 3          | Rubber Duck | 1         |

Как видите, таблицы имеют отношение один ко многим. Теперь, чтобы получить все документы и соответствующие предметы, я всегда делаю одно из двух:

Метод 1 - Многократные поездки (псевдокод):

 Documents = GetFromDB("select DocumentID, Owner " +
                       "from GeneralDetails") 
 For Each Document in Documents
{
    Display(Document["CreatedBy"])
    DocumentItems = GetFromDB("select Item, Quantity " + 
                              "from ItemDetails " + 
                              "where DocumentID = " + Document["DocumentID"] + "")
    For Each DocumentItem in DocumentItems
    {
        Display(DocumentItem["Item"] + " " + DocumentItem["Quantity"])
    }
}

Метод 2 - Значительные нерелевантные данные (псевдокод):

DocumentsAndItems = GetFromDB("select g.DocumentID, g.Owner, i.Item, i.Quantity " + 
                              "from GeneralDetails as g " +
                              "inner join ItemDetails as i " +
                              "on g.DocumentID = i.DocumentID")
//Display...

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

До тех пор, пока однажды я не увидел статью «Сделай Интернет быстрее», в которой говорится, что много поездок в базу данных плохо; с тех пор я использовал второй метод.

Во втором методе я избегал циклических переходов, используя внутреннее объединение для одновременного извлечения первой и второй таблиц, но это приводит к ненужным или избыточным данным. Смотрите набор результатов.

| DocumentID | Owner     | Item        | Quantity  |
| 1          | Naruto    | Marbles     | 20        |
| 1          | Naruto    | Cards       | 56        |
| 2          | Goku      | Yo-yo       | 1         |
| 2          | Goku      | Chess board | 3         |
| 2          | Goku      | GI Joe      | 12        |
| 3          | Taguro    | Rubber Duck | 1         |

Результирующий набор имеет избыточные DocumentID и Owner. Это похоже на ненормализованную базу данных.

Теперь вопрос в том, как избежать обходов и в то же время избежать избыточных данных?

Ответы [ 10 ]

4 голосов
/ 18 августа 2011

Метод, используемый ActiveRecord и другими ORM, заключается в выборе первой таблицы, объединении идентификаторов и последующем использовании этих идентификаторов в предложении IN для второго выбора.

SELECT * FROM ItemDetails WHEREDocumentId IN ([Список разделенных запятыми идентификаторов здесь])

Преимущества:

  • Нет избыточных данных

Недостатки:

  • Два запроса

Вообще говоря, первый метод называется «проблемой запроса N + 1», а решения - «готовой загрузкой».Я склонен видеть ваш «Метод 2» столь же предпочтительным, как и задержка для базы данных , как правило, , превосходящая размер избыточных данных над скоростью передачи данных, но YRMV.Как и почти все в программном обеспечении, это компромисс.

3 голосов
/ 18 августа 2011

Внутреннее объединение лучше, потому что база данных имеет больше возможностей для оптимизации.

Как правило, вы не можете создать такой запрос, который не приводит к избыточным результатам. Для этого реляционная модель слишком ограничительна. Я бы просто жил с этим: база данных отвечает за оптимизацию этих случаев.

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

| DocumentID | Owner     | Items                                   | Quantity    |
| 1          | Naruto    | Marbles, Cards                          | 20, 56      |
| 2          | Goku      | Yo-yo, Chess board, GI Joe, Rubber Duck | 1, 3, 12, 1 |

Но это, конечно, не соответствует первой нормальной форме - поэтому вам нужно будет проанализировать ее на клиенте. Если вы используете базу данных с поддержкой XML (например, Oracle или MS SQL Server), вы даже можете создать XML-файл на сервере и отправить его клиенту.

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

2 голосов
/ 18 августа 2011

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

Что-то вроде

DocumentItems = GetFromDB("select Item, Quantity " + 
                          "from ItemDetails " + 
                          "where DocumentID in (" + LISTING_OF_KEYS + ")")
1 голос
/ 09 июня 2016

Насколько я понимаю, у вас есть несколько вариантов

  1. Конкатрируйте ваши строки, чтобы все ваши элементы отображались без избыточных данных.т.е. "Marbles, Cards"
  2. Возвращает ваш запрос в виде сжатого XML-файла, который ваша программа может затем проанализировать, как если бы это была база данных.
    • Это дает вам преимущество только одной поездки, но вы также получаете все данные в одном файле, который может быть массивным.
  3. Этот предмет был бы моим человекомпредпочтение, реализовать форму ленивой загрузки.
    • Это означает, что «дополнительные» данные загружаются только при необходимости.Таким образом, хотя это имеет несколько поездок, эти поездки предназначены только для получения необходимых данных.
1 голос
/ 09 июня 2016

Ответ зависит от вашей задачи.

1. Если вы хотите создать список / отчет, вам нужен метод 2 с избыточными данными. Вы передаете больше данных по сети, но экономите время на создании контента.

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

3. Если вы хотите предварительно загрузить все данные в приложение, вы можете использовать XML. Это обеспечит ВСЕ не избыточные данные. Однако существует дополнительное программирование с XML-кодированием на SQL и декодированием на клиенте.

Я бы сделал что-то вроде этого для генерации XML на стороне SQL:

;WITH t AS (
    SELECT g.DocumentID, g.Owner, i.Item, i.Quantity
    FROM GeneralDetails AS g
    INNER JOIN ItemDetails AS i 
    ON g.DocumentID = i.DocumentID
)
SELECT 1 as Tag, Null as Parent, 
    DocumentID as [Document!1!DocumentID],
    Owner as [Document!1!Owner],
    NULL as [ItemDetais!2!Item],
    NULL as [ItemDetais!2!Quantity]
FROM t GROUP BY DocumentID, Owner
UNION ALL
SELECT 2 as Tag, 1 as Parent, DocumentID, Owner, Item, Quantity
FROM t 
ORDER BY [Document!1!DocumentID], [Document!1!Owner], [ItemDetais!2!Item], [ItemDetais!2!Quantity]
FOR XML EXPLICIT;
1 голос
/ 09 июня 2016

Если вы используете .NET и MS SQL Server, простое решение здесь состоит в том, чтобы изучить использование MARS (Multiple Active Resultsets). Вот пример блока кода, взятый прямо из справки Visual Studio 2015 в демонстрационной версии MARS:

using System;
using System.Data;
using System.Data.SqlClient;

class Class1
{
  static void Main()
  {
     // By default, MARS is disabled when connecting
     // to a MARS-enabled host.
     // It must be enabled in the connection string.
     string connectionString = GetConnectionString();

     int vendorID;
     SqlDataReader productReader = null;
     string vendorSQL = 
       "SELECT VendorId, Name FROM Purchasing.Vendor";
     string productSQL = 
       "SELECT Production.Product.Name FROM Production.Product " +
       "INNER JOIN Purchasing.ProductVendor " +
       "ON Production.Product.ProductID = " + 
       "Purchasing.ProductVendor.ProductID " +
       "WHERE Purchasing.ProductVendor.VendorID = @VendorId";

   using (SqlConnection awConnection = 
      new SqlConnection(connectionString))
   {
      SqlCommand vendorCmd = new SqlCommand(vendorSQL, awConnection);
      SqlCommand productCmd = 
        new SqlCommand(productSQL, awConnection);

      productCmd.Parameters.Add("@VendorId", SqlDbType.Int);

      awConnection.Open();
      using (SqlDataReader vendorReader = vendorCmd.ExecuteReader())
      {
        while (vendorReader.Read())
        {
          Console.WriteLine(vendorReader["Name"]);

          vendorID = (int)vendorReader["VendorId"];

          productCmd.Parameters["@VendorId"].Value = vendorID;
          // The following line of code requires
          // a MARS-enabled connection.
          productReader = productCmd.ExecuteReader();
          using (productReader)
          {
            while (productReader.Read())
            {
              Console.WriteLine("  " +
                productReader["Name"].ToString());
            }
          }
        }
      }
      Console.WriteLine("Press any key to continue");
      Console.ReadLine();
    }
  }
  private static string GetConnectionString()
  {
    // To avoid storing the connection string in your code,
    // you can retrive it from a configuration file.
    return "Data Source=(local);Integrated Security=SSPI;" + 
      "Initial Catalog=AdventureWorks;MultipleActiveResultSets=True";
  }
 }

Надеюсь, это приведет вас к пониманию. Существует множество различных подходов к вопросу о циклическом переключении, и многое из этого зависит от типа приложения, которое вы пишете, и хранилища данных, к которому вы подключаетесь. Если это интранет-проект и не существует большого количества одновременных пользователей, то большое количество обращений к базе данных - это не проблема или проблема, о которой вы думаете, за исключением того, как выглядит ваша репутация не иметь более обтекаемый код! (Гринь) Если это веб-приложение, то это уже другая история, и вы должны убедиться, что вы не возвращаетесь к колодцу слишком часто, если его вообще можно избежать. MARS - хороший ответ для решения этой проблемы, поскольку все возвращается с сервера за один раз, и тогда вам остается перебрать возвращенные данные. Надеюсь, что это полезно для вас!

1 голос
/ 09 июня 2016

Ваш второй метод определенно является подходом.Но вам не нужно выбирать столбцы, которые вы не собираетесь использовать.Поэтому, если вам нужны только Item и Quantity, сделайте следующее:

DocumentsAndItems = GetFromDB("select i.Item, i.Quantity " + 
                          "from GeneralDetails as g " +
                          "inner join ItemDetails as i " +
                          "on g.DocumentID = i.DocumentID")

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

0 голосов
/ 10 июня 2016

С тех пор, как я задал этот вопрос, я понял, что есть и другие области, в которых я могу оптимизировать свое приложение при получении данных. В таком случае я сделаю следующее:

  • Спросите себя, действительно ли мне нужно получить много документов вместе с их подпунктами? Обычно в пользовательском интерфейсе я отображаю записи в списке, только когда пользователю нужны подэлементы (если пользователь щелкает запись), я получаю их.

  • Если действительно необходимо отобразить много записей с подэлементами, например, публикацию / комментарии, я предоставлю только некоторые сообщения, подумайте о разбиении на страницы или предоставлю функцию «загрузить еще».

Подводя итог, я могу закончить ленивой загрузкой, извлекать данные только тогда, когда они нужны пользователю.

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

 recordSets = GetData
     ("select * from parentDocs where [condition] ;
        select * from subItems where [condition]")

 //join the parent documents and subitems here

Мне может понадобиться временная таблица здесь для родительских документов, чтобы я мог использовать ее для условия во втором запросе, поскольку мне нужно только извлечь подэлементы выбранных родительских документов.

Я также должен отметить, что выполнение эталонного теста лучше, чем просто применение принципов сразу, поскольку это действительно индивидуальный подход.

0 голосов
/ 08 июня 2016

Вы можете дополнительно оптимизировать этот процесс, извлекая нужные данные из двух таблиц отдельно.После этого вы можете либо просмотреть циклы записей, либо присоединиться к таблицам, чтобы получить тот же набор результатов, что и на сервере SQL.

С помощью ORM вы можете извлекать сущности по отдельности в два цикла - один для получения GeneralDetails и другой для получения ItemDetails после проверки GeneralDetails.DocumentId.Хотя есть два обхода БД, которые оптимизированы по сравнению с любым из двух других методов.

Вот пример NHibernate:

void XXX()
{
    var queryGeneral = uow.Session.QueryOver<GeneralDetails>();
    var theDate = DateTime.Now.Subtract(5);
    queryGeneral.AndRestrictionOn(c => c.SubmittedOn).IsBetween(theDate).And(theDate.AddDays(3));

    // Whatever other criteria applies.

    var generalDetails = queryGeneral.List();

    var neededDocIds = generalDetails.Select(gd => gd.DocumentId).Distinct().ToArray();

    var queryItems = uow.Session.QueryOver<ItemDetails>();
    queryItem.AndRestrictionOn(id => id.DocumentId).IsIn(neededDocs);

    var itemDetails = queryItems.List();

    // The records from both tables are now in the generalDetails and itemDetails lists so you can manipulate them in memory...
}

Я полагаю (нет живого примера) с набором данных ADO.NET вы можете фактически сохранить вторую поездку в БД.Вам даже не нужно присоединяться к результатам;это вопрос стиля кодирования и рабочего процесса, но обычно вы можете обновить свой пользовательский интерфейс, работая с двумя наборами результатов одновременно,

void YYY()
{
    var sql = "SELECT *  FROM GeneralDetails WHERE DateCreated BETWEEN '2015-06-01' AND '2015-06-20';";
    sql += @"
            WITH cte AS (
                SELECT DocumentId FROM GeneralDetails WHERE DateCreated BETWEEN '2015-06-01' AND '2015-06-20'
            )
            SELECT * FROM ItemDetails INNER JOIN cte ON ItemDetails.DocumentId = cte.DocumentId";

    var ds = new DataSet();

    using (var conn = new SqlConnection("a conn string"))
    using (var da = new SqlDataAdapter())
    {
        conn.Open();
        da.SelectCommand = conn.CreateCommand();
        da.SelectCommand.CommandText = sql;
        da.Fill(ds);
    }

    // Now the two table are in the dataset so you can loop through them and do your stuff...
}
  • Примечание. Я написал вышеупомянутый код исключительно в качестве примера.и не проверено!
0 голосов
/ 03 июня 2016

Каким-то образом в моем приложении с ~ 200 формами / экранами и базой данных с ~ 300 таблицами у меня никогда не было необходимости ни в первом, ни во втором методе.

В моем приложении довольно часто пользователь видит на экране две сетки (таблицы) рядом друг с другом:

  • главная GeneralDetails таблица со списком документов (обычно есть функция поиска, которая ограничивает результаты с помощью множества различных фильтров).

  • данные из таблицы ItemDetails для выбранного документа. Не для всех документов. Только для одного текущего документа. Когда пользователь выбирает другой документ в первой сетке, я (повторно) запускаю запрос, чтобы получить подробную информацию о выбранном документе. Только для одного выбранного документа.

Таким образом, нет соединения между главной таблицей и таблицей сведений. И нет цикла для извлечения деталей для всех основных документов.

Зачем вам нужны подробности для всех документов на клиенте?

Я бы сказал, что лучшие практики сводятся к здравому смыслу:

Всегда полезно передавать по сети только те данные, которые вам нужны, без избыточности. И всегда хорошо, чтобы количество запросов / запросов было как можно меньше. Вместо того, чтобы отправлять много запросов в цикле, отправьте один запрос, который вернет все необходимые строки. Затем нарежьте и нарежьте его клиенту, если это действительно необходимо.


Если есть необходимость каким-либо образом обрабатывать пакет документов вместе с их деталями, это отдельная история, и до сих пор мне всегда удавалось делать это на стороне сервера, не передавая все эти данные клиенту.

Если по какой-либо причине необходимо получить список всех основных документов вместе с подробной информацией для всех документов для клиента, я бы сделал два запроса без каких-либо циклов:

SELECT ... FROM GeneralDetails

SELECT ... FROM ItemDetails

Эти два запроса будут возвращать два массива данных, и при необходимости я объединю данные основной детали во внутренних структурах в памяти на клиенте.

...