Поддержка CoreData для индексов R-Tree была представлена в 2017 году. WWD C Сессия 210 2017 охватывает ее и предоставляет пример. Как вы увидите, суть в том, что вам нужно использовать функцию в строке формата предиката, чтобы указать, что следует использовать индекс. Есть еще один пример в WWD C 2018 сеанс 224 .
Возьмите немного более простой вариант вашего примера: объект с атрибутами location (latitude
и longitude
) и name
атрибут:
![Entity diagram](https://i.stack.imgur.com/UV5Jn.png)
Добавьте индекс выборки с именем «bylocation», укажите его тип как «R-Tree» и добавьте элементы индекса выборки для latitude
и longitude
:
![Index description](https://i.stack.imgur.com/xSSFc.png)
Немного измените свой код, чтобы отразить различные атрибуты et c. Подготовьте два отдельных предиката, один с использованием индекса, другой без него, и запустите их оба для сравнения:
let mainContext: NSManagedObjectContext
mainContext = persistentContainer.viewContext
mainContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
mainContext.undoManager = nil
mainContext.shouldDeleteInaccessibleFaults = true
mainContext.automaticallyMergesChangesFromParent = true
var locationObj: Region
let n = 10 // Just for demo purposes
for i in 1...n
{
locationObj = Region(context: mainContext)
locationObj.name = "Region \(i)"
locationObj.latitude = 40.000000 + 5.0 - Float.random(in: 0 ..< 10)
locationObj.longitude = 9.000000 + 5.0 - Float.random(in: 0 ..< 10)
if i % 1000 == 0 {
saveContext()
}
}
saveContext()
mainContext.reset()
let request: NSFetchRequest<Region> = Region.fetchRequest()
let requestIdx: NSFetchRequest<Region> = Region.fetchRequest()
let eps : Float = 1.0
let predicateBoundaryIdx = NSPredicate(format: "indexed:by:(latitude, 'bylocation') between { %lf, %lf } AND indexed:by:(longitude, 'bylocation') between { %lf, %lf }", 40.0-eps, 40.0+eps, 9.0-eps, 9.0+eps)
let predicateBoundary = NSPredicate(format: "latitude between { %lf, %lf } AND longitude between { %lf, %lf} ",40.000000-eps,40.000000+eps,9.000000-eps,9.000000+eps)
requestIdx.predicate = predicateBoundaryIdx;
request.predicate = predicateBoundary;
print("fetch index:")
do {
let result = try mainContext.fetch(requestIdx)
print("Count = \(result.count)")
} catch {
print("Error: \(error)")
}
mainContext.reset()
print("fetch no index:")
do {
let result = try mainContext.fetch(request)
print("Count = \(result.count)")
} catch {
print("Error: \(error)")
}
Запустите это с SQLDebug = 4, и затем вы увидите немного того, что происходит в журналы. Сначала создается база данных и добавляется таблица регионов, а затем индекс RTree. Триггеры создаются для добавления соответствующих данных в индекс при каждом изменении таблицы регионов:
CoreData: sql: CREATE TABLE ZREGION ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZLATITUDE FLOAT, ZLONGITUDE FLOAT, ZNAME VARCHAR )
CoreData: sql: CREATE VIRTUAL TABLE IF NOT EXISTS Z_Region_bylocation USING RTREE (Z_PK INTEGER PRIMARY KEY, ZLATITUDE_MIN, ZLATITUDE_MAX, ZLONGITUDE_MIN, ZLONGITUDE_MAX)
CoreData: sql: CREATE TRIGGER IF NOT EXISTS Z_Region_bylocation_INSERT AFTER INSERT ON ZREGION FOR EACH ROW BEGIN INSERT OR REPLACE INTO Z_Region_bylocation (Z_PK, ZLATITUDE_MIN, ZLATITUDE_MAX, ZLONGITUDE_MIN, ZLONGITUDE_MAX) VALUES (NEW.Z_PK, NEW.ZLATITUDE, NEW.ZLATITUDE, NEW.ZLONGITUDE, NEW.ZLONGITUDE) ; END
CoreData: sql: CREATE TRIGGER IF NOT EXISTS Z_Region_bylocation_UPDATE AFTER UPDATE ON ZREGION FOR EACH ROW BEGIN DELETE FROM Z_Region_bylocation WHERE Z_PK = NEW.Z_PK ; INSERT INTO Z_Region_bylocation (Z_PK, ZLATITUDE_MIN, ZLATITUDE_MAX, ZLONGITUDE_MIN, ZLONGITUDE_MAX) VALUES (NEW.Z_PK, NEW.ZLATITUDE, NEW.ZLATITUDE, NEW.ZLONGITUDE, NEW.ZLONGITUDE) ; END
CoreData: sql: CREATE TRIGGER IF NOT EXISTS Z_Region_bylocation_DELETE AFTER DELETE ON ZREGION FOR EACH ROW BEGIN DELETE FROM Z_Region_bylocation WHERE Z_PK = OLD.Z_PK ; END
Затем, когда дело доходит до выборки, вы можете увидеть два разных запроса, отправляемых в SQLite:
С индексом:
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZREGION t0 WHERE ( t0.Z_PK IN (SELECT n1_t0.Z_PK FROM Z_Region_bylocation n1_t0 WHERE (? <= n1_t0.ZLATITUDE_MIN AND n1_t0.ZLATITUDE_MAX <= ?)) AND t0.Z_PK IN (SELECT n1_t0.Z_PK FROM Z_Region_bylocation n1_t0 WHERE (? <= n1_t0.ZLONGITUDE_MIN AND n1_t0.ZLONGITUDE_MAX <= ?)))
, а журналы даже включают план запроса, используемый SQLite:
2 0 0 SEARCH TABLE ZREGION AS t0 USING INTEGER PRIMARY KEY (rowid=?)
6 0 0 LIST SUBQUERY 1
8 6 0 SCAN TABLE Z_Region_bylocation AS n1_t0 VIRTUAL TABLE INDEX 2:D0B1
26 0 0 LIST SUBQUERY 2
28 26 0 SCAN TABLE Z_Region_bylocation AS n1_t0 VIRTUAL TABLE INDEX 2:D2B3
Без индекса:
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZREGION t0 WHERE (( t0.ZLATITUDE BETWEEN ? AND ?) AND ( t0.ZLONGITUDE BETWEEN ? AND ?))
2 0 0 SCAN TABLE ZREGION AS t0
Из этого видно, что использование индекса включает в себя несколько довольно беспорядочных подзапросов. Я обнаружил, что в результате для небольших наборов данных индекс действительно замедляет работу. Аналогично, если набор результатов большой. Но если набор данных большой, а набор результатов небольшой, есть преимущество. Я предоставляю вам играть и решать, стоит ли игра свеч. Одна вещь, которую я не могу понять, - это то, что для использования индекса требуются два отдельных подвыбора, один для долготы, а другой для широты. Мне кажется (хотя, возможно, я чего-то упускаю) подрывается вся суть R-Trees, а именно их многомерность.