diff --git a/mo/web/table.py b/mo/web/table.py
index a6d08b8f302298172934c3307fbd5cd86898c680..c1e6a8f8053e967218b3d117813521ea1530d8a4 100644
--- a/mo/web/table.py
+++ b/mo/web/table.py
@@ -4,7 +4,7 @@ from flask import Response, url_for
 from html import escape
 import io
 from markupsafe import Markup
-from typing import Sequence, Optional
+from typing import Sequence, Optional, Iterable
 import werkzeug.exceptions
 
 import mo.csv
@@ -16,7 +16,12 @@ from mo.web import app
 class Column:
     key: str                # Jméno klíče ve slovnících
     name: str               # Název v hlavičce CSV
-    title: Optional[str]    # Titulek v HTML tabulkách
+    title: str              # Titulek v HTML tabulkách
+
+    def __init__(self, key: str, name: Optional[str] = None, title: Optional[str] = None):
+        self.key = key
+        self.name = name or key
+        self.title = title or self.name
 
 
 class Cell:
@@ -46,10 +51,10 @@ class CellLink(Cell):
 
 class Table:
     columns: Sequence[Column]
-    rows: Sequence[dict]
+    rows: Iterable[dict]
     filename: str
 
-    def __init__(self, columns: Sequence[Column], rows: Sequence[dict], filename: str):
+    def __init__(self, columns: Sequence[Column], rows: Iterable[dict], filename: str):
         self.columns = columns
         self.rows = rows
         self.filename = filename
@@ -58,7 +63,7 @@ class Table:
         tab = ['<table class=data>', '<tr>']
 
         for c in self.columns:
-            tab.append(f'\t<th>{c.title or c.name}')
+            tab.append(f'\t<th>{c.title}')
 
         for r in self.rows:
             tab.append('<tr>')
@@ -87,19 +92,45 @@ class Table:
 
         return out.getvalue()
 
-    def send_as(self, format: str) -> Response:
+    def to_csv_stream(self, dialect: str) -> Iterable[str]:
+        out = io.StringIO()
+        writer = csv.writer(out, dialect=dialect)
+
+        header = [c.name for c in self.columns]
+        writer.writerow(header)
+
+        nrows = 0
+        for row in self.rows:
+            r = [row.get(c.key) for c in self.columns]
+            writer.writerow(r)
+
+            nrows += 1
+            if nrows >= 100:
+                yield out.getvalue()
+                out.seek(0)
+                out.truncate()
+                nrows = 0
+
+        yield out.getvalue()
+
+    def send_as(self, format: str, streaming: bool = False) -> Response:
         if format == 'csv':
-            out = self.to_csv(dialect='excel')
+            dialect = 'excel'
             ctype = 'text/csv; charset=utf=8'
             filename = self.filename + '.csv'
         elif format == 'tsv':
-            out = self.to_csv(dialect='tsv')
+            dialect = 'tsv'
             ctype = 'text/tab-separated-values; charset=utf=8'
             filename = self.filename + '.tsv'
         else:
             raise werkzeug.exceptions.NotFound()
 
-        resp = app.make_response(out)
+        if streaming:
+            resp = Response(self.to_csv_stream(dialect=dialect))
+        else:
+            out = self.to_csv(dialect=dialect)
+            resp = app.make_response(out)
+
         resp.content_type = ctype
         resp.headers.add('Content-Disposition', f'attachment; filename={filename}')
         return resp